MySQL中索引基礎知識及使用規則

来源:https://www.cnblogs.com/eternityz/archive/2020/03/09/12450466.html
-Advertisement-
Play Games

目錄 " 一.InnoDB索引 " " 二.B+樹 " " 三.聚集索引和輔助索引 " " 四.索引實戰 " " 五.索引操作與規則 " 重建索引 索引覆蓋 最左首碼原則 索引下推 用索引和用索引快速定位卻別 " 六.普通索引和唯一索引如何選擇 " 前提 普通索引和唯一索引下的查詢 普通索引和唯一索 ...


目錄

一.InnoDB索引

InnoDB支持以下幾種索引:

  • B+樹索引
  • 全文索引
  • 哈希索引

本文將著重介紹B+樹索引。其他兩個全文索引和哈希索引只是做簡單介紹一筆帶過。

哈希索引是自適應的,也就是說這個不能人為干預在一張表生成哈希索引,InnoDB會根據這張表的使用情況來自動生成。

全文索引是將存在資料庫的整本書的任意內容信息查找出來的技術,InnoDB從1.2.x版本支持。每張表只能有一個全文檢索的索引。

B+樹索引是傳統意義上的索引,B+樹索引並不能根據鍵值找到具體的行數據,B+樹索引只能找到行數據所在的頁,然後通過把頁讀到記憶體,再在記憶體中查找到行數據。B+樹索引也是最常用的最為頻繁使用的索引。

二.什麼是B+樹

前提

  • 葉子節點: 沒有子節點的節點

  • 非葉子節點: 有子節點的節點

概念

  B+樹是一種平衡查找樹,其實先想想看為什麼要用平衡查找樹,不用二叉樹?普通的二叉樹可能因為插入的數據最後變成一個很長的鏈表,怎麼能提高搜索的速度呢?你可以想想,為什麼HashMap和ConcurrentHashMap在JDK8的時候,當鏈表大於8的時候把鏈表轉成紅黑樹(紅黑樹也是平衡查找樹)。技術思維是想通的,那麼答案無非是加快速度,性能咯。

一個B+樹有以下特征:

  • 有n個子樹的中間節點包含n個元素,每個元素不保存數據,只用來索引,所有數據都保存在葉子節點。
  • 所有葉子節點包含元素的信息以及指向記錄的指針,且葉子節點按關鍵字自小到大順序鏈接。
  • 所有的中間節點元素都同時存在於子節點,在子節點元素中是最大(或最小)元素。

  那麼我們先來看一個B+樹的圖

所有的數據都在葉子節點,且每一個葉子節點都帶有指向下一個節點的指針,形成了一個有序的鏈表。為什麼要有序呢?其實是為了範圍查詢。比如說select * from Table where id > 1 and id < 100; 當找到1後,只需順著節點和指針順序遍歷就可以一次性訪問到所有數據節點,極大提到了區間查詢效率。是不是範圍查詢的話hash就搞不定這個事情了?以下為B+樹的優勢:

  • 單一節點存儲更多元素,減少IO
  • 所有查詢都要找到葉子節點,查詢穩定
  • 所有葉子節點形成有序鏈表,方便範圍查詢

一般性情況,資料庫的B+樹的高度一般在2~4層,這就是說找到某一鍵值的行記錄最多需要2到4次邏輯IO,相當於0.02到0.04s。

三.聚集索引和輔助索引

聚集索引(聚簇索引)

  聚集索引是按表的主鍵構造的B+樹,葉子節點存放的為整張表的行記錄數據,每張表只能有一個聚集索引。優化器更傾向採用聚集索引。因為直接就能獲取行數據。

  請選擇自增id來做主鍵,不要非空UK列。避免大量分頁碎片。下麵來看一個聚集索引的圖:

​ 那麼很簡單了,每個葉子節點,都存有完整的行記錄。對於主鍵的查找速度那是相當的快,美滋滋。

輔助索引

  輔助索引也叫非聚集索引,葉子節點除了鍵值以外還包含了一個bookmark,用來告訴InnoDB在哪裡可以找到對應的行數據,InnoDB的輔助索引的bookmark就是相對應行數據的聚集索引鍵。也就是先獲取指向主鍵索引的主鍵,然後通過主鍵索引來找到一個完整的行。如果輔助索引的樹和聚集索引的樹的高度都是3,如果不是走主鍵索引走輔助索引的話,那麼需要6次邏輯IO訪問得到最終的數據頁。輔助索引和聚集索引的概念關係圖如下:

基於主鍵索引和普通索引的查詢有什麼區別?

  • 如果語句是 select * from T where ID=500,即主鍵查詢方式,則只需要搜索 ID 這棵 B+ 樹;
  • 如果語句是 select * from T where k=5,即普通索引查詢方式,則需要先搜索 k 索引 樹,得到 ID 的值為 500,再到 ID 索引樹搜索一次。這個過程稱為回表。

四.索引實戰

設計索引

  設計索引的時候,無論是組合索引還是普通索引等。一般經驗是,選擇經常被用來過濾記錄的欄位,高選擇性,高區分性。別把性別欄位設計索引,性別屬於低選擇性的。你可以選擇名字嘛,你好我大名叫苗嘉杏:)

  知道加索引快,但是也別亂加索引,插入以及更新索引的操作InnoDB都會維護B+樹的,多加很多索引只會導致效率降低!

  不要用重覆的索引,比如有個聯合索引是a,b,你又整個a列的普通索引。那不是搞事麽?

  不要在索引上用函數和like

一顆聚集索引B+樹可以放多少行數據?

  這裡我們先假設B+樹高為2,即存在一個根節點和若幹個葉子節點,那麼這棵B+樹的存放總記錄數為:根節點指針數*單個葉子節點記錄行數。假設一行記錄的數據大小為1k,那麼單個葉子節點(頁)中的記錄數=16K/1K=16。

  那麼現在我們需要計算出非葉子節點能存放多少指針,我們假設主鍵ID為bigint類型,長度為8位元組,而指針大小在InnoDB源碼中設置為6位元組,這樣一共14位元組,我們一個頁中能存放多少這樣的單元,其實就代表有多少指針,頁大小預設16K,即16kb/14b=1170。那麼可以算出一棵高度為2的B+樹,大概能存放1170*16=18720條這樣的數據記錄。

  根據同樣的原理我們可以算出一個高度為3的B+樹大概可以存放:1170*1170*16=21902400行數據。所以在InnoDB中B+樹高度一般為1-3層,它就能滿足千萬級的數據存儲。在查找數據時一次頁的查找代表一次IO,所以通過主鍵索引查詢通常只需要1-3次邏輯IO操作即可查找到數據。

Cardinality值

  如何判斷一個索引建立的是否好呢?可以用show index from指令查看Cardinality值,這個值是一個預估值,而不是一個準確值。每次對Cardinality值的統計都是隨機取8個葉子節點得到的。

  對於innodb來說,達到以下2點就會重新計算cardinality

  • 如果表中1/16的數據發生變化
  • 如果stat_modified_counter>200 000 0000

  實際應用中,(Cardinality/行數)應該儘量接近1。如果非常小則要考慮是否需要此索引。實戰一下,比如有一張表,我們來show index一下

mysql> show index from Order;
+---------+------------+------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table   | Non_unique | Key_name         | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------+------------+------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Order   |          0 | PRIMARY          |            1 | id          | A         |       99552 |     NULL | NULL   |      | BTREE      |         |               |
| Order   |          1 | IDX_orderId      |            1 | orderId     | A         |       96697 |     NULL | NULL   |      | BTREE      |         |               |
| Order   |          1 | IDX_productId    |            1 | productId   | A         |          52 |     NULL | NULL   |      | BTREE      |         |               |
+---------+------------+------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
rows in set (0.00 sec)

那麼可以看到IDX_productId這個索引的Cardinality比較低。 

需要強制刷新Cardinality值的話可以用:

analyze local table xxx;

五.索引操作與規則

重建索引

重建普通索引

alter table T drop index k;

alter table T add index(k);

重建主鍵索引

可行:

    alter table T engine=InnoDB 

不可行:

    alter table T drop primary key;
    alter table T add primary key(id);

覆蓋索引

ID為主鍵索引,k為普通索引.

如果執行的語句是 select ID from T where k between 3 and 5,這時只需要查 ID 的 值,而 ID 的值已經在 k 索引樹上了,因此可以直接提供查詢結果,不需要回表。也就是 說,在這個查詢裡面,索引 k 已經“覆蓋了”我們的查詢需求,我們稱為覆蓋索引。

由於覆蓋索引可以減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是一個常用的性能優化手段。

最左首碼原則

B+ 樹這種索引結構,可以利用索引的“最左首碼”,來定位 記錄。

(name,age,sex)利用最左首碼可以實現以下的索引(name)(name,age)(name,age,sex))

在建立聯合索引的時候,如何安 排索引內的欄位順序。

第一原則是,如果通過調整順序,可以少維護一個索引,那麼這個順序往往就是需要優先考慮採用的。

第二,考慮的原則就是空間

    比如:name 欄位是比 age 欄位大的 ,那我就建議你創建一個(name,age) 的聯合索引和一個 (age) 的單欄位 索引。這樣比(age,name)(name)占用空間少

索引下推

聯合索引(name, age)為例

mysql> select * from tuser where name like '張 %' and age=10 and ismale=1;

已經知道了最左首碼索引規則,所以這個語句在搜索索引樹的時候,只能用索引name來搜索 “張”,age是沒法用的,因為'zhang%'查詢的是一個範圍.

MySQL 5.6 引入的索引下推優化(index condition pushdown), 可以在索引遍歷過 程中,對索引中包含的欄位先做判斷,直接過濾掉不滿足條件的記錄,減少回表次數。

用索引和用索引快速定位卻別

前提

  • 表格260萬數據
  • id自增主鍵
  • Account普通索引
  • (Account,Cmd)聯合索引
  • 其實按照最左首碼原則Account普通索引與(Account,Cmd)聯合索引,只保留(Account,Cmd)聯合索引即可,因為通過(Account,Cmd)聯合索引也可以對Account進行索引. 此處都保留是想測試,如果兩者都存在的某些情況下,優化器如何選擇

EXPLAIN SELECT id from mt4order WHERE Account like '1';

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE mt4order NULL range account索引,account_cmd索引 account索引 768 NULL 1 100.00 Using where; Using index

EXPLAIN SELECT id from mt4order WHERE Account like '1%';

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE mt4order NULL range account索引,account_cmd索引 account索引 768 NULL 1716 Using where; Using index,

EXPLAIN SELECT id from mt4order WHERE Account like '%1%';

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE mt4order NULL index NULL account索引 768 NULL 2649814 11.11 Using where; Using index

EXPLAIN SELECT id from mt4order WHERE Account like '%1';

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE mt4order NULL index NULL account索引 768 NULL 2649814 11.11 Using where; Using index

總結:

  1. 第一個使用account索引快速定位到一行數據,extra為Using where; Using index,說明優先使用索引中覆蓋索引獲取了id信息,避免了回表(使用索引,並且使用索引快速查找)
  2. 第二個使用account索引快速定位,但是因為後麵包含一個%,所以按照最左首碼原則,對'1%'中的'1'進行索引快速查找,查詢了1716行數據,extra為Using where; Using index,說明優先使用索引中覆蓋索引獲取了id信息,避免了回表 (使用索引,並且使用索引快速查找)
  3. 第三個有使用account索引,因為是'%1%'是範圍查找,所以在account索引樹上進行了全面的查找,掃描了2649814行數據,extra中為Using where; Using index,此時只是使用了索引和覆蓋索引避免了回表,但是沒有使用索引快速定位查找,因為基於account索引掃描了全部的行(使用索引,沒有使用索引快速查找)
  4. 第四個有使用account索引,因為是'%1'是範圍查找,所以在account索引樹上進行了全面的查找,掃描了2649814行數據,extra中為Using where; Using index,此時只是使用了索引和覆蓋索引避免了回表,但是沒有使用索引快速定位查找,因為基於account索引掃描了全部的行(使用索引,沒有使用索引快速查找)

六.普通索引和唯一索引如何選擇

前提條件

普通索引和唯一索引下的查詢

  • 執行查詢的語句是 select id from T where k=5。

  • 對於普通索引來說,查找到滿足條件的第一個記錄 (5,500) 後,需要查找下一個記錄,直到碰到第一個不滿足 k=5 條件的記錄。

  • 對於唯一索引來說,由於索引定義了唯一性,查找到第一個滿足條件的記錄後,就會停止繼續檢索。

  • 這個不同帶來的性能差距會有多少呢?答案是,微乎其微。

  • InnoDB 的數據是按數據頁為單位來讀寫的。也就是說,當需要讀一條記錄的 時候,並不是將這個記錄本身從磁碟讀出來,而是以頁為單位,將其整體讀入記憶體。在 InnoDB 中,每個數據頁的大小預設是 16KB。

  • 所以說,當找到 k=5 的記錄的時候,它所在的數據頁就都在記憶體 里了。那麼,對於普通索引來說,要多做的那一次“查找和判斷下一條記錄”的操作,就 只需要一次指針尋找和一次計算。

  • 如果 k=5 這個記錄剛好是這個數據頁的最後一個記錄,那麼要取下一個記錄,必須 讀取下一個數據頁,這個操作會稍微複雜一些。但是,我們之前計算過,對於整型欄位,一個數據頁可以放近千個 key,因此出現這種情 況的概率會很低。

  • 所以,我們計算平均性能差異時,仍可以認為這個操作成本對於現在的 CPU 來說可以忽略不計。

普通索引和唯一索引下的更新

change buffer

  • 當需要更新一個數據頁時,如果數據頁在記憶體中就直接更新,
  • 而如果這個數據頁還沒有在 記憶體中的話,在不影響數據一致性的前提下,InooDB 會將這些更新操作緩存在 change buffer 中,這樣就不需要從磁碟中讀入這個數據頁了。
  • 在下次查詢需要訪問這個數據頁的 時候,將數據頁讀入記憶體,然後執行 change buffer 中與這個頁有關的操作。將 change buffer 中的操作應用到原數據頁,得到最新結果的過程稱為 merge。除了訪 問這個數據頁會觸發 merge 外,系統有後臺線程會定期 merge。在資料庫正常關閉 (shutdown)的過程中,也會執行 merge 操作。
  • 雖然名字叫作 change buffer,實際上它是可以持久化的數據。也就是 說,change buffer 在記憶體中有拷貝,也會被寫入到磁碟上。
  • 顯然,如果能夠將更新操作先記錄在 change buffer,減少讀磁碟,語句的執行速度會得 到明顯的提升。而且,數據讀入記憶體是需要占用 buffer pool 的,所以這種方式還能夠避 免占用記憶體,提高記憶體利用率。

使用change buffer

  • 只有普通索引才能使用change buffer,唯一索引不能使用

  • change buffer 用的是 buffer pool 里的記憶體,因此不能無限增大。

  • change buffer 的大 小,可以通過參數 innodb_change_buffer_max_size 來動態設置。這個參數設置為 50 的時候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。

更新流程

  • 第一種情況是,這個記錄要更新的目標頁在記憶體中
    • 對於唯一索引來說,找到 3 和 5 之間的位置,判斷到沒有衝突,插入這個值,語句執行結束;
    • 對於普通索引來說,找到 3 和 5 之間的位置,插入這個值,語句執行結束。
    • 普通索引和唯一索引對更新語句性能影響的差別,只是一個判斷,只會耗費微 小的 CPU 時間。
  • 第二種情況是,這個記錄要更新的目標頁不在記憶體中
    • 對於唯一索引來說,需要將數據頁讀入記憶體,判斷到沒有衝突,插入這個值,語句執行結束;
    • 對於普通索引來說,則是將更新記錄在 change buffer,語句執行就結束了。
    • 將數據從磁碟讀入記憶體涉及隨機 IO 的訪問,是資料庫裡面成本最高的操作之一。change buffer 因為減少了隨機磁碟訪問,所以對更新性能的提升是會很明顯的。

change buffer使用場景

  • 對於寫多讀少的業務來說,頁面在寫完以後馬上被訪問到的概率比較小,此時 change buffer 的使用效果最好。這種業務模型常見的就是賬單類、日誌類的系統。
  • 反過來,假設一個業務的更新模式是寫入之後馬上會做查詢,那麼即使滿足了條件,將更 新先記錄在 change buffer,但之後由於馬上要訪問這個數據頁,會立即觸發 merge 過 程。這樣隨機訪問 IO 的次數不會減少,反而增加了 change buffer 的維護代價。所以, 對於這種業務模式來說,change buffer 反而起到了副作用。
  • 如果所有的更新後面,都馬上伴隨著對這個記錄的查詢,那麼你應該關閉 change buffer。
  • 在使用機械硬碟時,儘量使用普通索引,然後把 change buffer 儘量開大,以確保這個“歷 史數據”表的數據寫入速度。

change buffer和 redo log

插入過程

  • 假設: 當前 k 索引樹的狀態,查找到位置後,k1 所在的數據頁在記憶體 (InnoDB buffer pool) 中,k2 所在的數據頁不在記憶體中

  • 在上面表的前提下執行下麵語句: mysql> insert into t(id,k) values(id1,k1),(id2,k2);

  • 分析這條更新語句,你會發現它涉及了四個部分:記憶體、redo log(ib_log_fileX)、 數據表空間(t.ibd)、系統表空間(ibdata1)。

  • 這條更新語句做瞭如下的操作(按照圖中的數字順序):

    • Page 1 在記憶體中,直接更新記憶體;
    1. Page 2 沒有在記憶體中,就在記憶體的 change buffer 區域,記錄下“我要往 Page 2 插
      入一行”這個信息
    2. 將上述兩個動作記入 redo log 中(圖中 3 和 4)。
  • 做完上面這些,事務就可以完成了。

  • 所以,你會看到,執行這條更新語句的成本很低,就是寫了兩處記憶體,然後寫了一處磁碟(兩次操作合在一起寫了一次磁碟,redo log),而且還是順序寫的。

  • 同時,圖中的兩個虛線箭頭,是後臺操作,不影響更新的響應時間。

查詢過程

  • 如果讀語句發生在更新語句後不久,記憶體中的數據都還在,那麼此時的這兩個讀操作就與 系統表空間(ibdata1)和 redo log(ib_log_fileX)無關了。所以,我在圖中就沒畫出 這兩部分。

  • 要執行 select * from t where k in (k1, k2)

  • 讀 Page 1 的時候,直接從記憶體返回。有幾位同學在前面文章的評論中問到,WAL 之後 如果讀數據,是不是一定要讀盤,是不是一定要從 redo log 裡面把數據更新以後才可以返回?其實是不用的。你可以看一下圖 3 的這個狀態,雖然磁碟上還是之前的數據, 但是這裡直接從記憶體返回結果,結果是正確的。

  • 要讀 Page 2 的時候,需要把 Page 2 從磁碟讀入記憶體中,然後應用 change buffer 里 面的操作日誌,生成一個正確的版本並返回結果。

  • 可以看到,直到需要讀 Page 2 的時候,這個數據頁才會被讀入記憶體。

  • redo log 主要節省的是隨機寫磁碟的 IO 消耗(轉成順序寫),而 change buffer 主要節省的則是隨機讀磁碟 的 IO 消耗。

    七.給字元串加索引

字元串欄位增加索引的方式

  1. 直接創建完整索引,這樣可能比較占用空間;
  2. 創建首碼索引,節省空間,但會增加查詢掃描次數,並且不能使用覆蓋索引;
  3. 倒序存儲,再創建首碼索引,用於繞過字元串本身首碼的區分度不夠的問題,不能使用覆蓋索引,不支持範圍掃描;
  4. 創建 hash 欄位索引,查詢性能穩定,有額外的存儲和計算消耗,跟第三種方式一樣,不能使用覆蓋索引,都不支持範圍掃描。

第一二種分析

完整索引和首碼索引的分析

你現在維護一個支持郵箱登錄的系統,用戶表是這麼定義的:

mysql> create table SUser(
 ID bigint unsigned primary key, 
 email varchar(64),
 ...
 )engine=innodb;

由於要使用郵箱登錄,所以業務代碼中一定會出現類似於這樣的語句:

mysql> select f1, f2 from SUser where email='xxx';

分別創建兩種索引

mysql> alter table SUser add index index1(email); 
或
mysql> alter table SUser add index index2(email(6));

第一個語句創建的 index1 索引裡面,包含了每個記錄的整個字元串;
而第二個語句創建 的 index2 索引裡面,對於每個記錄都是只取前 6 個位元組。
占用的空間會更小,這就是使用首碼索引的優勢

加入執行下麵的sql語句,兩種索引該如何執行

select id,name,email from SUser where email='[email protected]';

完整索引

  • 從 index1 索引樹找到滿足索引值是’[email protected]’的這條記錄,取得 ID2 的值;
  • 到主鍵上查到主鍵值是 ID2 的行,判斷 email 的值是正確的,將這行記錄加入結果集; (為什麼還要判斷email的正確性,這個是server 層的行為,以防email欄位返回的值不對, 其實反正肯定要讀入數據,順手判斷一下, 這個成本也並不大就是了)
  • 取 index1 索引樹上剛剛查到的位置的下一條記錄,發現已經不滿足 email='[email protected]’的條件了,迴圈結束。
  • 這個過程中,只需要回主鍵索引取一次數據,所以系統認為只掃描了一行。

首碼索引

  • 從 index2 索引樹找到滿足索引值是’zhangs’的記錄,找到的第一個是 ID1;
  • 到主鍵上查到主鍵值是 ID1 的行,判斷出 email 的值不 是’[email protected]’,這行記錄丟棄;
  • 取 index2 上剛剛查到的位置的下一條記錄,發現仍然是’zhangs’,取出 ID2,再到 ID 索引上取整行然後判斷,這次值對了,將這行記錄加入結果集;
  • 重覆上一步,直到在 idxe2 上取到的值不是’zhangs’時,迴圈結束。
  • 在這個過程中,要回主鍵索引取 4 次數據,也就是掃描了 4 行。

對比結果

  • 通過這個對比,你很容易就可以發現,使用首碼索引後,可能會導致查詢語句讀數據的次數變多。
  • 但是,對於這個查詢語句來說,如果你定義的 index2 不是 email(6) 而是 email(7),也 就是說取 email 欄位的前 7 個位元組來構建索引的話,即滿足首碼’zhangss’的記錄只有 一個,也能夠直接查到 ID2,只掃描一行就結束了。
  • 也就是說使用首碼索引,定義好長度,就可以做到既節省空間,又不用額外增加太多的查 詢成本。

使用首碼索引,如何確定應該使用多長的首碼

  • 我們在建立索引時關註的是區分度,區分度越高越好。因為區分度越高,意味著重覆的鍵值越少。

  • 依次選取不同長度的首碼來看這個值,比如我們要看一下 4~7 個位元組的首碼索引, 可以用這個語句:

    mysql> select
      count(distinct left(email,4))as L4, 
      count(distinct left(email,5))as L5, 
      count(distinct left(email,6))as L6, 
      count(distinct left(email,7))as L7,
    from SUser;
  • 使用首碼索引很可能會損失區分度,所以你需要預先設定一個可以接受的損失比 例,比如 5%。然後,在返回的 L4~L7 中,找出不小於 L * 95% 的值,假設這裡 L6、L7 都滿足,你就可以選擇首碼長度為 6。

首碼索引對覆蓋索引的影響

select id,email from SUser where email='[email protected]';

如果使用 index1(即 email 整個字元串的索引結構)的話,可以利用覆蓋索引, 從 index1 查到結果後直接就返回了,不需要回到 ID 索引再去查一次。而如果使用 index2(即 email(6) 索引結構)的話,就不得不回到 ID 索引再去判斷 email 欄位的值。
即使你將 index2 的定義修改為 email(18) 的首碼索引,這時候雖然 index2 已經包含了 所有的信息,但 InnoDB 還是要回到 id 索引再查一下,因為系統並不確定首碼索引的定義 是否截斷了完整信息。

結論: 首碼索引無法使用覆蓋索引

其他方式使用首碼索引

比如,我們國家的身份證號,一共 18 位,其中前 6 位是地址碼,所以同一個縣的人的身 份證號前 6 位一般會是相同的。

假設你維護的資料庫是一個市的公民信息系統,這時候如果對身份證號做長度為 6 的首碼 索引的話,這個索引的區分度就非常低了。

方法三: 使用倒序存儲

  • 如果你存儲身份證號的時候把它倒過來存,每次查詢的時 候,你可以這麼寫:

    mysql> select field_list from t where id_card = reverse('input_id_card_string');
  • 由於身份證號的最後 6 位沒有地址碼這樣的重覆邏輯,所以最後這 6 位很可能就提供了足 夠的區分度。當然了,實踐中你不要忘記使用 count(distinct) 方法去做個驗證。

方法四: 使用 hash 欄位

  • 可以在表上再創建一個整數欄位,來保存身份證的校驗 碼,同時在這個欄位上創建索引。

    mysql> alter table t add id_card_crc int unsigned, add index(id_card_crc);
  • 然後每次插入新記錄的時候,都同時用 crc32() 這個函數得到校驗碼填到這個新欄位。

  • 由於校驗碼可能存在衝突,也就是說兩個不同的身份證號通過 crc32() 函數得到的結果可能是相同的,所以你的查詢語句 where 部分要判斷 id_card 的值是否精確相同。

    mysql> select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='****'
  • 這樣,索引的長度變成了 4 個位元組,比原來小了很多。

第三種和第四種的異同點

相同點

  • 都不支持範圍查詢。倒序存儲的欄位上創建的索引是按照倒序字 符串的方式排序的,已經沒有辦法利用索引方式查出身份證號碼在 [ID_X, ID_Y] 的所有市 民了。同樣地,hash 欄位的方式也只能支持等值查詢。

不同點

  • 從占用的額外空間來看,倒序存儲方式在主鍵索引上,不會消耗額外的存儲空間,而 hash 欄位方法需要增加一個欄位。當然,倒序存儲方式使用 4 個位元組的首碼長度應該 是不夠的,如果再長一點,這個消耗跟額外這個 hash 欄位也差不多抵消了。

  • 在 CPU 消耗方面,倒序方式每次寫和讀的時候,都需要額外調用一次 reverse 函數, 而 hash 欄位的方式需要額外調用一次 crc32() 函數。如果只從這兩個函數的計算複雜 度來看的話,reverse 函數額外消耗的 CPU 資源會更小些。

  • 從查詢效率上看,使用 hash 欄位方式的查詢性能相對更穩定一些。因為 crc32 算出來 的值雖然有衝突的概率,但是概率非常小,可以認為每次查詢的平均掃描行數接近 1。 而倒序存儲方式畢竟還是用的首碼索引的方式,也就是說還是會增加掃描行數。

八.mysql選錯索引原因及處理方法

現象

應該使用某個索引的時候,但是卻使用了別的索引或者沒有使用索引

優化器選擇索引邏輯

  • 掃描的行數
  • 否使用臨時表
  • 是否排序

索引選擇異常和處理

  • 一種方法是,採用 force index 強行選擇一個索引。

    • # a是索引
      select * from t force index(a) where a between 10000 and 20000;
  • 第二種方法就是,我們可以考慮 修改語句,引導 MySQL 使用我們期望的索引。

  • 第三種方法是,在有些場景下,我們可以新建一個更合適的索引,來提供給優化器做選擇,或刪掉誤用的索引。

九.mysql對索引欄位進行函數操作導致不走索引搜索樹功能

1.條件欄位做函數操作

①.現象

假設你現在維護了一個交易系統,其中交易記錄表 tradelog 包含交易流水號 (tradeid)、交易員 id(operator)、交易時間(t_modified)等欄位。為了便於描 述,我們先忽略其他欄位。這個表的建表語句如下:

CREATE TABLE tradelog (

id int(11) NOT NULL,

tradeid varchar(32) DEFAULT NULL,

operator int(11) DEFAULT NULL,

t_modified datetime DEFAULT NULL,

PRIMARY KEY (id),

KEY tradeid (tradeid),
KEY t_modified (t_modified)

)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

假設,現在已經記錄了從 2016 年初到 2018 年底的所有數據,運營部門有一個需求是, 要統計發生在所有年份中 7 月份的交易記錄總數。這個邏輯看上去並不複雜,你的 SQL 語句可能會這麼寫:

mysql> select count(*) from tradelog where month(t_modified)=7;

由於 t_modified 欄位上有索引,於是你就很放心地在生產庫中執行了這條語句,但卻發現執行了特別久,才返回了結果。

如果你問 DBA 同事為什麼會出現這樣的情況,他大概會告訴你:如果對欄位做了函數計 算,就用不上索引了,這是 MySQL 的規定。

②.原因

現在你已經學過了 InnoDB 的索引結構了,可以再追問一句為什麼?為什麼條件是 where t_modified='2018-7-1’的時候可以用上索引,而改成 where month(t_modified)=7 的時候就不行了?

下麵是這個 t_modified 索引的示意圖。方框上面的數字就是 month() 函數對應的值。

如果你的 SQL 語句條件用的是 where t_modified='2018-7-1’的話,引擎就會按照上面 綠色箭頭的路線,快速定位到 t_modified='2018-7-1’需要的結果。

實際上,B+ 樹提供的這個快速定位能力,來源於同一層兄弟節點的有序性。

但是,如果計算 month() 函數的話,你會看到傳入 7 的時候,在樹的第一層就不知道該怎 麽辦了。

也就是說,對索引欄位做函數操作,可能會破壞索引值的有序性,因此優化器就決定放棄 走樹搜索功能。

需要註意的是,優化器並不是要放棄使用這個索引。

在這個例子里,放棄了樹搜索功能,優化器可以選擇遍歷主鍵索引,也可以選擇遍歷索引 t_modified,優化器對比索引大小後發現,索引 t_modified 更小,遍歷這個索引比遍歷 主鍵索引來得更快。因此最終還是會選擇索引 t_modified。

接下來,我們使用 explain 命令,查看一下這條 SQL 語句的執行結果。

key="t_modified"表示的是,使用了 t_modified 這個索引;我在測試表數據中插入了 10 萬行數據,rows=100335,說明這條語句掃描了整個索引的所有值;Extra 欄位的 Using index,表示的是使用了覆蓋索引。

③.解決方法

由於在 t_modified 欄位加了 month() 函數操作,導致了全索引掃描。為了能 夠用上索引的快速定位能力,我們就要把 SQL 語句改成基於欄位本身的範圍查詢。按照下 面這個寫法,優化器就能按照我們預期的,用上 t_modified 索引的快速定位能力了。

select count(*) from tradelog where

(t_modified >= '2016-7-1' and t_modified<'2016-8-1') or

(t_modified >= '2017-7-1' and t_modified<'2017-8-1') or

(t_modified >= '2018-7-1' and t_modified<'2018-8-1');

當然,如果你的系統上線時間更早,或者後面又插入了之後年份的數據的話,你就需要再把其他年份補齊。

到這裡我給你說明瞭,由於加了 month() 函數操作,MySQL 無法再使用索引快速定位功 能,而只能使用全索引掃描。

不過優化器在個問題上確實有“偷懶”行為,即使是對於不改變有序性的函數,也不會考慮使用索引。比如,對於 select * from tradelog where id + 1 = 10000 這個 SQL 語 句,這個加 1 操作並不會改變有序性,但是 MySQL 優化器還是不能用 id 索引快速定位 到 9999 這一行。所以,需要你在寫 SQL 語句的時候,手動改寫成 where id = 10000 -1 才可以。

2.隱式類型轉換

①.隱式類型轉換規則

我們一起看一下這條 SQL 語句:

mysql> select * from tradelog where tradeid=110717;

交易編號 tradeid 這個欄位上,本來就有索引,但是 explain 的結果卻顯示,這條語句需 要走全表掃描。你可能也發現了,tradeid 的欄位類型是 varchar(32),而輸入的參數卻是 整型,所以需要做類型轉換。

那麼,現在這裡就有兩個問題:

  • 數據類型轉換的規則是什麼?
  • 為什麼有數據類型轉換,就需要走全索引掃描?

先來看第一個問題,你可能會說,資料庫裡面類型這麼多,這種數據類型轉換規則更多,
我記不住,應該怎麼辦呢?

這裡有一個簡單的方法,看 select “10” > 9 的結果:

  1. 如果規則是“將字元串轉成數字”,那麼就是做數字比較,結果應該是 1;
  2. 如果規則是“將數字轉成字元串”,那麼就是做字元串比較,結果應該是 0。

驗證結果如圖 3 所示。

從圖中可知,select “10” > 9 返回的是 1,所以你就能確認 MySQL 里的轉換規則了: 在 MySQL 中,字元串和數字做比較的話,是將字元串轉換成數字。

②.原因

這時,你再看這個全表掃描的語句:

mysql> select * from tradelog where tradeid=110717;

就知道對於優化器來說,這個語句相當於:

mysql> select * from tradelog where CAST(tradid AS signed int) = 110717;

也就是說,這條語句觸發了我們上面說到的規則:對索引欄位做函數操作,優化器會放棄走樹搜索功能。

3.隱式字元編碼轉換

①.現象

假設系統里還有另外一個表 trade_detail,用於記錄交易的操作細節。為了便於量化分析和復現,我往交易日誌表 tradelog 和交易詳情表 trade_detail 這兩個表裡插入一些數 據。

mysql> CREATE TABLE trade_detail (

id int(11) NOT NULL,

tradeid varchar(32) DEFAULT NULL,

trade step int(11) DEFAULT NULL, /* 操作步驟 */

step_info varchar(32) DEFAULT NULL, /* 步驟信息 */

PRIMARY KEY (id),

KEY tradeid (tradeid)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into tradelog values(1, 'aaaaaaaa', 1000, now());

insert into tradelog values(2, 'aaaaaaab', 1000, now());

insert into tradelog values(3, 'aaaaaaac', 1000, now());

insert into trade_detail values(1, 'aaaaaaaa', 1, 'add');

insert into trade_detail values(2, 'aaaaaaaa', 2, 'update');
insert into trade_detail values(3, 'aaaaaaaa', 3, 'commit');
insert into trade_detail values(4, 'aaaaaaab', 1, 'add');
insert into trade_detail values(5, 'aaaaaaab', 2, 'update');
insert into trade_detail values(6, 'aaaaaaab', 3, 'update again');
insert into trade_detail values(7, 'aaaaaaab', 4, 'commit');
insert into trade_detail values(8, 'aaaaaaac', 1, 'add');
insert into trade_detail values(9, 'aaaaaaac', 2, 'update');
insert into trade_detail values(10, 'aaaaaaac', 3, 'update again');
insert into trade_detail values(11, 'aaaaaaac', 4, 'commit');

這時候,如果要查詢 id=2 的交易的所有操作步驟信息,SQL 語句可以這麼寫:

mysql> select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2;

我們一起來看下這個結果:

  • 第一行顯示優化器會先在交易記錄表 tradelog 上查到 id=2 的行,這個步驟用上了主 鍵索引,rows=1 表示只掃描一行;
  • 第二行 key=NULL,表示沒有用上交易詳情表 trade_detail 上的 tradeid 索引,進行 了全表掃描。

在這個執行計劃里,是從 tradelog 表中取 tradeid 欄位,再去 trade_detail 表裡查詢匹 配欄位。因此,我們把 tradelog 稱為驅動表,把 trade_detail 稱為被驅動表,把 tradeid 稱為關聯欄位。

接下來,我們看下這個 explain 結果表示的執行流程:

圖中得執行流程:

  • 第 1 步,是根據 id 在 tradelog 表裡找到 L2 這一行;
  • 第 2 步,是從 L2 中取出 tradeid 欄位的值;
  • 第 3 步,是根據 tradeid 值到 trade_detail 表中查找條件匹配的行。explain 的結果里 面第二行的 key=NULL 表示的就是,這個過程是通過遍歷主鍵索引的方式,一個一個 地判斷 tradeid 的值是否匹配。

進行到這裡,你會發現第 3 步不符合我們的預期。因為表 trade_detail 里 tradeid 欄位上 是有索引的,我們本來是希望通過使用 tradeid 索引能夠快速定位到等值的行。但,這裡 並沒有。

②.原因

如果你去問 DBA 同學,他們可能會告訴你,因為這兩個表的字元集不同,一個是 utf8, 一個是 utf8mb4,所以做表連接查詢的時候用不上關聯欄位的索引。這個回答,也是通常 你搜索這個問題時會得到的答案。

但是你應該再追問一下,為什麼字元集不同就用不上索引呢? 我們說問題是出在執行步驟的第 3 步,如果單獨把這一步改成 SQL 語句的話,那就是:

mysql> select * from trade_detail where tradeid=$L2.tradeid.value;

其中,$L2.tradeid.value 的字元集是 utf8mb4。

參照前面的兩個例子,你肯定就想到了,字元集 utf8mb4 是 utf8 的超集,所以當這兩個 類型的字元串在做比較的時候,MySQL 內部的操作是,先把 utf8 字元串轉成 utf8mb4 字元集,再做比較。

這個設定很好理解,utf8mb4 是 utf8 的超集。類似地,在程式設計語言里 面,做自動類型轉換的時候,為了避免數據在轉換過程中由於截斷導致數據 錯誤,也都是“按數據長度增加的方向”進行轉換的。

因此, 在執行上面這個語句的時候,需要將被驅動數據表裡的欄位一個個地轉換成 utf8mb4,再跟 L2 做比較。

也就是說,實際上這個語句等同於下麵這個寫法:

select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value;

CONVERT() 函數,在這裡的意思是把輸入的字元串轉成 utf8mb4 字元集。

這就再次觸發了我們上面說到的原則:對索引欄位做函數操作,優化器會放棄走樹搜索功能。到這裡,你終於明確了,字元集不同只是條件之一,連接過程中要求在被驅動表的索引欄位上加函數操作,是直接導致對被驅動表做全表掃描的原因。

作為對比驗證,我給你提另外一個需求,“查找 trade_detail 表裡 id=4 的操作,對應的 操作者是誰”,再來看下這個語句和它的執行計劃。

mysql>select l.operator from tradelog l , trade_detail d where d.tradeid=l.tradeid and d.id=4;

這個語句里 trade_detail 表成了驅動表,但是 explain 結果的第二行顯示,這次的查詢操 作用上了被驅動表 tradelog 里的索引 (tradeid),掃描行數是 1。

這也是兩個 tradeid 欄位的 join 操作,為什麼這次能用上被驅動表的 tradeid 索引呢?我 們來分析一下。

假設驅動表 trade_detail 里 id=4 的行記為 R4,那麼在連接的時候(圖 5 的第 3 步), 被驅動表 tradelog 上執行的就是類似這樣的 SQL 語句:

select operator from tradelog where traideid =$R4.tradeid.value;

這時候 $R4.tradeid.value 的字元集是 utf8, 按照字元集轉換規則,要轉成 utf8mb4,所 以這個過程就被改寫成:

select operator from tradelog where traideid =CONVERT($R4.tradeid.value USING utf8mb4);

你看,這裡的 CONVERT 函數是加在輸入參數上的,這樣就可以用上被驅動表的 traideid 索引。

③.解決方法

優化語句的方法:

select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2;

  • 比較常見的優化方法是,把 trade_detail 表上的 tradeid 欄位的字元集也改成 utf8mb4,這樣就沒有字元集轉換的問題了。

    • alter table trade_detail modify tradeid varchar(32) CHARACTER SET utf8mb4 default null;

  • 修改 SQL 語句的方法

    mysql> select d.* from tradelog l , trade_detail d where d.tradeid=CONVERT(l.tradeid USING utf8) and d.id=2;

    我主動把 l.tradeid 轉成 utf8,就避免了被驅動表上的字元編碼轉換,從 explain

    結果可以看到,這次索引走對了。

站在巨人的肩膀上摘蘋果:

https://time.geekbang.org/column/intro/100020801
https://www.cnblogs.com/lonelyxmas/p/10668426.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我需要搭建一個k8s的環境,使用VMware Workstation在網路配置上,比VisualBox好用很多;尤其是想用Xshell訪問自己的虛機。 上一次,成功搭建了Xshell訪問CentOS 6,VMware Workstation都使用預設配置,打開網路配置進行編輯: cd /etc/sy ...
  • tar 解包:tar zxvf FileName.tar 打包:tar czvf FileName.tar DirName gz 解壓1:gunzip FileName.gz 解壓2:gzip d FileName.gz 壓縮:gzip FileName .tar.gz和tgz 解壓:tar zxv ...
  • 本文主要介紹了 tcpdump 的基本語法和使用方法,並通過一些示例來展示它強大的過濾功能。 ...
  • Linux系統一般有4個主要部分: 內核、shell、文件系統和應用程式。內核、shell和文件系統一起形成了基本的操作系統結構,它們使得用戶可以運行程式、管理文件並使用系統。部分層次結構如圖1-1所示。 1. linux內核 Linux內核是世界上最大的開源項目之一,內核是與電腦硬體介面的易替換 ...
  • 一、伺服器端搭建: 1. 安裝 curl -s https://install.zerotier.com/ | sudo bash 2.給文件夾授權以及生成moon配置文件 sudo chmod 777 /var/lib/zerotier-onecd /var/lib/zerotier-onesud ...
  • 加速電腦開機關機速度 首先要感謝快手平臺,讓我能得到更多有用的知識^v^ 好了,下麵開始吧: 方法一 1. 按下Win + R鍵,彈出運行視窗,輸入msconfig,點擊確定。 2. 選擇>>選擇性啟動,取消勾選>>載入啟動項。 3. 點擊引導,選擇>>無GUI引導,點擊確定。 4. 點擊>>重新啟 ...
  • 先看拓撲圖:Ambassador 主要用來解決跨伺服器通訊,因為同伺服器通訊相對簡單,直接用--link參數,或者用bridge網路即可。 伺服器B的Docker2:centos想要訪問伺服器A的Docker1:nginx,但是不知道對方的埠和ip,這時就需要通過Ambassador來解決。(Am ...
  • 由於nginx功能強大,性能突出,越來越多的web應用採用nginx作為http和反向代理的web伺服器。而nginx的訪問日誌不管是做用戶行為分析還是安全分析都是非常重要的數據源之一。如何有效便捷的採集nginx的日誌進行有效的分析成為大家關註的問題。本文通過幾個實例來介紹如何通過filebeat... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...