【PostgreSQL】同じ計算が複数出る問い合わせにlateralは有効か?
今回やること
表題の通り。A
を求めれるのにカラムXとY
が必要で、B
やC
も同様にカラムXとY
あるいはA
の結果が必要なケースを想定している。
group byなどの集約に関するパフォーマンスの向上などについては触れられている記事を見かけるが、あまり表題の使い方について触れている記事が見当たらなかった。 見つけられなかっただけかもしれない。。
結論から言うと、こういう使い方でもパフォーマンスは上がった(ここでは書かないが、逆に、同じ計算が複数回出ないのにlateralを使うと遅くなってしまう。恐らくNestedLoop分のコスト。)。 個人的には可読性も上がるので、使うタイミングさえ見誤らなければ積極的に使っていきたい。
また、「副問い合わせのネストした箇所で計算すれば?」という意見はあるだろうが、多分正論。でも個人的にネスト量が増えるのが好きではないのです…。いやまあ、lateralも副問い合わせだし、ネストしてるけど。少なくともメインのfromは副問い合わせじゃないじゃないですかー…。
環境
- PostgreSQL 11.8, compiled by Visual C++ build 1914, 64-bit
テスト用テーブル
確認用に3つのテーブルを用意した。 今回は意味のある計算は一切考えないが、「商品」と「税」と「データ」を対象に、「数量 * 単価」と「数量 * 単価 * 税率」と「数量 * 単価 + 数量 * 単価 * 税率」を求めることとした。
create table item ( code text, price numeric, tax_code text, PRIMARY KEY (code) ); create table tax ( code text, rate numeric, PRIMARY KEY (code) ); create table data ( record_id bigserial, item_code text, quantity numeric, PRIMARY KEY (record_id) );
テストデータの作成
1万件程度で試したい(10万を超えるような仕事をしていないので…)。 全部同じだと計算にキャッシュが効きそうなイメージなので、一応多少はばらけるようにしているつもりだ。意味あるかもばらけているかもわからないが。
with a as( select generate_series(1, 26) c ) insert into item select chr(64 + c) as code, c * 100 as price, (c % 3)::text as tax_code from a ; insert into tax values('0', 0) ,('1', 8) ,('2', 10) ; with a as( select generate_series(0, 10000) c ) insert into data(item_code, quantity) select chr(65 + (c % 26)) as item_code, c % 10 + 1 as quantity from a ;
素直に書いた場合
SELECT句にて計算を全て書く方法
select data.record_id ,data.quantity * item.price as r1 ,data.quantity * item.price * tax.rate / 100::numeric as r2 ,data.quantity * item.price + data.quantity * item.price * tax.rate / 100::numeric as r3 from data left join item on data.item_code = item.code left join tax on item.tax_code = tax.code
lateralを用いた場合
lateralで複数回用いる値を先に計算する形(money
とtax_money
のこと)。
select data.record_id ,work_lateral.money as r1 ,work_lateral.tax_money as r2 ,work_lateral.money + work_lateral.tax_money as r3 from data left join item on data.item_code = item.code left join tax on item.tax_code = tax.code left join lateral( select data.quantity * item.price as money ,data.quantity * item.price * tax.rate / 100::numeric as tax_money ) work_lateral on true
explain analyze の実行結果
- 素直に書いたケース
----- 実行計画 ----- Hash Left Join (cost=54.42..487.25 rows=10001 width=104) (actual time=0.036..12.618 rows=10001 loops=1) Hash Cond: (item.tax_code = tax.code) -> Hash Left Join (cost=24.63..206.05 rows=10001 width=77) (actual time=0.027..2.306 rows=10001 loops=1) Hash Cond: (data.item_code = item.code) -> Seq Scan on data (cost=0.00..155.01 rows=10001 width=15) (actual time=0.011..0.475 rows=10001 loops=1) -> Hash (cost=16.50..16.50 rows=650 width=96) (actual time=0.013..0.013 rows=26 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 10kB -> Seq Scan on item (cost=0.00..16.50 rows=650 width=96) (actual time=0.005..0.008 rows=26 loops=1) -> Hash (cost=18.80..18.80 rows=880 width=64) (actual time=0.005..0.005 rows=3 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 9kB -> Seq Scan on tax (cost=0.00..18.80 rows=880 width=64) (actual time=0.004..0.004 rows=3 loops=1) Planning Time: 0.127 ms Execution Time: 12.794 ms --------------------------------------------------------------------------------
- lateralで書いたケース
----- 実行計画 ----- Nested Loop Left Join (cost=54.42..687.27 rows=10001 width=104) (actual time=0.189..10.882 rows=10001 loops=1) -> Hash Left Join (cost=54.42..262.23 rows=10001 width=77) (actual time=0.148..3.887 rows=10001 loops=1) Hash Cond: (item.tax_code = tax.code) -> Hash Left Join (cost=24.63..206.05 rows=10001 width=77) (actual time=0.033..2.430 rows=10001 loops=1) Hash Cond: (data.item_code = item.code) -> Seq Scan on data (cost=0.00..155.01 rows=10001 width=15) (actual time=0.013..0.568 rows=10001 loops=1) -> Hash (cost=16.50..16.50 rows=650 width=96) (actual time=0.013..0.013 rows=26 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 10kB -> Seq Scan on item (cost=0.00..16.50 rows=650 width=96) (actual time=0.006..0.008 rows=26 loops=1) -> Hash (cost=18.80..18.80 rows=880 width=64) (actual time=0.106..0.106 rows=3 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 9kB -> Seq Scan on tax (cost=0.00..18.80 rows=880 width=64) (actual time=0.005..0.007 rows=3 loops=1) -> Result (cost=0.00..0.02 rows=1 width=64) (actual time=0.000..0.000 rows=1 loops=10001) Planning Time: 0.138 ms Execution Time: 11.079 ms --------------------------------------------------------------------------------
結果
何度か試したが、おおむね前者が12ms、後者が11msで若干の優位性が見られた。
ちなみに、InsertのSQLを追加で発行し、data
テーブルが1110003
件で試した場合は前者が1400ms
、後者が1200ms
だった。
雑感
わりと使えることがわかって安心。 もちろん、Nested Loopが実行されるため、複数回同じ計算が出るようなこういうケース以外では素直に書いた方が速い(といってこれも同じ程度の差異しかでないが)。 処理速度も問題なんだけど、計算がからむと同じ内容なのかどうかを判断するのにも時間がかかるので、こういう形で共通化できるのは特筆すべき点だと思う。 select句にcase文の中に同じ計算を何度も入れられると読む側としてはMPがゴリゴリ削られる…(´・ω・`)
ある意味で変数的に使えるので、可読性向上ということでどうだろうか。
おまけ
気になったので副問い合わせで計算した場合の結果も貼り付ける。これが一番速いと思ってたが違った。試した限りでは1400ms
という結果になった。
select _money.record_id ,_money.r1 ,_money.r2 ,_money.r1 + _money.r2 as r3 from ( select data.record_id ,data.quantity * item.price as r1 ,data.quantity * item.price * tax.rate / 100::numeric as r2 from data left join item on data.item_code = item.code left join tax on item.tax_code = tax.code ) _money
実行計画を見ると、素直に書いた場合の結果と一緒なんだよなぁ。計算結果って遅延評価みたいな感じなんだろうか?
----- 実行計画 ----- Hash Left Join (cost=54.42..47991.07 rows=1110003 width=104) (actual time=0.037..1377.255 rows=1110003 loops=1) Hash Cond: (item.tax_code = tax.code) -> Hash Left Join (cost=24.63..20058.41 rows=1110003 width=77) (actual time=0.028..247.948 rows=1110003 loops=1) Hash Cond: (data.item_code = item.code) -> Seq Scan on data (cost=0.00..17101.03 rows=1110003 width=15) (actual time=0.012..63.836 rows=1110003 loops=1) -> Hash (cost=16.50..16.50 rows=650 width=96) (actual time=0.013..0.013 rows=26 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 10kB -> Seq Scan on item (cost=0.00..16.50 rows=650 width=96) (actual time=0.005..0.007 rows=26 loops=1) -> Hash (cost=18.80..18.80 rows=880 width=64) (actual time=0.005..0.005 rows=3 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 9kB -> Seq Scan on tax (cost=0.00..18.80 rows=880 width=64) (actual time=0.004..0.004 rows=3 loops=1) Planning Time: 0.137 ms Execution Time: 1395.279 ms --------------------------------------------------------------------------------