エグゼキュータ。LimitとSort
8.3からORDER BYとLIMITの組み合わせが改良されたということで、どう処理されるのか見てみた。参考にしたのは以下のページ。
マイコミジャーナルの記事ではoffsetをつけた場合には最適化が適用されないとあったが、PostgreSQL 8.3.5で試す限り、offsetをつけても最適化されているように見える(実行計画と実際の処理をdebugで確かめた)。
クエリ(explain analyze verboseつき)
1000000万件あるデータから10件を取り出す。
explain analyze verbose select * from accounts order by abalance limit 10
プランツリーと実行計画
{LIMIT :startup_cost 47485.60 :total_cost 47485.63 :plan_rows 10 :plan_width 97 :targetlist ( {TARGETENTRY :expr {VAR :varno 65001 :varattno 1 :vartype 23 :vartypmod -1 :varlevelsup 0 :varnoold 1 :varoattno 1 } :resno 1 :resname aid :ressortgroupref 0 :resorigtbl 16453 :resorigcol 1 :resjunk false } {TARGETENTRY :expr {VAR :varno 65001 :varattno 2 :vartype 23 :vartypmod -1 :varlevelsup 0 :varnoold 1 :varoattno 2 } :resno 2 :resname bid :ressortgroupref 0 :resorigtbl 16453 :resorigcol 2 :resjunk false } {TARGETENTRY :expr {VAR :varno 65001 :varattno 3 :vartype 23 :vartypmod -1 :varlevelsup 0 :varnoold 1 :varoattno 3 } :resno 3 :resname abalance :ressortgroupref 1 :resorigtbl 16453 :resorigcol 3 :resjunk false } {TARGETENTRY :expr {VAR :varno 65001 :varattno 4 :vartype 1042 :vartypmod 88 :varlevelsup 0 :varnoold 1 :varoattno 4 } :resno 4 :resname filler :ressortgroupref 0 :resorigtbl 16453 :resorigcol 4 :resjunk false } ) :qual <> :lefttree {SORT :startup_cost 47485.60 :total_cost 49985.76 :plan_rows 1000062 :plan_width 97 :targetlist ( {TARGETENTRY :expr {VAR :varno 65001 :varattno 1 :vartype 23 :vartypmod -1 :varlevelsup 0 :varnoold 1 :varoattno 1 } :resno 1 :resname aid :ressortgroupref 0 :resorigtbl 16453 :resorigcol 1 :resjunk false } {TARGETENTRY :expr {VAR :varno 65001 :varattno 2 :vartype 23 :vartypmod -1 :varlevelsup 0 :varnoold 1 :varoattno 2 } :resno 2 :resname bid :ressortgroupref 0 :resorigtbl 16453 :resorigcol 2 :resjunk false } {TARGETENTRY :expr {VAR :varno 65001 :varattno 3 :vartype 23 :vartypmod -1 :varlevelsup 0 :varnoold 1 :varoattno 3 } :resno 3 :resname abalance :ressortgroupref 1 :resorigtbl 16453 :resorigcol 3 :resjunk false } {TARGETENTRY :expr {VAR :varno 65001 :varattno 4 :vartype 1042 :vartypmod 88 :varlevelsup 0 :varnoold 1 :varoattno 4 } :resno 4 :resname filler :ressortgroupref 0 :resorigtbl 16453 :resorigcol 4 :resjunk false } ) :qual <> :lefttree {SEQSCAN :startup_cost 0.00 :total_cost 25874.62 :plan_rows 1000062 :plan_width 97 :targetlist ( {TARGETENTRY :expr {VAR :varno 1 :varattno 1 :vartype 23 :vartypmod -1 :varlevelsup 0 :varnoold 1 :varoattno 1 } :resno 1 :resname aid :ressortgroupref 0 :resorigtbl 16453 :resorigcol 1 :resjunk false } {TARGETENTRY :expr {VAR :varno 1 :varattno 2 :vartype 23 :vartypmod -1 :varlevelsup 0 :varnoold 1 :varoattno 2 } :resno 2 :resname bid :ressortgroupref 0 :resorigtbl 16453 :resorigcol 2 :resjunk false } {TARGETENTRY :expr {VAR :varno 1 :varattno 3 :vartype 23 :vartypmod -1 :varlevelsup 0 :varnoold 1 :varoattno 3 } :resno 3 :resname abalance :ressortgroupref 1 :resorigtbl 16453 :resorigcol 3 :resjunk false } {TARGETENTRY :expr {VAR :varno 1 :varattno 4 :vartype 1042 :vartypmod 88 :varlevelsup 0 :varnoold 1 :varoattno 4 } :resno 4 :resname filler :ressortgroupref 0 :resorigtbl 16453 :resorigcol 4 :resjunk false } ) :qual <> :lefttree <> :righttree <> :initPlan <> :extParam (b) :allParam (b) :scanrelid 1 } :righttree <> :initPlan <> :extParam (b) :allParam (b) :numCols 1 :sortColIdx 3 :sortOperators 97 :nullsFirst false } :righttree <> :initPlan <> :extParam (b) :allParam (b) :limitOffset <> :limitCount {CONST :consttype 20 :consttypmod -1 :constlen 8 :constbyval false :constisnull false :constvalue 8 [ 10 0 0 0 0 0 0 0 ] } } Limit (cost=47485.60..47485.63 rows=10 width=97) (actual time=3300.073..3300.073 rows=10 loops=1) -> Sort (cost=47485.60..49985.76 rows=1000062 width=97) (actual time=3300.073..3300.073 rows=10 loops=1) Sort Key: abalance Sort Method: top-N heapsort Memory: 18kB -> Seq Scan on accounts (cost=0.00..25874.62 rows=1000062 width=97) (actual time=0.000..1360.033 rows=1000000 loops=1) Total runtime: 3300.073 ms
Limitの処理はExecLimit()、Sortの処理はExecSort()で行われる。
- ExecLimit()
- offset指定なしは 0 を指定したのと同じ。
- 結果のタプルを下位ノードから受け取るが、offsetを満たすまではループして捨てる。
- 一度offsetを超えたら、limitになるまでは1件ごとに上位へタプルを返す。
- 結果がなくなるかlimitになったらNULLを返して処理終わり。
- ExecSort()
- 最初のアクセスでは、下位のノードから受け取ったタプルをひたすらtuplesort_puttupleslot()をつかってputする。今回の例だと1000000回。そしてtuplesort_performsort()でソートする。
- ソートされた結果をTupleTableSlotに詰めて返す。
- 2度目以降のアクセスではソート済みのデータを単に返すだけ。
- puttuple_common()
- tuplesort_puttupleslot()から呼び出される。
- 最初は(TSS_INITIALの状態)タプルをメモリにためていく。
- limitの2倍に達するか、メモリが足りなくなったらmake_bounded_heap()を呼び出し、limit分だけしか保持しないようにする。TSS_BOUNDEDの状態に移行する。
- TSS_BOUNDEDの状態になったら、limit分の件数だけを保持する。この例の場合、より小さい値がきたら、今までのなかでもっとも大きいデータを破棄(しているっぽい)。
まとめ
limit分の件数だけを保持し、それだけをソートするというのが8.3から最適化の内容らしい。8.2以前では、limitに関係なく全データをソートしていたのだろう。今回の例だとソート対象が10件か1000000件かだから違いは大きい。
tuplesort.cは今のところ軽くみただけ。いずれちゃんと読む。