発展原因を指せるようになる

遅いノードの見つけ方(4 つのサイン)

定義

実行計画からボトルネックを特定する手順は、(1) 各ノードの自分時間を引き算で出して降順に並べ、(2) loops が 1 より大きいノードは掛け算で総量に戻し、(3) 実測が見積りの 10 倍以上のノードを根本原因の候補にし、(4) Rows Removed by Filter で無駄読みを見る、の 4 つである。

手順にする理由

実行計画を眺めて Seq Scan を探す、という読み方だと当たりません。並んでいる数字が、そのままでは互いに比べられない形になっているからです。 理由は 2 つあります。

  • 表示は累計。そのノードの actual time には子の時間が全部入っています。 だから大きい数字を探すと、必ずいちばん上のノードに行き着きます
  • loops が付いていると 1 回あたりの平均。掛け算を戻すまで、他のノードと同じ土俵に乗りません

この計画では、その 2 つがまとめて効いています。表示がいちばん大きいノード(Limit 2160.379 ミリ秒)は、自分では 0.1 ミリ秒 しか使っていません。逆に表示がいちばん小さいノードが、全体の 58% を持っています。だからここでは、眺めるのをやめて順番の決まった手順にします。

題材はこのセクションの最初のページに出したのと同じ計画です。まずクエリ本体を出しておきます。何をしているか分からないものの内部を読んでも身に付かないためです。

SELECT c.name, count(*), sum(i.price * i.qty) AS total
FROM customers c
JOIN orders o      ON o.customer_code = c.code
JOIN order_items i ON i.order_id = o.id
WHERE o.status = 'shipped'
  AND o.ordered_at >= '2026-07-01'
  AND o.channel = 'web'
  AND o.payment = 'card'
  AND i.qty = 3
GROUP BY c.name
ORDER BY total DESC
LIMIT 10;

やっていることは「7 月以降の web / カード決済で発送済みの注文から、数量 3 の明細を集めて、顧客ごとの売上上位 10 件」。SQL としては素直な部類。

顧客 2 万件customers)・注文 200 万件orders)・明細 1200 万件order_items)に対して実行して、2.16 秒かかっています。この計画です。

 Limit  (cost=196523.68..196523.70 rows=10 width=30) (actual time=2160.299..2160.379 rows=10.00 loops=1)
   ->  Sort  (cost=196523.68..196526.29 rows=1046 width=30) (actual time=2160.293..2160.320 rows=10.00 loops=1)
         Sort Key: (sum((i.price * i.qty))) DESC
         Sort Method: top-N heapsort  Memory: 26kB
         ->  GroupAggregate  (cost=196477.54..196501.07 rows=1046 width=30) (actual time=2090.929..2158.921 rows=20000.00 loops=1)
               Group Key: c.name
               ->  Sort  (cost=196477.54..196480.15 rows=1046 width=22) (actual time=2090.883..2131.767 rows=500000.00 loops=1)
                     Sort Key: c.name
                     Sort Method: external merge  Disk: 16656kB
                     ->  Nested Loop  (cost=597.43..196425.08 rows=1046 width=22) (actual time=3.118..1561.364 rows=500000.00 loops=1)
                           ->  Hash Join  (cost=597.00..57141.20 rows=523 width=18) (actual time=3.087..177.650 rows=250000.00 loops=1)
                                 Hash Cond: (o.customer_code = c.code)
                                 ->  Seq Scan on orders o  (cost=0.00..56537.00 rows=526 width=11) (actual time=0.129..127.002 rows=250000.00 loops=1)
                                       Filter: ((ordered_at >= '2026-07-01'::date) AND (status = 'shipped'::text) AND (channel = 'web'::text) AND (payment = 'card'::text))
                                       Rows Removed by Filter: 1750000
                                 ->  Hash  (cost=347.00..347.00 rows=20000 width=21) (actual time=2.844..2.847 rows=20000.00 loops=1)
                                       Buckets: 32768  Batches: 1  Memory Usage: 1309kB
                                       ->  Seq Scan on customers c  (cost=0.00..347.00 rows=20000 width=21) (actual time=0.053..1.144 rows=20000.00 loops=1)
                           ->  Index Scan using order_items_order_id_idx on order_items i  (cost=0.43..266.09 rows=23 width=12) (actual time=0.005..0.005 rows=2.00 loops=250000)
                                 Index Cond: (order_id = o.id)
                                 Filter: (qty = 3)
                                 Rows Removed by Filter: 4
                                 Index Searches: 250000
 Planning Time: 3.419 ms
 Execution Time: 2162.557 ms

手 0: そのまま。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF

サイン 1 — 時間を持っているノードを出す

各ノードについて、次を計算します。

  1. actual time上端loops を掛ける (これがそのノード以下の合計時間)
  2. そこから子のぶんを同じやり方で計算して引く
  3. 残りがそのノードの自分の時間

まず 3 ノードで手を動かす

いきなり 10 ノードでやると大変なので、3 ノードしかない計画で 1 回やってみます。 こちらは loops が全部 1 なので、引き算だけで済みます。

 Nested Loop  (cost=0.85..28.26 rows=4 width=17) (actual time=0.041..0.051 rows=4.00 loops=1)
   ->  Index Scan using members_pkey on members m  (cost=0.42..8.44 rows=1 width=17) (actual time=0.022..0.022 rows=1.00 loops=1)
         Index Cond: (id = 42)
         Index Searches: 1
   ->  Index Scan using orders_s_member_idx on orders_s o  (cost=0.43..19.78 rows=4 width=8) (actual time=0.017..0.024 rows=4.00 loops=1)
         Index Cond: (member_id = 42)
         Index Searches: 1
 Planning Time: 0.311 ms
 Execution Time: 0.099 ms

3 ノードの小さな計画。loops はすべて 1。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF

上から順に、actual time上端の数字だけ拾います。 いちばん上の数字には子 2 つの時間が全部入っているので、引きます。

Nested Loop  0.051      ← いちばん上
  Index Scan using members_pkey on members m 0.022
  Index Scan using orders_s_member_idx on orders_s o 0.024
Nested Loop の自分の時間 = 0.051 - (0.022 + 0.024) = 0.005

いちばん上のノードが、実は自分ではほとんど何もしていません。子は葉なので、引くものがなく、表示がそのまま自分の時間になります。 結果はこうです。

自分の時間が大きい順
ノード自分の時間loops
Index Scan using orders_s_member_idx on orders_s o0.0ms(24.2%)1
Index Scan using members_pkey on members m0.0ms(22.2%)1
Nested Loop0.0ms(5.1%)1

これが手順の全部です。あとはノードが増えるだけ……ではありません。loops が 1 でないノードが混じった瞬間に、話が変わります。

同じことを本物の計画でやる

さっきの引き算の「同じやり方で」が肝です。子に loops が付いていたら、子も掛け算してから引きます。 ここを飛ばすと答えが変わります。実際に両方やって並べます。

loops を掛けずに引き算した場合(誤答)
ノード自分の時間
Nested Loop1383.7ms(64.0%)
Sort570.4ms(26.4%)
Seq Scan on orders o127.0ms(5.9%)
Hash Join47.8ms(2.2%)
GroupAggregate27.2ms(1.3%)
Index Scan using order_items_order_id_idx on order_items i10 / 10 位)0.0ms(0.0%)
loops を掛けて引き算した場合(正しい)
ノード自分の時間loops
Index Scan using order_items_order_id_idx on order_items i1250.0ms(57.8%)250000
Sort570.4ms(26.4%)1
Nested Loop133.7ms(6.2%)1
Seq Scan on orders o127.0ms(5.9%)1
Hash Join47.8ms(2.2%)1

1 回の掛け算で、1 位と最下位が入れ替わりました。

  • 掛けないと Nested Loop が 1 位に見える。表示上いちばん大きい数字(1561.364)を持っているのがこのノードだからです
  • 掛けると Nested Loop は 134ms まで落ち、内側の Index Scan が 1250ms・全体の 58% で 1 位になります

内側の表示は actual time=0.005..0.005 です。1 回あたり 0.005 ミリ秒で終わっているので、素朴に読むといちばん軽く見えます。 ところが loops=250000 なので、0.005 × 250000 = 1.25 秒。これが 2.16 秒の内訳の大半です。

サイン 2 — loops を掛ける

サイン 1 と 2 は独立していません。サイン 1 の引き算にサイン 2 が要るので、 この順で並べています。

loops が 1 より大きいノードでは、actual timerows1 回あたりの平均です。 詳しくはEXPLAIN ANALYZE の見方にあります。

引き算は「上限」も教えてくれる。actual time はミリ秒 3 桁で丸められるので、loops が大きいノードは掛けると上振れすることがあります。 そのときは親のサブツリー時間から、外側のぶんを引いた値が内側の上限です。 外側は loops=1 で丸めが増幅されないので、そちらは正確に読めます。

サイン 3 — 見積りと実測がずれているノードを探す

rows= は 2 か所に出ます。前半(cost=… の側)が見積り、後半(actual … の側)が実測です。 この計画では、外側の Seq Scan on orders がこうなっています。

  • 見積り rows=526 に対して、実測 rows=250000475 倍の外れ

これが Nested Loop が選ばれた理由です。外側が 526 行だと思っているので、「内側を 526 回まわす」計画が最安に見えます。 実際は 250,000 回まわります。loops=250000 の出どころはここです。

見るのは「見積りが小さすぎる」側だけです。多めに見積もっているノードは候補に入れません。 Nested Loop の暴発は必ず過小見積りで起きる(少なく見積もるから 何度もまわす計画が安く見える)ためで、多めに見積もったときは プランナが安全側の計画を選ぶので事故になりにくいからです。

この計画にちょうど反例があります。内側の Index Scanrows=23(見積り)に対して rows=2.00(実測)で11.5 倍ずれていますが、ずれの向きが逆なので候補には入りません。 「10 倍以上ずれたら候補」とだけ覚えると、真犯人ではないノードが混じります

サイン 4 — 読んで捨てている行を見る

Rows Removed by Filter は、読んだあとに条件に合わなくて捨てた行数です。この計画では 2 か所に出ます。

  • 外側の Seq Scan on orders: 1,750,000 行を捨てている。 200 万行読んで 25 万行しか使っていません
  • 内側の Index Scan: ループ 1 回ごとに 4 行捨てている。 6 行読んで 2 行だけ使っています

条件が Index Cond 側にあれば読む前に効き、Filter 側にあると読んでから捨てます。違いはIndex Cond と Filter の違いに。

直して、順位表を作り直す

犯人が分かったので直します。ここから先は全部実測です。

手 1: 内側にテーブル本体を読ませない

内側が遅いのは、インデックスを引いたあとテーブル本体を読みに行っているからです。 必要な列をインデックスに含めてしまえば、テーブルを読まずに済みます。

CREATE INDEX order_items_covering
    ON order_items (order_id) INCLUDE (price, qty);
 Limit  (cost=60395.21..60395.23 rows=10 width=30) (actual time=934.953..934.974 rows=10.00 loops=1)
   ->  Sort  (cost=60395.21..60397.87 rows=1064 width=30) (actual time=934.949..934.956 rows=10.00 loops=1)
         Sort Key: (sum((i.price * i.qty))) DESC
         Sort Method: top-N heapsort  Memory: 26kB
         ->  GroupAggregate  (cost=60348.28..60372.22 rows=1064 width=30) (actual time=878.864..933.942 rows=20000.00 loops=1)
               Group Key: c.name
               ->  Sort  (cost=60348.28..60350.94 rows=1064 width=22) (actual time=878.847..910.373 rows=500000.00 loops=1)
                     Sort Key: c.name
                     Sort Method: external merge  Disk: 16656kB
                     ->  Nested Loop  (cost=597.43..60294.78 rows=1064 width=22) (actual time=8.830..404.174 rows=500000.00 loops=1)
                           ->  Hash Join  (cost=597.00..57141.20 rows=523 width=18) (actual time=8.802..109.528 rows=250000.00 loops=1)
                                 Hash Cond: (o.customer_code = c.code)
                                 ->  Seq Scan on orders o  (cost=0.00..56537.00 rows=526 width=11) (actual time=0.086..69.011 rows=250000.00 loops=1)
                                       Filter: ((ordered_at >= '2026-07-01'::date) AND (status = 'shipped'::text) AND (channel = 'web'::text) AND (payment = 'card'::text))
                                       Rows Removed by Filter: 1750000
                                 ->  Hash  (cost=347.00..347.00 rows=20000 width=21) (actual time=8.554..8.554 rows=20000.00 loops=1)
                                       Buckets: 32768  Batches: 1  Memory Usage: 1309kB
                                       ->  Seq Scan on customers c  (cost=0.00..347.00 rows=20000 width=21) (actual time=0.048..1.306 rows=20000.00 loops=1)
                           ->  Index Only Scan using order_items_covering on order_items i  (cost=0.43..5.80 rows=23 width=12) (actual time=0.001..0.001 rows=2.00 loops=250000)
                                 Index Cond: (order_id = o.id)
                                 Filter: (qty = 3)
                                 Rows Removed by Filter: 4
                                 Heap Fetches: 0
                                 Index Searches: 250000
 Planning Time: 1.754 ms
 Execution Time: 935.912 ms

手 1: Index Only Scan に変わり、Heap Fetches: 0(テーブル本体を 1 回も触っていない)。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF

2.16 秒 → 0.94 秒。内側は Index Only Scan になり、Heap Fetches: 0 が出ています(Heapはテーブル本体のこと。インデックスから本体を取りに行った回数が 0 という意味)。 カバリングインデックスの話はカバリングインデックスに。

自分の時間が大きい順
ノード自分の時間loops
Sort506.2ms(54.1%)1
Index Only Scan using order_items_covering on order_items i250.0ms(26.7%)250000
Seq Scan on orders o69.0ms(7.4%)1
Nested Loop44.6ms(4.8%)1

1 位が入れ替わりました。内側を直したので、今度は Sort が 1 位です。Sort Method: external merge Disk: 16656kB と出ていて、並べ替えがメモリに収まらず一時ファイルに書いています

手 2: ソートをメモリに載せる

SET work_mem = '128MB';
 Limit  (cost=60395.21..60395.23 rows=10 width=30) (actual time=834.246..834.271 rows=10.00 loops=1)
   ->  Sort  (cost=60395.21..60397.87 rows=1064 width=30) (actual time=834.242..834.250 rows=10.00 loops=1)
         Sort Key: (sum((i.price * i.qty))) DESC
         Sort Method: top-N heapsort  Memory: 26kB
         ->  GroupAggregate  (cost=60348.28..60372.22 rows=1064 width=30) (actual time=795.088..832.844 rows=20000.00 loops=1)
               Group Key: c.name
               ->  Sort  (cost=60348.28..60350.94 rows=1064 width=22) (actual time=795.064..807.410 rows=500000.00 loops=1)
                     Sort Key: c.name
                     Sort Method: quicksort  Memory: 31820kB
                     ->  Nested Loop  (cost=597.43..60294.78 rows=1064 width=22) (actual time=5.537..390.310 rows=500000.00 loops=1)
                           ->  Hash Join  (cost=597.00..57141.20 rows=523 width=18) (actual time=5.513..113.784 rows=250000.00 loops=1)
                                 Hash Cond: (o.customer_code = c.code)
                                 ->  Seq Scan on orders o  (cost=0.00..56537.00 rows=526 width=11) (actual time=0.202..74.525 rows=250000.00 loops=1)
                                       Filter: ((ordered_at >= '2026-07-01'::date) AND (status = 'shipped'::text) AND (channel = 'web'::text) AND (payment = 'card'::text))
                                       Rows Removed by Filter: 1750000
                                 ->  Hash  (cost=347.00..347.00 rows=20000 width=21) (actual time=5.180..5.183 rows=20000.00 loops=1)
                                       Buckets: 32768  Batches: 1  Memory Usage: 1309kB
                                       ->  Seq Scan on customers c  (cost=0.00..347.00 rows=20000 width=21) (actual time=0.098..2.167 rows=20000.00 loops=1)
                           ->  Index Only Scan using order_items_covering on order_items i  (cost=0.43..5.80 rows=23 width=12) (actual time=0.001..0.001 rows=2.00 loops=250000)
                                 Index Cond: (order_id = o.id)
                                 Filter: (qty = 3)
                                 Rows Removed by Filter: 4
                                 Heap Fetches: 0
                                 Index Searches: 250000
 Planning Time: 1.372 ms
 Execution Time: 836.672 ms

手 2: Sort Method が quicksort に変わり、一時ファイルが消えた。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF

0.94 秒 → 0.84 秒。変わったのは Sort Method の 1 行だけです。external merge Disk: 16656kBquicksort Memory: 31820kB になりました。 この行の読み方はソートとメモリに。

ここでもう一度、順位表を作り直します。

自分の時間が大きい順
ノード自分の時間loops
Sort417.1ms(49.9%)1
Index Only Scan using order_items_covering on order_items i250.0ms(29.9%)250000
Seq Scan on orders o74.5ms(8.9%)1
Hash Join34.1ms(4.1%)1

潰したはずの Sort が、まだ 1 位のままです。一時ファイルへの書き出しは消えましたが、並べ替えそのものは残っているためです。「1 位を直せば 2 位が上がってくる」とは限りません。同じノードが 1 位に残ることもあるので、直したら必ず作り直して確かめます。

やってはいけない直し方

サイン 3 で「見積りが 475 倍外れている」と分かったので、統計を直せば速くなりそうに見えます。4 つの条件に相関があることを DB に教えてみます。

CREATE STATISTICS orders_corr (mcv, ndistinct)
    ON status, ordered_at, channel, payment FROM orders;
ANALYZE orders;
 Limit  (cost=675525.13..675525.16 rows=10 width=30) (actual time=3643.530..3643.561 rows=10.00 loops=1)
   ->  Sort  (cost=675525.13..675575.13 rows=20000 width=30) (actual time=3643.527..3643.541 rows=10.00 loops=1)
         Sort Key: (sum((i.price * i.qty))) DESC
         Sort Method: top-N heapsort  Memory: 26kB
         ->  HashAggregate  (cost=674892.94..675092.94 rows=20000 width=30) (actual time=3640.755..3642.266 rows=20000.00 loops=1)
               Group Key: c.name
               ->  Hash Join  (cost=61405.16..669894.56 rows=499838 width=22) (actual time=177.490..3572.437 rows=500000.00 loops=1)
                     Hash Cond: (o.customer_code = c.code)
                     ->  Hash Join  (cost=60808.16..662424.78 rows=499838 width=15) (actual time=173.815..3496.492 rows=500000.00 loops=1)
                           Hash Cond: (i.order_id = o.id)
                           ->  Seq Scan on order_items i  (cost=0.00..550000.00 rows=4068800 width=12) (actual time=0.302..2510.303 rows=4000000.00 loops=1)
                                 Filter: (qty = 3)
                                 Rows Removed by Filter: 8000000
                           ->  Hash  (cost=56537.00..56537.00 rows=245693 width=11) (actual time=169.596..169.597 rows=250000.00 loops=1)
                                 Buckets: 262144  Batches: 2  Memory Usage: 7431kB
                                 ->  Seq Scan on orders o  (cost=0.00..56537.00 rows=245693 width=11) (actual time=0.325..131.356 rows=250000.00 loops=1)
                                       Filter: ((ordered_at >= '2026-07-01'::date) AND (status = 'shipped'::text) AND (channel = 'web'::text) AND (payment = 'card'::text))
                                       Rows Removed by Filter: 1750000
                     ->  Hash  (cost=347.00..347.00 rows=20000 width=21) (actual time=3.599..3.599 rows=20000.00 loops=1)
                           Buckets: 32768  Batches: 1  Memory Usage: 1309kB
                           ->  Seq Scan on customers c  (cost=0.00..347.00 rows=20000 width=21) (actual time=0.234..1.421 rows=20000.00 loops=1)
 Planning Time: 6.770 ms
 Execution Time: 3644.138 ms

別枝: 見積りは直った(rows=245693)が、計画がまるごと入れ替わった。
PostgreSQL 18.6 / 2026-08-22 採取 / 並列・JIT は OFF

見積りは直りました。ところが 3.64 秒に伸びています。計画を見ると、Nested Loop が消えてHash Join が 2 つと、明細テーブル 1200 万行の全表スキャンに 変わっています。

プランナは正確な見積りをもとに「25 万回のインデックス参照より 1200 万行を順に読む方が安い」と 判断しました。コストモデル上はその判断で正しく、実測では間違っています。バラバラに読む手間を順に読む手間の 4 倍と仮定した既定値(random_page_cost = 4.0) が、キャッシュの効いた環境ではインデックスを使う側を高く見積もりすぎるためです。

統計の更新は「計画が変わる」操作であって「速くする」操作ではありません。確実に言えるのは計画が別物になることだけで、速いか遅いかは環境で変わります。 見積りが外れる仕組みそのものは見積り行数の内訳にあります。

まとめ

  1. 全ノードの自分の時間を出して降順に並べる(loops を掛けてから引く)
  2. 1 位が分かったら直す
  3. 順位表を作り直す。2 位が 1 位に上がってくる
  4. 見積りが小さすぎるノードは根本原因の候補だが、統計を直せば速くなるとは限らない
採取環境

PostgreSQL 18.6postgres:18 公式イメージ / 設定は既定値)。2026-08-22 採取。計画ごとにコンテナを再起動して 5 回まわし、中央値の run を載せています。

次の 2 つだけ既定から変えています。

SET max_parallel_workers_per_gather = 0;
SET jit = off;
  • 並列を切っている理由 — 既定のままだと Gather / Parallel Seq Scan が入り、loops=2ワーカー間の平均という別の意味で現れます。このセクションが教える 「loops は繰り返し回数」と衝突するので切っています
  • JIT を切っている理由 — 計画の末尾に JIT のブロックが増えるだけで、読み方の話には要らないためです

手元で同じ計画を出すときも、この 2 行を先に打ってください。打たないと形が変わります。実行時間の絶対値はマシンとキャッシュの状態で 何倍も動くので、比率で読むのが前提です。

よくある疑問

Q.いちばん時間がかかっているノードを直せば終わりですか?
A.終わりません。1 位を直すと 2 位が 1 位に上がってきます。このページの例でも、内側のインデックス参照を直した瞬間にソートが 1 位になります。直したら順位表を作り直してください。
Q.見積りが外れているノードを見つけたら、統計を更新すればいいですか?
A.更新すると見積りは直りますが、速くなるとは限りません。このページの例では計画が Nested Loop から Hash Join へまるごと入れ替わり、かえって遅くなりました。統計の更新は「計画が変わる」操作であって「速くする」操作ではありません。
Q.自分の時間を引き算したら負の数になりました
A.actual time はミリ秒 3 桁で丸められるので、loops が大きいノードでは掛け算した瞬間に誤差も loops 倍されます。その場合は親のサブツリー時間が上限を与えるので、そこで頭打ちにして読みます。

関連トピック

もっと学びたい方へ(おすすめ書籍)

SQL実践入門 ── 高速でわかりやすいクエリの書き方
ミック

「なぜこの書き方が速いのか」を実行計画から説明する一冊。条件分岐・集約・結合・更新のそれぞれで、良い書き方と悪い書き方を対比しながら読める。

Amazon で見る →
おすすめ
達人に学ぶDB設計徹底指南書 第2版
ミック

テーブル設計と正規化、パフォーマンス考慮のインデックス設計まで実務レベルで学べる定番書。第2版ではクラウド対応も強化。

Amazon で見る →
おすすめ
楽々ERDレッスン (CodeZine BOOKS)
羽生章洋

ER 図をどう「使える設計」に落とすか、実務の判断まで踏み込んだ入門書。エンティティの切り出しから多対多の扱いまで具体例が豊富。

Amazon で見る →
おすすめ
スッキリわかるSQL入門 第4版 ドリル256問付き!
中山清喬/飯田理恵子

ドリル 256 問を実際に打ちながら進める SQL の入門書。付属のブラウザ環境で演習できるので、SELECT から結合・集約までを環境構築で止まらずに通せる。

Amazon で見る →
達人に学ぶSQL徹底指南書 第2版
ミック

SQLの本質的な使い方と、インデックスが効くクエリの書き方を学べる。ウィンドウ関数など現代SQLも網羅。

Amazon で見る →
SQLアンチパターン 第2版
Bill Karwin

実務でやりがちなSQL・DB設計のアンチパターンとその回避策を体系的に学べる。

Amazon で見る →
[改訂3版]内部構造から学ぶPostgreSQL
勝俣智成 ほか

PostgreSQLの内部構造・ストレージ・インデックス機構を丁寧に解説。設計と運用計画の鉄則が学べる。

Amazon で見る →
情報処理教科書 データベーススペシャリスト 2025年版
三好康之

IPAデータベーススペシャリスト試験の総合対策書。インデックス関連は本サイトと合わせて学ぶと理解が深まる。

Amazon で見る →
理論から学ぶデータベース実践入門
奥野幹也

リレーショナルモデルの理論から、インデックス設計を含む実務で使えるSQLまで解説。

Amazon で見る →

本セクションはAmazonアソシエイトのリンクを含みます。

オンライン個別指導

もっと深くDBを学びたい方へ。

たいてっくが、SQL・データベース設計・パフォーマンスチューニング・IPAデータベーススペシャリスト対策まで、1対1で学習をサポートします。まずは無料相談から。

無料相談を予約する →
受講者の声 (DB・SQL コース)
menta レビュー原文を見る →
  • 教え方も上手で、お人柄も良いメンターです。DB周りの知識はもちろん、何より、しっかり教えてあげようという姿勢がとてもありがたかったです。データベース、SQLの学習を考えている方にはおススメです。
    H 様DB・SQL コース受講
  • 体系的に知識を教えてくださり、実際の業務でも大変役立っております。特に短い時間で効率よく知識の習得や、練習をできているのは期待以上でした。
    M 様DB・SQL コース受講
  • 大変充実したコンテンツでわかりやすいご説明をありがとうございました。基本的な質問にも丁寧にご説明いただき、また業務のご相談にも乗って頂き大変有意義な時間でした。
    K 様DB・SQL コース受講