Db2で「統計情報は最新なのにSQLが遅い」の原因を切り分ける
「統計情報は最新なのに、なぜか遅い」——Db2のSQLチューニングで、いちばん多く聞く相談かもしれません。
RUNSTATS を流しても実行計画が改善しない。インデックスを作ったのに使われない。昨日まで一瞬だったSQLが今朝から3倍かかる。これらは「統計情報が古い」という単一の原因では説明できません。実際には複数のレイヤーが絡み合っています。
この記事では、Db2で遅いSQLに出くわしたときに、統計情報 → パッケージキャッシュ → 実行計画 の順にどこを確認していけば原因にたどり着けるかを、実機(Db2 12.1)で動かしながら整理します。
STEP1:まず統計情報の「鮮度」を疑う
チューニングの出発点は、オプティマイザーが参照している統計情報が新しいかどうかです。SYSCAT.TABLES で行数(CARD)と最終取得時刻(STATS_TIME)を確認します。
SELECT TABNAME, CARD, STATS_TIME
FROM SYSCAT.TABLES
WHERE TABSCHEMA = '<スキーマ名>'
ORDER BY STATS_TIME ASC;
実機で流すと、こういう結果が返ります。
TABNAME CARD STATS_TIME
----------- ------- --------------------------
CL_SCHED -1 -
EMPLOYEE -1 -
DEPARTMENT -1 -
ここで注目は CARD = -1。これはそのテーブルで RUNSTATS が一度も実行されていない状態です。オプティマイザーは行数すら把握できておらず、デフォルト値で当て推量の実行計画を作ります。STATS_TIME が空、あるいは何ヶ月も前なら、まずここを疑います。
インデックスやカラムのカーディナリティも同様に確認できます。
-- インデックスの統計(COLNAMESの先頭に + / - で昇降順)
SELECT INDNAME, COLNAMES, FULLKEYCARD, STATS_TIME
FROM SYSCAT.INDEXES
WHERE TABSCHEMA = '<スキーマ名>' AND TABNAME = '<テーブル名>';
-- カラムのカーディナリティ
SELECT COLNAME, COLCARD
FROM SYSCAT.COLUMNS
WHERE TABSCHEMA = '<スキーマ名>' AND TABNAME = '<テーブル名>'
ORDER BY COLNO;
RUNSTATS をいつ流すか、自動RUNSTATSを残すかどうかは別記事にまとめています。
STEP2:統計は最新なのに遅い——4つのパターン
RUNSTATS を流して STATS_TIME が今なのに改善しない。ここからが本題です。現場で多いのは次の4パターンです。
パターン1:データに偏りがある
カーディナリティが低いカラム(ステータス区分など)に値の偏りがあると、通常の RUNSTATS では「どの値が何件あるか」まで表現できず、オプティマイザーが件数を読み違えます。分布統計を取ると改善します。
RUNSTATS ON TABLE <スキーマ>.<テーブル>
WITH DISTRIBUTION AND DETAILED INDEXES ALL;
分布統計が取れているかは SYSCAT.COLDIST で確認できます(TYPE='F' が頻度統計、'Q' が分位統計)。
SELECT COLNAME, TYPE, SEQNO, COLVALUE, VALCOUNT
FROM SYSCAT.COLDIST
WHERE TABSCHEMA = '<スキーマ>' AND TABNAME = '<テーブル>' AND TYPE = 'F'
ORDER BY COLNAME, SEQNO;
パターン2:統計は新しいが、実行計画キャッシュが古い
見落としがちなのがこれです。RUNSTATS を流しても、パッケージキャッシュに古い実行計画が残っていると、古い計画で実行され続けます。動的SQLを再コンパイルさせるにはキャッシュのフラッシュが必要です。
FLUSH PACKAGE CACHE DYNAMIC;
パターン3:複合インデックスのカラム順序が合っていない
複合インデックスは先頭カラムから順にしか使えません。WHERE句が先頭カラムを含まないと、インデックスは無視されます。実機で見たインデックスを例にすると——
COLNAMES = '+DEPT+DIV' (先頭が DEPT、次が DIV)
WHERE DEPT = 'A' → 使われる(先頭カラムあり)
WHERE DIV = 1 → 使われない(先頭 DEPT をスキップ)
WHERE DEPT = 'A' AND DIV = 1 → 使われる
「インデックスを作ったのに使われない」の多くは、この先頭カラム問題です。
パターン4:WHERE句の関数適用でインデックスが無効化される
カラムに関数を掛けると、そのカラムのインデックスは使えなくなります。
-- NG: インデックスが効かない
WHERE YEAR(created_at) = 2026
-- OK: 範囲条件に書き換えるとインデックスが効く
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01'
db2exfmt を使った実行計画の読み方は本の第9章で解説しています。
STEP3:パッケージキャッシュで「遅い犯人SQL」を名指しする
「どのSQLが遅いのか」がそもそも分からない場合、パッケージキャッシュを見ればトップ nを名指しできます。まずキャッシュのヒット率から。
SELECT
DEC(FLOAT(PKG_CACHE_LOOKUPS - PKG_CACHE_INSERTS) /
FLOAT(PKG_CACHE_LOOKUPS) * 100, 5, 2) AS HIT_RATIO_PCT,
PKG_CACHE_LOOKUPS, PKG_CACHE_INSERTS
FROM TABLE(MON_GET_DATABASE(-2)) AS T
WHERE PKG_CACHE_LOOKUPS > 0;
ヒット率が90%を下回っていたら、後述のリテラル値埋め込みが疑わしいです。次に、実行時間の長いSQLトップ10を取り出します。
SELECT
SUBSTR(STMT_TEXT, 1, 80) AS SQL_TEXT,
NUM_EXECUTIONS,
DEC(FLOAT(STMT_EXEC_TIME) / FLOAT(NUM_EXECUTIONS), 15, 2) AS AVG_EXEC_MS,
ROWS_READ,
SORT_OVERFLOWS
FROM TABLE(MON_GET_PKG_CACHE_STMT(NULL, NULL, NULL, -2)) AS T
WHERE NUM_EXECUTIONS > 0
ORDER BY AVG_EXEC_MS DESC
FETCH FIRST 10 ROWS ONLY;
ROWS_READ が異常に多いSQLはフルスキャンの疑い、SORT_OVERFLOWS が出ていればソートがソートヒープに収まらずディスクにあふれています。ORDER BY を ROWS_READ DESC に変えれば「読みすぎているSQL」の観点でも並べ替えられます。
ここで出る値は、その文がキャッシュに載ってからの累積です。常時監視に使うなら2時点の差分を取らないと、「今遅いSQL」ではなく「これまでに時間を使ったSQL」が並びます。
STEP4:パラメーターマーカーを使う(ヒット率激減の主犯)
-- NG: 値ごとに別SQL扱い → 毎回コンパイル
SELECT * FROM orders WHERE customer_id = 12345
SELECT * FROM orders WHERE customer_id = 67890
-- OK: 1回コンパイルしてキャッシュ再利用
SELECT * FROM orders WHERE customer_id = ?
アプリ改修がすぐに難しい場合の緩和策として KEEPDYNAMIC(切断後も計画を保持)や REOPT(実値で再最適化)がありますが、いずれも状況次第で逆効果になるため、検証環境での確認が前提です。
STEP5:実行計画を実際に読む
ここまでの当たりを裏取りするのが実行計画です。まずExplainテーブルを作り(初回のみ)、EXPLAIN して db2exfmt で整形します。
-- 初回のみ:Explainテーブル作成
CALL SYSPROC.SYSINSTALLOBJECTS('EXPLAIN', 'C',
CAST(NULL AS VARCHAR(128)), CAST(NULL AS VARCHAR(128)));
-- 対象SQLの計画を取得
EXPLAIN PLAN FOR
SELECT * FROM <スキーマ>.<テーブル> WHERE col = 'value';
db2exfmt -d <DB名> -n % -s % -g TIC -w -1 -# 0 -o /tmp/explain.txt
db2exfmt の出力はこんな形です(実機の例)。
Access Plan:
Total Cost: 6.80856
RETURN
FETCH
IXSCAN TABLE: DB2INST1
読むべきは主に3点です。
| 見る場所 | 意味 | 注意点 |
|---|---|---|
| アクセス方法 |
IXSCAN(索引)/ TBSCAN(表スキャン) |
大きい表で TBSCAN が出たら要調査 |
| 推定行数(ROWCOUNT) | オプティマイザーの見積もり件数 | 実件数と大きくズレていれば統計が疑わしい |
| ソート(SORT) | ソートの有無とコスト | 不要なソートが混じっていないか |
小さいテーブルなら TBSCAN が最適なこともあります。問題になるのは、数百万行の表に TBSCAN が選ばれているときです。
番外:昨日まで速かったSQLが突然遅くなったら
実行計画が入れ替わった可能性が高いです。原因の多くは次の3つ。
原因1:RUNSTATS が走り、オプティマイザーの判断が変わった
原因2:データ量が増え、閾値を越えて計画が切り替わった
原因3:フィックスパック適用でオプティマイザーの挙動が変わった
まとめ
| 確認順 | 見るもの | 着眼点 |
|---|---|---|
| 1 | SYSCAT.TABLES |
CARD=-1 は統計未取得。STATS_TIME が古ければ再取得 |
| 2 | 分布統計 / index順序 / 関数適用 | 統計が新しくても遅い4パターン |
| 3 | MON_GET_PKG_CACHE_STMT |
遅い犯人SQLを名指しする |
| 4 | パラメーターマーカー | リテラル埋め込みはヒット率を激減させる |
| 5 |
EXPLAIN + db2exfmt
|
大表の TBSCAN・推定行数のズレ・不要ソート |
📕 もっと深く:Db2運用を12章で体系化した本を書きました
この記事で扱ったSQLチューニングは、Db2運用の一領域にすぎません。
Db2はRDBMS市場でシェア数%。情報が少なく、頼れるのは膨大な公式マニュアルだけ——そんな現状を変えたくて、「なぜこの値にするのか」「この障害のとき次に何を見るのか」という、マニュアルに載っていない現場の文脈を1冊にしました。実機で検証した内容と、Db2の設計上そうなる理由をまとめました。
アーキテクチャ/構成パラメーター選定/セキュリティ/バックアップ・リカバリ/HADR/pureScale/日常点検の自動化/メモリ・ロック競合/SQLチューニング(本記事の深掘り版)/RUNSTATS・REORG/MON_GET監視/現場の難題集まで、全12章+運用シェルスクリプト集です。
「統計は最新なのに遅い」以外にも、扱ってほしいテーマがあればコメントで教えてください。
Discussion