ぱと隊長日誌

ブログ運用もエンジニアとしての生き方も模索中

PostgreSQL の実行計画に現れる One-Time Filter の読み解き方

テーマ

PostgreSQLの実行計画に現れる One-Time Filter とは、クエリ―実行時に1回だけ評価すればよいフィルターのことです。

例えば、次のようなクエリーで現れます。

SELECT * FROM pg_type WHERE CURRENT_DATE = '2018-12-01'::date;

このWHERE句はクエリー実行時に1度だけ評価すれば決まります。このような場合、実行計画に One-Time Filter として出力されます。

この One-Time Filter について、以下の観点で調べてみました。

  • Filter と One-Time Filter の違い
  • One-Time Filter の出現パターン

検証は PostgreSQL 11.0 で行いました。

なお、PostgreSQLのバージョンによって実行計画の出力結果に若干の違いがあるようですので、ご注意ください。

Filter と One-Time Filter の違い

実際の実行計画で確認します。

(1) Filter のみ
=# EXPLAIN ANALYZE SELECT * FROM pg_type WHERE typtype = 'b';
                                               QUERY PLAN
--------------------------------------------------------------------------------------------------------
 Seq Scan on pg_type  (cost=0.00..13.75 rows=142 width=252) (actual time=0.013..0.131 rows=143 loops=1)
   Filter: (typtype = 'b'::"char")
   Rows Removed by Filter: 239

Filter はそのノードで取得した各行に対して評価を行います。(1)だと pg_type の382行のうち、239行がフィルタによって削除され、143行が残ったとわかります。

(2) One-Time Filter のみ
=# EXPLAIN ANALYZE SELECT * FROM pg_type WHERE CURRENT_DATE = '2018-12-01'::date;
                                       QUERY PLAN
-----------------------------------------------------------------------------------------
 Result  (cost=0.01..12.80 rows=380 width=252) (actual time=0.004..0.004 rows=0 loops=1)
   One-Time Filter: (CURRENT_DATE = '2018-12-01'::date)
   ->  Seq Scan on pg_type  (cost=0.01..12.80 rows=380 width=252) (never executed)

One-Time Filter は1回限りの評価です。(2)では One-Time Filter が先に評価され、WHERE句を満たさないことが分かったため、pg_type のスキャンは実行されませんでした。

(3) Filter と One-Time Filter
=# EXPLAIN ANALYZE SELECT * FROM pg_type WHERE typtype = 'b' AND CURRENT_DATE = '2018-12-01'::date;
                                       QUERY PLAN
-----------------------------------------------------------------------------------------
 Result  (cost=0.01..13.75 rows=142 width=252) (actual time=0.004..0.004 rows=0 loops=1)
   One-Time Filter: (CURRENT_DATE = '2018-12-01'::date)
   ->  Seq Scan on pg_type  (cost=0.01..13.75 rows=142 width=252) (never executed)
         Filter: (typtype = 'b'::"char")

WHERE句に Filter と One-Time Filter となる条件を組み合わせた場合が(3)になります。Filter は Seq Scan ノードで、One-Time Filter は Result ノードで評価されています。この場合も(2)と同様に One-Time Filter が先に評価され、Seq Scan は実行されませんでした。

One-Time Filter の出現パターン

調査の過程で見つかった主なパターンをご紹介します。他にもあればぜひご連絡ください。

PostgreSQLプランナはプランニングの段階で評価結果の true / false が明確な場合、それに応じたプランを生成しているようです。

(4)
=# EXPLAIN SELECT * FROM pg_type WHERE true;
                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on pg_type  (cost=0.00..12.80 rows=380 width=252)
(5)
=# EXPLAIN SELECT * FROM pg_type WHERE 'a' IN ('a', 'b', 'c');
                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on pg_type  (cost=0.00..12.80 rows=380 width=252)

(4)のようにWHERE句が true であることが明確な場合、One-Time Filter が出力されません。これは(5)のようにプランニングの段階で評価可能な場合も含まれています。

(6)
=# EXPLAIN SELECT * FROM pg_type WHERE false;
                 QUERY PLAN
--------------------------------------------
 Result  (cost=0.00..0.00 rows=0 width=235)
   One-Time Filter: false
(7)
=# EXPLAIN SELECT * FROM pg_type WHERE 'd' IN ('a', 'b', 'c');
                 QUERY PLAN
--------------------------------------------
 Result  (cost=0.00..0.00 rows=0 width=235)
   One-Time Filter: false

(6)のようにWHERE句が false であることが明確な場合、One-Time Filter は出力されますが、スキャン(例:Seq Scan)は出力されません。プランニングの段階で評価可能な(7)でも同様です。

(8)
=# EXPLAIN SELECT * FROM pg_type WHERE CURRENT_DATE = '2018-12-01'::date;
                            QUERY PLAN
------------------------------------------------------------------
 Result  (cost=0.01..12.80 rows=380 width=252)
   One-Time Filter: (CURRENT_DATE = '2018-12-01'::date)
   ->  Seq Scan on pg_type  (cost=0.01..12.80 rows=380 width=252)

(8)の場合はプランニングの段階でWHERE句を評価できないため、そのまま One-Time Filter に出力されています。実行する際に One-Time Filter を評価し、条件を満たさなければ下位ノードの Seq Scan は実行されません。

(9)
=# EXPLAIN SELECT * FROM pg_type WHERE typtype = 'b' AND EXISTS (SELECT 1 WHERE 1+1=3);
                            QUERY PLAN
------------------------------------------------------------------
 Result  (cost=0.01..13.76 rows=142 width=252)
   One-Time Filter: $0
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.01 rows=1 width=0)
           One-Time Filter: false
   ->  Seq Scan on pg_type  (cost=0.01..13.76 rows=142 width=252)
         Filter: (typtype = 'b'::"char")

(9)の場合、WHERE句が false となることは明白ですが、プランニングの段階ではそこまで評価せず、そのままプランに出力されています。

EXISTS句が "One-Time Filter: $0" として出力されています。これは EXISTS句内のクエリがその他のクエリの結果に依存しておらず、単独で実行可能かつ1回限りの評価でよいためと思われます。

EXISTS句内の "SELECT 1 WHERE 1+1=3" は "InitPlan 1 (returns $0)" として出力されています。このSELECT結果が $0 に代入され、"One-Time Filter: $0" で評価されます。

このケースで Seq Scan は実行されません。

参考資料

PostgreSQL プラン・ツリーの概要
Resultノードの説明の中で One-Time Filter について少し触れられています。

特定の分野を究めるとY型人材になる

先日、こんなツイートを見かけました。

これは自分の経験からもいえ、かつチームメンバーにも繰り返し話していることそのものでした。そこで、この機会にまとめておくことにします。

私は茶道を学んでいたことがあります。茶道の稽古のメインは作法や点て方ですが、毎回の稽古は床の間にある掛物(掛け軸)と花を拝見するところから始まりました。そして、掛け軸の言葉の意味や花について説明がありました。
掛け軸は書道、花は華道へとつながります。つまり、茶道を究めようとすれば、書道と華道についての理解も必要になるのですよ、という教えでした。

私はITエンジニアです。そこで例えるのであれば、データベースを究めようと思うと、データベース製品に関する知識・経験だけでなく、CPU・メモリ・ストレージ・ネットワーク・OS・アプリケーションなど、様々なレイヤーの知識と経験が求められます。

つまり、どんな分野であっても、何かを究めるには周辺分野に関する理解が必要になるということです。
ある分野を究めていけば、自分の持っている知識や経験だけでは進めなくなる地点があります。そして、自分に何が足りないのかを気づく瞬間があります。それはもしかしたら自分にとって興味のない分野かもしれません。そこに時間を割くことは回り道に見えるかもしれません。でも、それに取り組むことが、さらに進むために必要なことなのです。

〇(I,T,Π...)型人材とよく例えられますが、今回のケースでいえばY型人材といえるのではないでしょうか。Yの下半分の棒は究めようとしている分野であり、Yの上半分はそれを支える土台です。
土台をイメージするのであれば、逆Yと例えたほうがよいかもしれません。しっかりとした土台があってこそ、高みを目指すことができます。

何かを深く追求すれば、いつか壁にぶつかります。それは自分に足りないところがあるからかもしれません。そこに気が付いたのであれば、それを補う努力をすることが必要です。今の自分の範囲内で済まそうとしても、またすぐに壁にぶつかることになります。

土台を築くことは単なる回り道ではありません。そこで新たな発見があるかもしれないし、別のことを究める土台にもなりえます。そしてなにより、自分の幅が広がることになります。

自分が興味を持ったことにとことん打ち込むことで、必然的に周辺知識や経験を身につける。それはY型人材の戦略といえるのではないでしょうか。

PostgreSQL にてWHERE句で=演算子の左辺・右辺に同じ列名を指定すると行数推定が不正確になることがある

はじめに

WHERE句で=演算子の左辺・右辺に同じ列名を指定したクエリを考えます。

SELECT count(*) FROM seq WHERE value = value;

これは以下のクエリと等価です。

SELECT count(*) FROM seq WHERE value IS NOT NULL;

value = valuevalue = NULL であれば、NULL = NULL となり、これは unknown と評価されるためです。

NULL に比較述語を適用した結果が、常に unknown になってしまうからです。クエリの結果として選択されるのは、WHERE句の条件評価がtrueになる行のみです。false や unknown の行は選択されません。
引用:達人に学ぶ SQL徹底指南書 1-3 3値論理とNULL

達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)

達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)

  • 作者:ミック
  • 発売日: 2008/02/07
  • メディア: 単行本(ソフトカバー)

冒頭の2つのクエリが等価であれば実行計画も同じになりそうなものですが、PostgreSQL 10 以前では行数推定が不正確となるケースがあります。これを実例で確認します。また、比較として Oracle Database でも確認しました。

挙動検証

PostgreSQL 10

PostgreSQL 10.3 で検証を行いました。
また、これより以前のバージョンでも同様の事象を確認しました。

=# CREATE TABLE seq(value integer);

=# INSERT INTO seq(value)
-#   SELECT generate_series(1, 999);

=# INSERT INTO seq(value) VALUES (NULL);

=# vacuum analyze seq;

=# EXPLAIN SELECT count(*) FROM seq;
                         QUERY PLAN
-------------------------------------------------------------
 Aggregate  (cost=17.50..17.51 rows=1 width=8)
   ->  Seq Scan on seq  (cost=0.00..15.00 rows=1000 width=0)
(2 rows)

=# EXPLAIN SELECT count(*) FROM seq WHERE value = value;
                        QUERY PLAN
----------------------------------------------------------
 Aggregate  (cost=17.51..17.52 rows=1 width=8)
   ->  Seq Scan on seq  (cost=0.00..17.50 rows=5 width=0)
         Filter: (value = value)
(3 rows)

=# EXPLAIN SELECT count(*) FROM seq WHERE value IS NOT NULL;
                         QUERY PLAN
------------------------------------------------------------
 Aggregate  (cost=17.50..17.51 rows=1 width=8)
   ->  Seq Scan on seq  (cost=0.00..15.00 rows=999 width=0)
         Filter: (value IS NOT NULL)
(3 rows)

WHERE句無し、もしくは value IS NOT NULL のケースでは Seq Scan の行数推定(rows)の値が正確です。

ですが、value = value のケースでは Seq Scan の行数推定(rows)の値が不正確です。本来は999となるべきところが5と推定されています。これは=演算子の左辺・右辺に同じ列名が指定されるケースをプランナが考慮していないために起こっています。

PostgreSQL 11

PostgreSQL 11.0 で検証を行いました。

=# CREATE TABLE seq(value integer);

=# INSERT INTO seq(value)
-#   SELECT generate_series(1, 999);

=# INSERT INTO seq(value) VALUES (NULL);

=# vacuum analyze seq;

=# EXPLAIN SELECT count(*) FROM seq;
                         QUERY PLAN
-------------------------------------------------------------
 Aggregate  (cost=17.50..17.51 rows=1 width=8)
   ->  Seq Scan on seq  (cost=0.00..15.00 rows=1000 width=0)
(2 rows)

testdb=# EXPLAIN SELECT count(*) FROM seq WHERE value = value;
                         QUERY PLAN
------------------------------------------------------------
 Aggregate  (cost=17.50..17.51 rows=1 width=8)
   ->  Seq Scan on seq  (cost=0.00..15.00 rows=999 width=0)
         Filter: (value IS NOT NULL)
(3 rows)

=# EXPLAIN SELECT count(*) FROM seq WHERE value IS NOT NULL;
                         QUERY PLAN
------------------------------------------------------------
 Aggregate  (cost=17.50..17.51 rows=1 width=8)
   ->  Seq Scan on seq  (cost=0.00..15.00 rows=999 width=0)
         Filter: (value IS NOT NULL)
(3 rows)

value = value のケースでも Seq Scan の行数推定(rows)の値が正確となりました。これは PostgreSQL 11 で改良されました。

Reduce var = var to var IS NOT NULL where equivalent (Tom Lane)

PostgreSQL: Documentation: 11: E.11. Release 11

改良に関する情報は以下のコミットログ及びディスカッションを参照ください。

◆ コミットログ
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=8ec5429e2f422f4d570d4909507db0d4ca83bbac

◆ ディスカッション
PostgreSQL: Discussion on missing optimizations

ディスカッションのスレッドでは value = value をヒント句代わりに利用しているケースがあること、それは PostgreSQL 11 で書き換えが必要であることも述べられています。

The tautological col = col comparison on is occasionally used as a planner "hint" to correct for row count overestimates. Not a great solution, but PostgreSQL doesn't really have a better way to guide the planner. Those queries will now have to do something else, like col = col + 0, which still works.

PostgreSQL: Re: Discussion on missing optimizations

Oracle Database

Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 で検証を行いました。

SQL> CREATE TABLE seq(value integer);

SQL> INSERT INTO seq(value)
SQL>   SELECT LEVEL FROM DUAL CONNECT BY LEVEL <= 999;

SQL> INSERT INTO seq(value) VALUES (NULL);

SQL> COMMIT;

SQL> exec DBMS_STATS.GATHER_SCHEMA_STATS(NULL);

SQL> EXPLAIN PLAN FOR SELECT count(*) FROM seq;

SQL> SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());

-------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Cost (%CPU)| Time     |
-------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |      |     1 |            |          |
|   2 |   TABLE ACCESS FULL| SEQ  |  1000 |     2   (0)| 00:00:01 |
-------------------------------------------------------------------

SQL> EXPLAIN PLAN FOR SELECT count(*) FROM seq WHERE value = value;

SQL> SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |     4 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |      |     1 |     4 |            |          |
|*  2 |   TABLE ACCESS FULL| SEQ  |   999 |  3996 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
   2 - filter("VALUE" IS NOT NULL)

SQL> EXPLAIN PLAN FOR SELECT count(*) FROM seq WHERE value IS NOT NULL;

SQL> SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());

---------------------------------------------------------------------------
| Id  | Operation          | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     1 |     4 |     2   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE    |      |     1 |     4 |            |          |
|*  2 |   TABLE ACCESS FULL| SEQ  |   999 |  3996 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
   2 - filter("VALUE" IS NOT NULL)

全てのケースで TABLE ACCESS FULL の Rows は正しく見積もられています。

また、刮目すべき点として、value = value のケースで "VALUE" IS NOT NULL のフィルタが適用されています。つまり、プランナは value = valuevalue IS NOT NULL に等しいと判断し、書き換えを行ったと推測されます。

まとめ

クエリに=演算子の左辺・右辺に同じ列名を指定する条件 (value = value) を組み込み、結果として正しく動いたとしても、プランナを混乱させる元となります。それは、不適切な実行計画が選択され、性能劣化の一因となるかもしれません。

冒頭の説明で示した通り、value = valuevalue IS NOT NULL と等価であったりするので、クエリ見直しの際は value = value を単に削除するのではなく、それに伴う影響を十分に検討したうえで書き換えを行いましょう。

更新情報

2018/10/22

PostgreSQL 11 についての情報を追加しました。

2018/10/28

PostgreSQL 11 の検証結果を追加しました。また、これに伴い一部修正を行いました。

PostgreSQL 11 で改良されたことを教えてくださった @nori_shinoda さんと @tjtakahashi さん に深く感謝申し上げます。