假设我有一个如下所示的架构:
-- Many rows
CREATE TABLE t1(i INTEGER PRIMARY KEY, c1 INTEGER, c2 INTEGER);
-- t1's rows with c1 even
CREATE VIEW t1_filtered(i, c1, c2) AS
SELECT i, c1, c2 FROM t1 WHERE c1 % 2 == 0;
-- The real WHERE clause is slightly more complex.
假设该表t1
包含数百万行:
INSERT INTO t1(i, c1, c2)
SELECT value, random(), random() FROM generate_series(1, 5000000);
t1
假设我想获取具有最高偶数的行的索引以及具有偶数的c1
行的计数:c1
c2
SELECT
(SELECT i FROM t1_filtered ORDER BY c1 DESC LIMIT 1),
(SELECT count(*) FROM t1_filtered WHERE c2 % 2 == 0);
真正的ORDER BY
子句要复杂得多,但这足以说明我的问题。
在我看来,这应该只需要一次扫描就可以实现t1
,但是EXPLAIN QUERY PLAN
说这个查询扫描了t1
两次:
QUERY PLAN
|--SCAN CONSTANT ROW
|--SCALAR SUBQUERY 1
| |--SCAN t1
| `--USE TEMP B-TREE FOR ORDER BY
`--SCALAR SUBQUERY 2
`--SCAN t1
如果我加入两个子查询而不是将它们写为结果列,那么查询计划会有所不同,但仍然有两次扫描t1
:
QUERY PLAN
|--CO-ROUTINE (subquery-1)
| |--SCAN t1
| `--USE TEMP B-TREE FOR ORDER BY
|--MATERIALIZE (subquery-2)
| `--SCAN t1
|--SCAN (subquery-1)
`--SCAN (subquery-2)
无论如何,我希望这个查询是这样的伪代码:
var top_row = {i: NULL, c1: 0};
var count = 0;
for each {i, c1, c2} in t1:
if c1 % 2 == 0:
if c1 > top_row.c1:
top_row = {i, c1};
if c2 % 2 == 0:
count = count + 1;
return {top_row.i, count};
我怎样才能让查询规划器知道这只需要一次扫描?
更新,2024-05-09:我尝试了Charlieface 提出的查询。根据EXPLAIN QUERY PLAN
,它确实使查询规划器仅使用一次扫描t1
......
QUERY PLAN
|--CO-ROUTINE t
| |--CO-ROUTINE (subquery-4)
| | |--SCAN t1
| | `--USE TEMP B-TREE FOR ORDER BY
| `--SCAN (subquery-4)
`--SCAN t
...但它的运行速度明显比我的原始查询慢:使用测试SQLite REPLSELECT (...), (...)
中的两个查询,我发现(对于包含 500 万行随机数据的示例表)我的原始查询的平均运行时间为 2.22 秒样本标准差为 0.03 秒,该查询的平均值为 5.57 秒。开发人员。0.23秒。.timer on
t1
我的直觉直觉并没有向我建议,但研究查询计划的“USE TEMP B-TREE FOR ORDER BY”确实建议我在t1(c1)
. 这确实加快了我原来的查询速度,使得 st 平均需要 1.09 秒。开发人员。0.02秒。然而,令我惊讶的是,索引显然使 Charlieface 的查询花费了更长的时间,甚至可能使其无法终止——我在等待 108 秒后中断了它,然后在重试 32 秒后中断了它,然后我没有'不要再试一次。
Charlieface 的查询确实通过将扫描次数减少t1
到 1 来回答了我的问题,但相对于我原来的查询,它的实际性能较差,使我不愿意接受它作为答案。我希望这不会“移动球门柱”太多。我确实将其标记为query-performance
,因此性能从一开始就是我问题的一部分。
更新,2024-05-09 #2:使用CREATE INDEX t1_c1 ON t1(c1)
,我的原始查询的查询计划变为
QUERY PLAN
|--SCAN CONSTANT ROW
|--SCALAR SUBQUERY 1
| `--SCAN t1 USING COVERING INDEX t1_c1
`--SCALAR SUBQUERY 2
`--SCAN t1
Charlieface 查询的查询计划变为
QUERY PLAN
|--CO-ROUTINE t
| |--CO-ROUTINE (subquery-4)
| | `--SCAN t1 USING INDEX t1_c1
| `--SCAN (subquery-4)
`--SCAN t
根据Charlieface 的评论CREATE INDEX index_per_comment660077_339327 ON t1 (c1 DESC) WHERE (c1 % 2 = 0)
(SQLite 不支持),查询计划分别变为:INCLUDE
QUERY PLAN
|--SCAN CONSTANT ROW
|--SCALAR SUBQUERY 1
| `--SCAN t1 USING COVERING INDEX index_per_comment660077_339327
`--SCALAR SUBQUERY 2
`--SCAN t1 USING INDEX index_per_comment660077_339327
QUERY PLAN
|--CO-ROUTINE t
| |--CO-ROUTINE (subquery-4)
| | `--SCAN t1 USING INDEX index_per_comment660077_339327
| `--SCAN (subquery-4)
`--SCAN t
对于 SQLite,如果聚合查询包含单个 min() 或 max() 函数,则输出中使用的列的值取自获得 min() 或 max() 值的行。这是一个特点。
该查询不需要索引,并且具有以下简单的查询计划:
该查询将在一次扫描中计算
MAX(c1)
和,并将返回在实现的同一行中找到的值。COUNT(*)
i
MAX(c1)
EXPLAIN 将显示 SQLite OPcodes 中已编译的查询,您可以看到生成的代码与您想要的伪代码完全相同。
您可以使用行编号和条件聚合