聊聊數據庫中的爛索引

背景

索引是數據庫中用於加速查詢的常用組件,它通過對數據冗餘和重組織來加速SQL查詢。通常來說,恰當的索引可以提升系統的查詢性能。 關於索引存在一些誤解,如:索引總是能提升查詢性能,因此索引越多越好,比如下圖中的例子

只看收益,不看代價是不行的。分佈式數據庫系統一般支持兩類索引:由分佈式全局事務維護的全局索引、由本地事務維護的本地索引。這兩類索引都會不同程度影響系統的寫入性能,下圖展示了建立不同數量的索引時,對系統的寫入性能的影響。

可以看出創建1個全局索引,就會使系統的寫入性能降低至原來的約30%;單看MySQL,在創建8個索引(本地索引)的情況下,寫入性能會降低至原來的85%(引用自我們的歷史文章TiDB、OceanBase、PolarDB-X、CockroachDB二級索引寫入性能測評,感興趣的讀者可深入閱讀)。 因此,在我們享受索引帶來的查詢加速收益時,還需關注其引入的維護開銷。特別是當引入一個索引沒能帶來預期收益、或者帶來的開銷遠大於其帶來的查詢加速收益時,索引反而成爲一種負擔。我們稱這類索引爲爛索引,避開它們可以幫助數據庫獲得更好的寫入性能。 回顧文章開頭舉例的表 warehouse,你能看出其中有幾個爛索引嗎?我們先討論一下應用中常見的爛索引,然後在文末公佈答案。

低頻訪問索引和許久未訪問索引

新建的索引並未按照預期目的被數據庫優化器使用時,就是一個爛索引,它隱藏在數據庫中,消耗着寫入性能,卻並未帶來查詢性能增益,及時發現這類索引並進行清理是十分必要的。此外,還有一些索引在一段時間內被高頻使用,但隨着業務的變動,這些索引不再被使用,但卻一直被遺留下來,這也是爛索引。 對於上述情況,PolarDB-X提供了INFORMATION_SCHEMA.GLOBAL_INDEXES視圖,用於查詢表中全局索引被使用的情況,有了它,哪些全局索引在“磨洋工”,哪些全局索引“出工又出力”,一目瞭然。

低選擇性索引

索引的選擇性是指不重複的索引值的個數(也常被稱爲基數)和數據表的記錄總數(#T)的比值,可由定義知道它的取值範圍在 1/#T 到 1 之間。索引的選擇性越高則查詢效率越高,因爲選擇性高的索引可以幫助數據庫在查找時過濾掉更多無效的行。一個正面例子是主鍵索引,由於主鍵是不重複的,因此其選擇性爲最大值1,數據庫利用主鍵查找數據時效率很高。一個反面的例子是,在性別、isDelete等屬性上建索引。 如何發現這些低選擇性的索引呢?最直接的辦法是人工檢查每個索引的真實含義,排除掉“性別”“Delete標誌”之類含義的索引。此外對於全局索引,PolarDB-X支持用INFORMATION_SCHEMA.GLOBAL_INDEXES視圖查看全局索引的基數和記錄總數,我們可以根據這兩個指標算出索引的選擇性。

重複索引

重複索引是指在相同的列上按照相同的順序創建了同類型的索引,Polardb-X不會禁止用戶創建多個重複的索引。由於數據庫在寫入數據時,需要同步維護索引,因此多個重複的索引就需要數據庫分別維護,此外優化器在優化查詢語句時,也需要對這些重複索引逐個考慮,這會影響性能。 刻意引入重複索引的場景不常見,但不小心引入卻是可能的。如下面的SQL是PolarDB-X中的單表,

用戶可能想創建一個主鍵,然後爲其加上unique限制,然後再加上索引以供其查詢使用,實際上上述寫法會創建出3個相互重複的索引,其實並不需要這麼做。 一些索引從定義上來看是非重複索引,但從效果上來看,又是重複的。比如下面的建表語句,

一些用戶可能會將查詢SQL的where條件用到的列都建成索引,因此創建了索引 idx_id_name 。但是通常數據庫在構建索引的時候,都會在索引的value屬性中填入主鍵,以方便回表。因此索引 idx_name 的數據中是包含了主鍵 id 的,idx_name 和 idx_name_id效果相同。請避免構建這樣的索引。

冗餘索引

冗餘索引和重複索引有所不同,如果創建了索引 (A, B),再創建索引 (A),後者就成了冗餘索引。因爲(A) 是 (A, B)的前綴索引,優化器使用索引時存在“最左匹配原則”,即會優先使用索引中的左側列進行匹配,索引 (A, B) 是可以當做索引 (A) 來使用的。 冗餘索引經常發生在爲數據表添加新索引的時候,一些用戶更傾向於添加新索引,而不是在現有索引上進行擴展。我們應當優先考慮在已有的索引上做擴展,而非隨意添加新索引。如果確需添加新索引,也應當格外注意新引入的索引是否是一個冗餘索引,又或者新索引是否會讓舊有的索引變成冗餘索引。當然,一味地擴展現有索引也不可取,可能會導致索引長度過長,從而影響其他使用該索引的SQL,這是一個trade off。 除了考慮“最左匹配原則”,我們還需注意unique約束。在有unique約束的情況下,一些看起來冗餘的索引,實際上卻並不冗餘。

這裏索引 idx_id_name 是無法完全替代索引 idx_id 的,因爲索引 idx_id 除了方便按照id進行查找的作用外,還可以約束id不重複,而索引 idx_id_name 只能保證 (id, name) 不重複。

全局索引分區規則重複

像PolarDB-X這樣的 Shared-Nothing 架構的分佈式數據庫一般會引入“分區”的概念,用戶在建表時指定一個或若干個列爲分區鍵,數據會在數據庫內部按照分區鍵進行路由,從而將數據存儲至不同的DN節點。如果一個查詢語句的where條件中包含分區鍵,優化器就可以快速定位到一個具體分區並進行數據查找,但如果查詢語句的where條件不含分區鍵,該查詢就需要掃描全部分區,這有些類似於單機mysql的全表掃描,全分區掃描對於分佈式數據庫來說開銷很大。 在實際數據庫投入生產使用時,一個維度的分區往往不夠靈活,將查詢語句的where條件限制在必須包含“分區列”不夠自由。分佈式數據庫一般會支持全局索引,它冗餘了主表上的部分數據,並採用與主表不同的分區鍵,查詢時首先根據全局索引的分區鍵定位到一個分區,然後從分區中查到主表的分區鍵和主鍵,最後回表得到完整數據。 全局索引讓用戶的查詢語句不再受到“where條件必須包含主表分區列”的限制,且能避免全分區掃描的代價。從上文可知,用好全局索引的前提是設計良好的全局索引的分區方式,尤其是要避免全局索引和主表的分區方式重複,比如下面的表結構中,全局索引g_id和主表tb4的分區方式完全一致,g_id讓系統付出了寫入代價,卻沒有帶來查詢性能的增益。

全局索引分區大小不均勻

全局索引需要指定分區鍵,它的數據是按照分區規則存放於PolarDB-X的不同DN節點中的。設想,如果全局索引的分區規則設計的不夠好,就會導致分區不均,一些DN節點存儲大量數據,且承受大量的讀寫負載,而另一部分DN節點處於空閒狀態。這造成了資源浪費,且會使數據庫系統過早地到達性能瓶頸。 如下圖,假設有一個業務系統建立了 seller_order 賣家訂單信息表,該業務系統的特點是絕大部分訂單來自於少數幾個大賣家。我們只關注 seller_order 表上的全局索引 g_seller_id,它使用賣家的seller_id做分區鍵。我們假設有個大賣家的訂單量佔全部系統的一半,其在全局索引g_seller_id上的數據被路由到P5分區。可以看到P5分區會承受其它分區數倍的負載。

良好的全局索引應當保證數據儘可能均勻分佈在不同分區。

全局索引中的range分區

在PolarDB-X中使用range分區作爲全局索引的分區策略時應該額外注意,儘量避免將時間列作爲分區列。

如上建表語句所示,全局索引 g_tm 使用了 tm 作爲range分區的分區列,其默認值爲當前時間。這裏我們只考慮全局索引g_tm,其分區p5是一個catch-all 分區,在'2023-07-01 11:00:00'時間點以後,所有待插入的新數據都會被路由到p5分區(這是由新數據的tm列的值以及全局索引g_tm的路由規則決定的),因此p5分區會成爲數據寫入的瓶頸,p5分區所在DN上的數據量也將一直累積。 未來PolarDB-X將針對這一場景做出優化,但目前我們不推薦本例中的用法。

總結

我們先來回答一下文章開頭提出的問題。warehouse表中有4個爛索引,分別是:重複索引idx_id(與主鍵重複)、重複索引idx_id_order_name(和主鍵效果一致)、冗餘索引idx_order_id_order_name(索引idx_order_id_order_name_item_id可以代替它)、低選擇性索引idx_deleted_order_id。 本文總結了一些常見的爛索引及其低效的原因,定期檢查和清理這些爛索引,可以有效提升數據庫的寫入性能。可能有讀者會問,表太多、索引太多,沒精力挨個檢查怎麼辦? 沒關係,PolarDB-X最新推出inspect index功能,支持一鍵自動診斷爛索引,還能給出原因和整改建議,本文提到的爛索引都能識別。

作者:未啓

點擊立即免費試用雲產品 開啓雲上實踐之旅!

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載

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