「Seq Scan は必ず悪い」
表の大部分を返すなら、先頭から順に読んだほうが効率的です。例えば、1 ページに 100 行ある表から全体の 10% の行を取り出そうとすると、結局はほぼすべてのページに触れます。インデックス経由でページをばらばらに読むよりも、Seq Scan を選ぶほうが合理的です。
DBC Tech Academy / PostgreSQL / 中級
なぜ、このクエリは遅いのか。プランナーは、実際の速さを確かめているわけではありません。候補ごとにコストを見積もり、その値が最も小さい方法を選びます。ここでは、コストをどう見積もるのかをひも解き、実行計画から遅さの原因を突き止める手順まで解説します。
見積もりはわずか 1 行。ところが、実際には 48,210 行ありました。この大きなずれが、内側を 48,210 回も繰り返す計画につながっています。
Cost, not speed
プランナーが選ぶのは、実測で最も速かった方法ではなく、見積もったコストが最も小さい方法です。コストは秒数ではありません。ページの読み出しや行の処理に重みを付けて算出する、比較のための数値です。
プランナーは複数の実行方法についてコストを計算し、最も小さいものを選びます。その時点では実測値を見られないため、見積もりが外れると、適切な計画を選べません。
基準となるのは、「1 ページを先頭から順に読み出す処理」を 1.0 とした相対値です。既定では次の値が使われますが、設定で変更できます。
既定では、ランダム読み出しの重みは順次読み出しの 4 倍です。この差が、インデックスを使うか、どの結合方式を選ぶかという判断に大きく影響します。
Anatomy of a node
実行計画は木構造で表されます。各行をクリックして、ノードの役割と注目すべき数値を確認してみましょう。
行をクリックすると解説が切り替わります。
actual time は、配下の処理も含めた累計値です。そのノードだけにかかった時間は、子ノードの時間を差し引いて求めます。loops が 1 より大きい場合、actual time と rows はループ 1 回あたりの平均です。処理全体の量を見るときは、loops を掛け戻します。rows と実測の rows を比べます。最も大きくずれ始めたノードが、原因を探る手掛かりです。Where the plan flips
100 万行、1 万ページの表から、条件に合う行を取り出す場面を考えます。選択率(条件に合う行の割合)を動かして、3 つのスキャン方法のコストがどう変わるかを確かめてみましょう。
| 方式 | 読むページ | 評価する行 | コスト |
|---|
相関は、インデックスの順序と行の物理的な並びがどれだけ一致しているかを表します。1.0 に近いほど、必要な行が近いページにまとまります。SSD の特性に合わせて random_page_cost を下げると、インデックスが選ばれやすくなります。
How cost is computed
コストの正体は、5 つの重みと、読むページ数・処理する行数の掛け算です。式が分かれば、実行計画に並ぶ数字を自分で検算でき、どこを削れば下がるのかも見えてきます。
基準は seq_page_cost = 1.0、つまりページを 1 枚、順番に読む手間です。他のすべては、その何倍かで表されます。コストが秒ではないというのは、この意味です。
| パラメータ | 既定値 | 数える対象 |
|---|---|---|
seq_page_cost | 1.0 | 順番に読むページ 1 枚 |
random_page_cost | 4.0 | 飛び飛びに読むページ 1 枚 |
cpu_tuple_cost | 0.01 | 表から行を 1 件取り出す |
cpu_index_tuple_cost | 0.005 | インデックスの項目を 1 件たどる |
cpu_operator_cost | 0.0025 | 演算子や関数を 1 回評価する |
ページの重みは、行の重みの 100 倍以上です。コストの大小を決めるのは、ほとんどの場合、何行を処理するかではなく何ページ読むかです。
1 万ページ・100 万行の表を、条件 1 つで絞り込む場合です。
実行計画には cost=0.00..22500.00 と現れます。条件を 2 つに増やせば最後の項が 2 倍になり、25,000 です。
絞り込んだ結果が 1 行でも、この値は変わりません。条件は全行に対して評価されるからです。Rows Removed by Filter が大きいクエリが遅いのは、まさにこの部分を払っているためです。
cost=0.29..22500.00 の左は最初の 1 行を返せるまで、右はすべて返し終わるまでの見積もりです。Seq Scan や Index Scan は読んだ順に返せるので、左はほぼ 0 になります。
一方、Sort やハッシュ表の構築は、入力を最後まで読み切らないと 1 行も返せません。こうしたノードでは、コストのほぼ全部が左側に乗ります。実行計画で cost=18452.11..18702.11 のように左右が近い値になっていたら、そのノードは先に全部読むタイプだと判断できます。
この区別が効いてくるのが LIMIT です。10 行だけ必要なら、プランナーは「最初の 1 行までのコスト+残りを 10 行分だけ按分した額」で候補を比べます。左側の大きい計画はここで不利になり、Sort より Index Scan が選ばれやすくなります。LIMIT を付けた途端に計画が変わるのは、これが理由です。
Index Scan の手間は、インデックスをたどる分と、該当行が載っている表のページを読む分に分かれます。後者が、相関によって大きく変わります。
相関が 1.0 なら、必要な行は表の中で連続して並んでいます。触るページは少なく、しかも前から順に読めるため、seq_page_cost に近い重みで数えられます。相関が 0 なら、行はばらばらの位置にあり、ほぼ行数と同じ枚数のページを random_page_cost で読むことになります。
プランナーは、この 2 つの端の値を相関の 2 乗で補間します。相関 0.5 は真ん中ではなく、ランダム側に寄った見積もりになります。相関 0.9 でようやく 8 割方が連続読み扱いです。
相関は、設定ではなく観測値です。pg_stats の correlation 列に、列ごとの値が入っています。追記中心の表の created_at は 1.0 に近く、更新の多い列では時間とともに下がります。CLUSTER で並べ替えれば上がりますが、その後の更新でまた崩れていきます。
Nested Loop の内側のように、同じインデックスを何度もたどる場合、2 回目以降はすでにキャッシュへ載っている可能性が高くなります。プランナーは effective_cache_size を手掛かりに、繰り返しのうち何割がキャッシュから読めるかを推定し、その分コストを割り引きます。
この設定はメモリを確保しません。「OS のキャッシュも含めれば、これくらいは載るはずだ」とプランナーへ伝えるだけの数字です。実際より小さく設定していると、繰り返しの割引が効かずインデックスが不利に見積もられ、Seq Scan が選ばれやすくなります。
Bitmap Index Scan は、条件に合う行が載っているページ番号だけを先に集めます。続く Bitmap Heap Scan は、その一覧をページ番号の順に読むため、同じページを何度も読み直さずに済みます。コストも、順読みとランダム読みの中間の重みで数えられます。選択率が中くらいのときにこの方式が選ばれるのは、そのためです。
実行計画の Heap Blocks: exact=... は行の位置まで記録できたページ、lossy=... はビットマップが work_mem に収まらずページ単位まで粗くした分です。lossy に入ったページは丸ごと読み直し、条件を全行に対して評価し直します。work_mem を上げれば解消します。
ひとつ上のシミュレーターは、この式をそのまま使っています。実際のプランナーはさらに、インデックスの階層の深さ、部分インデックスの選択率、行外に追い出された大きな列の読み出しなどを積み上げます。ただし桁を動かすのはページ数と相関なので、どこで方式が入れ替わるかは、この式のままで追えます。
Choosing a join
次は、2 つの表を結合する場面です。外側の行数と作業メモリを動かし、3 つの結合方式のコストがどう変わるかを比べます。
Hash Join では、内側の表からメモリ上にハッシュ表を作ります。work_mem に収まらなければ複数のバッチに分け、中間結果をディスクへ書き出すため、コストが大きく増えます。一方、Merge Join は両側の並びをそろえる必要があるので、結合キーの順に読める場合に有利です。
Nested Loop: 外側の行数が少ない場面に向いています。内側にインデックスがあれば、外側の 1 行ごとに数ページを読むだけで済みます。ただし、外側の行数が増えるにつれて処理量も増えていきます。
Hash Join: 等値結合で、内側のハッシュ表が work_mem に収まる場面に向いています。両方の表を一度ずつ読めばよいため、大量の行を結合するときに強い方式です。
Merge Join: 両側を結合キーの順に読める場面に向いています。並びをそろえるためのソートが必要になると、その分のコストが加わります。
Nested Loop の内側に大きな loops が付いていたら、まず外側の行数の見積もりを疑います。1 行しかないと見積もっていた外側から実際には数万行が流れると、内側の処理も数万回繰り返されます。
Sort Method: external merge Disk: ...kB と表示されていれば、work_mem に収まらなかったデータがディスクへ書き出されています。
Hash Batches が 1 より大きい場合も、ハッシュ表がメモリに収まっていないと判断できます。
Parallel execution
表がある程度の大きさを超えると、プランナーは処理を複数のワーカーに分担させます。このとき実行計画に現れる行数は、これまでの読み方のままでは合いません。
Parallel Seq Scan の rows=16070 は、ワーカー 1 本あたりの平均です。loops=3 が示すとおり、実際に読んだのは 3 本分。16,070 × 3 = 48,210 行が全体の量で、それが Gather の行に現れています。
loops が 1 より大きければ 1 回あたりの平均、という原則はここでも同じです。違うのは、ループの正体が繰り返しではなく並列だという点だけです。
ワーカーの本数は、リーダーを含めて数えます。Workers Launched が 2 でも loops は 3 です。問い合わせを起こしたプロセス自身も、待つのではなく分担に加わるためです。
Workers Planned と Workers Launched を見比べます。Launched のほうが少ないことがあります。同時に走る他のクエリが max_parallel_workers を使い切っていると、計画どおりの本数を確保できません。この場合、同じクエリなのに実行のたびに速さが変わります。rows=20088 と rows=16070 の比較であり、48,210 と比べるのではありません。SET max_parallel_workers_per_gather = 0; で切り、同じクエリを実行します。速さが変わらなければ、原因は別にあります。min_parallel_table_scan_size(8MB)に満たない表は分担の対象になりません。max_parallel_workers_per_gather の既定は 2 で、これを 0 にすると並列は起きません。The rest of the nodes
実行計画には、ここまでで扱った以外にも多くのノードが現れます。読み方の原則は変わりません。内側から読み、見積もりと実測を比べます。それぞれが何をしていて、どこに危険信号が出るのかを一覧にします。
| ノード | 現れる場面 | 読むときの注目点 |
|---|---|---|
Append | UNION ALL、パーティション表 | 子の数。never executed の子は実際には読まれていません |
Merge Append | 子を並び順のまま束ねるとき | 子ごとに Sort が付いていれば、その分の作業メモリが要ります |
Bitmap Index Scan | 選択率が中くらいの絞り込み | 集めるのはページ番号だけです。行はまだ読んでいません |
Bitmap Heap Scan | 上のページ番号から実際に行を読む | Heap Blocks の lossy。出ていれば work_mem 不足です |
BitmapAnd / BitmapOr | 複数のインデックスを組み合わせる | どのインデックスが実際に使われたか |
Memoize | Nested Loop の内側で同じキーが繰り返し引かれる | Hits / Misses / Evictions |
Materialize | 内側を一度作って何度も走査する | 元の行数。大きいまま実体化していないか |
HashAggregate | GROUP BY(並び順を問わない) | Batches と Disk Usage。2 以上ならメモリ不足です |
GroupAggregate | 入力がすでにキー順に並んでいる GROUP BY | 直下に Sort が付いていないか。付いていれば本体はそちらです |
WindowAgg | ウィンドウ関数 | PARTITION BY / ORDER BY の組ごとに Sort が必要になります |
Incremental Sort | 並べ替えキーの前半だけがすでに並んでいる | Presorted Groups。まとめて並べ替えるより省メモリです |
Unique | DISTINCT、UNION | 直下の Sort が本体のコストです |
SetOp | INTERSECT、EXCEPT | 両側を読み切ってから判定します |
CTE Scan | 実体化された WITH 句 | loops。何度も走査されていないか |
Subquery Scan | 上位へ展開できなかった副問い合わせ | 上の条件が中へ押し込まれていない兆候です |
Function Scan | 集合を返す関数の呼び出し | 行数が決め打ちで見積もられていないか |
Result | 定数だけの評価、条件が常に偽のとき | One-Time Filter: false なら配下は一度も実行されません |
ProjectSet | SELECT 句に集合を返す関数を書いたとき | 1 行の入力から複数行が出るため、上位の行数が跳ねます |
Limit | LIMIT / OFFSET | 配下が途中で止まっているか。OFFSET の分は捨てるために読んでいます |
LockRows | SELECT ... FOR UPDATE | 行ロックの取得です。待ち時間はここに現れます |
ModifyTable | INSERT / UPDATE / DELETE | 本体は配下のスキャンです。トリガーの時間は末尾に別行で出ます |
Nested Loop の内側で同じキーが何度も引かれる場合、PostgreSQL は結果をハッシュ表に覚えておき、2 回目以降はそこから返します。実行計画では次のように出ます。
Hits が大きければ、内側の走査はその回数だけ省けています。逆に Evictions が増えていれば、覚えきれずに捨てているということです。この表の大きさは work_mem × hash_mem_multiplier で決まります。
Misses が Hits と同じくらいなら、効いていません。キーがほとんど重複していないということなので、この場合に見るべきは Memoize ではなく、外側の行数の見積もりです。
PostgreSQL 12 より前は、WITH は必ず先に実行され、結果を丸ごと保持していました。上位の条件は中へ入らず、これを利用して意図的に評価順を固定する書き方も広く使われていました。
12 以降は、参照が 1 回だけで副作用がなく、再帰でもない WITH は本体へ展開されます。展開されれば上位の条件が中へ押し込まれ、インデックスも使われます。このとき実行計画に CTE Scan は現れません。
同じ結果を何度も参照するなら実体化、1 度しか使わず絞り込みを効かせたいなら展開が有利です。CTE Scan の loops が大きければ、実体化した結果を繰り返し走査しています。
never executed は、時間が 0 だという意味ではありません。そのノードが一度も呼ばれなかったという意味です。Limit で途中で止まった場合や、条件が常に偽で One-Time Filter: false が付いた場合に現れます。ROWS を指定していない関数は 1000 行と見積もられます。実際が 5 行でも 100 万行でも同じ 1000 行です。関数の結果を結合に使うと、この決め打ちがそのまま計画の判断材料になります。CREATE FUNCTION ... ROWS 5 で実態に近い値を教えられます。Sort は、単独では現れません。必ず何かの前処理です。GroupAggregate、Unique、Merge Join、WindowAgg の直下に付きます。Sort が重いときに直すべきは、多くの場合その上のノードの選択のほうです。rows と実測の rows を比べます。そこがずれていなければ、そのノードは原因ではありません。Partition pruning
パーティション表では、条件から不要なパーティションを除く判断が入ります。効いているかどうかは、実行計画に何が現れていないかで読み取ります。
月ごとに 13 個のパーティションを持つ表ですが、実行計画に並んでいるのは 2 個だけです。残りは条件に合う行がないと判断され、実行計画から取り除かれました。
枝刈りには 2 つの時点があります。条件の値が計画時に分かっていれば、そのとき除かれます。この場合、除かれたパーティションは実行計画に痕跡すら残しません。
値が実行時にしか決まらない場合は、実行しながら除きます。その結果が Subplans Removed: 11 です。プレースホルダを使ったクエリや、副問い合わせの結果で絞る場合がこれにあたります。
ANALYZE なしの EXPLAIN では、全パーティションが並びます。実行時の枝刈りは実行しないと起きないためです。枝刈りを確かめたいときは、必ず EXPLAIN (ANALYZE, BUFFERS) で見てください。
Append の子が全部並んでいたら、まずこれを疑います。WHERE date_trunc('month', created_at) = '2026-09-01' ではなく、created_at >= '2026-09-01' AND created_at < '2026-10-01' と範囲で書きます。統計が使えるという意味でも、こちらが有利です。SET plan_cache_mode = 'force_custom_plan'; で毎回値を見た計画に固定できます。enable_partitionwise_join と enable_partitionwise_aggregate は既定で無効なので、必要なら明示的に有効化します。計画時間とメモリは増えます。Estimation drift
行数の見積もりを外すと、その誤差は上位のノードへ伝わり、結合を重ねるたびに大きくなります。まずは、実行計画の深い位置でずれ始めたノードを探しましょう。
プランナーは、条件ごとの選択率を掛け合わせて行数を見積もります。しかし、2 つの条件に相関があると、それぞれが独立しているという前提が崩れ、実際よりも小さく見積もることがあります。
例えば、「都道府県 = 東京」と「市区町村 = 渋谷区」は独立した条件ではありません。それぞれの選択率を単純に掛けると、該当する行数を実際よりもはるかに少なく見積もってしまいます。その結果、Nested Loop が選ばれ、内側の処理を数万回も繰り返すことになります。
下のスライダーでは、最初の見積もりが実測値とどれだけずれたとき、2 回の結合を経て誤差がどこまで膨らむかを確認できます。
単一表の絞り込み
1 回目の結合の後
2 回目の結合の後
結合のたびに誤差が重なるため、1 段目で 10 倍ずれると、3 段目では桁そのものが変わります。
まずは ANALYZE を実行し、統計情報を最新の状態にします。それでも見積もりが改善しなければ、相関のある列に対して拡張統計を作成します。
特定の列だけ値の分布が偏っている場合は、その列の統計目標を上げ、より細かなヒストグラムを作らせます。
Fixing the estimate
乖離を見つけたら、次は直す番です。プランナーは表そのものではなく、統計情報を見て行数を見積もります。ずれの原因は、多くの場合そちらにあります。
大量の投入や削除の直後は、自動処理が追いつく前に統計が実態から離れます。pg_stat_user_tables の last_analyze と last_autoanalyze を見て、最後に集計された時刻を確認してください。
ANALYZE を実行しただけで見積もりが直り、計画が変わることは珍しくありません。設定に手を付ける前に、ここを確かめます。
既定では、1 列につき 100 個の代表値と度数分布を保持します(default_statistics_target)。値の種類が極端に多い列や、偏りの大きい列では、この粒度で足りません。
全体設定を上げると、すべての表の集計時間と保持量が増えます。問題の列だけを上げるのが基本です。効いたかどうかは pg_stats の n_distinct と most_common_vals で確かめられます。
プランナーは既定で、条件どうしが互いに独立だと仮定します。都道府県が「神奈川県」である行が全体の 7%、市区町村が「横浜市」である行が 3% なら、両方を満たす行は 0.21% と見積もります。実際には横浜市はすべて神奈川県にあるので、正しくは 3% です。14 倍の過小見積もりです。
この関係を教えるのが拡張統計です。
dependencies は「片方が決まれば他方も絞られる」関係、ndistinct は組み合わせの種類数、mcv はよく出る組み合わせを保持します。作成後は必ず ANALYZE を実行してください。
WHERE date_trunc('month', created_at) = '2026-09-01' のように列を加工すると、created_at の統計は使えません。プランナーは決め打ちの推定値を使うため、大きく外れます。
式インデックスを作ると、その式そのものの統計が集められます。
より確実なのは、列を加工せずに範囲で書くことです。created_at >= '2026-09-01' AND created_at < '2026-10-01' と書けば、元の列の統計がそのまま使えます。
直したあとは、もう一度 EXPLAIN ANALYZE を取ります。見積もりが実測に近づいたか、そして計画そのものが変わったかを確認してください。行数が合っても計画が変わらない場合、原因は統計ではありません。
Where the time goes
見積もりと実測が一致しているのに遅い場合、原因は行数ではなく読み書きの量にあります。BUFFERS を付けると、各ノードが何ページ触ったかが表示されます。
shared hit は共有バッファから読めたページ、read はそこになくディスク(または OS のキャッシュ)へ取りに行ったページです。同じクエリの 1 回目と 2 回目で時間が違う理由は、ここに現れます。
dirtied は内容を変更したページ、written は書き出したページです。SELECT でも、行の可視性の記録を更新して dirtied が増えることがあります。
temp が出たら、メモリ不足です。上の例では Sort Method: external merge と temp read/written が同時に出ています。ソートがメモリに収まらず、一時ファイルへ書き出しています。
Rows Removed by Filter が大きくないか確かめてください。work_mem が足りていません。Sort Method が quicksort になれば解消です。Hash Batches が 2 以上のときも同じ原因です。read=8124 はおよそ 63MB です。所要時間と突き合わせると、読み出しの速度が妥当かどうかを判断できます。Compiled at run time
見積もりコストが一定の値を超えると、PostgreSQL は条件式や集計の評価を機械語へコンパイルしてから実行します。速くするための仕組みですが、短いクエリでは、この準備そのものが所要時間の大半を占めることがあります。
実際にデータを読んで集計したのは 41 ms ほどで、残りはコンパイルに費やされています。この時間は Execution Time に含まれます。Planning Time ではありません。実行計画の下だけを見て「集計が遅い」と判断すると、原因を取り違えます。
4 つの内訳のうち、時間を食いやすいのは Optimization と Emission です。Deforming は行から列を取り出す処理、Expressions は WHERE 句や集計式の評価を指します。
並列実行では、ワーカーごとにコンパイルされます。表示される時間はその合計なので、ワーカー 3 本なら 3 倍の値が並びます。実際に待たされる時間は 1 本分ですが、CPU はその分使われています。
JIT が動くかどうかは、プランナーが出した総コストだけで決まります。実際に何行処理するかは関係ありません。
| パラメータ | 既定値 | 超えたときに起きること |
|---|---|---|
jit_above_cost | 100,000 | コンパイルを行う |
jit_inline_above_cost | 500,000 | 関数の呼び出しを展開する |
jit_optimize_above_cost | 500,000 | 最適化をかける(いちばん重い) |
ここに、見積もりの誤差が直結します。実際は 100 行しか返らないのに、統計が古くて 500 万行と見積もっていれば、コストは閾値を軽く超え、100 行のためにコンパイルが走ります。JIT の無駄打ちは、多くの場合、統計の問題として現れています。
これで速くなるなら、原因は JIT です。SET jit_above_cost = 500000; のように閾値を上げれば、本当に重いクエリだけを対象にできます。判断の目安は、コンパイルに費やす時間を、実行時間の短縮で取り返せるかです。
取り返せるのは、数百万行を読みながら式を繰り返し評価するような分析系のクエリです。数十ミリ秒で終わる問い合わせが大量に飛ぶ用途では、閾値を上げるか無効にしたほうが、全体としては速くなります。
列とパーティションが多いほど、Functions が増えます。数百の列を扱うクエリでは生成される関数が数百個に達し、コンパイルだけで秒単位になることがあります。実行計画に JIT: の節を見つけたら、まず Functions の数と Total を確かめてください。
Read the symptom
現場でよく目にする 3 つの実行計画を用意しました。それぞれ、どこに原因があるのかを考えてみましょう。
Settings, last
設定値はサーバー全体に効きます。1 本のクエリのために全体を動かす前に、そのクエリの中で解けることを尽くします。
SET work_mem = '64MB'; をそのセッションだけに適用します。SET はその接続だけに効き、切断で元に戻ります。RESET work_mem; でいつでも戻せます。shared hit の割合が違います。比較するなら、同じ条件で複数回実行した値どうしで比べてください。In production
実行計画を取る操作そのものにも、気を付ける点があります。あわせて、数あるクエリのどれから手を付けるかの決め方を整理します。
BEGIN; と ROLLBACK; で囲んでください。EXPLAIN (ANALYZE, BUFFERS) が既定の形だと考えてください。読み方はバッファの節で扱っています。EXPLAIN (ANALYZE, TIMING OFF, BUFFERS) で計測を省けます。検証環境の実行計画は、本番と一致しません。データ量も統計情報も違うため、プランナーの判断が変わります。判断材料にするなら、本番と同等のデータ量で確かめてください。
pg_stat_statements で total_exec_time の大きい順に並べ、上位から着手します。3 秒かかるクエリが 1 日 10 回より、80 ミリ秒のクエリが 100 万回のほうが、サーバー全体への影響ははるかに大きくなります。min_exec_time と max_exec_time が桁で違うなら、原因は計画そのものではなく、キャッシュの状態やロック待ち、並列ワーカーを確保できたかどうかにあります。auto_explain を設定し、auto_explain.log_min_duration を超えたクエリの実行計画だけをログへ残します。log_analyze と log_buffers も有効にしておくと、実測値まで残ります。Myth vs. reality
実行計画に慣れている人でも、判断を誤りやすいポイントを整理します。
表の大部分を返すなら、先頭から順に読んだほうが効率的です。例えば、1 ページに 100 行ある表から全体の 10% の行を取り出そうとすると、結局はほぼすべてのページに触れます。インデックス経由でページをばらばらに読むよりも、Seq Scan を選ぶほうが合理的です。
コストは計画同士を比べるための相対値であり、単位は秒ではありません。実行時にデータがキャッシュへ載っているかどうかも、そのまま反映されるわけではありません。所要時間を知りたい場合は、ANALYZE を付けて実測します。
actual time は配下の処理も含めた累計値で、loops が 1 より大きければ、ループ 1 回あたりの平均です。原因は、時間の大きなノードではなく、実行計画の深い位置で行数の見積もりを外したノードにあることが少なくありません。時間だけで判断せず、見積もりと実測のずれを追いましょう。
Knowledge check
答えを当てるだけでなく、なぜそう判断できるのかを説明できれば合格です。
選択肢を1つ選んでください。
Where to look first
目次