通過對包含列的索引進行大量更新來節省性能
Points ------------------ PK QuestionId int (+30.000.000 distinct values) PK EventId int (large batches where 80.000 rows have the same EventId) Value smallint
該表大約有 4000 萬行,並且存在性能問題。
主要有兩個查詢:
在QuestionId上:
- 大約 3000 萬個不同
QuestionId
的值(變化很大)- 繁忙時間有很多查詢(每分鐘數千次)
在EventId上:
- 將有 +150.000 行的批量更新
where EventId=X
來設置Value=NULL
在非常忙碌的時刻。所以我首先想到的要獲得最佳性能是我製作
EventId,QuestionId
了ClusteredIndex,以便批量更新可以輕鬆找到所有靠近彼此的EventId並直接更新值。我的第二個想法是添加一個包含
QuestionId
和包含列Value
的索引,以便它可以直接從索引中讀取值(EventId
在這種情況下無關緊要)。但後來我想:聚集索引是否重要?由於索引中的包含列值也需要在批量更新期間進行更新。
- 雖然不犧牲查詢性能 - 是否可以快速(幾秒鐘)獲得批量更新,或者我是否必須接受這個過程在不升級硬體的情況下總是很慢。
- 任何其他想法設置 ClusteredIndex / 索引的最佳方法是什麼?
我知道理論上我應該測試一切並衡量它,但該網站是活躍的並且被大量使用。
我是一名獨立開發人員,我沒有資源聘請某人。任何估計的猜測和想法都會非常有幫助,因為這已經給了我正確的方向!
因此,如果您的主要訪問路徑有問題,那麼最有意義的唯一聚集索引將是
(QuestionId, EventId)
。添加第二個索引
EventId
可能沒有用,因為索引可能沒有足夠的選擇性,並且查詢引擎將決定讀取整個表而不是做大量工作來讀取它的大部分更快。或者,如果您始終完全或部分基於 查詢
EventId
,則 的聚集索引(EventId,Questionid)
更合適,並且具有使您的更新基於EventId
需要更少的 I/O 來完成的額外好處。我不會包含
Value
附加索引,因為這實際上會複製整個表(只是聚集在不同的列上),並且您的更新將需要更長的時間,因為Value
必須在聚集索引和附加索引之間保持同步。在某個時候沒有免費的午餐,正確的解決方案可能是選擇具有支持最多案例的前導列的聚集索引,然後添加 RAM/CPU/更快的儲存來處理整個表(或大它的塊)必須被讀取或寫入。有 4000 萬行和如此狹窄的表格,我無法想像這是更多 RAM無法解決的問題。
根據您的 SQL Server 版本,您還可以查看頁面壓縮是否會顯著減小表大小,因為這會減少對磁碟的讀/寫次數(額外的 CPU 成本被更少的磁碟操作所抵消)。在你的情況下,我的猜測是它不會,但它正在尋找。
因此,如果我理解正確(以目前的知識),您的直覺將是聚集索引
QuestionId,EventId
,然後是 批量更新的正常索引?EventId
僅當主要用途是返回特定
QuestionIds
而不考慮EventId
. 您可以嘗試 中的附加索引EventId
,但您可能會發現它並不經常(或根本不)用於更新(或更新仍然需要比您想要的更長的時間),具體取決於 EventIds 在您的數據中的分佈方式到QuestionId
.您還必須確定總體上對您更重要的是什麼 - 選擇性能或更新性能。如果更新是痛點,
(EventId,QuestionId)
無疑會是更好的選擇。鑑於 的唯一值的數量QuestionId
,在該列上添加索引可能對SELECT
性能有用,但這將取決於QuestionId
分佈方式以及您一次搜尋的數量。在任何一種情況下,保持最新統計數據都是至關重要的。
一個非常簡單的例子(為了完整起見):
假設我們有一個 DBMS,它維護一個聚集索引並每頁儲存 4 行。我們有一個主鍵為 的表
(QuestionId, EventId)
和一個附加列,Value
。如果我們將聚集索引創建為
(QuestionId, EventId)
,我們想像中的 DBMS 中的數據(粗略地說)儲存如下:Page | QuestionId | EventId | Value ----------------------------------- A | 1 | 2 | ... A | 1 | 3 | ... A | 1 | 6 | ... A | 1 | 7 | ... B | 1 | 8 | ... B | 1 | 10 | ... B | 1 | 11 | ... B | 2 | 2 | ... C | 3 | 2 | ... C | 4 | 1 | ... C | 5 | 6 | ... C | 5 | 7 | ... D | 6 | 1 | ... D | 7 | 2 | ... D | 7 | 6 | ... D | 7 | 8 | ...
因此,如果我需要執行基於 的操作
QuestionId
,引擎將不必讀取不必要的頁面。但是,如果我需要執行基於 的操作
EventId
,我將不得不讀取整個表(聚集索引掃描),除非我添加一個額外的索引,它看起來像這樣(並且需要四頁):EventId | QuestionId -------------------- 1 | 4 1 | 6 2 | 1 2 | 2 2 | 3 2 | 7 3 | 1 6 | 1 6 | 5 6 | 7 7 | 1 7 | 5 8 | 1 8 | 7 10 | 1 11 | 1
這個索引對某些人來說是選擇性的
EventIds
,但在極端情況下(EventId = 2
)仍然需要讀取整個表,並且對於某些情況(EventId = 6
)我們的優化器可能會決定搜尋索引和讀取表比讀取整個表更昂貴桌子。如果我們改為聚集在
EventId, QuestionId
我們的表上,如下所示:Page | EventId | QuestionId | Value ----------------------------------- A | 1 | 4 | ... A | 1 | 6 | ... A | 2 | 1 | ... A | 2 | 2 | ... B | 2 | 3 | ... B | 2 | 7 | ... B | 3 | 1 | ... B | 6 | 1 | ... C | 6 | 5 | ... C | 6 | 7 | ... C | 7 | 1 | ... C | 7 | 5 | ... D | 8 | 1 | ... D | 8 | 7 | ... D | 10 | 1 | ... D | 11 | 1 | ...
任何基於 的操作
EventId
都只會讀取表的必要部分,並且像我們的第一個實例一樣,任何基於 的操作QuestionId
都需要掃描而不需要額外的索引。如果我們在 上創建索引QuestionId
,則索引將是:QuestionId | EventId -------------------- 1 | 2 1 | 3 1 | 6 1 | 7 1 | 8 1 | 10 1 | 11 2 | 2 3 | 2 4 | 1 5 | 6 5 | 7 6 | 1 7 | 2 7 | 6 7 | 8
因此,與第一個範例一樣,該索引對某些問題更有用,而對其他問題則不太有用。因為
QuestionId = 1
優化器可能會說讀取一半索引然後查找一半表的成本不值得,並且只會讀取整個表而不是使用索引。如果我們包含
Value
在索引中,我們現在必須在同一個事務中更改表和索引。最好的情況是,這會使任何操作的工作加倍。在最壞的情況下,這需要讀取整個表或索引(它只是表的副本)並可能鎖定。現在可以使用您的實際數據添加額外的索引
QuestionId
或EventId
將提供很多好處。但這並不能解決所有問題,而且插入/更新/刪除的成本可能不值得。