第3回:PostgreSQL – SQLチューニング

技術者向け・データベースの技術情報発信

はじめに

こんにちは、マルチDBソリューション部の川本です。今回の技術情報は、PostgreSQL9.5(Amazon RDS for PostgreSQL)で実施したSQLチューニングについてです。

対象は、AテーブルからBテーブルを引き算し、差分抽出を行うSQLです。

ベン図でいうと以下の通りです。

dbexpert_03_01.png
  • Aテーブルはバッチ処理によりCSVデータを元に毎朝作成されます。データ件数は約1400万件あり、3万件/日ずつ増加します。(INSERTのみ)
【インポートコマンド】
bunzip2 -c A.csv.bz2|psql ... -c "copy A (...) from stdin with csv header;"
  • Bテーブルは上記処理の直前にAテーブルをリネームして再作成されます。
【Bテーブル作成コマンド】
drop table B;
alter table A rename to B;

テーブル同士の引き算なので、まずはじめは「EXCEPT」で実行してみました。
まずは、A-Bが何件になるのか、PKとなっている列のみを指定し確認してみます。
【EXCEPT(PK列のみ)】
explain analyze select f_date, job_no from a except select f_date, job_no from b;
                                                                                         QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------
 SetOp Except  (cost=5726757.03..5943140.85 rows=1444107 width=12) (actual time=266861.353..266963.120 rows=31043 loops=1)
   ->  Sort  (cost=5726757.03..5798884.97 rows=28851176 width=12) (actual time=199330.965..235575.801 rows=28851575 loops=1)
         Sort Key: "*SELECT* 1".f_date, "*SELECT* 1".job_no
         Sort Method: external merge  Disk: 846080kB
         ->  Append  (cost=0.56..1165664.52 rows=28851176 width=12) (actual time=3.410..148250.519 rows=28851575 loops=1)
               ->  Subquery Scan on "*SELECT* 1"  (cost=0.56..583459.23 rows=14441067 width=12) (actual time=3.408..45819.531 rows=14441309
                     ->  Index Only Scan using pk_a on a  (cost=0.56..439048.57 rows=14441067 width=12) (actual time=3.404..
                           Heap Fetches: 0
               ->  Subquery Scan on "*SELECT* 2"  (cost=0.56..582205.29 rows=14410109 width=12) (actual time=0.080..45535.699 rows=14410266
                     ->  Index Only Scan using pk_b on b  (cost=0.56..438104.20 rows=14410109 width=12) (act
                           Heap Fetches: 0
 Planning time: 0.121 ms
 Execution time: 267124.140 ms
(13 行)

A-Bは31043行となり、3万件/日の情報ともあっているので問題なさそうです。
次に取得したいテーブル全列を指定してSQLを実行してみます。
【EXCEPT】
explain analyze select * from a except select * from b;
^CCancel request sent
ERROR:  canceling statement due to user request
        

全然帰ってこないので、いったん処理を中断しました。

EXCEPTについて、他の書き方が無いか調べてみると、LEFT OUTER JOINしてから、NULLのものだけ抽出する処理に置き換えられそうなので、SQLを書き換えて実行してみました。
【LEFT OUTER JOIN+IS NULL】
explain analyze 
 select a.* from a left outer join b on a.f_date=b.f_date and a.job_no=b.job_no where b.job_no is null;
                                                                                 QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------
 Merge Anti Join  (cost=1.12..45711313.54 rows=294152 width=1821) (actual time=761117.664..763055.743 rows=31043 loops=1)
   Merge Cond: ((a.f_date = b.f_date) AND (a.job_no = b.job_no))
   ->  Index Scan using pk_a on a  (cost=0.56..45087508.35 rows=14441067 width=1821) (actual time=0.009..702237.768 rows=1
   ->  Index Only Scan using pk_b on b  (cost=0.56..438104.20 rows=14410109 width=12) (actual time=0.011..
         Heap Fetches: 0
 Planning time: 0.690 ms
 Execution time: 763086.863 ms ---> 約13分
(7 行)

結果は、31043行取得で、実行時間が約13分で帰ってくるようになりました。

上記実行計画の中で、pk_aを使用したIndex Scanのコストが、pk_bを使用したIndex Only Scanより10倍くらい大きいので、この部分を何とかできないかいろいろ試行錯誤してみると、以下のようにSQLを書き換えることで早くなりました。
【INNER JOIN+LEFT OUTER JOIN】
explain analyze 
 select c.* from a c inner join (
 select a.f_date,a.job_no from a left outer join b on a.f_date=b.f_date and a.job_no=b.job_no where b.job_no is null) b 
 on c.f_date=b.f_date and c.job_no=b.job_no
;
                                                                                    QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=1.68..3342052.84 rows=99809 width=1821) (actual time=63039.197..65246.290 rows=31043 loops=1)
   ->  Merge Anti Join  (cost=1.12..1062853.75 rows=294152 width=12) (actual time=63038.419..63138.294 rows=31043 loops=1)
         Merge Cond: ((a.f_date = b.f_date) AND (a.job_no = b.job_no))
         ->  Index Only Scan using pk_a on a  (cost=0.56..439048.57 rows=14441067 width=12) (actual time=0.011..15599.310
               Heap Fetches: 0
         ->  Index Only Scan using pk_b on b  (cost=0.56..438104.20 rows=14410109 width=12) (actual time=0
               Heap Fetches: 0
   ->  Index Scan using pk_a on a c  (cost=0.56..7.74 rows=1 width=1821) (actual time=0.061..0.063 rows=1 loops=31043)
         Index Cond: ((f_date = a.f_date) AND (job_no = a.job_no))
 Planning time: 1.030 ms
 Execution time: 65277.855 ms ---> 1分5秒
(11 行)

結果は、31043行取得で、実行時間が約1分で帰ってくるようになりました。

SQLのチューニングポイントは以下となります。
  • 先にaテーブルとbテーブルのpk列同士で外部結合+null値を行い、pkの差分抽出を行う。
  • 差分抽出したpkを元に、全カラムを取得する