「查詢規劃器」的任務是找出完成 SQL 陳述式的最佳演算法或「查詢計畫」。從 SQLite 3.8.0 版 (2013-08-26) 開始,查詢規劃器元件已經過重新編寫,使其運行速度更快並產生更好的計畫。此重寫版本稱為「新一代查詢規劃器」或「NGQP」。
本文概述查詢規劃的重要性,描述查詢規劃中固有的一些問題,並概述 NGQP 如何解決這些問題。
NGQP 幾乎總是比舊版查詢規劃器更好。然而,可能存在一些舊版應用程式在不知不覺中依賴舊版查詢規劃器中未定義和/或次優的行為,而將這些舊版應用程式升級到 NGQP 可能會導致效能下降。本文考慮了這種風險,並提供了一個檢查清單,以降低風險並修復任何出現的問題。
本文檔著重於 NGQP。有關涵蓋整個 SQLite 歷史的 SQLite 查詢規劃器的更一般性概述,請參閱「SQLite 查詢最佳化器概述」和「索引如何運作」文件。
對於針對索引較少的單個表格的簡單查詢,通常有一個明顯的最佳演算法選擇。但是對於更大、更複雜的查詢,例如具有多個索引和子查詢的多路聯結,可能有數百、數千或數百萬種合理的計算結果的演算法。查詢規劃器的任務是從眾多可能性中選擇單個「最佳」查詢計畫。
查詢規劃器是 SQL 資料庫引擎如此有用和強大的原因。(這適用於所有 SQL 資料庫引擎,而不僅僅是 SQLite。)查詢規劃器使程式設計師免於選擇特定查詢計畫的繁瑣工作,從而讓程式設計師能夠將更多精力集中在更高級的應用程式問題上,並為終端使用者提供更多價值。對於查詢計畫選擇明顯的簡單查詢,這很方便但並不十分重要。但是隨著應用程式、綱要和查詢變得更加複雜,一個聰明的查詢規劃器可以極大地加快和簡化應用程式開發的工作。能夠告訴資料庫引擎所需的內容,然後讓資料庫引擎找出擷取該內容的最佳方式,具有驚人的力量。
撰寫一個好的查詢規劃器,与其说是科学,不如说是一門藝術。查詢規劃器必須在資訊不完整的情況下工作。它無法在不實際執行計劃的情況下確定任何特定計劃需要多長時間。因此,在比較兩個或多個計劃以找出哪個「最佳」時,查詢規劃器必須做出一些猜測和假設,而這些猜測和假設有時會是錯誤的。一個好的查詢規劃器是指它能夠經常找到正確的解決方案,以至於應用程式開發人員很少需要介入。
SQLite 使用巢狀迴圈計算聯結,聯結中的每個資料表都有一個迴圈。(WHERE 子句中的 IN 和 OR 運算子可能會插入額外的迴圈。SQLite 也會考慮這些,但為了簡潔起見,我們在本文中將忽略它們。)每個迴圈上可能會使用一個或多個索引來加速搜尋,或者迴圈可能是讀取資料表中每一行的「全表掃描」。因此,查詢規劃分解為兩個子任務:
選擇巢狀順序通常是更具挑戰性的問題。一旦建立了聯結的巢狀順序,每個迴圈的索引選擇通常就很明顯了。
當啟用查詢規劃器穩定性保證 (QPSG) 時,只要滿足以下條件,SQLite 將始終為任何給定的 SQL 陳述式選擇相同的查詢計劃:
QPSG 預設為停用。它可以在編譯時使用 SQLITE_ENABLE_QPSG 編譯時選項啟用,或在執行時透過呼叫 sqlite3_db_config(db,SQLITE_DBCONFIG_ENABLE_QPSG,1,0) 啟用。
QPSG 意味著,如果您的所有查詢在測試期間都能有效地執行,並且如果您的應用程式沒有更改結構描述,那麼 SQLite 不會突然決定開始使用不同的查詢計劃,從而可能在您的應用程式發佈給使用者後造成效能問題。如果您的應用程式在實驗室中可以正常運作,那麼它在部署後將繼續以相同的方式運作。
用戶端/伺服器 SQL 資料庫引擎通常不提供此保證。在用戶端/伺服器 SQL 資料庫引擎中,伺服器會持續追蹤資料表大小以及索引品質的統計資訊,而查詢規劃器會使用這些統計資訊來協助選擇最佳計劃。隨著資料庫中內容的添加、刪除或更改,統計資訊將會演變,並可能導致查詢規劃器開始對某些特定查詢使用不同的查詢計劃。通常,新計劃對於不斷變化的資料結構會更好。但有時新的查詢計劃會導致效能下降。對於用戶端/伺服器資料庫引擎,通常會有一位資料庫管理員 (DBA) 來處理這些偶爾出現的問題。但是,DBA 無法修復像 SQLite 這樣的嵌入式資料庫中的問題,因此 SQLite 會小心地確保計劃在部署後不會意外更改。
需要注意的是,更改 SQLite 版本可能會導致查詢計劃的更改。相同版本的 SQLite 將始終選擇相同的查詢計劃,但如果您重新連結應用程式以使用不同版本的 SQLite,則查詢計劃可能會更改。在極少數情況下,SQLite 版本的更改可能會導致效能下降。這就是您應該考慮將應用程式靜態連結到 SQLite,而不是使用可能會在您不知情或不受控制的情況下更改的全系統 SQLite 共用程式庫的原因之一。
另請參閱
「TPC-H Q8」是來自交易處理效能委員會的測試查詢。在 SQLite 3.7.17 及更早版本中,查詢規劃器無法為 TPC-H Q8 選擇良好的執行計畫。而且已經確定,無論如何調整舊版查詢規劃器都無法解決這個問題。為了找到 TPC-H Q8 查詢的良好解決方案,並持續改善 SQLite 查詢規劃器的品質,重新設計查詢規劃器變得勢在必行。本節試圖解釋為何需要重新設計,以及新一代查詢規劃器 (NGQP) 的不同之處以及如何解決 TPC-H Q8 問題。
TPC-H Q8 是一個八向聯結。如上所述,查詢規劃器的主要任務是找出八個迴圈的最佳巢狀順序,以盡量減少完成聯結所需的工作量。以下圖表顯示了 TPC-H Q8 案例中此問題的簡化模型:
在圖表中,查詢 FROM 子句中的 8 個表格都以標有 FROM 子句術語的大圓圈表示:N2、S、L、P、O、C、N1 和 R。圖表中的弧線表示計算每個術語的估計成本,假設弧線的起點位於外迴圈中。例如,將 S 迴圈作為 L 的內迴圈執行的成本為 2.30,而將 S 迴圈作為 L 的外迴圈執行的成本為 9.17。
這裡的「成本」是對數的。使用巢狀迴圈時,工作量是相乘而不是相加的。但是習慣上將圖形視為具有加權值的,因此圖形顯示了各種成本的對數。圖表顯示 S 位於 L 內部時的成本優勢約為 6.87,但這意味著當 S 迴圈位於 L 迴圈內部而不是外部時,查詢執行速度大約快 963 倍。
標有「*」的小圓圈的箭頭表示在沒有相依性的情況下執行每個迴圈的成本。最外層的迴圈必須使用此 *-成本。內迴圈可以選擇使用 *-成本或假設其他術語之一位於外迴圈中的成本,以產生最佳結果為準。可以將 *-成本視為表示多個弧線的簡寫符號,每個弧線來自圖表中其他節點之一。因此,該圖表是「完整的」,表示圖表中每對節點之間都存在弧線(一些是明確的,一些是隱含的)。
尋找最佳查詢計畫的問題等同於在圖表中找到一條造訪每個節點恰好一次的最小成本路徑。
(註:以上 TPC-H Q8 圖表中的成本估計是由 SQLite 3.7.16 中的查詢規劃器計算的,並使用自然對數轉換。)
上述查詢規劃器問題的呈現是一種簡化。成本是估計值。在實際執行迴圈之前,我們無法知道執行迴圈的真實成本。SQLite 根據 WHERE 子句中找到的索引和約束的可用性來猜測執行迴圈的成本。這些猜測通常相當準確,但有時也可能會有偏差。使用 ANALYZE 指令收集有關資料庫的額外統計資訊,有時可以讓 SQLite 更準確地估計成本。
成本是由多個數值組成,而不是圖表中顯示的單一數值。SQLite 為每個循環計算幾個不同的估計成本,這些成本適用於不同的時間點。例如,有一個「設置」成本,它只在查詢開始時產生一次。設置成本是為尚未建立索引的表格計算查詢時索引的成本。然後是運行循環每一步的成本。最後,還有一個對循環產生行數的估計,這是估計內部循環成本所需的信息。如果查詢具有 ORDER BY 子句,則排序成本可能會發揮作用。
在一般查詢中,依賴關係不必在單個循環上,因此依賴關係矩陣可能無法表示為圖形。例如,其中一個 WHERE 子句約束可能是 S.a=L.b+P.c,這意味著 S 循環必須是 L 和 P 的內部循環。這種依賴關係無法繪製為圖形,因為沒有辦法讓弧線同時源自兩個或多個節點。
如果查詢包含 ORDER BY 子句或 GROUP BY 子句,或者查詢使用 DISTINCT 關鍵字,那麼選擇一條通過圖形的路徑,使資料列自然地以排序順序出現是有利的,這樣就不需要單獨的排序步驟。自動消除 ORDER BY 子句可以帶來很大的效能差異,因此這是在完整實作中需要考慮的另一個因素。
在 TPC-H Q8 查詢中,設置成本都可以忽略不計,所有依賴關係都在單個節點之間,並且沒有 ORDER BY、GROUP BY 或 DISTINCT 子句。因此,對於 TPC-H Q8,上面的圖形是需要計算內容的合理表示。一般情況會涉及許多額外的複雜性,為了清晰起見,本文的其餘部分將忽略這些複雜性。
在 3.8.0 版(2013 年 8 月 26 日)之前,SQLite 在搜尋最佳查詢計劃時始終使用「最近鄰居」或「NN」啟發式演算法。NN 啟發式演算法對圖形進行單次遍歷,始終選擇成本最低的弧線作為下一步。NN 啟發式演算法在大多数情况下都表現得非常好。而且 NN 速度很快,因此 SQLite 即使對於大型的 64 路連接也能快速找到好的計劃。相比之下,其他進行更廣泛搜尋的 SQL 資料庫引擎在連接中的表格數量超過 10 或 15 個時往往會陷入困境。
遺憾的是,NN 為 TPC-H Q8 計算的查詢計劃並非最佳。使用 NN 計算的計劃是 R-N1-N2-S-C-O-L-P,成本為 36.92。上一句中的符號表示 R 表格在最外層循環中運行,N1 在下一個內層循環中,N2 在第三個循環中,依此類推,直到 P 在最內層循環中運行。通過圖形的最短路徑(通過窮舉搜尋找到)是 P-L-O-C-N1-R-S-N2,成本為 27.38。差異看起來可能不大,但請記住,成本是對數的,因此最短路徑比使用 NN 啟發式演算法找到的路徑快近 750 倍。
解決此問題的一種方法是將 SQLite 更改為對最佳路徑進行窮舉搜尋。但是窮舉搜尋所需的時間與 K! 成正比(其中 K 是連接中表格的數量),因此當您超過 10 路連接時,運行 sqlite3_prepare() 的時間會變得非常長。
NGQP 使用一種新的啟發式演算法來搜尋通過圖形的最佳路徑:「N 個最近鄰居」(以下簡稱「N3」)。使用 N3,演算法不會只為每一步選擇一個最近鄰居,而是在每一步中追蹤 N 個最佳路徑,其中 N 為某個小的整數。
假設 N=4。那麼對於 TPC-H Q8 圖形,第一步是找到訪問圖形中任何單個節點的四條最短路徑
R (成本:3.56)
N1 (成本:5.52)
N2 (成本:5.52)
P (成本:7.71)
第二步從上一步的四條路徑開始,找出造訪兩個節點的四條最短路徑。如果兩條或多條路徑相同(它們具有相同的已造訪節點集,但順序可能不同),則只保留第一條且成本最低的路徑。我們有
R-N1 (成本:7.03)
R-N2 (成本:9.08)
N2-N1 (成本:11.04)
R-P (成本:11.27)
第三步從四條最短的雙節點路徑開始,找出四條最短的三節點路徑
R-N1-N2 (成本:12.55)
R-N1-C (成本:13.43)
R-N1-P (成本:14.74)
R-N2-S (成本:15.08)
以此類推。TPC-H Q8 查詢中有 8 個節點,因此這個過程會重複執行 8 次。在 K 路聯結的一般情況下,儲存需求為 O(N),計算時間為 O(K*N),這明顯比 O(2K) 的精確解快得多。
但是 N 應該選擇什麼值呢?可以嘗試 N=K。這使得演算法的時間複雜度為 O(K2),這實際上仍然相當有效率,因為 K 的最大值是 64,而且 K 很少超過 10。但這對於 TPC-H Q8 問題來說還不夠。在 TPC-H Q8 中,當 N=8 時,N3 演算法找到成本為 29.78 的解 R-N1-C-O-L-S-N2-P。這比 NN 有很大的改進,但仍然不是最佳的。當 N 為 10 或更大時,N3 才能找到 TPC-H Q8 的最佳解。
NGQP 的初始實作對於簡單查詢選擇 N=1,對於雙表聯結選擇 N=5,對於所有包含三個或更多表格的聯結選擇 N=10。這個選擇 N 的公式可能會在後續版本中更改。
→ 更新:本節已過時,僅供歷史參考。本節在 NGQP 推出初期相當重要。但十年過去了,NGQP 已經成功部署到數十億台設備上,而且大家都已升級,沒有任何效能衰退的回報提交給 SQLite 開發人員。升級風險已經消失。本節僅供歷史參考。現代讀者可以跳至查詢規劃器檢查清單。←
對於大多數應用程式來說,從舊版查詢規劃器升級到 NGQP 幾乎不需要考慮或額外的工作。只需將舊版 SQLite 替換為新版 SQLite 並重新編譯,應用程式就會執行得更快。沒有 API 變更,也沒有編譯程序的修改。
但是,與任何查詢規劃器的變更一樣,升級到 NGQP 的確帶有引入效能衰退的小風險。這裡的問題不在於 NGQP 不正確、有錯誤或不如舊版查詢規劃器。如果有關於索引選擇性的可靠資訊,NGQP 應該總是能選擇與以前一樣好或更好的計劃。問題在於某些應用程式可能使用了低品質和低選擇性的索引,而沒有執行 ANALYZE。舊版查詢規劃器會檢查每個查詢的更少可能的實作方式,因此它們可能會因為運氣好而偶然找到一個好的計劃。另一方面,NGQP 會查看更多查詢計劃的可能性,並且可能會選擇一個理論上更好的查詢計劃(假設索引良好),但在實務中卻導致效能衰退,因為資料的形狀。
重點
只要 NGQP 可以存取 SQLITE_STAT1 檔案中準確的 ANALYZE 資料,與先前的查詢規劃器相比,它總是能找到相同或更好的查詢計劃。
只要綱要中不包含索引在其最左欄位中具有超過約 10 或 20 個相同值的索引,NGQP 就總是能找到一個好的查詢計劃。
並非所有應用程式都符合這些條件。幸運的是,即使沒有這些條件,新一代查詢規劃器 (NGQP) 通常仍然可以找到良好的查詢計畫。然而,效能下降的情況確實會發生(但很少見)。
Fossil DVCS 是一個用於追蹤所有 SQLite 原始碼的版本控制系統。Fossil 儲存庫是一個 SQLite 資料庫檔案。(讀者可以將此遞迴關係作為一個獨立練習來思考。)Fossil 既是 SQLite 的版本控制系統,也是 SQLite 的測試平台。每當 SQLite 進行增強時,Fossil 都是首批測試和評估這些增強功能的應用程式之一。因此,Fossil 是 NGQP 的早期採用者。
不幸的是,NGQP 導致了 Fossil 的效能下降。
Fossil 提供的眾多報告之一是單一分支的變更時間軸,顯示該分支的所有合併與分支。請參閱 https://www.sqlite.org/src/timeline?nd&n=200&r=trunk 作為此類報告的典型範例。產生此類報告通常只需要幾毫秒。但在升級到 NGQP 後,我們注意到這份報告在儲存庫的主幹上需要將近 10 秒的時間。
用於產生分支時間軸的核心查詢如下所示。(讀者不需要理解此查詢的細節。後續會有說明。)
SELECT blob.rid AS blobRid, uuid AS uuid, datetime(event.mtime,'localtime') AS timestamp, coalesce(ecomment, comment) AS comment, coalesce(euser, user) AS user, blob.rid IN leaf AS leaf, bgcolor AS bgColor, event.type AS eventType, (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid AND tagxref.rid=blob.rid AND tagxref.tagtype>0) AS tags, tagid AS tagid, brief AS brief, event.mtime AS mtime FROM event CROSS JOIN blob WHERE blob.rid=event.objid AND (EXISTS(SELECT 1 FROM tagxref WHERE tagid=11 AND tagtype>0 AND rid=blob.rid) OR EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid WHERE tagid=11 AND tagtype>0 AND pid=blob.rid) OR EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid WHERE tagid=11 AND tagtype>0 AND cid=blob.rid)) ORDER BY event.mtime DESC LIMIT 200;
這個查詢並不特別複雜,但它仍然取代了數百甚至數千行程式的程式碼。此查詢的要點是:掃描 EVENT 資料表,尋找符合以下三個條件中任何一個的最近 200 個簽入:
第一個條件會顯示所有主幹簽入,而第二和第三個條件會將合併到主幹或從主幹分支出來的簽入也包含在內。這三個條件由查詢 WHERE 子句中的三個以 OR 連接的 EXISTS 陳述式實現。NGQP 造成的效能下降是由第二和第三個條件引起的。兩者的問題相同,因此我們將只檢查第二個條件。第二個條件的子查詢可以改寫(略做簡化,但不影響實質)如下:
SELECT 1 FROM plink JOIN tagxref ON tagxref.rid=plink.cid WHERE tagxref.tagid=$trunk AND plink.pid=$ckid;
PLINK 資料表儲存簽入之間的父子關係。TAGXREF 資料表將標籤映射到簽入。為了參考,這兩個資料表的相關結構部分如下所示:
CREATE TABLE plink( pid INTEGER REFERENCES blob, cid INTEGER REFERENCES blob ); CREATE UNIQUE INDEX plink_i1 ON plink(pid,cid); CREATE TABLE tagxref( tagid INTEGER REFERENCES tag, mtime TIMESTAMP, rid INTEGER REFERENCE blob, UNIQUE(rid, tagid) ); CREATE INDEX tagxref_i1 ON tagxref(tagid, mtime);
只有兩種合理的方法可以實現這個查詢。(還有許多其他可能的演算法,但沒有其他演算法可以爭奪「最佳」演算法的稱號。)
找出簽入 $ckid 的所有子項,並測試每個子項是否具有 $trunk 標籤。
找出所有具有 $trunk 標籤的簽入,並測試每個簽入是否是 $ckid 的子項。
直觀地,我們人類理解演算法 1 是最好的。每個簽入可能只有少數子項(一個子項是最常見的情況),並且每個子項都可以在對數時間內測試 $trunk 標籤。實際上,演算法 1 在實務中是較快的選擇。但 NGQP 沒有直覺。NGQP 必須使用嚴謹的數學計算,而演算法 2 在數學上略勝一籌。這是因為,在沒有其他資訊的情況下,NGQP 必須假設索引 PLINK_I1 和 TAGXREF_I1 的品質相同且選擇性相同。演算法 2 使用 TAGXREF_I1 索引的一個欄位和 PLINK_I1 索引的兩個欄位,而演算法 1 只使用每個索引的第一個欄位。由於演算法 2 使用了更多的索引資料,因此 NGQP 判斷它是較佳的演算法是正確的。分數很接近,演算法 2 只是勉強領先演算法 1。但演算法 2 在這裡確實是正確的選擇。
不幸的是,在此應用程式中,演算法 2 比演算法 1 慢。
問題在於索引的品質並不相同。一個簽入 (check-in) 很可能只有一個子項。因此,PLINK_I1 的第一個欄位通常會將搜尋範圍縮小到只剩單一行。但有成千上萬個簽入被標記為「trunk」,所以 TAGXREF_I1 的第一個欄位在縮小搜尋範圍方面幾乎沒有幫助。
除非已在資料庫上執行 ANALYZE,否則 NGQP 無法得知 TAGXREF_I1 在此查詢中幾乎無用。ANALYZE 命令會收集各種索引品質的統計資訊,並將這些統計資訊儲存在 SQLITE_STAT1 表格中。透過存取這些統計資訊,NGQP 可以輕鬆地選擇演算法 1 作為最佳演算法,而且優勢明顯。
為什麼舊版查詢規劃器沒有選擇演算法 2?很簡單:因為 NN 演算法根本沒有考慮演算法 2。規劃問題的圖表如下所示
在左側的「未執行 ANALYZE」的情況下,NN 演算法選擇迴圈 P (PLINK) 作為外迴圈,因為 4.9 小於 5.2, resulting in 路徑 P-T,也就是演算法 1。NN 在每一步只考慮單一最佳選擇,所以它完全忽略了 5.2+4.4 的計劃比 4.9+4.8 略微便宜的事實。但 N3 演算法會追蹤雙向聯結的 5 個最佳路徑,因此它最終會因為總成本略低而選擇路徑 T-P。路徑 T-P 就是演算法 2。
請注意,執行 ANALYZE 後,成本估算更符合實際情況,NN 和 N3 都選擇了演算法 1。
(備註:最近兩張圖表中的成本估算是由 NGQP 使用以 2 為底的對數和與舊版查詢規劃器略有不同的成本假設計算得出的。因此,後面兩張圖表中的成本估算無法直接與 TPC-H Q8 圖表中的成本估算進行比較。)
在儲存庫資料庫上執行 ANALYZE 立即解決了效能問題。然而,我們希望 Fossil 堅固耐用,並且無論其儲存庫是否經過分析都能始終快速運作。因此,我們修改了查詢,使用 CROSS JOIN 運算子來代替普通的 JOIN 運算子。SQLite 不會重新排序 CROSS JOIN 的表格。這是 SQLite 的一個長期特性,專門設計用於允許知識淵博的程式設計師強制執行特定的迴圈巢狀順序。將聯結更改為 CROSS JOIN(只增加了一個關鍵字)後,無論是否使用 ANALYZE 收集統計資訊,NGQP 都被迫選擇更快的演算法 1。
我們說演算法 1「更快」,但这並不完全正確。演算法 1 在常見的儲存庫中更快,但可以建構一個儲存庫,其中每個簽入都在一個不同且唯一命名的分支上,並且所有簽入都是根簽入的子項。在這種情況下,TAGXREF_I1 會比 PLINK_I1 更具選擇性,而演算法 2 確實會是更快的選擇。然而,這種儲存庫在實務中不太可能出現,因此在本例中,使用 CROSS JOIN 語法硬編碼迴圈巢狀順序是一個合理的解決方案。
前面的文字寫於 2013 年初,在 SQLite 3.8.0 版首次發布之前。這一段是在 2021 年年中新增的。雖然之前的討論仍然正確,但查詢規劃器已進行了許多改進,使得整個章節基本上沒有意義。
在 2017 年,Fossil 經過強化,開始使用新的 PRAGMA optimize 語句。每當 Fossil 即將關閉與其儲存庫的資料庫連線時,它會先執行「PRAGMA optimize」,如果需要,這將會接著執行 ANALYZE。通常不需要執行 ANALYZE,因此執行此操作不會造成明顯的效能損失。但偶爾會在儲存庫資料庫中的幾個資料表上執行 ANALYZE。正因如此,像這裡描述的查詢規劃問題便不再出現在 Fossil 中。由於 ANALYZE 會定期執行以保持 sqlite_stat1 資料表為最新狀態,因此不再需要手動調整查詢。我們已經很久沒有調整 Fossil 中的查詢了。
因此,目前避免此類問題的建議是在關閉每個資料庫連線之前執行「PRAGMA optimize」。或者,如果您的應用程式長時間執行且從不關閉任何資料庫連線,則大約每天執行一次「PRAGMA optimize」。也請考慮在任何結構描述變更後執行「PRAGMA optimize」。
不要驚慌!查詢規劃器選擇較差規劃的情況實際上相當罕見。您的應用程式不太可能會遇到任何問題。如果您沒有遇到效能問題,則無需擔心這些問題。
建立適當的索引。大多數 SQL 效能問題的產生並非因為查詢規劃器問題,而是因為缺乏適當的索引。請確保所有大型查詢都有可用的索引。大多數效能問題都可以透過一或兩個 CREATE INDEX 命令解決,而無需變更應用程式程式碼。
避免建立低品質的索引。(就本檢查清單而言)低品質索引是指資料表中有 10 或 20 個以上的列,其索引最左欄的值相同。尤其要避免使用布林值或「列舉」欄作為索引的最左欄。
本文前一節中描述的 Fossil 效能問題的產生是因為 TAGXREF 資料表中有超過一萬個項目,其 TAGXREF_I1 索引的最左欄(TAGID 欄)的值相同。
如果必須使用低品質索引,請務必執行 ANALYZE。只要查詢規劃器知道索引的品質低落,低品質索引就不會造成查詢規劃器混淆。查詢規劃器得知此資訊的方式是透過 SQLITE_STAT1 資料表的內容,該內容是由 ANALYZE 命令計算的。
當然,只有當您的資料庫中已有相當多的內容時,ANALYZE 才能有效運作。在建立預期會累積大量資料的新資料庫時,您可以執行命令「ANALYZE sqlite_schema」來建立 SQLITE_STAT1 資料表,然後使用一般 INSERT 語句預先填入 sqlite_stat1 資料表,其內容描述應用程式的典型資料庫 — 也許是您在實驗室中對已填入完善的範本資料庫執行 ANALYZE 後擷取的內容。或者,您也可以在關閉資料庫連線之前執行「PRAGMA optimize」,以便根據需要自動執行 ANALYZE,以保持 sqlite_stat1 資料表為最新狀態。
檢測您的程式碼。新增邏輯,讓您可以快速輕鬆地得知哪些查詢耗時過長。然後專注於處理這些特定的查詢。
為了鼓勵您停止閱讀,此清單的其餘部分現在已灰顯。
使用 unlikely() 和 likelihood() SQL 函數。SQLite 通常假設 WHERE 子句中無法由索引使用的條件很有可能為真。如果此假設不正確,則可能導致查詢計劃不理想。unlikely() 和 likelihood() SQL 函數可用於向查詢規劃器提供關於 WHERE 子句中可能不為真的條件的提示,從而幫助查詢規劃器選擇最佳計劃。
使用 CROSS JOIN 語法對可能在未分析的資料庫中使用低質量索引的查詢強制執行特定的迴圈嵌套順序。SQLite 以特殊方式處理 CROSS JOIN 運算子,強制左側表格相對於右側表格成為外迴圈。
使用單元 "+" 運算子取消 WHERE 子句條件的資格。如果查詢規劃器堅持為特定查詢選擇低質量索引,而有更高質量的索引可用,那麼在 WHERE 子句中謹慎使用單元 "+" 運算子可以強制查詢規劃器放棄低質量索引。盡可能避免使用此技巧,尤其是在應用程式開發週期的早期避免使用。請注意,如果涉及類型關聯性,則在等式表達式中添加單元 "+" 運算子可能會更改該表達式的結果。
使用 INDEXED BY 語法對問題查詢強制選擇特定索引。與前兩點一樣,盡可能避免此步驟,尤其是在開發早期避免這樣做,因為這顯然是過早的優化。
SQLite 中的查詢規劃器通常在選擇用於執行 SQL 陳述式的快速演算法方面表現出色。傳統查詢規劃器如此,新的 NGQP 更是如此。在某些情況下,由於資訊不完整,查詢規劃器可能會選擇不理想的計劃。NGQP 比傳統查詢規劃器發生這種情況的頻率要低,但仍有可能發生。只有在這種少見的情況下,應用程式開發人員才需要介入並幫助查詢規劃器做出正確的選擇。在一般情況下,NGQP 只是 SQLite 的一項新增強功能,它使應用程式運行速度略快,並且無需開發人員進行新的思考或操作。
此頁面最後修改時間:世界協調時間 2024-05-10 14:30:36