MySQL是如何優化模糊匹配like的SQL?

在開發過程當中,常常會碰到一些業務場景,須要以徹底模糊匹配的方式查找數據,就會想到用like %xxx% 或者 like %xxx的方式去實現,並且即便列上有選擇率很高的索引,也不會被使用。在MySQL中能夠經過ICP特性,全文索引,基於生成列索引解決這類問題,下面就從索引條件下推ICP,全文索引,基於生成列索引及如何利用它們解決模糊匹配的SQL性能問題。mysql

索引條件下推ICP

ICP介紹

MySQL 5.6開始支持ICP(Index Condition Pushdown),不支持ICP以前,當進行索引查詢時,首先根據索引來查找數據,而後再根據where條件來過濾,掃描了大量沒必要要的數據,增長了數據庫IO操做。在支持ICP後,MySQL在取出索引數據的同時,判斷是否能夠進行where條件過濾,將where的部分過濾操做放在存儲引擎層提早過濾掉沒必要要的數據,減小了沒必要要數據被掃描帶來的IO開銷。在某些查詢下,能夠減小Server層對存儲引擎層數據的讀取,從而提供數據庫的總體性能。sql

ICP具備如下特色數據庫

image-20201114133318328

ICP相關控制參數數組

index_condition_pushdown:索引條件下推默認開啓,設置爲off關閉ICP特性。緩存

mysql>show variables like 'optimizer_switch';
| optimizer_switch | index_condition_pushdown=on
# 開啓或者關閉ICP特性
mysql>set optimizer_switch = 'index_condition_pushdown=on | off';
複製代碼

ICP處理過程

假設有用戶表users01(id, name, nickname, phone, create_time),表中數據有11W。因爲ICP只能用於二級索引,故在name,nickname列上建立複合索引idx_name_nickname(name,nickname),分析SQL語句**select * from users01 where name = 'Lyn' and nickname like '%SK%'**在ICP關閉和開啓下的執行狀況。markdown

關閉ICP特性的SQL性能分析函數

disable_icp_001

開啓profiling進行跟蹤SQL執行期間每一個階段的資源使用狀況。性能

mysql>set profiling  = 1;
複製代碼

關閉ICP特性分析SQL執行狀況優化

mysql>set optimizer_switch = 'index_condition_pushdown=off';
複製代碼
mysql>explain select * from users01 where name = 'Lyn' and nickname like '%SK%';
|  1 | SIMPLE      | users01 | NULL       | ref  | idx_name_nickname | idx_name_nickname | 82      | const | 29016 |   100.00 | Using where |
#查看SQL執行期間各階段的資源使用
mysql>show profile cpu,block io for query 2;
| Status                         | Duration | CPU_user | CPU_system | Block_ops_in | Block_ops_out |
+--------------------------------+----------+----------+------------+--------------+---------------+
| starting                       | 0.000065 | 0.000057 |   0.000009 |            0 |             0 |
..................
| executing                      | 0.035773 | 0.034644 |   0.000942 |            0 |             0 |#執行階段耗時0.035773秒。
| end                            | 0.000015 | 0.000006 |   0.000009 |            0 |             0 |
#status狀態變量分析
| Handler_read_next | 16384          |  ##請求讀的行數
| Innodb_data_reads | 2989           |  #數據物理讀的總數
| Innodb_pages_read | 2836           |  #邏輯讀的總數
| Last_query_cost   | 8580.324460    |  #SQL語句的成本COST,主要包括IO_COST和CPU_COST。
複製代碼

經過explain分析執行計劃,SQL語句在關閉CP特性的狀況下,走的是複合索引idx_name_nickname,Extra=Using Where,首先經過複合索引idx_name_nickname前綴從存儲引擎中讀出name = 'Lyn'的全部記錄,而後在Server端用where過濾nickname like '%SK%'狀況。ui

Handler_read_next=16384說明掃描了16384行的數據,SQL實際返回只有12行數,耗時50ms。對於這種掃描大量數據行,只返回少許數據的SQL,能夠從兩個方面去分析。

  1. **索引選擇率低:**對於符合索引(name,nickname),name做爲前導列出現where條件,CBO都會選擇走索引,由於掃描索引比全表掃描的COST要小,但因爲name列的基數不高,致使掃描了索引中大量的數據,致使SQL性能也不過高。

    Column_name: name Cardinality: 6 能夠看到users01表中name的不一樣的值只有6個,選擇率6/114688很低。

  2. **數據分佈不均勻:**對於where name = ?來講,name數據分佈不均勻時,SQL第一次傳入的值返回結果集很小,CBO就會選擇走索引,同時將SQL的執行計劃緩存起來,之後無論name傳入任何值都會走索引掃描,這實際上是不對的,若是傳入name的值是Fly100返回表中80%的數據,這是走全表掃描更快。

| name      | count(*) |
+---------------+----------+
| Grubby        |    12    |
| Lyn           |    1000  |
| Fly100        |    98100 |
複製代碼

在MySQL 8.0推出了列的直方圖統計信息特性,主要針對索引列數據分佈不均勻的狀況進行優化。

開啓ICP特性的性能分析

enable_icp_001

開啓ICP特性分析SQL執行狀況

mysql>set optimizer_switch = 'index_condition_pushdown=on';
複製代碼
#執行計劃
|  1 | SIMPLE      | users01 | NULL       | ref  | idx_name_nickname | idx_name_nickname | 82      | const | 29016 |    11.11 | Using index condition |
#status狀態變量分析
| Handler_read_next | 12             |
| Innodb_data_reads | 2989           |
| Innodb_pages_read | 2836           |
| Last_query_cost   | 8580.324460    |
複製代碼

從執行計劃能夠看出,走了複合索引idx_name_nickname,Extra=Using index condition,且只掃描了12行數據,說明使用了索引條件下推ICP特性,SQL總共耗時10ms,跟關閉ICP特性下相比,SQL性能提高了5倍。

ICP特性/項目 掃描方式 掃描行數 返回行數 執行時間
關閉ICP Using where 16384 12 50ms
開啓ICP Using index condition 12 12 10ms

開啓ICP特性後,因爲nickname的like條件能夠經過索引篩選,存儲引擎層經過索引與where條件的比較來去除不符合條件的記錄,這個過程不須要讀取記錄,同時只返回給Server層篩選後的記錄,減小沒必要要的IO開銷。

Extra顯示的索引掃描方式

  • **using where:**查詢使用索引的狀況下,須要回表去查詢所需的數據。
  • **using index condition:**查詢使用了索引,可是須要回表查詢數據。
  • **using index :**查詢使用覆蓋索引的時候會出現。
  • **using index & using where:**查詢使用了索引,可是須要的數據都在索引列中能找到,不須要回表查詢數據。

模糊匹配改寫優化

在開啓ICP特性後,對於條件**where name = 'Lyn' and nickname like '%SK%'**能夠利用複合索引(name,nickname)減小沒必要要的數據掃描,提高SQL性能。但對於where nickname like '%SK%'徹底模糊匹配查詢可否利用ICP特性提高性能?首先建立nickname上單列索引idx_nickname。

mysql>alter table users01 add index idx_nickname(nickname);
#SQL執行計劃
|  1 | SIMPLE      | users01 | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 114543 |    11.11 | Using where |
複製代碼

從執行計劃看到type=ALL,Extra=Using where走的是所有掃描,沒有利用到ICP特性。

輔助索引idx_nickname(nickname)內部是包含主鍵id的,等價於(id,nickname)的複合索引,嘗試利用覆蓋索引特性將SQL改寫爲select Id from users01 where nickname like '%SK%' **。

|  1 | SIMPLE      | users01 | NULL       | index | NULL          | idx_nickname | 83      | NULL | 114543 |    11.11 | Using where; Using index |
複製代碼

從執行計劃看到,type=index,Extra=Using where; Using index,索引全掃描,可是須要的數據都在索引列中能找到,不須要回表。利用這個特色,將原始的SQL語句先獲取主鍵id,而後經過id跟原表進行關聯,分析其執行計劃。

select  * from users01 a , (select id from users01 where nickname like '%SK%') b where a.id = b.id;
複製代碼
|  1 | SIMPLE      | users01 | NULL       | index  | PRIMARY       | idx_nickname | 83      | NULL            | 114543 |    11.11 | Using where; Using index |
|  1 | SIMPLE      | a       | NULL       | eq_ref | PRIMARY       | PRIMARY      | 4       | test.users01.id |      1 |   100.00 | NULL                     |
複製代碼

從執行計劃看,走了索引idx_nickname,不須要回表訪問數據,執行時間從60ms下降爲40ms,type = index 說明沒有用到ICP特性,可是能夠利用Using where; Using index這種索引掃描不回表的方式減小資源開銷來提高性能。

全文索引

MySQL 5.6開始支持全文索引,能夠在變長的字符串類型上建立全文索引,來加速模糊匹配業務場景的DML操做。它是一個inverted index(反向索引),建立fulltext index時會自動建立6個auxiliary index tables(輔助索引表),同時支持索引並行建立,並行度能夠經過參數innodb_ft_sort_pll_degree設置,對於大表能夠適當增長該參數值。

刪除全文索引的表的數據時,會致使輔助索引表大量delete操做,InnoDB內部採用標記刪除,將已刪除的DOC_ID都記錄特殊的FTS_*_DELETED表中,但索引的大小不會減小,須要經過設置參數innodb_optimize_fulltext_only=ON後,而後運行OPTIMIZE TABLE來重建全文索引。

全文索引特徵

  • 從MySQL 5.7開始內置了ngram全文檢索插件,用來支持中文分詞,而且對MyISAM和InnoDB引擎有效。

  • 因爲全文索引的緩存和批量處理的特性,Insert&Update操做是在事務提交時處理,只能看到提交後的數據。

  • 全文索引使用函數MATCH() ….. AGAINST()來進行檢索,MATCH()中列個數及順序必須和索引定義保持一致。

  • 只能用於InnoDB和MyISAM的表,不支持分區表,不支持%通配符搜索。

  • MATCH()列表與表的全文索引定義列徹底匹配。

  • MySQL優化器Hint對於全文索引會被限制。

兩種檢索模式

  • IN NATURAL LANGUAGE MODE:默認模式,以天然語言的方式搜索,AGAINST('看風' IN NATURAL LANGUAGE MODE ) 等價於AGAINST('看風')。

  • **IN BOOLEAN MODE:**布爾模式,表是字符串先後的字符有特殊含義,如查找包含SK,但不包含Lyn的記錄,能夠用+,-符號。

    AGAINST('+SK -Lyn' in BOOLEAN MODE);

image-20201114162701510

這時查找nickname like '%Lyn%',經過反向索引關聯數組能夠知道,單詞Lyn存儲於文檔4中,而後定位到具體的輔助索引表中。

全文索引分析

對錶users01的nickname添加支持中文分詞的全文索引

mysql>alter table users01 add fulltext index idx_full_nickname(nickname) with parser ngram;
複製代碼

查看數據分佈

#設置當前的全文索引表
mysql>set global innodb_ft_aux_table = 'test/users01';
#查看數據文件
mysql>select * from information_schema.innodb_ft_index_cache;
+--------+--------------+-------------+-----------+--------+----------+
| WORD   | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
+--------+--------------+-------------+-----------+--------+----------+
.............
| 看風   |            7 |           7 |         1 |      7 |        3 |
| 笑看   |            7 |           7 |         1 |      7 |        0 |
複製代碼

全文索引相關對象分析

#全文索引對象分析
mysql>SELECT table_id, name, space from INFORMATION_SCHEMA.INNODB_TABLES where name like 'test/%';
|     1198 | test/users01                                       |   139 |
#存儲被標記刪除同時從索引中清理的文檔ID,其中_being_deleted_cache是_being_deleted表的內存版本。
|     1199 | test/fts_00000000000004ae_being_deleted            |   140 |
|     1200 | test/fts_00000000000004ae_being_deleted_cache      |   141 |
#存儲索引內部狀態信息及FTS_SYNCED_DOC_ID
|     1201 | test/fts_00000000000004ae_config                   |   142 | 
#存儲被標記刪除但沒有從索引中清理的文檔ID,其中_deleted_cache是_deleted表的內存版本。
|     1202 | test/fts_00000000000004ae_deleted                  |   143 |
|     1203 | test/fts_00000000000004ae_deleted_cache            |   144 |
複製代碼

模糊匹配優化

對於SQL語句後面的條件nickname like '%看風%'默認狀況下,CBO是不會選擇走nickname索引的,該寫SQL爲全文索引匹配的方式:match(nickname) against('看風')

mysql>explain select * from users01 where match(nickname) against('看風');
|  1 | SIMPLE      | users01 | NULL       | fulltext | idx_full_nickname | idx_full_nickname | 0       | const |    1 |   100.00 | Using where; Ft_hints: sorted |
複製代碼

使用了全文索引的方式查詢,type=fulltext,同時命中全文索引idx_full_nickname,從上面的分析可知,在MySQL中,對於徹底模糊匹配%%查詢的SQL能夠經過全文索引提升效率。

生成列

MySQL 5.7開始支持生成列,生成列是由表達式的值計算而來,有兩種模式:VIRTUAL和STORED,若是不指定默認是VIRTUAL,建立語法以下:

col_name data_type [GENERATED ALWAYS] AS (expr)  [**VIRTUAL** | **STORED**] [NOT NULL | NULL]
複製代碼
image-20201117003251243

生成列特徵

  • VIRTUAL生成列用於複雜的條件定義,可以簡化和統一查詢,不佔用空間,訪問列是會作計算。
  • STORED生成列用做物化緩存,對於複雜的條件,能夠下降計算成本,佔用磁盤空間。
  • 支持輔助索引的建立,分區以及生成列能夠模擬函數索引。
  • 不支持存儲過程,用戶自定義函數的表達式,NONDETERMINISTIC的內置函數,如NOW(), RAND()以及不支持子查詢

生成列使用

#添加基於函數reverse的生成列reverse_nickname
mysql>alter table users01 add reverse_nickname varchar(200) generated always as (reverse(nickname));
#查看生成列信息
mysql>show columns from users01;
| reverse_nickname | varchar(200) | YES  |     | NULL              | VIRTUAL GENERATED | #虛擬生成列
複製代碼

模糊匹配優化

對於where條件後的like '%xxx'是沒法利用索引掃描,能夠利用MySQL 5.7的生成列模擬函數索引的方式解決,具體步驟以下:

  1. 利用內置reverse函數將like '%風雲'反轉爲like '雲風%',基於此函數添加虛擬生成列。
  2. 在虛擬生成列上建立索引。
  3. 將SQL改寫成經過生成列like reverse('%風雲')去過濾,走生成列上的索引。

添加虛擬生成列並建立索引。

mysql>alter table users01 add reverse_nickname varchar(200) generated always as (reverse(nickname));
mysql>alter table users01 add index idx_reverse_nickname(reverse_nickname);
#SQL執行計劃
|  1 | SIMPLE      | users01 | NULL       | range | idx_reverse_nickname | idx_reverse_nickname | 803     | NULL |    1 |   100.00 | Using where |
複製代碼

能夠看到對於like '%xxx'沒法使用索引的場景,能夠經過基於生成列的索引方式解決。

總結

介紹了索引條件下推ICP特性,全文索引以以及生成列特性,利用這些特性能夠對模糊匹配like %xxx%或like %xxx的業務SQL進行優化,能夠有效下降沒必要要的數據讀取,減小IO掃描以及CPU開銷,提升服務的穩定性。對於MySQL每一個版本發佈的新特性,尤爲是跟優化器和SQL相關的,應該去關注和了解,可能會發現適合本身業務場景的特性。

相關文章
相關標籤/搜索