他のより洗練された RDBMS とは対照的に、sqlite にはルールベースのクエリ オプティマイザがあります。つまり、実行計画はクエリの記述方法 (および句の順序) に大きく依存します。これにより、オプティマイザーは非常に予測可能になり、sqlite が実行計画を生成する方法を知っていれば、この予測可能性を利用して問題を解決できます。
最初のアイデアは、(num1>?) や (num1=? and num2>?) のようなさまざまな句が互いに素な結果を生成しており、これらの結果が互いに自然にソートされることに注意することです。クエリがサブクエリ (それぞれが条件の一部を処理する) に分割され、並べ替えられた結果が生成される場合、サブクエリが正しい順序で実行されていれば、すべての結果セットの連結も並べ替えられます。
たとえば、次のクエリを考えてみましょう。
select * from table1 where num1=? and num2>? order by num1,num2
select * from table1 where num1>? order by num1,num2
これらのクエリによって生成される 2 つの結果セットは互いに素であり、最初の結果セットの行は常に 2 番目の結果セットの行の前に並べられます。
2 番目のアイデアは、sqlite が LIMIT 句を処理する方法を理解することです。実際には、クエリの先頭でカウンターを宣言し、選択された行ごとにこのカウンターをデクリメントしてテストするため、クエリを早期に停止できます。
たとえば、次のクエリを考えてみましょう。
.explain
explain select * from (
select * from table1 where num1=? and num2>?
union all
select * from table1 where num1>?
) limit 10;
sqlite は、クエリで指定された順序でサブクエリを評価します。最初のサブクエリが 10 を超える行を返す場合、2 番目のサブクエリは実行されません。プランを表示することで簡単に確認できます。
addr opcode p1 p2 p3 p4 p5 comment
---- ------------- ---- ---- ---- ------------- -- -------------
0 Trace 0 0 0 00
1 Integer 10 1 0 00
2 Variable 1 2 2 00
3 Goto 0 44 0 00
4 OpenRead 3 3 0 keyinfo(6,BINARY,BINARY) 00
5 SCopy 2 4 0 00
6 IsNull 4 23 0 00
7 SCopy 3 5 0 00
8 IsNull 5 23 0 00
9 Affinity 4 2 0 cd 00
10 SeekGt 3 23 4 2 00
11 IdxGE 3 23 4 1 01
12 Column 3 1 6 00
13 IsNull 6 22 0 00
14 Column 3 0 7 00
15 Column 3 1 8 00
16 Column 3 2 9 00
17 Column 3 3 10 00
18 Column 3 4 11 00
19 Column 3 5 12 00
20 ResultRow 7 6 0 00
21 IfZero 1 23 -1 00
22 Next 3 11 0 00
23 Close 3 0 0 00
24 IfZero 1 43 0 00
25 Variable 3 13 1 00
26 OpenRead 4 3 0 keyinfo(6,BINARY,BINARY) 00
27 SCopy 13 14 0 00
28 IsNull 14 42 0 00
29 Affinity 14 1 0 c 00
30 SeekGt 4 42 14 1 00
31 Column 4 0 6 00
32 IsNull 6 41 0 00
33 Column 4 0 7 00
34 Column 4 1 8 00
35 Column 4 2 9 00
36 Column 4 3 10 00
37 Column 4 4 11 00
38 Column 4 5 12 00
39 ResultRow 7 6 0 00
40 IfZero 1 42 -1 00
41 Next 4 31 0 00
42 Close 4 0 0 00
43 Halt 0 0 0 00
44 Transaction 0 0 0 00
45 VerifyCookie 0 3 0 00
46 TableLock 0 2 0 table1 00
47 Goto 0 4 0 00
カウンタはステップ 1 で宣言され、ステップ 21、24、40 でデクリメント/テストされます。
これら 2 つの注意事項を組み合わせることで、きれいではありませんが、効率的な実行計画を生成するクエリを提案できます。
SELECT * FROM (
SELECT * FROM ( SELECT * FROM table1
WHERE num1 == ? AND num2 == ? AND num3 == ? AND num4 == ? AND num5 == ? AND num6 > ?
ORDER BY num1, num2, num3, num4, num5, num6 )
UNION ALL
SELECT * FROM ( SELECT * FROM table1
WHERE num1 == ? AND num2 == ? AND num3 == ? AND num4 == ? AND num5 > ?
ORDER BY num1, num2, num3, num4, num5, num6 )
UNION ALL
SELECT * FROM ( SELECT * FROM table1
WHERE num1 == ? AND num2 == ? AND num3 == ? AND num4 > ?
ORDER BY num1, num2, num3, num4, num5, num6 )
UNION ALL
SELECT * FROM ( SELECT * FROM table1
WHERE num1 == ? AND num2 == ? AND num3 > ?
ORDER BY num1, num2, num3, num4, num5, num6 )
UNION ALL
SELECT * FROM ( SELECT * FROM table1
WHERE num1 == ? AND num2 > ?
ORDER BY num1, num2, num3, num4, num5, num6 )
UNION ALL
SELECT * FROM ( SELECT * FROM table1
WHERE num1 > ?
ORDER BY num1, num2, num3, num4, num5, num6 )
) LIMIT ?;
外側のクエリでは「order by」句は必要ないため、sqlite がすべてのサブクエリを実行する必要がないことに注意してください。したがって、正しい行数になったら停止できます。サブクエリの順序は重要です。
「ユニオンオール」の前に「オーダーバイ」を使用できないため、第 2 レベルの内部サブクエリが必要です。それらは sqlite によって最適化されているため、問題にはなりません。
777K 行を含むダミー テーブルでは、最初のクエリのコストは次のとおりです。
strace -c -eread,lseek sqlite3 toto.db < q1.sql
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
63.57 0.001586 0 18556 read
36.43 0.000909 0 18544 lseek
------ ----------- ----------- --------- --------- ----------------
100.00 0.002495 37100 total
私の費用は次のとおりです。
strace -c -eread,lseek sqlite3 toto.db < q3.sql
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
-nan 0.000000 0 18 read
-nan 0.000000 0 8 lseek
------ ----------- ----------- --------- --------- ----------------
100.00 0.000000 26 total