1 年後のショップと、この計器盤
3,200 万行の注文明細データを前に、4つの計器と4つのタブの役割、実行計画ツリーの見方、キャッシュの影響を把握します。
- 1 年後のテーブルは、第一部と何が同じで何が違うか
- この計器盤の 4 つの数字と 4 つのタブは、何を見せているか
- 同じクエリの 2 回目が速いのはなぜか
膨れ上がったデータ: 1年後の注文明細テーブル
第一部 0 章ではわずか数十 KB だったテーブルが、order_items だけで約 2GB にまで膨張しています。table_rows 列はオプティマイザ用の推定行数であり、正確な行数は後ほど実際にカウントします。テーブル構造の変化を確認してみましょう。
第一部のテーブル設計と比較して、意図的な 2 つの違いがあります。いずれもデータ規模が急成長した実務現場で頻繁に下される設計判断です。
1 つ目は、order_items テーブルに customer_id、status、ordered_at 列が複製(冗長化)されている点です。 第一部 4 章では「同じ事実を複数の場所に書かない」という正規化の原則を学びましたが、ここではあえてその原則を破っています。 最大の理由は、この EC サイトで最もアクセス頻度が高いクエリが「特定の顧客の購入履歴・明細一覧」であり、正規化したままでは毎回巨大な orders テーブルと JOIN する高負荷が発生するためです。 列を複製しておけば、order_items テーブル単体へのアクセスだけで高速に応答できます。 もちろん代償として、注文ステータス変更時には両方のテーブルを同時に更新(トランザクション保護)しなければなりません。 このように「参照頻度が圧倒的に高く、更新経路を限定できる」ケースにおいて、意図的に正規化を崩す手法を非正規化と呼びます。
2 つ目は、外部キー制約を設定していない点です。 第一部 4 章で学んだ外部キー制約は整合性を強力に守る反面、毎秒数千行規模の高速書き込みが発生する環境では、1 行追加するたびに親テーブルの存在確認を行う I/O コストが無視できなくなります。 そのため大規模分散システムでは、データベース制約をあえて外し、アプリケーション側のバリデーションで整合性を担保する設計を選択することがあります。 「制約を外すと何が危険に晒されるか」を第一部で理解した上で、意図を持って外す判断を行うことが極めて重要です。
パフォーマンスの測定: 計器盤の見方
第一部 8 章で登場した計器パネルが、この第二部における最も重要な診断ツールになります。まずはクエリを実行し、上段の 4 つの計器を確認してみましょう。
実行に数秒の遅延が発生したはずです。最終的に返ってきた行数はたったの 1 行ですが、データベースが走査した「読んだ行数」は 3,200 万行 に達し、アクセス方法は Table scan(全件走査)となっています。 これが本番システムでアラートが鳴り響く原因そのものです。各計器の意味を整理しましょう。
- 実行時間 — データベースがクエリの処理に費やした実測時間。1 秒を超えると赤、20ms 未満で青色になります。
- 読んだ行数 — 最終結果を作成するために InnoDB ストレージエンジンが実際にディスクやメモリから読み出した行数(
Innodb_rows_read)。「返した行数」との乖離が大きいほど、無駄な読み出しが発生していることを意味します。 - 返した行数 — クライアントに返却された最終的な行数。
- アクセス方法 — 実行計画の探索アルゴリズム。Table scan は全行走査、Index lookup はインデックスによる直接参照を示します。
オプティマイザの思考を見る: 実行計画ツリー
「実測プラン」タブを開き、右上の「生テキスト」をクリックすると、データベースエンジンが出力した実行計画の詳細テキストが表示されます。
-> Aggregate: count(0), avg(order_items.unit_price) (cost=3.6e+6 rows=1) (actual time=4500..4500 rows=1 loops=1)
-> Filter: (order_items.customer_id = 123456) (cost=3.3e+6 rows=3.2e+6) (actual time=190..4500 rows=64 loops=1)
-> Table scan on order_items (cost=3.3e+6 rows=32e+6) (actual time=0.5..3900 rows=32e+6 loops=1)これが実行計画(Execution Plan)です。データベースがどのようなステップでクエリを処理したかを記録したツリー構造であり、下から上に向かって読み解きます。 最下段で order_items テーブルの全 3,200 万行を読み込み(Table scan)、その中から customer_id = 123456 の行だけをフィルタリングし(Filter)、最上段でカウントと平均値を集計(Aggregate)しています。 この最適な実行手順を自動的に策定するデータベース内部のエンジンをオプティマイザと呼びます。
各タブの役割は以下の通りです。
- 結果 — クエリが返したデータ一覧。
- 推定プラン — クエリ実行前にオプティマイザが事前算出した見積もり計画(
EXPLAIN FORMAT=TREE)。表示される rows 数はすべて統計情報に基づく推定値です。 - 実測プラン — 実際にクエリを完走させて計測した実測付きの実行計画(
EXPLAIN ANALYZE)。見積もりと実測の乖離を視覚的に確認できます。 - 統計 — クエリ実行に伴って発生したディスク I/O ページ数、一時テーブル作成、ソート処理などのサーバー内部メトリクス差分。
「推定」と「実測」の 2 つが存在するのは、オプティマイザが実行前の推定値をもとに実行計画を決定するからです。 上記の推定プランでは「対象の顧客 ID に該当する行は約 320 万行あるだろう」と過大に見積もっています(実際は 64 行)。 オプティマイザの事前見積もりが狂うと、不適切な探索ルートが選ばれて深刻な遅延障害が発生します。その典型例は第 3 章で体験します。
本番でよくある罠: 全件 COUNT(*) の重さ
第一部 3 章では一瞬で終わった SELECT COUNT(*) を、3,200 万行のテーブルに対して実行してみます。どのくらい時間がかかるか確かめてみてください。
結果が返るまでに数秒かかったはずです。InnoDB はテーブル全体の正確な行数をメタデータとして保持していないため、COUNT(*) を実行するとテーブル内の全レコードを 1 行ずつ実際に数え上げます。 管理画面のダッシュボードに「累計注文数」などを安易に配置してしまうと、画面を開くたびにこの数秒の全件スキャンが走り、システム全体の CPU を食いつぶす原因になります。
キャッシュの温度: コールドリードとウォームリード
同じクエリを間髪入れずに 2 回連続で実行し、実行時間と統計タブの Innodb_buffer_pool_reads(ディスク読み込みページ数)を比較してみましょう。
2 回目のほうが圧倒的に高速で、Innodb_buffer_pool_reads が 0 に近くなっていることが確認できます。 1 回目はストレージ(ディスク)からメモリへデータを読み込みましたが、2 回目はメモリ(バッファプール)上にキャッシュされたデータをそのまま利用できたためです。 実行時間にはクエリ自体の効率性だけでなく「キャッシュが効いているか(キャッシュの温度)」が大きく影響します。 本教材でクエリの性能を公正に比較する際は、必ず 2 回以上実行し、キャッシュが温まった状態(ウォーム状態)での数値を比較してください。
本環境での操作ルール
- 1 回に実行できる SQL 文は 1 つです。
- SELECT クエリは 60 秒で自動タイムアウトします。「タイムアウトして完了しなかった」という事象自体が重要な学びとなる章もあります。
- インデックス作成(CREATE INDEX)は 3,200 万行に対して 1〜2 分程度の時間を要します。
- 各章は前の章で作成したインデックスを前提に進行する場合があります。各章冒頭の「前提条件」をご確認ください。
3,200 万行の大規模データベースでは、非正規化や制約の省略といった実務的な設計判断が加わります。計器盤の「読んだ行数」と「アクセス方法」をもとに性能を診断していきましょう。
次の章へ
次章では、マイページの重いクエリ(顧客 123456 の明細取得)を実際にチューニングしていきます。 なぜわずか 64 行の結果を取得するために 3,200 万行ものデータを走査しなければならなかったのか、インデックスの内部構造(B-tree)を解剖しながら解決していきましょう。