第20期:索引設計(前綴索引)

image

這裏主要介紹 MySQL 的前綴索引。從名字上來看,前綴索引就是指索引的前綴,固然這個索引的存儲結構不能是 HASH,HASH 不支持前綴索引。mysql

先看下面這兩行示例數據:sql

你是中國人嗎?是的是的是的是的是的是的是的是的是的是的
肯定是中國人?是的是的是的是的是的是的是的是的是的是的

這兩行數據有一個共同的特色就是前面幾個字符不一樣,後面的全部字符內容都同樣。那面對這樣的數據,咱們該如何創建索引呢?segmentfault

大體有如下 3 種方法:函數

  1. 拿整個串的數據來作索引

    這種方法來的最簡單直觀,可是會形成索引空間極大的浪費。重複值太多,進而索引中無用數據太多,不管寫入或者讀取都產生極大資源消耗。優化

  2. 將字符拆開,將一部分作索引

    把數據前面幾個字符和剩餘的部分字符分拆爲兩個字段 r1_prefix,r1_other,針對字段 r1_prefix 創建索引。若是排除掉表結構更改這塊影響,那這種方法無疑是最好的。spa

  3. 把前面 6 個字符截取出來的子串作一個索引

可否不拆分字段,又能避免太多重複值的冗餘?咱們今天討論一下前綴索引。設計

前綴索引

前綴索引就是基於原始索引字段,截取前面指定的字符個數或者字節數來作的索引。3d

MySQL 基本上大部分存儲引擎都支持前綴索引,目前只有字符類型或者二進制類型的字段能夠創建前綴索引。好比:CHAR/VARCHAR、TEXT/BLOB、BINARY/VARBINARY。code

  • 字符類型基於前綴字符長度,f1(10) 指的前 10 個字符;
  • 二進制類型基於字節大小,f1(10) 指的是前 10 個字節長度;
  • TEXT/BLOB 類型只支持前綴索引,不支持整個字段建索引。

舉個簡單例子,表 t1 有兩個字段,針對字段 r1 有兩個索引,一個是基於字段 r1 的普通二級索引,另一個是基於字段r1的前綴索引。對象

10px;">`<localhost|mysql>show create table t1\G
*************************** 1\. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `r1` varchar(300) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_r1` (`r1`),
  KEY `idx_r1_p` (`r1`(6))
) ENGINE=InnoDB AUTO_INCREMENT=32755 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

<localhost|mysql>select count(*) from t1;
+----------+
| count(*) |
+----------+
|    24576 |
+----------+
1 row in set (0.04 sec)

下面分別是表 t1 只有 idx_r1 和 idx_r1_p 的表空間文件大小,很明顯,前綴索引很是有優點。

# idx_r1
root@debian-ytt1:/var/lib/mysql/3306/ytt# du -sh
26M     .
 # idx_r1_p
root@debian-ytt1:/var/lib/myzsql/3306/ytt# du -sh
20M     .

接下來查詢以 sample 關鍵詞開頭的記錄條數。

<localhost|mysql>select count(*) from t1 where r1 like 'sample%';
+----------+
| count(*) |
+----------+
|        4 |
+----------+
1 row in set (0.00 sec)

對應的執行計劃。能夠看出,MySQL 選擇了體積較小的前綴索引 idx_r1_p。

<localhost|mysql>explain select count(*) from t1 where r1 like 'sample%'\G
*************************** 1\. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: range
possible_keys: idx_r1,idx_r1_p
          key: idx_r1_p
      key_len: 27
          ref: NULL
         rows: 4
     filtered: 100.00
        Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)` </pre>

那這裏能夠看到,爲什麼會選擇用 r1(6) 來作前綴,而不是 r1(5) 或者其餘的?下面的 SQL 語句列出了全部基於關鍵詞 sample 的可選值,SQL 1 - SQL 6 基於關鍵詞的前綴長度不一樣。

SQL 1 - SQL 6 的前綴長度依次爲 6 - 1 個字符。

# SQL 1
select count(*) from t1 where r1 like 'sample%';

# SQL 2
select count(*) from t1 where r1 like 'sampl%';

# SQL 3
select count(*) from t1 where r1 like 'samp%';

# SQL 4
select count(*) from t1 where r1 like 'sam%';

# SQL 5
select count(*) from t1 where r1 like 'sa%';

# SQL 6
select count(*) from t1 where r1 like 's%';

那能否設計一個合適的前綴索引來讓以上 6 條 SQL 的執行都可以達到最優呢?答案是確定的。前提是計算出在當前記錄下,被索引字段每一個前綴對比整個字段的分散比率值,也叫前綴索引的可選擇性(索引字段的可選性,我有另一篇文章專門介紹),這個值選擇的合適與否,直接影響到前綴索引的運行效率。

如下把字段 r1 可選擇性查出來,結果爲 0.0971,以後只須要計算每一個前綴對應的數據分散比率是否和這個值相等或者無限接近便可。

<localhost|mysql>SELECT TRUNCATE(COUNT(DISTINCT r1) / COUNT(r1),4) 'taotal_pct' FROM t1;
+------------+
| taotal_pct |
+------------+
|     0.0971 |
+------------+
1 row in set (0.13 sec)

爲了找到最合適的索引前綴長度, 我寫了一個簡單的函數,用來依次返回字段 r1 每一個前綴長度的數據分散比率。函數 func_calc_prefix_length 返回一個 JSON 對象,對象的 KEY 和 VALUE 分別記錄了前綴長度以及對應的分散比率。

DELIMITER $

USE `ytt`$

DROP FUNCTION IF EXISTS `func_calc_prefix_length`$

CREATE DEFINER=`ytt`@`%` FUNCTION `func_calc_prefix_length`() RETURNS JSON
BEGIN
      DECLARE v_total_pct DECIMAL(20,4);
      DECLARE v_prefix_pct DECIMAL(20,4);
      DECLARE v_result JSON DEFAULT '[]';
      DECLARE i TINYINT DEFAULT 1;

  SELECT TRUNCATE(COUNT(DISTINCT r1) / COUNT(r1),4) INTO v_total_pct FROM t1;
  label1:LOOP
    SELECT TRUNCATE(COUNT(DISTINCT LEFT(r1,i)) / COUNT(r1),4) INTO v_prefix_pct FROM t1; 
    SET v_result = JSON_ARRAY_APPEND(v_result,'/pre>,JSON_OBJECT(i,v_prefix_pct));       
    IF v_prefix_pct >= v_total_pct THEN
      LEAVE label1;        
    END IF;        
    SET i = i + 1;
  END LOOP;
  RETURN v_result;
END$
DELIMITER ;

調用下這個函數:

<localhost|mysql>SELECT func_calc_prefix_length() AS prefix_length\G
*************************** 1\. row ***************************
prefix_length: [{"1": 0.0003}, {"2": 0.0005}, {"3": 0.0008}, {"4": 0.0013}, {"5": 0.0093}, {"6": 0.0971}]
1 row in set (0.32 sec)

函數結果彙總了每一個不一樣的前綴對應的數據分散比率。由此數據能夠看到,在當前數據的分佈範圍內,前綴爲 6 是最合適的,6 最接近於字段 r1 的所有數據分佈比率。因此以上 SQL 1 - SQL 6 均可以基於前綴爲 6 的索引很好的運行。

執行下 SQL 6,

<localhost|mysql>select count(*) from t1 where r1 like 's%';
+----------+
| count(*) |
+----------+
|       29 |
+----------+
1 row in set (0.00 sec)

那前綴索引有沒有可能用於以下 SQL?

# SQL 7

select count(*) from t2 where r1 like '%sample';

表 t2 和表 t1 結構一致,數據分佈有些不一樣。針對 SQL 7 這樣的查詢,過濾條件左邊是通配符 %,沒有具體的值,此時沒法使用索引,SQL 7 只能全表掃描,查詢時間 0.1 秒。

<localhost|mysql>select count(*) from t2 where r1 like '%sample';
+----------+
| count(*) |
+----------+
|        4 |
+----------+
1 row in set (0.10 sec)

查看下 sample 爲後綴的表記錄樣例。

<localhost|mysql>select * from t2 where r1 like '%sample' limit 1\G
*************************** 1\. row ***************************
id: 14
r1: mysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmysqlmyssample
1 row in set (0.13 sec)

針對此種情形,有兩種優化方法:

第一,能夠把數據按照後綴作一個拆分,後綴部分單獨爲一個字段,而後給這個字段加一個索引。除了要加字段,此方法很完美~

建一個表 t3,把表 t2 的數據導進去。

CREATE TABLE `t3` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `r1` varchar(300) DEFAULT NULL,
  `suffix_r1` varchar(6) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_suffix_r1` (`suffix_r1`)
) ENGINE=InnoDB

<localhost|mysql>insert into t3 select id,r1,right(r1,6) from t2;
Query OK, 24576 rows affected (19.05 sec)
Records: 24576  Duplicates: 0  Warnings: 0

再次執行 SQL 7,查詢瞬間出來結果。

<localhost|mysql>select count(*) from t3 where suffix_r1 = 'sample';
+----------+
| count(*) |
+----------+
|        4 |
+----------+
1 row in set (0.00 sec)
第二,能夠把數據反轉過來後創建前綴索引查詢記錄。對錶 t2 克隆一張表 t4。
<localhost|mysql>insert into t4 select id,reverse(r1) from t2;
Query OK, 24576 rows affected (5.25 sec)
Records: 24576  Duplicates: 0  Warnings: 0

查詢關鍵詞進行反轉查詢,

<localhost|mysql>select count(*) from t4  where r1 like 'elpmas%';
+----------+
| count(*) |
+----------+
|        4 |
+----------+
1 row in set (0.00 sec)

再看下查詢計劃,走了前綴索引。不過這樣的缺點是查詢記錄的時候須要在 SQL 層處理記錄數據,加上反轉函數。

<localhost|mysql>explain select count(*) from t4  where r1 like 'elpmas%'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t4
   partitions: NULL
         type: range
possible_keys: idx_r1_p
          key: idx_r1_p
      key_len: 27
          ref: NULL
         rows: 4
     filtered: 100.00
        Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)

總結

今天大體講了下 MySQL 前綴索引的定義規則以及簡單使用場景,歡迎你們批評指正。


關於 MySQL 的技術內容,大家還有什麼想知道的嗎?趕忙留言告訴小編吧!

image

相關文章
相關標籤/搜索