小巧、快速、可靠。
任選三項。
SQL 查詢剖析

1. 概觀

SQLite 內建支援 SQL 查詢剖析,但預設未啟用。若要啟用查詢剖析支援,必須使用 下列選項 編譯 SQLite

-DSQLITE_ENABLE_STMT_SCANSTATUS

使用此選項編譯 SQLite 可啟用 sqlite3_stmt_scanstatus_v2() API,提供存取各種剖析指標的功能。本頁面後續內容將討論 SQLite 命令列殼層 使用這些指標產生的剖析報告,而非直接討論 API。

殼層產生的剖析報告與 EXPLAIN QUERY PLAN 指令產生的查詢計畫報告非常類似。本頁面假設讀者熟悉此格式。

在使用上述選項編譯的命令列殼層中,可使用「.scanstats on」指令啟用查詢剖析

sqlite> .scanstats on

啟用後,殼層會在執行每一個 SQL 查詢後自動輸出查詢剖析。可使用「.scanstats off」停用查詢剖析。例如

sqlite> .scanstats on
sqlite> SELECT a FROM t1, t2 WHERE a IN (1,2,3) AND a=d+e;
QUERY PLAN (cycles=255831538 [100%])
|--SEARCH t1 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=60048488 [23%] loops=1 rows=3)
`--SCAN t2                                           (cycles=133558052 [52%] loops=3 rows=150000)

2. 簡單案例 - 列、迴圈和循環

考慮具有下列結構的資料庫

CREATE VIRTUAL TABLE ft USING fts5(text);
CREATE TABLE t1(a, b);
CREATE TABLE t2(c INTEGER PRIMARY KEY, d);

然後,在先執行「.scanstats on」後

sqlite3> SELECT * FROM t1, t2 WHERE t2.c=t1.a;
<...query results...>
QUERY PLAN (cycles=1140768 [100%])
|--SCAN t1                                           (cycles=455974 [40%] loops=1 rows=500)
`--SEARCH t2 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=619820 [54%] loops=500 rows=250)

上述範例中,在「<...query results...>」片段之後的文字,是剛才執行的聯結查詢的剖析報告。剖析報告中類似 EXPLAIN QUERY PLAN 輸出的部分,指出查詢是透過對「t1」表格執行全表掃描,並針對每個拜訪的列,對「t2」表格以整數主鍵進行查詢。

「SCAN t1」列上的「loops=1」標記表示這個迴圈 (「t1」表格的全表掃描) 執行了一次。「rows=500」表示單次掃描拜訪了 500 列。

「SEARCH t2 USING ...」列包含註解「loops=500」,表示這個「迴圈」(實際上是透過整數主鍵進行查詢) 執行 500 次。這是有道理的,因為它針對「t1」全表掃描拜訪的每一列執行一次。「rows=250」表示這些 500 個迴圈總共拜訪了 250 列。換句話說,在「t2」表格上的整數主鍵查詢中,只有半數成功,另一半的查詢找不到列。

SEARCH 或 SCAN 條目的迴圈計數不一定與外部迴圈輸出的列數相同。例如,如果將上述查詢修改如下

sqlite3> SELECT * FROM t1, t2 WHERE t1.b<=100 AND t2.c=t1.a;
<...query results...>
QUERY PLAN (cycles=561002 [100%])
|--SCAN t1                                           (cycles=345950 [62%] loops=1 rows=500)
`--SEARCH t2 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=128690 [23%] loops=100 rows=50)

這次,即使「SCAN t1」迴圈仍然拜訪 500 列,「SEARCH t2」查詢只執行 100 次。這是因為 SQLite 能夠捨棄不符合「t1.b<=100」約束的「t1」列。

「週期」測量是基於 CPU 時間戳記計數器,因此是牆上時鐘時間的良好代理。對於上述查詢,週期總數為 561002。對於兩個迴圈(「SCAN t1...」和「SEARCH t2...」)中的每一個,週期計數代表可直接歸因於該迴圈的運算所花費的時間。具體來說,這是花在導覽和從該迴圈的資料庫 b 樹中萃取資料的時間。這些值從未完全加總為查詢的總週期,因為 SQLite 執行其他內部運算,而這些運算無法直接歸因於任一迴圈。

「SCAN t1」迴圈的週期計數為 345950 - 查詢總數的 62%。由「SEARCH t1」迴圈執行的 100 次查詢耗費 128690 個週期,佔總數的 23%。

當使用虛擬表格時,「列」和「迴圈」指標與常規 SQLite 表格的迴圈具有相同的意義。「週期」測量是與迴圈相關的虛擬表格方法中消耗的總週期。例如

sqlite3> SELECT * FROM ft('sqlite'), t2 WHERE t2.c=ft.rowid;
<...query results...>
QUERY PLAN (cycles=836434 [100%]
|--SCAN ft VIRTUAL TABLE INDEX 0:M1                  (cycles=739602 [88%] loops=1 rows=48)
`--SEARCH t2 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=62866 [8%] loops=48 rows=25)

在此情況下,fts5 表格「ft」上的單一查詢(迴圈 = 1)傳回 48 列(列 = 48)並消耗 739602 個週期(週期 = 739602),這大約是總查詢時間的 88%。

3. 複雜案例 - 列、迴圈和週期

使用與前一節相同架構,考慮這個更複雜的範例

sqlite3> WITH cnt(i) AS (
  SELECT 1 UNION SELECT i+1 FROM cnt WHERE i<100
)
SELECT
  *, (SELECT d FROM t2 WHERE c=ft.rowid)
FROM
  (SELECT count(*), a FROM t1 GROUP BY a) AS v1 CROSS JOIN
  ft('sqlite'),
  cnt
WHERE cnt.i=ft.rowid AND v1.a=ft.rowid;
<...query results...>
QUERY PLAN (cycles=177665334 [100%])
|--CO-ROUTINE v1                                        (cycles=4500444 [3%])
|  |--SCAN t1                                           (cycles=397052 [0%] loops=1 rows=500)
|  `--USE TEMP B-TREE FOR GROUP BY
|--MATERIALIZE cnt                                      (cycles=1275068 [1%])
|  |--SETUP
|  |  `--SCAN CONSTANT ROW
|  `--RECURSIVE STEP
|     `--SCAN cnt                                       (cycles=129166 [0%] loops=100 rows=100)
|--SCAN v1                                              (loops=1 rows=500)
|--SCAN ft VIRTUAL TABLE INDEX 0:M1=                    (cycles=161874340 [91%] loops=500 rows=271)
|--SCAN cnt                                             (cycles=7336350 [4%] loops=95 rows=9500)
`--CORRELATED SCALAR SUBQUERY 3                         (cycles=168538 [0%] loops=37)
   `--SEARCH t2 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=94724 [0%] loops=37 rows=21)

上述範例最複雜的部分是了解查詢計畫 - 報告中也會由 EXPLAIN QUERY PLAN 指令產生的一部分。其他重點如下

下列查詢使用自動索引和外部排序

sqlite> SELECT * FROM
  t2,
  (SELECT count(*) AS cnt, d FROM t2 GROUP BY d) AS v2
WHERE v2.d=t2.d AND t2.d>100
ORDER BY v2.cnt;
<...query results...>
QUERY PLAN (cycles=6234376 [100%])
|--MATERIALIZE v2                                     (cycles=2351916 [38%])
|  |--SCAN t2                                         (cycles=188428 [3%] loops=1 rows=250)
|  `--USE TEMP B-TREE FOR GROUP BY
|--SCAN t2                                            (cycles=455736 [7%] loops=1 rows=250)
|--CREATE AUTOMATIC INDEX ON v2(d, cnt)               (cycles=1668380 [27%] loops=1 rows=250)
|--SEARCH v2 USING AUTOMATIC COVERING INDEX (d=?)     (cycles=932824 [15%] loops=200 rows=200)
`--USE TEMP B-TREE FOR ORDER BY                       (cycles=662456 [11%] loops=1 rows=200)

重點如下

4. 規劃器估計

除了「.scanstats on」用於啟用剖析和「.scanstats off」用於停用剖析外,shell 也接受「.scanstats est」

sqlite> .scanstats est

這會啟用一種特殊的剖析報告,其中包含與查詢剖析的每個「SCAN...」和「SEARCH...」元素相關聯的兩個額外值

sqlite> SELECT a FROM t1, t2 WHERE a IN (1,2,3) AND a=d+e ORDER BY a;
<query results...>
QUERY PLAN (cycles=264725190 [100%]
|--SEARCH t1 USING INTEGER PRIMARY KEY (rowid=?)     (cycles=60511568 [23%] loops=1 rows=3 rpl=3.0 est=3.0)
`--SCAN t2                                           (cycles=139461608 [53%] loops=3 rows=150000 rpl=50000.0 est=1048576.0)

5. 低階剖析資料

也支援「.scanstats vm」指令。這會啟用低階剖析報告,顯示每個 VM 指令執行的次數,以及執行時經過的時脈週期百分比

sqlite> .scanstats vm

然後

sqlite> SELECT count(*) FROM t2 WHERE (d % 5) = 0;
<query results...>
addr  cycles  nexec   opcode         p1    p2    p3    p4             p5  comment      
----  ------  ------  -------------  ----  ----  ----  -------------  --  -------------
0     0.0%    1       Init           1     18    0                    0   Start at 18
1     0.0%    1       Null           0     1     1                    0   r[1..1]=NULL
2     0.0%    1       OpenRead       0     2     0     2              0   root=2 iDb=0; t2
3     0.0%    1       ColumnsUsed    0     0     0     2              0   
4     0.0%    1       Explain        4     0     0     SCAN t2        0   
5     0.0%    1       CursorHint     0     0     0     EQ(REM(c1,5),0) 0   
6     0.0%    1       Rewind         0     14    0                    0   
7     46.86%  150000    Column         0     1     3                    0   r[3]= cursor 0 column 1
8     18.94%  150000    Remainder      4     3     2                    0   r[2]=r[3]%r[4]
9     5.41%   150000    ReleaseReg     3     1     0                    0   release r[3] mask 0
10    12.09%  150000    Ne             5     13    2                    80  if r[2]!=r[5] goto 13
11    1.02%   30000     ReleaseReg     2     1     0                    0   release r[2] mask 0
12    2.95%   30000     AggStep1       0     0     1     count(0)       0   accum=r[1] step(r[0])
13    12.72%  150000  Next           0     7     0                    1   
14    0.0%    1       AggFinal       1     0     0     count(0)       0   accum=r[1] N=0
15    0.0%    1       Copy           1     6     0                    0   r[6]=r[1]
16    0.0%    1       ResultRow      6     1     0                    0   output=r[6]
17    0.01%   1       Halt           0     0     0                    0   
18    0.0%    1       Transaction    0     0     1     0              1   usesStmtJournal=0
19    0.0%    1       Integer        5     4     0                    0   r[4]=5
20    0.0%    1       Integer        0     5     0                    0   r[5]=0
21    0.0%    1       Goto           0     1     0                    0   

此頁面最後修改於 2023-07-27 20:27:55 UTC