高性能索引優化策略(三):索引列的次序該如何排列更合適?

在衆多困擾索引使用的原因中,其中最常見的一個是索引中列的次序。正確的次序依賴於使用索引的查詢,因此需要考慮怎樣選擇索引次序以便數據行的排序火分組能夠從中受益(這個僅在二叉樹索引有用,哈希索引和其他類型的索引並沒有像二叉樹索引那樣對數據進行排序)。

在二叉樹索引中多列的順序意味着會首先對最左列進行排序,然後纔是其他列。因此,爲滿足ORDER BY,GROUP BY和DISTINCT的條件的查詢,索引可能會按正向或逆向掃描進行排序。

結果就是,索引列的次序在多列索引中極其重要。這個次序有可能強化或弱化性能。接下來會通過很多例子說明這種情況。有一個古老的值得推薦的原則:將最具篩選性的列放在索引的第一位。這個建議多有用?在某些例子中是有用的,但是與避免隨機I/O和排序相比,就沒有那麼重要了(有很多特殊的例子,因此沒有一個普適性的原則。這裏只是告訴你這個原則未必有你想的那麼重要)。

在沒有排序和分組的時候,將最具篩選性的列放在第一位會是一個好主意,因爲這時候索引僅僅是優化WHERE條件的查詢。在這類場景下,這樣的索引確實能夠足夠快地篩選出想要的數據。然而,這不僅僅依賴於列的篩選性,還同樣依賴於查找數據行的值——值的離散性。這和我們選擇一個好的前綴索引長度是類似的。你可能會需要選擇一個合適的索引列次序去儘可能地滿足最頻繁查詢的篩選性。

以下面的查詢爲例:

SELECT * FROM payment WHERE staff_id = 2 AND customer_id = 584;

你應該在(staff_id, customer_id)創建一個索引或者是以相反的次序創建索引嗎?我們可以運行一些查詢去檢查數據表數據的離散性來決定哪個次序更具備篩選性。讓我們將查詢轉換一下,去統計候選項的數量:

SELECT SUM(staff_id = 2), SUM(customer_id = 584) FROM payment;
--------------------------------------------------------------
SUM(staff_id = 2):7992
SUM(customer_id = 584): 30

根據首要原則來看,我們應該將customer_id放在第一位,因爲這個條件匹配的數據行更少。然後我們再來看看指定了customer_id後的staff_id的篩選性怎麼樣:

SELECT SUM(staff_id = 2) FROM payment WHERE customer_id=584;
--------------------------------------------------------------
SUM(staff_id = 2):17

請慎用這項技巧,因爲這個結果是依賴於特定的常量的。如果你對這個查詢這樣優化你的索引可能其他的查詢並不會表現得很好。服務器的性能可能全部受影響或者部分查詢並不像我們預期那樣運行。

如果你使用像pt-query-digest的工具來分析最壞的情況,這個可能是一個有效的途徑去看看什麼索引是最適合你的查詢和數據的。但是,如果你沒有特殊的樣例去運行,也許使用那個古老的首要原則會更好 —— 這是根據整體情況來選擇的,而不是單個查詢。

SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity, 
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(*)
FROM payment;
--------------------------------------------------------------
staff_id_selectivity: 0.0001
customer_id_selectivity: 0.0373
COUNT(*): 16049

customer_id具有更高的篩選性,因此答案是將這列放在第一位:

ALTER TABLE payment ADD KEY(customer_id, staff_id);

對於前綴索引,與正常的基數相比,特殊值的問題會更多。例如,我們會發現有些應用將未登錄的用戶當作遊客——在session表中會有一個特殊的user ID,而其他地方記錄着這個用戶的活動。包括這樣的user ID的查詢與其他查詢相比可能差別很大。這是因爲通常會有很多未登錄的session記錄。我們會發現系統賬戶會導致同樣的問題。一個應用有一個魔法的管理員賬戶,這並不是真正的用戶,而是整個網站的每一個用戶的“好友”——以便這個賬戶可以發送狀態通知和其他消息。這個用戶有龐大的好友列表,結果會導致網站的性能問題。

這在現實中非常典型。任何系統無關的用戶,即便它不是應用管理的一個錯誤決定,也會導致問題。那些真正擁有許多好友、照片、狀態消息和點讚的用戶,可能會面臨假用戶混在一起的麻煩。

以下是我們曾經見到過的一個真實案例。一個供用戶交流產品體驗和經驗的產品論壇,在一些特殊的場景中運行十分緩慢。

SELECT COUNT(DISTINCT threadId) AS COUNT_VALUE
FROM Message
WHERE (groupId = 10137) AND (userId = 1288826) AND (anonyous = 0)
ORDERBY priority DESC, modifiedDate DESC;

這個查詢看似是索引沒有用好,因此客戶讓我們檢查看看能不能優化,EXPLAIN的結果如下:

id: 1
select_type: SIMPLE
table:Message
key: ix_groupId_userId
key_len: 18
ref: const, const
rows: 1251162
Extra: Using where

MySQL選擇的索引是(groupId, userId),在沒有數據列的基數信息時,這看起來是十分正確的選擇。然而,當我們檢查有多少行數據匹配那個user ID和group Id時,出現了不同的情況:

SELECT COUNT(*), SUM(groupId = 10137),
SUM(userId = 1288826), SUM(anonymous = 0)
FROM Message;
-----------------------------------------------------
count(*): 4142217
sum(groupId = 10137): 4092654
sum(userId = 1288826):1288496
sum(anonymous = 0): 4141934

這樣的結果顯示這個分組基本上覆蓋了整個數據表。這個用戶有130萬行相關的數據。這個案例裏,索引根本沒法解決這樣的問題。這是因爲數據是從別的應用遷移過來的,而所有的消息都被賦予了管理員用戶,並在在導入過程中分進了一個組。這個問題的解決方案是修改應用的代碼去識別特殊的用戶和分組,而不是對這個用戶的查詢問題。

這個小故事要告訴大家的是首要原則是有用的,但是需要小心的是,平均性能並不能代表特殊案例的性能,而特殊案例可能拖垮整個應用的性能。

最後,雖然關於選擇性和基數的首要原則是十分有吸引力的,但其他因素,例如排序,分組和範圍條件在WHERE條件中也可能會對查詢的性能影響很大。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章