建立並維護獨立的查詢表,針對應用程式在查詢時常用的資料,當資料儲存庫沒有提供合適的次級索引時。 此方法透過避免在查詢未使用主鍵或分割鍵時進行完整資料掃描,提升讀取效能。
內容和問題
許多資料儲存庫會使用主鍵來組織一組實體的資料。 應用程式可以使用此金鑰來尋找和擷取數據。 下圖展示了一個資料儲存庫依主鍵 Customer ID 組織的客戶資訊範例。
圖片顯示一張兩欄的客戶紀錄表。 第一欄「主鍵(客戶ID)」包含客戶 ID 1 到 9,後接省略號 ID 1000 及另一個省略號。 第二欄「客戶資料」包含由姓氏和城鎮加上省略號組成的客戶資料。 可見的例子包括ID 1,對應姓氏史密斯及鎮名Redmond,以及ID 2,對應姓氏Jones及鎮名西雅圖。 另一排顯示史密斯在芝加哥,另一排則顯示他在雷德蒙德。 另一列顯示瓊斯在芝加哥。
雖然主鍵對於根據該鍵值取得資料的查詢很有價值,但需要根據其他欄位取得資料的應用程式,無法使用該查詢的主鍵。 在客戶範例中,如果應用程式只藉由參考客戶所在城鎮的值來查詢數據,應用程式就無法使用客戶標識碼主鍵來擷取客戶。 若要執行僅參考城鎮的查詢,應用程式可能需要擷取並檢查每一份客戶紀錄,這過程可能很慢。
許多關係資料庫管理系統都支援次要索引。 次級索引是一種由一個或多個非主要(次要)鍵欄位組織的獨立資料結構。 次要索引則表示每個索引值的資料儲存位置。 次要索引中的專案通常會依次要索引鍵的值排序,以便快速查閱數據。 資料庫管理系統通常會自動維護這些索引。
關聯式資料庫允許多個次級索引支援各種查詢模式。 例如,在關聯式資料庫中的 Customers 資料表中,若 Customer ID 是主鍵,若應用程式經常以客戶所在城鎮查詢,則在城鎮欄位上加一個次級索引會很有幫助。
雖然次級索引在關聯系統中很常見,但並非所有資料儲存都提供相同的功能。 有些資料儲存庫缺乏次要索引,而另一些則提供無法滿足工作負載查詢、分割或效能需求的索引。 在這些情況下,應用程式必須在全掃描與手動索引管理之間做出選擇。
解決方案
建立一個索引表,依照指定的鍵組織資料。 以下節將介紹三種常見的索引表結構策略。 接下來的兩節描述了索引表在特定情境下的應用。
標準結構策略
以下策略常用來結構索引表。 根據你的情境所需的次級索引數量以及應用程式所執行的查詢性質來選擇策略。
完全反正規化
完全非正規化會在每個索引資料表中複製資料,但會按照主鍵以外的其他鍵來組織這些資料。 下圖展示了依城鎮和姓氏組織相同客戶資訊的索引表。
此策略適用於資料變動不頻繁且讀取量大的負載。 隨著更新速率增加,維護每一份副本會增加處理負擔(參見 一致性複雜度)。 對於大量資料集,儲存副本也可能需要大量空間。
正規化指數
正規化索引表則依非主鍵的鍵來組織資料。 正規化索引表會使用主鍵來參照原始資料,而不是重複該資料,如下圖所示。 原始數據稱為事實數據表。
Tip
此處的 事實表 指的是由索引所引用的權威來源表。 它並不代表有 維度資料模型。
這項技術可節省空間,並減少維護重複數據的額外負荷。 缺點是應用程式必須使用次要金鑰進行兩次查找操作來尋找資料。 應用程式必須先在索引表中找到資料的主鍵,然後用主鍵在事實表中查詢資料。
部分反正規化
部分非正規化會產生重複頻繁檢索欄位的索引表,並依非主鍵的鍵來組織。 事實表被參考以存取較少被存取的欄位。 下圖顯示每個索引表中常見存取資料的重複情況。
此策略平衡了前兩種方法。 你可以用一次查詢快速取得常見查詢的資料,空間和維護負擔也不如複製整個資料集那麼大。
複合鍵
有些應用程式經常透過指定數值組合來查詢資料,例如「尋找所有居住在雷德蒙且姓氏為 Smith 的客戶。」在這種情況下,從多個屬性建立索引鍵,例如這裡的 Town 和 LastName 屬性。
使用保留元件邊界的編碼,避免不同值組合產生相同的鍵。 如果查詢依賴於鍵序,也要確保編碼依據資料庫的鍵排序規則維持所需的排序順序。
下圖展示了基於綜合鍵的索引表。 鍵數會依城鎮排序,然後對城鎮值相同的紀錄依姓氏排序。
圖示左側為索引表,右側為事實表。 事實表依照主鍵 Customer ID 組織客戶資料。 每個客戶資料列包含一個顧客姓氏、城鎮,以及一個省略號表示其他資料。 索引表由結合城鎮與姓氏的複合鍵組成。 其第二欄為客戶參考(ID)及常見查詢資料,包含客戶 ID 與省略號。 箭頭從索引表列延伸到事實表,每個箭頭都指向該索引項目所參考的客戶ID列。 交錯的箭頭說明,依複合鍵排序的項目可以對應到事實表中不同位置的客戶資料列。
分片資料上的索引表
索引表可以加快對分片資料的查詢操作。 當分片鍵經過雜湊處理時,它們特別有用。 下圖顯示一個分片金鑰為客戶 ID 雜湊值的範例。 索引表依非雜湊值 Town 和 LastName 組織各條目,並在每個條目中儲存對應的雜湊分片鍵。
此配置支援對非雜湊值的範圍與排序查詢,並提供從正確分片擷取每筆記錄所需的路由資訊。 例如,像是「找出所有住在 Redmond 的客戶」這樣的查詢,可以在索引表的連續區塊中找出相符的項目。 應用程式接著使用儲存在索引表中的分片鍵,依循指向客戶資料的參考。
問題和考慮
在決定如何實施此模式時,請考慮以下幾點:
維護費用。 維持次級指數可能會增加相當多的營運成本。 分析並理解你的應用程式所使用的查詢。 只有在你可能經常使用索引表時才建立它們。 不要建立推測性的索引表來支援應用程式不執行或只偶爾執行的查詢。 隨著查詢模式數量增加,索引表數量也會增加,每個索引表都增加了監控、維護與除錯的操作表面積。 請權衡每個索引表的查詢效益與維護其他衍生資料結構的營運成本。 定期檢視現有索引表,找出並移除不再被查詢的索引表。
儲存與吞吐量成本。 在索引表中重複資料會隨著索引表數量及複製欄位大小的增加,增加儲存成本。 維護多份資料副本也會增加工作量。 每次對索引資料表的寫入都會消耗輸送量容量,例如 Azure 資料表儲存體 中受儲存體帳戶限制約束的交易次數,或 Azure Cosmos DB 中的要求單位。 成本不僅限於儲存,還延伸至寫入端吞吐量。
雙重抬頭懲罰。 將索引數據表實作為參考原始數據的標準化結構,需要應用程式執行兩個查閱作業來尋找數據。 第一個作業會搜尋索引表以擷取主鍵,而第二個作業會使用主鍵來擷取數據。
一致性複雜度。 如果系統在大型資料集中包含多個索引表,維持索引表與原始資料之間的一致性可能會很困難。 如果原始資料和索引條目無法在同一交易中更新,請以最終一致性模型來設計應用程式。 在確認寫入前,先將來源端的每次變更持久化記錄下來。 例如,取用資料庫變更摘要、使用 Transactional Outbox 模式 在與來源變更相同的交易中寫入寄件匣記錄,或在變更來源資料之前先將命令排入佇列。 在基於指令的方法中,使用工作者來處理指令並更新來源資料及其索引。 不要先更新原始資料,然後又獨立發佈索引更新訊息,因為這些操作間的失敗可能會讓索引變得過時。
將非同步消費者設計為具備冪等性,因為訊息傳遞和重試可能會導致同一項更新執行多次。 同一來源記錄的更新也可能出現錯序。 在每次索引更新中包含來源版本號或序號,並且只有在其版本比索引中的版本更新時,才套用該更新。 對於刪除,請保留有版本化的墓碑或等效的高水位標記,這樣延遲的舊更新就無法重新建立已刪除的索引條目。 在來源資料寫入與非同步索引更新之間,對索引表的查詢可能會傳回來源資料中已更新或刪除之記錄的過時參照,也可能遺漏最近新增的記錄。
分區索引表。 索引表本身可能被分割或分片,這增加了查詢路由的複雜度,並要求分割策略與索引設計的查詢模式保持一致。
使用此模式的時機
當應用程式經常需要使用非主鍵(或分片鍵)來檢索資料,且資料儲存庫不原生支援次要索引,或其原生索引無法滿足工作負載的查詢、分割或效能需求時,請使用此模式。
在下列情況下,此模式可能不適用:
數據是揮發性的。 資料變動頻繁,寫入速率超過索引表非同步刷新的速度。 陳舊視窗會持續擴大,直到索引永久過時,使其失去效用,且維護索引資料表所增加的儲存與吞吐量負擔超過查詢所節省的成本。
您使用的是不區分大小寫的金鑰。 被選為索引表次要鍵的欄位是無區別性的,且只能有少量值(例如,一個記錄項目是否活躍的布林欄位)。 索引表會產生全部的儲存與吞吐量成本,但查詢選擇性極低。
資料值的分布偏斜。 在索引表中選為次要鍵的欄位,其資料值分布高度偏斜。 例如,如果 90% 的記錄在某欄位中包含相同值,那麼建立並維護索引表以根據該欄位查找資料,可能會比依序掃描資料產生更多負擔。 然而,如果查詢經常鎖定剩餘 10% 的值,這個索引仍然有用。
工作負載設計
架構師應評估如何在工作負載設計中使用索引表模式,以達成Azure Well-Architected框架支柱所涵蓋的目標與原則。 下表提供此模式如何支援每個要素目標的指引。
| 支柱 | 此模式如何支援支柱目標 |
|---|---|
| 可靠性 透過建立冗餘並在故障時保留功能,幫助你的工作負載達成韌 性與復原目標 。 | 非同步索引維護可防止臨時索引更新失敗阻礙原始資料寫入。 冪等處理、死符處理、監控與對帳有助於在失敗後恢復索引一致性。 - RE:07 自我保護 - RE:10 監視 |
| 效能效率 可透過調整、數據和程式碼的優化, 有效率地協助您的工作負載符合需求 。 | 索引表有助於快速查詢非主鍵欄位,無需完整資料掃描。 對於分片資料存放區,索引表可以依非雜湊值來組織項目,以支援僅靠分片鍵無法有效處理的範圍查詢和排序查詢。 - PE:05 擴展和分區 - PE:08 數據效能 |
如同任何設計決策,若此模式在某一支柱內引入取捨,請將其與其他支柱的目標相較。
Example
考慮一個儲存電影資訊的應用程式。 假設目錄龐大且閱讀量大,每部電影只有一個主要類型,演員查詢頻繁,演員資料變動不頻繁,且短暫的索引延遲是可以接受的。 Table Storage 將每個實體儲存為一組結構化的命名屬性。 每個實體都包含一個 PartitionKey、 RowKey和一個時間戳,且同一表格中的實體可以擁有不同的屬性集合。
表格儲存體使用由 RowKey 和 PartitionKey 組成的複合主鍵。 該 PartitionKey 值決定實體所儲存的分割區。 在分割區內,該 RowKey 值可唯一識別一個實體。 表格儲存體已針對同時指定兩個索引鍵,或在單一分割區內擷取連續資料列索引鍵值範圍的查詢進行最佳化。
Tip
Table Storage 支援同一資料表中實體的交易更新,並透過 實體群組交易進行分割。 交易不能橫跨事實表和獨立的索引表。 若要以原子方式更新事實實體和索引實體,請將它們儲存在同一個資料表中,且具有相同的 PartitionKey。 實體群組交易每批限制為 100 個實體,最大有效載荷為 4 MiB。
在這個例子中,請建立一個 Azure 表格,為每個類型設置分割區,使用編碼的類型識別碼作為分割鍵,並以穩定且獨特的電影識別碼作為列鍵。 將類型和電影名稱存為屬性。 下圖使用可讀的名稱取代識別碼,以便理解範例。
如果應用程式也需要藉由主演演員查詢電影,這個方法就不太有效。 在這種情況下,建立一個獨立的 Azure 表格作為索引表。 使用編碼且穩定的演員識別碼作為分割鍵,電影識別碼作為列鍵。 將演員和電影名字存為屬性。 下圖使用可讀的名稱取代識別碼。 如果一部電影有多位演員,同一部電影會分成多個分區。
下圖顯示了演員索引表。
設計導覽
電影表使用類型作為分割鍵,這表示依類型篩選的查詢能有效地執行連續列鍵範圍的分割掃描。 然而,資料表儲存體僅支援在 RowKey 和 PartitionKey 上建立單一叢集索引。 它沒有次級索引。 像「尋找所有由特定演員主演的電影」這樣的查詢,需要在每個類型分割區進行完整的表格掃描,這在大規模上成本很高。
演員索引表透過反向存取模式來解決此限制。 每個演員識別碼會變成分割鍵,每個電影識別碼也會變成列鍵,因此基於演員的查詢會被解析為高效的分割區查找。 由於每個分割區僅包含單一演員的電影,因此查詢會回傳一段連續範圍內的實體,而無須掃描無關的資料。
由於電影和演員的項目使用不同的資料表和分割鍵,這些項目無法共享實體群組交易。 透過持久的非同步變更捕捉機制維護演員索引,並設計應用程式以容忍短暫的查詢陳舊。
索引表採用部分非正規化:每個項目重複常用欄位(例如其他演員的名稱),因此最頻繁的查詢僅能透過索引表一次查詢即可回答。 對於較少被存取的欄位,該項目會包含原始電影資料表中的類型分區鍵,讓系統能夠對該類型分區執行精準點查詢,以取得完整記錄。 此設計在查詢速度與儲存成本及維護開銷間取得平衡。
下一步
資料表儲存查詢設計指引描述 了使用
PartitionKey及RowKey的次級索引模式,包括將索引條目儲存在同一分割區或分開分割區的方法。資料分割策略 說明如何根據查詢模式、資料分布、交易需求及擴展性目標選擇分割區與列鍵。
優化資料效能的架構策略 ,為評估查詢模式、索引、分割區及監控需求提供指引。
Azure Cosmos DB 中的一致性等級描述了在 Azure Cosmos DB 中維護索引表時相關的一致性模型。
相關資源
當您實作此模式時,下列模式也可能相關: