小巧、快速、可靠。
三者选其二。
SQL 查询分析

1. 概述

SQLite 内置了对 SQL 查询进行分析的支持,但默认情况下未启用。为了启用对查询分析的支持,SQLite 必须使用 以下选项 编译。

-DSQLITE_ENABLE_STMT_SCANSTATUS

使用此选项构建 SQLite 将启用 sqlite3_stmt_scanstatus_v2() API,该 API 提供对各种分析指标的访问。本页的其余部分讨论了 SQLite 命令行 shell 使用这些指标生成的分析报告,而不是直接使用 API。

shell 生成的分析报告与 EXPLAIN QUERY PLAN 命令生成的查询计划报告非常相似。本页假设读者熟悉此格式。

在使用上述选项编译的命令行 shell 中,使用 ".scanstats on" 命令启用查询分析。

sqlite> .scanstats on

启用后,shell 会在执行每个 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)

上面示例中 ".scanstats on" 后面的文本是刚刚执行的联接查询的分析报告。分析报告中与 EXPLAIN QUERY PLAN 输出类似的部分表明查询是通过对表 "t1" 进行全表扫描来实现的,并对表 "t2" 上的每个访问行进行 INTEGER PRIMARY KEY 查询。

"SCAN t1" 行上的 "loops=1" 表示此循环(对表 "t1" 进行全表扫描)恰好运行了一次。"rows=500" 表示该次扫描访问了 500 行。

"SEARCH t2 USING ..." 行包含注释 "loops=500",表示此 "循环"(实际上是通过 INTEGER PRIMARY KEY 查询)运行了 500 次。这很有道理 - 它针对 "t1" 的全表扫描访问的每一行运行一次。"rows=250" 表示总的来说,这 500 个循环访问了 250 行。换句话说,只有表 t2 上的一半 INTEGER PRIMARY KEY 查询成功,另一半查询没有找到任何行。

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" 行。

"cycles" 测量值基于 CPU 时间戳计数器,因此是实际时间的良好指标。对于上面的查询,总循环数为 561002。对于两个循环("SCAN t1..." 和 "SEARCH t2..."),循环计数表示在可以直接归因于该循环的运算中花费的时间。具体来说,这是在该循环的数据库 B 树中进行导航和提取数据所花费的时间。这些值永远无法完全加起来成为查询的总循环数,因为 SQLite 执行的其他内部运算不直接归因于任何循环。

"SCAN t1" 循环的循环计数为 345950,约占查询总时间的 62%。"SEARCH t1" 循环执行的 100 次查询使用了 128690 个循环,约占总时间的 23%。

当使用虚拟表时,"rows" 和 "loops" 指标与常规 SQLite 表的循环具有相同的含义。"cycles" 测量值是在与循环相关的虚拟表方法中消耗的总循环数。例如

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" 的单个查询(loops=1)返回了 48 行(rows=48),并消耗了 739602 个循环(cycles=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