postgresql sqlチューニング入門 実践編(pgcon14j)
TRANSCRIPT
1
やまそふと
PostgreSQL SQL チューニング入門 実践編
PostgreSQL カンファレンス 2014株式会社アシスト
山田 聡
2
本セッションについて
"PostgreSQL SQL チューニング入門 入門編"の続きです細かいアクセスパスの説明は省略します
不参加の方は前セッションの資料とあわせて、後で復習をオススメします
入門セッションのため、ゆっくり進行でお送りします
3
Who am I ?
名前:山田 聡(やまだ さとし)
会社:株式会社アシスト
仕事:PostgreSQL+PPASのサポート (●racleも...)
PostgreSQL歴:3年
興味:機械学習,軽量言語(Python,JS等)
4
今日の目的
EXPLAIN ANALYZEで問題点を発見できるようになりましょう
5
アジェンダ
1.実行プランの強制
2.EXPLAIN と EXPLAIN ANALYZE
3.問題解決例
4.まとめ
6
アジェンダ
1.実行プランの強制
2.EXPLAIN と EXPLAIN ANALYZE
3.問題解決例
4.まとめ
7
1.実行プランの強制
PostgreSQLには様々なアクセスパスがある
通常はPostgreSQLが最適なパスを選ぶ
(稀に)選んで欲しくないパスになるケースがある
8
そんな時に役に立つのがそんな時に役に立つのが"実行プランの強制"
9
1.実行プランの強制
SET enable_演算子 = off;
プランナーがある演算子を使おうとするのを「強く思いとどまらせる」ことができる
SETを行ったセッションのみに影響する
演算子毎に設定可能(ON/OFF)enable_bitmapscan
enable_hashagg
enable_hashjoin
enable_indexscan
enable_indexonlyscan
enable_material
enable_mergejoin
enable_nestloop
enable_seqscan
enable_sort
enable_tidscan
10
1.実行プランの強制
強く思いとどまらせるとは?
指定したアクセスパスの始動コストに100000000.0を足す
指定したアクセスパスが選択されなくなるわけではない
→例えばseqscanを選択しないようにするとアクセスパスがなくなる可能性があるため 完全に無効化はしない
sampledb=# explain analyze select * from pgbench_accounts; QUERY PLAN ----------------------------------------------------------------------------------------------- Seq Scan on pgbench_accounts
(cost=10000000000.00..10000025874.00 rows=1000000 width=97) (actual time=0.008..159.306 rows=1000000 loops=1)
Total runtime: 285.398 ms
11
1.実行プランの強制
プランを強制してみる
sampledb=# explain analyze select * from pgbench_accounts where aid > 1; QUERY PLAN ------------------------------------------------------------------------ Seq Scan on pgbench_accounts
(cost=0.00..28374.00 rows=1000000 width=97) (actual time=0.011..224.378 rows=999999 loops=1)
Filter: (aid > 1) Rows Removed by Filter: 1 Total runtime: 345.456 ms(4 rows)
sampledb=# SET enable_seqscan = off;SETsampledb=# explain analyze select * from pgbench_accounts where aid > 1; QUERY PLAN ------------------------------------------------------------------------ Index Scan using pgbench_accounts_pkey on pgbench_accounts
(cost=0.42..42169.43 rows=1000000 width=97)(actual time=0.041..480.360 rows=999999 loops=1)
Index Cond: (aid > 1) Total runtime: 603.765 ms(3 rows)
初期状態はSeq Scan(cost=28374.00)
変更後はIndex Scan(cost=42169.43)
12
これさえあればPostgreSQLでもプランが自由自在?
13
1.実行プランの強制
いつ使うの?プランの切り分け作業
開発時にどうしても特定プランにしたい時
なぜ無闇につかっちゃだめなの?
人はプランナーより賢くない(Tom Laneでもない限り)
しかし、プランナーは推測しかしない
適切なコスト変数の設定を
統計情報更新のため定期的なANALYZEを
まずはEXPLAIN ANALYZEで問題点を把握しましょう
14
アジェンダ
1.実行プランの強制
2.EXPLAIN と EXPLAIN ANALYZE
3.問題解決例
4.まとめ
15
2.EXPLAIN と EXPLAIN ANALYZE
EXPLAINプランナーが作成した"最良の"実行計画を確認するコマンド
コストや行数は統計情報を元にした推定EXPLAIN ANALYZE
EXPLAINの出力に追加の情報を加えるオプション
実際にSQLを実行して情報を取得する
負荷のかかるSQLは注意
DMLの変更に注意
"実行時間"や"実際の行数"を取得する
16
2.EXPLAIN と EXPLAIN ANALYZE
EXPLAIN (ANALYZE)の読み方
実行計画は各ステップをノードとするツリー構成
インデントが深いところから実行
子ノードの結果を親ノードが受ける
コスト・実行時間は子ノードからの累積
17
実際の結果を見てみましょう
18
2.EXPLAIN と EXPLAIN ANALYZE
実行SQL:EXPLAIN ANALYZE
SELECT e.empno,d.dname FROM emp e JOIN dept d ON e.deptno=d.deptno ;
Column | Type ----------+----------------------------- empno | integer ename | character varying(10) job | character varying(9) mgr | integer hiredate | timestamp without time zone sal | integer comm | integer deptno | integer
EMP表
Column | Type --------+----------------------- deptno | integer dname | character varying(14) loc | character varying(13)
DEPT表
19
2.EXPLAIN と EXPLAIN ANALYZE
実行結果
Hash Join(cost=1.09..2.32 rows=4 width=50) (actual time=0.100..0.120 rows=14 loops=1) Hash Cond:(e.deptno = d.deptno)
-> Seq Scan on emp e (cost=0.00..1.14 rows=14 width=8)
(actual time=0.013..0.022 rows=14 loops=1) -> Hash
(cost=1.04..1.04 rows=4 width=50) (actual time=0.024..0.024 rows=4 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 1kB -> Seq Scan on dept d
(cost=0.00..1.04 rows=4 width=50) (actual time=0.011..0.015 rows=4 loops=1)
①
②
③
④
20
2.EXPLAIN と EXPLAIN ANALYZE
実行結果をツリーにすると…
Seq Scan on emp ecost=0.00..1.14
time=0.013..0.022
Hash Joincost=1.09..2.32time=0.100..0.120
Hashcost=1.04..1.04time=0.024..0.024
Seq Scan on dept dcost=0.00..1.04
time=0.011..0.015
cost=1.04+0time=0.015+0.009
cost=1.04+1.14+0.5time=0.024+0.022+0.074
21
2.EXPLAIN と EXPLAIN ANALYZE
EXPLAIN ANALYZESELECT e.empno,d.dname,s.grade FROM emp e JOIN dept d ON e.deptno=d.deptnoJOIN salgrade s on e.sal between s.losal and s.hisalwhere e.job='SALESMAN';
Column | Type ----------+----------------------------- empno | integer ename | character varying(10) job | character varying(9) mgr | integer hiredate | timestamp without time zone sal | integer comm | integer deptno | integer
EMP表
Column | Type --------+----------------------- deptno | integer dname | character varying(14) loc | character varying(13)
DEPT表
Column | Type --------+--------- grade | integer losal | integer hisal | integer
SALGRADE表
22
2.EXPLAIN と EXPLAIN ANALYZE
もうちょっと複雑な出力結果
Nested Loop(cost=0.00..3.39 rows=1 width=50)(actual time=0.031..0.089 rows=4 loops=1)
Join Filter: ((emp.sal >= s.losal) AND (emp.sal <= s.hisal)) Rows Removed by Join Filter: 16 -> Nested Loop
(cost=0.00..2.26 rows=1 width=54)(actual time=0.022..0.051 rows=4 loops=1) Join Filter: (emp.deptno = d.deptno) Rows Removed by Join Filter: 12 -> Seq Scan on emp
(cost=0.00..1.18 rows=1 width=12)(actual time=0.011..0.018 rows=4 loops=1)Filter: ((job)::text = 'SALESMAN'::text)Rows Removed by Filter: 10
-> Seq Scan on dept d(cost=0.00..1.04 rows=4 width=50)(actual time=0.001..0.003 rows=4 loops=4)
-> Seq Scan on salgrade s(cost=0.00..1.05 rows=5 width=8)(actual time=0.001..0.002 rows=5 loops=4)
①
②
③
④
⑤
23
2.EXPLAIN と EXPLAIN ANALYZEの違い
実行結果をツリーにすると…
Nested Loop
Nested Loop
Seq Scan on emp Seq Scan on dept d
Seq Scan on dept d x 4
Seq Scan on salgrade s x 4
Seq Scan on salgrade s (cost=0.00..1.05 rows=5 width=8)
(actual time=0.001..0.002 rows=5 loops=4)
24
2.EXPLAIN と EXPLAIN ANALYZEの違い
EXPLAIN ANALYZEの結果を見るポイント
インデントが深いところから
出力結果は子ノードからの累積
各ステップのcost/rows(見積,実際)/actual timeに注目
疑うべきポイント
actual timeが跳ね上がっているステップは怪しい
rowsが見積もりと離れている箇所は怪しい
costに比べてactual timeが長い箇所は怪しい
25
実際に問題を解決してみましょう!
26
アジェンダ
1.実行プランの強制
2.EXPLAIN と EXPLAIN ANALYZE
3.問題解決例
4.まとめ
27
3.問題解決例
Column | Type --------------+--------- exception_id | integer(primary key) complete | boolean
EXCEPTION表
Column | Type -------------------------+--------- exception_notice_map_id | integer exception_id | integer notice_id | integer
EXCEPTION_NOTICE_MAP表
complete列の分布
TRUE
FALSE
IndexIndex
● indexは両表のexception_id列のみ作成
● complete列はFalseのデータが1%未満
(1000行/10000000行)
28
3.問題解決例
実行するSQL
EXPLAIN ANALYZESELECT exception_id,exception_notice_map_idFROM exceptionJOIN exception_notice_map USING (exception_id)WHERE complete is false and notice_id=3;
29
3.問題解決例
結果Hash Join (cost=175782.31..405182.03 rows=53172 width=8) (actual time=1834.952..1844.389 rows=9 loops=1) Hash Cond: (exception.exception_id = exception_notice_map.exception_id) -> Seq Scan on exception (cost=0.00..144263.00 rows=5000000 width=4) (actual time=789.879..790.120 rows=1000 loops=1) Filter: (complete IS FALSE) Rows Removed by Filter: 9999000 -> Hash (cost=174037.01..174037.01 rows=106344 width=8) (actual time=1044.821..1044.821 rows=100202 loops=1) Buckets: 4096 Batches: 4 Memory Usage: 690kB -> Seq Scan on exception_notice_map (cost=0.00..174037.01 rows=106344 width=8) (actual time=0.081..991.670 rows=100202 loops=1) Filter: (notice_id = 3) Rows Removed by Filter: 9900798
Total runtime: 1844.486 ms
30
● 状況整理
– 最上位のノードはrows=9 →9行戻すSQL
– 結合はHash Join– 処理時間は1844.486 ms(約2秒)
もっと早くならないかな?
31
3.問題解決例
見積との差をチェック
Hash Join (cost=175782.31..405182.03 rows=53172 width=8) (actual time=1834.952..1844.389 rows=9 loops=1) Hash Cond: (exception.exception_id = exception_notice_map.exception_id) -> Seq Scan on exception (cost=0.00..144263.00 rows=5000000 width=4) (actual time=789.879..790.120 rows=1000 loops=1) Filter: (complete IS FALSE) Rows Removed by Filter: 9999000 -> Hash (cost=174037.01..174037.01 rows=106344 width=8) (actual time=1044.821..1044.821 rows=100202 loops=1) Buckets: 4096 Batches: 4 Memory Usage: 690kB -> Seq Scan on exception_notice_map (cost=0.00..174037.01 rows=106344 width=8) (actual time=0.081..991.670 rows=100202 loops=1) Filter: (notice_id = 3) Rows Removed by Filter: 9900798
Total runtime: 1844.486 ms
OK!
OK!
ずれてる!?
32
3.問題解決例
exception表でcompelete is Falseの行は
5000000行くらいかな結合相手も行が多いしたくさん
もどりそうだからHashJoinしよう
(cost=0.00..144263.00 rows=5000000 width=4)(actual time=789.879..790.120 rows=1000 loops=1)
1000行しかなかった…統計情報が古い気がする…
プランナー プランナー
33
そうだ、ANALYZE、しよう
34
3.問題解決例
ANALYZE exceptionしてみた
Nested Loop (cost=0.43..152601.93 rows=11 width=8) (actual time=792.030..794.257 rows=9 loops=1) -> Seq Scan on exception (cost=0.00..144262.43 rows=1000 width=4) (actual time=790.677..790.885 rows=1000 loops=1) Filter: (complete IS FALSE) Rows Removed by Filter: 9999000 -> Index Scan using idx_nmap_exception_id on exception_notice_map (cost=0.43..8.33 rows=1 width=8) (actual time=0.003..0.003 rows=0 loops=1000) Index Cond: (exception_id = exception.exception_id) Filter: (notice_id = 3) Rows Removed by Filter: 1 Total runtime: 817.182 ms
ずれがなくなった!!
早くなった!1844.486 ms→817.182 ms
35
ANALYZEで最新の統計を使いましょう!
36
3.問題解決例
再度結果を確認
Nested Loop (cost=0.43..152601.93 rows=11 width=8) (actual time=792.030..794.257 rows=9 loops=1) -> Seq Scan on exception (cost=0.00..144262.43 rows=1000 width=4) (actual time=790.677..790.885 rows=1000 loops=1) Filter: (complete IS FALSE) Rows Removed by Filter: 9999000 -> Index Scan using idx_nmap_exception_id on exception_notice_map (cost=0.43..8.33 rows=1 width=8) (actual time=0.003..0.003 rows=0 loops=1000) Index Cond: (exception_id = exception.exception_id) Filter: (notice_id = 3) Rows Removed by Filter: 1 Total runtime: 817.182 ms
1%未満の行にSeq Scanでアクセスしている
complete列の分布
TRUE
FALSE
37
3.問題解決例
Seq Scanを辞めたいならINDEXを張るのが定石
でもcomplete列はTrue/Falseの2種類しかない
カーディナリティが低いのでINDEX作成の負荷が心配
INDEXを使って欲しいのがFalseの時だけ
INDEXをつけるのは難しいかな・・・?
38
そうだ部分インデックスがあるじゃないか!
CREATE INDEX idx_is_complete ON exception(complete) WHERE complete IS false;
39
3.問題解決例
部分インデックスとは
条件を満たす行のみを保持するインデックス
頻出値にインデックスを付けずに済むため
インデックスのサイズが小さいインデックスの更新処理が発生しにくいので
更新パフォーマンスが有利
40
3.問題解決例
部分INDEX作ったら...
Nested Loop (cost=0.71..8347.79 rows=11 width=8) (actual time=0.266..5.241 rows=9 loops=1) -> Index Scan using idx_is_complete on exception (cost=0.28..8.29 rows=1000 width=4) (actual time=0.073..0.680 rows=1000 loops=1) Index Cond: (complete = false) -> Index Scan using idx_nmap_exception_id on exception_notice_map (cost=0.43..8.33 rows=1 width=8) (actual time=0.004..0.004 rows=0 loops=1000) Index Cond: (exception_id = exception.exception_id) Filter: (notice_id = 3) Rows Removed by Filter: 1
Total runtime: 5.286 ms
Index Scanが使われるようになった!
817.182 ms->5.286 ms160倍早い!
41
PostgreSQLの色々な機能を活用しよう!・部分インデックス・Materialized View 等(細かい部分はマニュアルで)
42
アジェンダ
1.実行プランの強制
2.EXPLAIN と EXPLAIN ANALYZE
3.問題解決例
4.まとめ
43
4.まとめ
EXPLAIN ANALYZEで問題を探すなら
インデントが深いところから
見積もりがずれているところから
時間が伸びているところから
44
4.まとめ
問題に対処するなら
見積もりがおかしかったら→ANALYZE してみましょう
アクセス行数が少ないのにSeq Scanだったら→インデックスを検討しましょう
PostgreSQLの機能を活用しましょう
最新のPostgreSQLを使いましょう
45
それでも解決しない時はどうすればいいのだろう?
46
4.まとめ
メーリングリストに投稿してみましょう
まず自分でデバッグしてみる
PostgreSQLのバージョンを書く
VACUUMとANALYZEを正確に実行してあること
EXPLAIN ANALYZEの結果を必ず書く
クエリ、テーブル、データもできれば含める
[email protected] (英語)[email protected] (日本語)
47
ご清聴ありがとうございました
参考資料(サイト)Explaining Explain ~ PostgreSQLの実行計画を読む ~ http://lets.postgresql.jp/documents/technical/query_tuning/explaining_explain_ja.pdf/view内部を知って業務に活かす PostgreSQL研究所第4回 http://www2b.biglobe.ne.jp/~caco/webdb-pdfs/vol29.pdf Robert Haas blog http://rhaas.blogspot.com/2011/10/index-only-scans-weve-got-em.html問合せ最適化インサイド http://www.slideshare.net/ItagakiTakahiro/ss-4656848象と戯れ http://postgresql.g.hatena.ne.jp/umitanuki/20110425/1303752697Explaining Explain 第2回 http://www.postgresql.jp/wg/shikumi/study20_materialsExplaining Explain 第3回 http://www.postgresql.jp/wg/shikumi/study21_materials
参考資料(書籍)PostgreSQL 全機能バイブル(技術評論社)PostgreSQL 設計・運用計画の鉄則(技術評論社)
48
おまけ
実際の運用に際して有用な機能
auto_explain(contribモジュール)
自動的にexplainしてくれる
自動的にログに書いてくれる
実行時間等の条件を指定可能
psqlで試せないSQLでも取得可能
49
おまけ
マテリアライズドビューが効くケース
EXPLAIN ANALYZESELECT notice_id,count(*) as countFROM exceptionJOIN exception_notice_map USING (exception_id)GROUP BY notice_id order BY count;
50
おまけ
マテリアライズドビューが効くケース
Sort (cost=864852.14..864852.64 rows=200 width=4) (actual time=78419.788..78419.803 rows=100 loops=1) Sort Key: (count(*)) Sort Method: quicksort Memory: 20kB -> HashAggregate (cost=864842.50..864844.50 rows=200 width=4) (actual time=78419.706..78419.736 rows=100 loops=1) -> Hash Join (cost=303459.50..814837.50 rows=10000000 width=4) (actual time=27756.900..75436.393 rows=10000000 loops=1) Hash Cond: (exception_notice_map.exception_id = exception.exception_id) -> Seq Scan on exception_notice_map (cost=0.00..149035.00 rows=10000000 width=8) (actual time=0.040..2860.579 rows=10000000 loops=1) -> Hash (cost=144263.00..144263.00 rows=10000000 width=4) (actual time=27575.905..27575.905 rows=10000000 loops=1) Buckets: 8192 Batches: 256 Memory Usage: 929kB -> Seq Scan on exception (cost=0.00..144263.00 rows=10000000 width=4) (actual time=0.021..2511.089 rows=10000000 loops=1)
Total runtime: 78419.966 ms
OK!
OK!
OK!
OK!
たぶんOK
たぶんOK。。。だけど遅すぎる
51
そうだマテリアライズドビューがあるじゃないか!※9.3〜
CREATE MATERIALIZED VIEW exception_notice_summaryASSELECT notice_id,count(*) as countFROM exceptionJOIN exception_notice_map USING (exception_id)GROUP BY notice_id ;
52
おまけ
マテリアライズドビューとは
実データを保持するビュー
計算済みのデータを保持するため、問合せ負荷が低い
最新の状態を反映するには定期的なリフレッシュが必須
DWH系の処理に向いている
53
おまけ
結果
Sort (cost=135.34..140.19 rows=1940 width=12) (actual time=0.112..0.141 rows=100 loops=1) Sort Key: count Sort Method: quicksort Memory: 20kB -> Seq Scan on exception_notice_summary (cost=0.00..29.40 rows=1940 width=12) (actual time=0.010..0.046 rows=100 loops=1)
Total runtime: 0.187 ms
explain analyze SELECT * FROM exception_notice_summaryORDER BY count;