本文へ移動

ホーム / SRE / 第4部 データとアプリケーション

第4部 データとアプリケーション・第7章

データベース

★★★ 必須34トピック・面接 17問

Web系SREでは障害原因の多くがDBに絡むため、仕組みと運用の両方を問われる。MySQL(InnoDB)とPostgreSQLを前提に整理する。

別のページで開く →

DBラボを読み込んでいます…

ひとことで

データの形と、どう読み書きするかで選ぶ。迷ったらまずRDB。特定の用途に強いDBを、必要な部分にだけ組み合わせる。

種類特徴向く用途苦手なこと例
RDB表とSQL、トランザクション、JOIN業務データ全般、お金や在庫書き込みの水平分散MySQL、PostgreSQL、Aurora
KVSキーで1件を超高速に読み書きキャッシュ、セッション、カウンタ複雑な検索・集計Redis、DynamoDB
ドキュメントDBJSONをそのまま保存、形が自由項目がまちまちなデータ、CMS複数の文書をまたぐJOINMongoDB、DocumentDB、Firestore
ワイドカラム大量の書き込みを多数のノードに分散ログ、IoT、時系列の大量データアドホックな検索Cassandra、Bigtable
グラフDBノードとつながりを保存し、たどるSNSの関係、推薦、不正検知大量の集計Neo4j、Neptune
時系列DB時刻つきの数値を効率よく保存・集計メトリクス、センサー一般的な業務データTimestream、InfluxDB、Prometheus
検索エンジン全文検索、あいまい検索、集計検索窓、ログ分析トランザクション、正確な更新OpenSearch、Elasticsearch
NewSQLSQLとトランザクションを保ったまま水平分散RDBでは収まらない超大規模コスト、運用の難しさ、レイテンシSpanner、TiDB、CockroachDB、Aurora DSQL
選ぶときの質問考え方
強い整合性が必要かお金・在庫・予約ならRDB(またはNewSQL)
アクセスパターンは決まっているかキーで引くだけならKVS(DynamoDBは事前にアクセスパターンを設計する)
規模はどれくらいか多くの場合はRDB+リードレプリカ+キャッシュで足りる。NewSQLは本当に必要なときだけ
誰が運用するかマネージドサービスを選び、チームが運用できる数に絞る
検索や分析は別か本体はRDB、検索はOpenSearch、分析はデータウェアハウスに同期する
データの形と、読み書きのしかたで選ぶRDBSQLとトランザクション向く用途迷ったらまずこれMySQL・PostgreSQL・AuroraKVSキーで高速に読み書き向く用途キャッシュ・セッションRedis・DynamoDBドキュメントJSONをそのまま保存向く用途形がまちまちなデータMongoDB・DocumentDBワイドカラム大量の書き込みを分散向く用途ログ・IoTCassandra・Bigtableグラフつながりをたどる向く用途SNS・推薦・不正検知Neo4j・Neptune時系列時刻つきの数値向く用途メトリクス・センサーTimestream・InfluxDB検索全文検索・集計向く用途検索窓・ログ分析OpenSearch・ElasticsearchNewSQLSQLのまま水平分散向く用途超大規模で強い整合性Spanner・TiDB・CockroachDB1つのDBで全部まかなわず、RDBを中心に「キャッシュはRedis、検索はOpenSearch」のように組み合わせるのが一般的
データベースの種類

「流行っているから」ではなく、整合性・アクセスパターン・規模・運用体制で選ぶ。

面接ではこう答える

データの形と読み書きのパターンで選びます。お金や在庫など強い整合性が必要な業務データはRDBが基本で、キーで高速に引くキャッシュやセッションはKVS、形がまちまちなデータはドキュメントDB、つながりをたどるならグラフDB、メトリクスは時系列DB、全文検索は検索エンジンです。NewSQLはRDBで収まらない規模で検討します。実際にはRDBを中心に、用途ごとに組み合わせます。

ひとことで

DB設計は「業務の言葉を整理する(概念)→ 表に落とす(論理)→ 製品と性能に合わせる(物理)」の3段階。最後に実際のクエリで確かめる。

DB設計は、いきなりCREATE TABLEを書くのではなく、3つの段階で考えると抜け漏れが減ります。概念設計では、業務に出てくる「もの」(ユーザー、商品)と「出来事」(注文、支払い)をエンティティとして洗い出し、関係をER図にします。論理設計では、それを表・列・キーに落とし、正規化で重複と矛盾をなくします。物理設計では、使うDB製品に合わせて型・インデックス・パーティションを決め、性能を確かめます。

段階やること成果物
概念設計業務の用語集、エンティティと関係の洗い出し用語集、概念ER図
論理設計表・列・主キー・外部キー・制約、正規化論理ER図、テーブル定義
物理設計型・インデックス・パーティション・文字コード・容量の見積もりDDL、インデックス設計、容量計画

どの段階でも、業務の人と同じ言葉を使うことが大事です。「顧客」と「会員」と「ユーザー」が同じものか違うものかを最初に決めておかないと、後で列やテーブルが重複します。最後に、主要な画面やAPIで実際に発行するSQLを書いてみて、無理なくデータが取れるか、インデックスで支えられるかを確認します。

DB設計の進め方:業務の言葉から始め、最後に「実際のクエリ」で確かめる1 要件と用語を集める業務の流れ・画面・帳票から名詞を拾う2 エンティティを決めるユーザー・注文など「もの」と「出来事」3 関係を引く1対1・1対多・多対多(ER図)4 属性・キーを決める主キー・一意・NOT NULL・型5 正規化する第3正規形まで重複と矛盾をなくす6 クエリで確かめる主要な画面・APIのSQLを書いてみる7 必要なら非正規化計測した性能問題だけを根拠に8 運用を決める削除・履歴・保持・マイグレーション方針
DB設計の進め方

6の「クエリで確かめる」を飛ばすと、正しいけれど遅い設計になりがち。

面接ではこう答える

DB設計は、業務のもの・出来事をエンティティとして洗い出してER図にする概念設計、表・列・キーに落として正規化する論理設計、型やインデックスなど製品と性能に合わせる物理設計の3段階で進めます。最後に主要な画面のSQLを書いて、データが無理なく取れ、インデックスで支えられるかを確かめます。

ひとことで

表は「行(1件のデータ)」と「列(項目)」でできている。行を一意に指すのが主キー、他の表の行を指すのが外部キー。表同士のつながり方は3種類しかない。

用語意味例
テーブル(表)・行・列同じ種類のデータの集まり。行が1件、列が項目users表の1行が1人のユーザー
主キー(PK)行を一意に識別する列(の組)。NULL不可・重複不可users.id
候補キー主キーになれる列(一意で、最小限のもの)users.email も一意なら候補キー
複合キー複数の列の組み合わせで一意になるキー(order_id, product_id)
外部キー(FK)他の表の主キーを参照する列。存在しない値を入れられないorders.user_id → users.id
一意制約(UNIQUE)重複を禁止メールアドレス
NOT NULL・CHECK・DEFAULT空を禁止・値の範囲を制限・既定値quantity > 0
カーディナリティ関係の多さ(1対1・1対多・多対多)。インデックスの文脈では値の種類の多さ1人のユーザーに多くの注文

関係は3種類です。1対1はユーザーとプロフィールのように片方に1つだけ対応し、多くは同じ表にまとめるか、更新頻度や権限の違いで分けます。1対多はユーザーと注文のような最も多い関係で、外部キーは「多」の側(注文)に置きます。多対多は注文と商品のように互いに複数対応する関係で、直接は表せないので中間テーブル(注文明細)を作り、1対多を2つに分けます。

外部キーの削除時の動作意味使いどころ
RESTRICT / NO ACTION参照されている行は消せない(既定)ほとんどの場合はこれ
CASCADE親を消すと子も一緒に消える親と運命をともにする子(明細など)。大量削除で長いロックに注意
SET NULL親を消すと子の外部キーをNULLに関係が切れても子は残したい場合

外部キー制約は整合性を守ってくれますが、大きな表では更新や削除のたびにチェックとロックが走るので、性能やマイグレーションを理由に張らない現場もあります。その場合は、アプリでの整合性チェックと定期的な不整合の検出をセットにします。

テーブル同士のつながりは「1対1」「1対多」「多対多」の3種類1対11人に1つユーザープロフィール1対多1人が何回も注文ユーザー注文多対多互いに複数注文注文明細中間テーブル商品線の端が「|」なら1、「鳥の足(三つ又)」なら多。多対多はそのまま表せないので、中間テーブルで1対多を2つに分ける1対多の外部キーは「多」の側に置く(注文テーブルに user_id)
3種類のリレーション

多対多は必ず中間テーブルに分解する。中間テーブル自体が「注文明細」のような意味のある表になることが多い。

面接ではこう答える

表は行と列でできていて、主キーで行を一意に指し、外部キーで他の表の行を参照します。関係は1対1、1対多、多対多の3種類で、1対多の外部キーは多の側に置き、多対多は中間テーブルで1対多を2つに分けて表します。外部キー制約は整合性を守りますが、大きな表ではロックや性能への影響も考えます。

ひとことで

正規化は、同じ事実を1か所にだけ持つように表を分ける手順。重複がなくなるので、更新したのに一部だけ古い、という矛盾が起きなくなる。

正規化されていない表では、3つの異常が起きます。データを直すときに複数の行を直す必要があり、一部を直し忘れると矛盾する更新異常、まだ注文のない商品を登録できないような挿入異常、最後の注文を消すと商品の情報まで消える削除異常です。正規化は、これらを段階的になくす手順です。

考え方の土台は関数従属です。「Xが決まればYが1つに決まる」とき、YはXに関数従属している(X→Y)と言います。たとえば商品IDが決まれば商品名が決まるので、商品ID→商品名です。

正規形ルールなくすもの例
第1正規形(1NF)1つのマスに1つの値。繰り返しの列を作らない複数の値・繰り返しグループ「本, ペン」を1行ずつに分ける
第2正規形(2NF)主キーの「一部だけ」で決まる列をなくす部分関数従属(注文ID, 商品ID)のうち商品IDだけで決まる商品名を商品表へ
第3正規形(3NF)主キー以外の列で決まる列をなくす推移的関数従属注文→顧客ID→顧客名 の顧客名を顧客表へ
ボイス・コッド正規形(BCNF)すべての決定項(Xの側)が候補キーである候補キー以外が決定項になる従属講師→科目 のような関係を分ける
第4・第5正規形独立した多値の組み合わせや、結合で復元できる冗長を分ける多値従属・結合従属実務で意識することは少ない

実務では第3正規形までを目標にするのが一般的です。覚え方は「キーに、キー全体に、キー以外の何にも依存しない」。第1正規形は「キーに依存する(1つの値)」、第2正規形は「キー全体に依存する(一部ではなく)」、第3正規形は「キー以外の何にも依存しない」に対応します。

第1正規形:1つのマスには1つの値だけ。繰り返しを行に分ける非正規形:注文🔑注文ID顧客商品1佐藤本, ペン2鈴木ノート「本, ペン」では検索も集計も難しい第1正規形:注文明細🔑注文ID🔑商品顧客数量1本佐藤11ペン佐藤22ノート鈴木11行1商品。主キーは(注文ID, 商品)の組やってはいけない例:カンマ区切りで複数の値を入れる、「商品1」「商品2」「商品3」のように列を増やすまだ問題が残る:顧客名が明細の行ごとに重複している → 第2・第3正規形へ
第1正規形

列を「商品1・商品2・商品3」と増やすのも第1正規形違反。4つ目が来た瞬間に破綻する。

第2正規形:主キーの「一部だけ」で決まる列を、別のテーブルに分ける第1正規形:注文明細🔑注文ID🔑商品ID数量商品名定価1P11本15001P22ペン2002P13本1500商品名と定価は「商品ID」だけで決まる(部分関数従属)→ 商品名を変えると全部の明細を直す必要がある注文明細🔑注文ID🔑商品ID数量1P111P222P13商品🔑商品ID商品名定価P1本1500P2ペン200実務の注意:注文時の価格は、定価が後で変わっても残す必要があるので、明細に「販売単価」として別に持つ(定価のコピーではなく「その時いくらで売ったか」という別の事実)
第2正規形

複合主キーの表で、その一部だけで決まる列がないかを探す。

第3正規形:主キー以外の列で決まる列を、別のテーブルに分ける第2正規形:注文🔑注文ID顧客ID顧客名ランク注文日1C1佐藤ゴールド11/12C1佐藤ゴールド11/53C2鈴木一般11/6注文ID → 顧客ID → 顧客名・ランク(推移的関数従属)→ ランクが上がったら過去の注文の行まで全部直すことに注文🔑注文ID↗顧客ID注文日1C111/12C111/53C211/6顧客🔑顧客ID顧客名ランクC1佐藤ゴールドC2鈴木一般覚え方:「キーに、キー全体に、キー以外の何にも依存しない(The key, the whole key, and nothing but the key)」
第3正規形

「主キーではない列」から決まる列がないかを探す。

面接ではこう答える

正規化は、同じ事実を1か所にだけ持つように表を分ける手順で、更新異常・挿入異常・削除異常を防ぎます。第1正規形で1つのマスに1つの値にし、第2正規形で複合主キーの一部だけで決まる列(部分関数従属)を分け、第3正規形で主キー以外の列で決まる列(推移的関数従属)を分けます。実務では第3正規形までを基本にし、性能のための非正規化は計測した問題に対してだけ行います。

別のページで開く →

DBラボを読み込んでいます…

ひとことで

正規化した上で、読み取りの速さのために意図して重複を持つのが非正規化。ただし「注文時の価格」のように、そもそも別の事実であるものは重複ではない。

正規化すると、データを取り出すときにJOINが増えます。読み取りが非常に多く、JOINや集計が性能のボトルネックになると分かった場合に限り、あえて重複を持つのが非正規化です。非正規化したら、元のデータとのずれが起きないよう、更新の仕組みを決めます。

よくある非正規化例整合性の保ち方
集計値を持つ記事に「いいね数」の列同じトランザクションで更新、または定期的に再計算
よく使う値をコピーコメントに投稿者の表示名名前変更時に一括更新するか、多少のずれを許容
検索用の表を別に作る一覧画面用にJOIN済みの表やマテリアライズドビュー更新時に作り直す。検索はOpenSearchに同期
読み取り専用の複製レポート用の集計テーブルバッチで作成し、「いつ時点か」を表示
重複ではない「その時点の値」理由
注文明細の販売単価商品の定価が後で変わっても、その時いくらで売ったかは変わらない
注文の配送先住所ユーザーが引っ越しても、その注文の配送先は変わらない
請求書の税率・会社名法令や社名が変わっても、発行時点の内容を残す必要がある

この区別は面接でもよく聞かれます。「正規化すれば価格は商品表だけに持つべき」と答えると、過去の注文の金額が変わってしまう設計になります。何が「現在の値」で、何が「出来事が起きた時点の値」なのかを見分けることが、実務の設計では正規化の知識以上に大事です。

面接ではこう答える

非正規化は、正規化したうえで、読み取りの性能のために意図して重複を持つことで、集計値や表示名のコピー、検索用の表などがあります。行う場合は計測した問題を根拠にし、元のデータと同期する仕組みを決めます。一方で、注文時の販売単価や配送先住所は、商品やユーザーの現在の値とは別の「その時点の事実」なので、重複ではなく明細側に持つのが正しい設計です。

ひとことで

「ユーザーが商品を注文する」というよくある題材で、エンティティ・関係・キー・その時点の値の持ち方をまとめて確認する。

ユーザー(users)は複数の住所(addresses)と複数の注文(orders)を持ちます。注文と商品(products)は多対多なので、注文明細(order_items)を中間テーブルにします。商品はカテゴリ(categories)に属し、カテゴリは親カテゴリを持つ階層です。

CREATE TABLE users (
  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  email       VARCHAR(255) NOT NULL,
  name        VARCHAR(100) NOT NULL,
  created_at  DATETIME(6)  NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  UNIQUE KEY uq_users_email (email)
) DEFAULT CHARSET = utf8mb4;

CREATE TABLE orders (
  id           BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id      BIGINT UNSIGNED NOT NULL,
  status       VARCHAR(20)  NOT NULL,          -- pending / paid / shipped / canceled
  ship_address VARCHAR(500) NOT NULL,          -- 注文時点の住所を保存
  ordered_at   DATETIME(6)  NOT NULL,
  KEY idx_orders_user_ordered (user_id, ordered_at),  -- 「ある人の最近の注文」用
  CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE order_items (
  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  order_id    BIGINT UNSIGNED NOT NULL,
  product_id  BIGINT UNSIGNED NOT NULL,
  quantity    INT NOT NULL CHECK (quantity > 0),
  unit_price  INT NOT NULL,                    -- 注文時点の販売単価(円)
  UNIQUE KEY uq_items (order_id, product_id),
  CONSTRAINT fk_items_order   FOREIGN KEY (order_id)   REFERENCES orders(id),
  CONSTRAINT fk_items_product FOREIGN KEY (product_id) REFERENCES products(id)
);
設計のポイント理由
email に一意制約同じメールで二重登録させない。アプリのチェックだけでは同時登録をすり抜ける
(user_id, ordered_at) の複合インデックスマイページの「最近の注文」をソートなしで取れる
unit_price・ship_address を注文側に保存定価の改定や引っ越しで過去の注文が変わらないように
status は決まった値だけCHECK制約や状態の表で、ありえない状態を入れさせない。状態遷移もアプリで守る
在庫(stock)の更新UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0 で、売り越しを防ぐ
設計例:小さなECサイトのER図usersPK idemail (UNIQUE)namecreated_ataddressesPK idFK user_idpostal_codeaddressordersPK idFK user_idstatusship_addressordered_atorder_itemsPK idFK order_idFK product_idquantityunit_priceproductsPK idFK category_idnamepricestockcategoriesPK idnameFK parent_id・order_items.unit_price と orders.ship_address は、注文した時点の値をあえて複製する (後で価格改定や引っ越しがあっても、過去の注文の内容が変わらないように)・categories.parent_id は自分自身を参照(親子の階層)。stock は同時更新に注意(行ロック・条件付きUPDATE)
ECサイトのER図

面接のシステム設計でも、最初にこのくらいの粒度のER図を描けると議論が進めやすい。

面接ではこう答える

ユーザー、住所、注文、注文明細、商品、カテゴリに分け、注文と商品の多対多は注文明細で表します。メールには一意制約、マイページ用に(user_id, ordered_at)の複合インデックスを張ります。注文時の販売単価と配送先は注文側に保存し、在庫は条件付きのUPDATEで売り越しを防ぎます。

ひとことで

主キーは「変わらない・短い・並び順が都合よい」ものを選ぶ。業務の値(メールなど)ではなく、意味のないIDを使うのが基本。

方式内容長所短所
自然キーメール、社員番号など業務の値をそのまま使う意味が分かる値が変わると、参照しているすべての表を直す必要
サロゲートキー(代理キー)意味のない連番やUUIDを使う変わらない。短い業務上の一意性は別に一意制約で守る
連番(AUTO_INCREMENT・シーケンス)DBが1ずつ増やす小さく速い。挿入が右端に集まり効率的件数を推測される。複数DBでの生成やマージが難しい
UUIDv4ランダムな128ビットどこでも衝突せず生成できるランダムなので挿入位置がばらけ、書き込み・容量・キャッシュ効率が落ちる
UUIDv7先頭が時刻の128ビット(RFC 9562)どこでも生成でき、時刻順に並ぶ連番よりは大きい。生成時刻が分かる
Snowflake型ID時刻+マシン番号+連番の64ビット時刻順・64ビットに収まる生成する仕組みの運用が必要

InnoDBでは主キーの順にデータそのものが並ぶので、主キーの選び方が書き込み性能に直結します。また、セカンダリインデックスはすべて主キーの値を持つので、主キーが大きいとすべてのインデックスが大きくなります。外部に見せるIDと内部のIDを分け、URLには推測されにくいID(UUIDなど)を、内部の結合には連番を使う、という使い分けもよく行われます。

主キーの並び方で、B+Treeへの書き込み方が変わるInnoDBでは主キーの順にデータそのものが並ぶ(クラスタインデックス)。青が新しく書き込まれるページ連番(AUTO_INCREMENT)1〜100101〜200201〜300301〜400最新常に右端に追加。ページがぎっしり埋まり、メモリにも乗りやすいUUIDv4(ランダム)分割分割分割あちこちに挿入され、ページ分割が多発。すき間が増え、キャッシュに乗る割合が下がるUUIDv7(時刻順)最新先頭が時刻なので、連番とほぼ同じく右端に追加される
主キーと書き込み位置

UUIDを使うならv7。ランダムなv4を主キーにして書き込みが遅くなる事例は多い。

面接ではこう答える

主キーは変わらず短いものを選び、業務の値をそのまま使う自然キーより、意味のないサロゲートキーを使い、業務上の一意性は一意制約で守るのが基本です。単一のDBなら連番が小さく速く、分散して生成するならUUIDですが、InnoDBでは主キーの順にデータが並ぶので、ランダムなUUIDv4ではなく時刻順のUUIDv7を選びます。外部に見せるIDと内部のIDを分けることもあります。

ひとことで

型は後から変えるのが最も大変な部分の1つ。金額・日時・文字列の3つで事故が多い。NULLは「不明」を意味し、比較の結果も不明になる。

データおすすめ避けたいこと
金額円なら整数(INT・BIGINT)、小数が必要ならDECIMALFLOAT・DOUBLE(2進数の誤差で計算が合わない)
日時UTCで保存し、表示時に利用者のタイムゾーンへ変換。秒未満が必要なら精度を指定サーバーのローカル時刻で保存。MySQLのTIMESTAMPは2038年までしか扱えない
文字列MySQLは utf8mb4(絵文字を含む全文字)。照合順序(大文字小文字・濁点の区別)を決めるMySQLの utf8(3バイトまでで絵文字が入らない)
真偽値BOOLEAN(MySQLは TINYINT(1))、NOT NULL で既定値つきNULLを含む3状態になってしまう
状態・区分決まった値ならCHECK制約や区分の表。増減が多いなら表で管理意味の分からない数値(1, 2, 3)だけ
JSON形が決まっていない付加情報検索・結合・制約に使う項目まで入れる
大きなデータ画像やファイルはS3に置き、DBにはキー(パス)だけDBにBLOBで入れる(バックアップとレプリケーションが重くなる)

NULLは「値がない・不明」を表します。NULLとの比較(= NULL)は真でも偽でもなく「不明」になるので、IS NULLで判定します。WHERE col <> 'A' はNULLの行を含まず、集計関数のCOUNT(col)はNULLを数えない、といった落とし穴があります。不要なNULLは事故の元なので、基本はNOT NULLにし、本当に「ない」ことに意味がある列だけNULLを許します。

面接ではこう答える

金額は誤差の出ない整数かDECIMALで、日時はUTCで保存して表示時に変換し、文字列はMySQLならutf8mb4にします。画像などの大きなデータはS3に置いてDBにはパスだけを持ちます。NULLは比較の結果が不明になり、集計やWHERE句で思わぬ結果になるので、基本はNOT NULLにし、意味のある場合だけNULLを許します。

ひとことで

「消す」と「履歴を残す」をどう設計するかで、復元のしやすさ、監査、性能、個人情報の扱いが変わる。

削除の設計には、行を本当に消す物理削除、削除日時などの印をつけて残す論理削除、別の表やストレージに移してから消すアーカイブがあります。論理削除は手軽ですが、すべてのクエリに「削除されていない」条件が必要になり、一意制約(同じメールアドレスで再登録)が効かなくなり、表が太り続けるという問題があります。

履歴の持ち方内容向くもの
更新日時・更新者の列created_at、updated_at、updated_byすべての表の基本
履歴テーブル変更のたびに旧い値を別の表に保存「いつ誰が何を変えたか」が必要なデータ
有効期間を持つvalid_from・valid_to で、ある時点の値を引ける料金表、権限、組織
状態の遷移を記録注文の状態が変わるたびに1行追加注文、申請、ワークフロー
イベントとして保存「残高」ではなく「入金・出金」の出来事を積み上げる(イベントソーシング)お金、ポイント、在庫の移動
DBの変更データの取り込み(CDC)binlogやWALから変更を流して別の場所に保存監査、分析基盤への同期

個人情報については、退会後に「消してほしい」という要求や法令に応えられることも必要です。論理削除のまま残し続けると、それに応えられません。退会時に個人を特定できる列を消す・匿名化する処理と、バックアップやログに残る分の扱いも決めておきます。

「消す」には3つの設計がある。復元・監査・性能のどれを重視するかで選ぶ物理削除DELETE FROM usersWHERE id = 42;○ 単純・テーブルが軽い○ 一意制約がそのまま使える× 消したら戻せない× 「いつ誰が」が残らない→ バックアップと監査ログで補う論理削除UPDATE users SETdeleted_at = NOW() ...○ すぐ戻せる× 全クエリに条件が必要× 一意制約が効かない (削除済みと同じメール)× テーブルが太り続ける履歴・アーカイブINSERT INTO deleted_usersSELECT ...; DELETE ...○ 本体はきれいなまま○ 復元・監査ができる○ 古いものは別の場所へ× 移す処理の作り込み→ 監査が必要なデータ向け
削除の3つの方式

「全部論理削除」にすると後で困る。データの種類ごとに方式を決める。

面接ではこう答える

削除には物理削除・論理削除・アーカイブがあり、論理削除はすぐ戻せる反面、全クエリに条件が必要で一意制約が効かず、表が太り続けます。履歴は、更新者と日時の列、履歴テーブル、有効期間、状態遷移の記録、イベントとしての保存などをデータの性質で使い分けます。個人情報は退会時の削除や匿名化に応えられるように設計します。

別のページで開く →

DBラボを読み込んでいます…

ひとことで

最初は楽に見えて、データが増えたり要件が変わったりしたときに必ず困る設計のパターン。レビューで見つけたら早めに直す。

アンチパターン何が困るか直し方
カンマ区切りで複数の値(ジェイウォーク)検索・集計・結合ができない。値の整合性を守れない中間テーブルで多対多に
EAV(エンティティ・属性・値の汎用表)型・必須・一意の制約が効かず、1件を取り出すのに多数の行を集める決まった属性は列に、可変な部分だけJSON列に
ポリモーフィック関連(種類と IDの2列で何でも指す)外部キーが張れず、壊れた参照が生まれる対象ごとに外部キー列か中間テーブルを分ける
何でもJSONに入れるインデックス・制約・型が効かず、遅くて壊れやすい検索・結合・制約に使う項目は列に出す
列を増やして繰り返す(tel1, tel2, tel3)4つ目が来たら破綻。検索も面倒子テーブルに行として持つ
何でもNULL許容「値がない」の意味が曖昧になり、集計を間違える基本はNOT NULL
外部キーも一意制約も張らないアプリのバグや同時実行で不整合が入るDBで守れる制約はDBで守る
意味の分からない名前flag1、data、type などが乱立業務の用語集に合わせて命名
1つの巨大な表に全部ロックの競合、変更の影響範囲が大きい更新頻度や責任で分ける
最初は楽でも、後から必ず困る4つの設計カンマ区切りで複数の値よくある形tags = "aws,sre,go"直し方中間テーブル(post_tags)で多対多にする何でも入るEAVよくある形attributes(entity_id, key, value)型も必須もチェックできない直し方決まっている属性は列に。可変部分だけJSON列ポリモーフィック関連よくある形comments(target_type, target_id)外部キーが張れない直し方対象ごとに外部キー列か中間テーブルを分けるとりあえずJSONに全部よくある形data JSON に検索条件の項目まで索引・制約・型が効かない直し方検索・結合・制約に使う項目は列に出す
代表的なアンチパターン

共通するのは「DBに守らせられることを、アプリやJSONに押し付けている」こと。

面接ではこう答える

代表的なアンチパターンは、カンマ区切りで複数の値を入れる、何でも入るEAVの表、種類とIDの2列で何でも指すポリモーフィック関連、検索に使う項目までJSONに入れる、などです。いずれもDBの制約やインデックスが効かず、データが増えたり要件が変わったりしたときに遅くなり壊れやすくなります。中間テーブル、列への切り出し、外部キーと一意制約で直します。

ひとことで

複数の顧客企業(テナント)のデータを1つのシステムで扱うとき、どこで分けるかは、安全性・コスト・運用の手間のトレードオフになる。

方式内容向くもの主なリスク
共有テーブル全テナントが同じ表を使い、tenant_id列で区別テナント数が多い一般的なSaaSWHERE条件の付け忘れで他社のデータが見える。大口の負荷が全体に影響
スキーマ分割同じDBの中で、テナントごとにスキーマを分ける数十〜数百のテナントマイグレーションをテナント数だけ実行。数が増えると重い
DB・アカウント分割テナントごとにDB(やAWSアカウント)を分ける大口顧客、規制の厳しい業界コストと運用の手間が最大
共有テーブルで安全にするには内容
すべての表にtenant_id主キーや一意制約・インデックスにもtenant_idを含める
アプリで必ず条件をつけるORMの既定のスコープ(デフォルトスコープ)で、tenant_idの条件を自動で付ける
DBでも守るPostgreSQLの行レベルセキュリティ(RLS)で、接続中のテナント以外の行を見えなくする
テストで確かめる他のテナントのデータが取れないことを自動テストにする
大口テナントの負荷レート制限、必要なら大口だけ専用のDBに移す(混在させる)
マルチテナント:分けるほど安全で重く、まとめるほど安く軽い共有テーブル1つのDB・1つのテーブルtenant_id = Atenant_id = Btenant_id = C○ 安い・運用が楽・数が多くてよい× WHERE忘れで他社のデータが漏れる× 大口テナントの負荷が全体に影響→ 行レベルセキュリティで補強スキーマ分割1つのDB・テナントごとのスキーマABC○ 論理的に分かれる× テナント数ぶんマイグレーション× 数が増えるとDBが重くなる→ 数十〜数百テナント向けDB(アカウント)分割ABC○ 完全に分離・個別に復元○ 大口や規制の厳しい顧客向け× コストと運用の手間が最大→ 混在させる設計も多い
マルチテナントの3つの方式

1つに決めず、「普段は共有、大口だけ分離」と混在させる設計も多い。

面接ではこう答える

マルチテナントは、全テナントが同じ表を使ってtenant_idで区別する共有テーブル、スキーマを分ける方式、DBやアカウントを分ける方式があり、分けるほど安全でコストと運用の手間が増えます。共有テーブルでは、全表とインデックスにtenant_idを含め、ORMの既定のスコープで条件を自動で付け、PostgreSQLなら行レベルセキュリティでDB側でも守ります。大口テナントだけ分離する混在型もよく使います。

ひとことで

RDBは「データを正しく分けてから、クエリを考える」。DynamoDBは「読み方を先に決めて、それに合わせてキーとデータを作る」。

DynamoDBのようなKVS・ドキュメント型のDBでは、JOINがなく、キー以外の条件で効率よく検索できません。そのため、設計の最初にアクセスパターン(どんな画面で、どの条件で、何件を、どの順番で読むか)を全部書き出し、それぞれが1回のQueryで済むように、パーティションキー(PK)とソートキー(SK)を設計します。読み方に合わせて、同じデータを複数の形で持つこともためらいません。

用語・テクニック内容
パーティションキー(PK)データを分散させる単位。同じPKのデータはまとめて読める。偏ると特定のパーティションに負荷が集中する
ソートキー(SK)同じPKの中での並び順。「ORDER#2026-11」で始まるものだけ、のように範囲で絞れる
GSI(グローバルセカンダリインデックス)別のキーで引けるようにする索引。新しい読み方が必要になったら追加
シングルテーブル設計複数の種類のデータを1つの表に入れ、1回のQueryで関連データをまとめて取る
Scanを避ける全件を読むScanは遅く高い。想定外の検索が必要なら、検索エンジンや分析基盤に同期する

アクセスパターンが固まっていて大量のアクセスがある場合には非常に強い一方、要件が頻繁に変わる初期のサービスや、柔軟な検索・集計が必要な業務データでは、RDBの方が向いていることが多いです。

DynamoDBは「どう読むか」から逆算して、キーを設計する1つのテーブルに複数の種類を入れる(シングルテーブル設計の例)🔑PK(パーティションキー)🔑SK(ソートキー)属性USER#42PROFILEname=佐藤USER#42ORDER#2026-11-01total=3200USER#42ORDER#2026-11-05total=800ORDER#9001ITEM#P1qty=1RDBとの違い・先にアクセスパターン(「ユーザーの最近の注文を10件」など)を列挙し、それを1回のQueryで取れるキーにする・PKで同じパーティションにまとめ、SKの並び順で範囲を絞る(例:SKが ORDER# で始まり新しい順)・JOINはできないので、読み方に合わせて必要なデータを重複して持つことをためらわない・別の読み方が必要になったらGSI(別のキーの索引)を足す。想定外の検索はScanになり高くつく
DynamoDBのキー設計の例

「後から検索条件を増やしたい」がよくある要件なら、まずRDBを検討する。

面接ではこう答える

RDBはデータを正規化してからクエリを考えますが、DynamoDBは先にアクセスパターンを列挙し、それぞれが1回のQueryで済むようにパーティションキーとソートキーを設計し、読み方に合わせてデータを重複して持ちます。新しい読み方にはGSIを足し、全件を読むScanは避けます。アクセスパターンが固まった大規模な用途に強く、要件が変わりやすい場面ではRDBが向きます。

ひとことで

スキーマ変更は、アプリのデプロイより戻しにくく、大きな表ではロックで止まる危険がある。レビューでは「ロック・時間・戻し方」を確認する。

確認すること理由・対策
表の大きさと変更の種類大きな表の列追加・型変更・インデックス作成は、時間とロックの影響が大きい
ロックの影響MySQLのオンラインDDLでも、開始と終了でメタデータロックを取る。長いトランザクションがあると待ち行列ができ、全体が止まる
ツールの選択MySQLは gh-ost・pt-online-schema-change、PostgreSQLは CREATE INDEX CONCURRENTLY など
後方互換Expand/Contractで、新旧のアプリが同時に動いても壊れない順番で変える
実行のタイミング負荷の低い時間帯。大きな変更はステージングで本番相当のデータ量で試す
戻し方ロールバック手順を事前に用意。列の削除やデータの変換は戻せないので最後に
マイグレーションの管理ツール(Flyway、Rails migrations など)で履歴を管理し、CIから適用
レプリカへの影響大量の変更はレプリケーション遅延を起こす。小分けにする

具体的な手順は、インフラ設計の章のExpand / Contractも参照してください。

面接ではこう答える

スキーマ変更はロールバックしにくく、大きな表ではロックで処理が止まる危険があるので、表の大きさと変更の種類、メタデータロックと長いトランザクション、オンラインDDLやgh-ostなどのツール、Expand/Contractによる後方互換、実行の時間帯、戻し方を確認します。列の削除など戻せない操作は最後に回し、大量の変更は小分けにしてレプリケーション遅延を防ぎます。

  • インデックスなしでは全行を順に読む(フルスキャン、O(n))

  • B+Tree インデックスは木を根から辿るため O(log n)。100万行でも数回のページ読み込みで目的の行に届く

  • リーフノード同士が連結されているので、範囲検索(BETWEEN、>、ORDER BY)にも強い

  • InnoDB はデータ自体が主キーの B+Tree(クラスタインデックス)。セカンダリインデックスは主キー値を持ち、そこから本体を引く

B+Treeは数段たどるだけで目的の行に届く例:id = 72 を探す(青い経路)50 | 100ルート20 | 3565 | 80120 | 150… 49まで50 58 6265 72 7780 88 95100以降 …50 ≤ 72 < 100リーフ同士がつながっているので、範囲検索も横にたどるだけで速い
B+Treeインデックスの探索 · 3段

ルートで「50以上100未満」、次の段で「65以上80未満」と絞り込み、リーフで行に届く。全件を読む必要がない。

別のページで開く →

DBラボを読み込んでいます…

7.15B-TreeとB+Tree

用語★★★ 必須
ひとことで

データを「並んだ状態のまま」木に入れて、少ない読み込みで目的の行にたどり着くための構造。DBのインデックスの正体。

ディスクやメモリからデータを読むときは、1件ずつではなく「ページ」という固まり単位(InnoDBは16KB、PostgreSQLは8KB)で読みます。だから検索を速くするには、比較の回数よりも読むページの数を減らすことが大事です。B-Treeは、1つのノード(=1ページ)に多くのキーを並べて持たせ、枝分かれを大きくすることで木を浅くし、読むページ数を少なくした木構造です。

B+TreeはB-Treeの改良版で、違いは2つです。①行データ(または行の場所)を一番下のリーフにだけ置き、上の内部ノードは道案内用のキーだけを持つ。②リーフ同士を左右に鎖でつなぐ。①により内部ノードにより多くのキーが入って木がさらに浅くなり、②により範囲検索や並べ替えが「最初のリーフを探して、あとは横にたどるだけ」で済みます。MySQL(InnoDB)もPostgreSQLも、通常のインデックスはこのB+Tree系です。

B-TreeB+Treeハッシュインデックス
行データの場所すべてのノードリーフだけバケット
等価検索(id = 5)O(log n)O(log n)平均O(1)
範囲検索・並べ替え木を上下に行き来リーフを横にたどる(得意)できない
主な使われ方ファイルシステムなどRDBのインデックス全般メモリ上のKVS、一部のDB

補足:書き込みが非常に多いシステムでは、まずメモリに溜めてまとめてディスクに書くLSM-Tree(RocksDB、Cassandraなど)も使われます。書き込みは速いが、読み取りや整理(コンパクション)に負荷がかかる、という逆のトレードオフです。

B+Treeは行データをリーフに集め、リーフ同士を鎖でつなぐB-Treeすべてのノードがキーと行データを持つ30行60行10行20行40行50行70行80行範囲検索(20〜70)は、木を上下に何度も行き来する必要があるB+Tree(MySQL InnoDB・PostgreSQLのインデックス)内部ノードは道案内のキーだけ。行データはリーフだけ306010行20行30行40行50行60行70行80行次へ次へ・内部ノードにデータがない分、1ノードに多くのキーが入る → 木が浅くなる → 読むページが減る・範囲検索(20〜70)は、最初のリーフを見つけたら横にたどるだけ・どのキーも必ずリーフまで降りるので、検索にかかる時間が一定
B-TreeとB+Treeの構造の違い

内部ノードが軽いほど木が浅くなり、リーフがつながっているほど範囲検索が速い。

面接ではこう答える

B+Treeは、行データをリーフにだけ置いてリーフ同士をつないだ木構造です。内部ノードがキーだけなので1ページに多くのキーが入り、100万行でも3段程度で目的の行に届きます。リーフがつながっているので、範囲検索は最初のリーフを見つけたら横にたどるだけで済みます。

行数フルスキャンで読む行B+Treeで辿る段数(目安)
1万行1万行2〜3段
100万行100万行3段
1億行1億行4段

B+Tree は1ノード(1ページ、InnoDBは16KB)に数百のキーを持てるため、木がとても浅い。上の方のノードはメモリ(バッファプール)に載るので、実際のディスク読み込みはさらに少ない。

テーマ要点
デメリットINSERT / UPDATE / DELETE のたびに更新が走り書き込みが遅くなる。ストレージも消費
カーディナリティ値の種類が少ない列(性別、フラグ)は絞り込み効果が低い
複合インデックス左端の列から順にしか使えない(leftmost prefix)。等価条件の列を先、範囲条件の列を後に
カバリングインデックス必要な列がすべてインデックスにあれば本体を読まずに済む(EXPLAIN で Using index)
効かなくなる書き方列に関数や計算をかける、LIKE '%foo'、暗黙の型変換、OR の多用
ハッシュインデックス等価検索は O(1) だが範囲検索に使えない

インデックスが (user_id, status, created_at) の場合:

WHERE句使われるか理由
user_id = 1使われる先頭列
user_id = 1 AND status = 'active'使われる先頭から連続
user_id = 1 AND status = 'active' ORDER BY created_at使われる(ソートも不要)3列すべて順に一致
status = 'active'基本的に使われない先頭列がない
user_id = 1 AND created_at > '2026-01-01'user_id 部分のみstatus を飛ばしている
user_id > 100 AND status = 'active'user_id 部分のみ範囲条件の後ろの列は絞り込みに効きにくい
複合インデックスは左の列から順に並ぶ「電話帳」INDEX (user_id, status, created_at) の中身。上から順に並んでいるuser_idstatuscreated_at1active01-051active02-111active03-201banned01-302active01-022active04-183banned02-09user_id=1 AND status='active'連続した範囲なので一気に読めるstatus='active' だけ行があちこちに散らばる→ インデックスが効かない
複合インデックスの並び

先頭の列から条件がそろっているほど、読む範囲が連続して小さくなる。

別のページで開く →

DBラボを読み込んでいます…

  1. slow query log やAPM、Performance Insights で遅いクエリを特定

  2. EXPLAIN(PostgreSQLは EXPLAIN ANALYZE)で実行計画を確認

  3. type(ALL はフルスキャン)、rows(見積もり行数)、Extra(Using filesort、Using temporary)に注目

  4. インデックス追加、クエリ書き換え、N+1解消、キャッシュで改善

  5. 改善前後の数値を比べて効果を確認

第12章「パフォーマンスチューニング」:クエリとインデックスのチューニングを図で詳しく

別のページで開く →

DBラボを読み込んでいます…

項目注目する値意味
typeconst / eq_ref / ref / range / index / ALL左ほど効率的。ALL はフルスキャン、index はインデックスの全走査
key使われたインデックス名NULL ならインデックス未使用
rows調べる見積もり行数返す行数より極端に多ければ非効率
filtered条件で残る割合低いと無駄に読んでいる
ExtraUsing indexカバリングインデックスで本体を読んでいない(良い)
ExtraUsing filesortソートにインデックスを使えず別途ソート(重い)
ExtraUsing temporary一時テーブルを作成(GROUP BY 等、重い)
性質意味
Atomicity(原子性)全部成功か全部失敗。途中状態を残さない
Consistency(一貫性)制約を満たした状態から満たした状態へ移る
Isolation(分離性)同時実行されるトランザクションが互いに干渉しない
Durability(永続性)コミットしたら障害でも消えない(WAL / redo log)

別のページで開く →

DBラボを読み込んでいます…

ひとことで

複数の操作を「全部成功」か「全部なかったこと」のどちらかにまとめる仕組み。裏ではMVCCとWALが支えている。

トランザクションは、BEGINで始めてCOMMITで確定し、途中で問題があればROLLBACKで開始前の状態に戻します。多くのDBはオートコミットが既定で、BEGINを書かなければSQL1文ごとに自動でCOMMITされます。複数の更新をまとめたいときは、明示的にトランザクションで囲む必要があります。

性質意味支えている仕組み
Atomicity(原子性)全部成功か全部取り消しundoログ(変更前の値の記録)
Consistency(一貫性)制約(一意・外部キーなど)を常に満たす制約チェック、アプリのロジック
Isolation(分離性)同時に動く他のトランザクションと干渉しないMVCC、ロック
Durability(永続性)COMMITしたら障害でも消えないWAL・redoログとfsync

MVCC(多版型同時実行制御)は、行を書き換えるときに古い版を消さずに残す仕組みです。読む側は自分が始まった時点の版(スナップショット)を見るので、書き込み中の行でも待たずに読めます。その代わり古い版が溜まるので、定期的な掃除(InnoDBのパージ、PostgreSQLのVACUUM)が必要です。何時間も開きっぱなしのトランザクションがあると、その時点以降の古い版を消せずに肥大化し、性能が落ちます。

WAL(Write-Ahead Logging、先行書き込みログ)は、データ本体を書き換える前に、変更内容をログへ順番に追記してディスクに確定させる仕組みです。ログへの追記は順番に書くだけなので速く、COMMITはログが確定した時点で成功を返せます。データ本体への反映は後でまとめて行い、途中でクラッシュしても再起動時にログを読み直せば復元できます。このログはレプリケーション(PostgreSQLはWAL、MySQLはbinlog)や、任意の時刻に戻すPITRにも使われます。

よくある問題原因対策
ロック待ちで処理が詰まる長いトランザクションがロックを持ち続けるトランザクションを短くする、外部API呼び出しを中に入れない
ストレージや性能が徐々に悪化長時間開いたトランザクションで古い版を掃除できない長時間トランザクションを監視して終了させる
書き込みのレイテンシが高いCOMMITごとのfsyncが遅いディスクIOPSの高いストレージ、まとめてコミット
一部だけ更新された状態が残るトランザクションで囲んでいない関連する更新を1つのトランザクションにまとめる
全部成功したら確定、途中で失敗したら全部なかったことにする例:口座Aから口座Bへ1万円を振り込むBEGINAから1万円引くBに1万円足すCOMMIT(確定)ここで障害・エラーROLLBACK(取り消し)引いた1万円も元に戻るトランザクションがないと「Aからは引かれたのにBには入っていない」状態が残りうるこれを防ぐのが原子性(Atomicity)。ACIDの他の3つは、分離性・一貫性・永続性
トランザクションのCOMMITとROLLBACK

途中で失敗しても「片方だけ反映」が起きないのが原子性。

MVCC:書き換え中も古い版を残すので、読む人は待たなくてよい同じ行(在庫)の複数の版版1:在庫 10コミット済み版2:在庫 9Bが変更中(未コミット)トランザクションA読むだけ版1を読むトランザクションBUPDATE中・Aは開始時点の「スナップショット」を見るので、Bのコミット前の値は見えない・読み取りが書き込みを待たず、書き込みも読み取りを待たない・古い版は不要になったら掃除される(InnoDBはパージ、PostgreSQLはVACUUM)・長いトランザクションがあると古い版を消せず、肥大化や性能低下の原因になる
MVCCの仕組み

読み取りがロック待ちをしないので、読み取りの多いWebサービスと相性が良い。

先にログへ書いて確定し、データ本体は後でまとめて反映する(WAL)クラッシュしても、ログを読み直せばコミット済みの変更を復元できる1 変更を受け付けUPDATEなど2 ログに追記WAL・redoログに順番に書く(速い)3 ディスクに確定fsyncで確実に書き込む4 COMMIT成功ここで初めて「成功」と返す5 後で本体に反映データファイルはまとめて書く障害が起きたら再起動時にログを読み直して復元レプリケーション同じログをレプリカに送って再生するPITRバックアップ+ログで任意の時刻に戻す
WALによる書き込みと復旧

COMMITの速さ、クラッシュからの復旧、レプリケーション、PITRは全部このログが支えている。

面接ではこう答える

トランザクションは複数の操作を全部成功か全部取り消しにまとめる仕組みで、ACIDを満たします。分離性はMVCCで、古い版を残して読み取りが書き込みを待たないようにしています。永続性はWALで、データ本体より先にログを確定させることで、クラッシュしてもログから復元できます。同じログがレプリケーションやPITRにも使われます。

別のページで開く →

DBラボを読み込んでいます…

分離レベルダーティリードファジーリードファントムリード
Read Uncommitted起きる起きる起きる
Read Committed起きない起きる起きる
Repeatable Read起きない起きない標準では起きる(InnoDBはギャップロックでほぼ防ぐ)
Serializable起きない起きない起きない

デフォルトは MySQL(InnoDB)が Repeatable Read、PostgreSQL が Read Committed。

別のページで開く →

DBラボを読み込んでいます…

異常何が起きるか具体例
ダーティリード他のトランザクションの未コミットの変更が読めるAが残高を0に更新中(未コミット)、Bが0を読む、Aがロールバック → Bは存在しなかった値を使った
ファジーリード(非再現リード)同じ行を2回読むと値が変わるBが残高を読む → Aが更新してコミット → Bが再度読むと違う値
ファントムリード同じ条件で2回検索すると行数が変わるBが「今日の注文」を数える → Aが注文を追加しコミット → Bが再度数えると1件増えている
ロストアップデート同時更新で片方の更新が消えるAとBが在庫10を読み、それぞれ1減らして9を書く → 本来8のはずが9

ロストアップデートは分離レベルだけでは防げないことが多い。UPDATE stock = stock - 1 のようにDB側で計算する、SELECT FOR UPDATE、楽観ロックで防ぐ。

同時に読んで書くと、片方の更新が消える(ロストアップデート)トランザクションADB(在庫)トランザクションB在庫 = 10 を読む在庫 = 10 を読む10 − 1 = 9 を書く10 − 1 = 9 を書く2個売れたのに在庫は9(本来は8)対策:UPDATE stock = stock − 1 / SELECT … FOR UPDATE / version列で楽観ロック
ロストアップデートの発生例

読み取りと書き込みの間に他の更新が入り込むのが原因。DB側で計算するかロックで防ぐ。

テーマ要点
MVCC更新時に古いバージョンを残し、読み取りが書き込みをブロックしない仕組み
行ロック・ギャップロックInnoDB はインデックスに対してロックを取る。インデックスがないと広範囲がロックされる
デッドロック2つのトランザクションが互いのロックを待つ状態。ロック取得順の統一、トランザクションを短く、リトライで対処
楽観ロックversion 列で更新時に競合を検知。競合が少ない場面向き
悲観ロックSELECT ... FOR UPDATE で先にロック。在庫など競合が多い場面向き
長時間トランザクションロック保持、undo肥大、PostgreSQLでは VACUUM が進まない原因に

第3章「コンピュータの基礎(CPU・メモリ・OS)」:競合状態とロックの基本を図で詳しく

別のページで開く →

DBラボを読み込んでいます…

項目楽観ロック悲観ロック
仕組みversion列を持ち、更新時に「読んだときと同じversionか」を条件にする読み取り時に SELECT ... FOR UPDATE でロック
競合時更新が0件になり、アプリがリトライやエラー表示後から来た側が待たされる
向いている場面競合が少ない(プロフィール編集、管理画面)競合が多い(在庫、座席予約、残高)
リスク競合が多いとリトライが多発ロック待ち、デッドロック、スループット低下

別のページで開く →

DBラボを読み込んでいます…

項目MySQL(InnoDB)PostgreSQL
デフォルト分離レベルRepeatable ReadRead Committed
MVCCの実装古いバージョンを undo ログに保持行の新しいバージョンをテーブル内に追記
不要データの掃除パージスレッドが自動VACUUM が必要(autovacuum)。止まると肥大化
テーブル構造主キーによるクラスタインデックスヒープ(主キー順に並ばない)
レプリケーションバイナリログによる論理・行ベースWALによる物理レプリケーション、論理レプリケーション
特徴的な機能シンプルで高速な読み取り豊富なデータ型、部分インデックス、拡張機能
テーマ問われ方と要点
レプリケーション遅延「書いた直後に読めない」バグの原因。更新直後の読み取りはプライマリに向ける
コネクション枯渇max_connections超過。接続プール、RDS Proxy / PgBouncer、アプリ側の接続リーク確認
N+1問題ループ内で1件ずつクエリ。JOIN や一括取得(IN句)で解消
大きいテーブルのALTERロックで書き込みが止まる。オンラインDDL、gh-ost / pt-online-schema-change、段階的マイグレーション
フェイルオーバー非同期レプリは切り替え時にデータ欠損の可能性。Aurora はストレージ共有で短時間
バックアップとリストアスナップショット+バイナリログで PITR。リストア手順を実際に試しているかが重要
スケーリング読み取りはリードレプリカ、書き込みはシャーディングや垂直分割。キャッシュで負荷を逃がす

第8章「ストレージ」:ストレージの性能(IOPS)を図で詳しく

レプリケーションは、プライマリ(書き込みを受ける DB)の変更を、レプリカ(複製)に送り続ける仕組みです。読み取りの負荷をレプリカに逃がすことと、プライマリが壊れたときにレプリカを昇格させて動き続けることの2つに使います。

方式の違いは「アプリにコミットの完了を返す前に、レプリカへの到達をどこまで待つか」です。待つほどフェイルオーバーでデータを失いにくくなりますが、書き込みは遅くなります。障害対応では「切り替えで直前の書き込みが消えるか(RPO:失ってよいデータの範囲)」、日常では「更新したのに古い値が見える(レプリケーション遅延)」の原因として効いてきます。

方式仕組みデータ欠損書き込み性能
非同期プライマリはレプリカの反映を待たずにコミットフェイルオーバー時に欠損の可能性速い
準同期少なくとも1台が受信したらコミットほぼ防げるやや遅い
同期すべてのレプリカが反映したらコミットなし遅い
Auroraストレージ層で6コピー(3AZ)、4つの書き込み完了でコミットほぼなし速い

下の「ブラウザで試す」の図は非同期の場合です。プライマリでコミットが返ったあとで変更(MySQL の binlog、PostgreSQL の WAL)がレプリカに送られるため、更新した直後にレプリカを読むと古い値が返るところと、プライマリの障害でレプリカを昇格させたときに、まだ届いていなかった変更が失われうるところに注目してください。最後に、アプリが新しいプライマリへ接続し直す流れも見られます。

別のページで開く →

ネットワーク図を読み込んでいます…

原因対策
大量の UPDATE / DELETE を一度に実行小分けにしてバッチ実行
レプリカのスペック不足プライマリと同等以上にする
長時間のクエリがレプリカで動いている重い集計は分析用DBへ分離
シングルスレッドでの適用並列レプリケーションを有効化
アプリの読み取り先更新直後の読み取りはプライマリへ(read-your-writes)
書いた直後にレプリカから読むと、古い値が返るユーザーアプリプライマリレプリカプロフィール更新UPDATEOK画面を再読み込みSELECT更新前の値遅れてレプリケーション対策:更新直後の読み取りはプライマリへ(read-your-writes)
レプリケーション遅延で古い値が返る流れ

非同期レプリケーションでは、書き込みの成功とレプリカへの反映にずれがある。

手段効果コスト・複雑さ使いどころ
クエリ・インデックス改善大小まず最初にやる
キャッシュ(Redis等)読み取り負荷を大幅削減中(整合性の管理)読み取りが多い
リードレプリカ読み取りを分散小〜中(遅延の考慮)読み取りが多い
垂直スケール全体を底上げ小(停止を伴う場合あり)手早く改善したい
機能単位でDBを分割負荷とリスクを分離中サービスの分割と合わせて
シャーディング書き込みも分散大(JOIN不可、再分割が困難)書き込みが限界のとき
ひとことで

DBの負荷のほとんどは、アプリが「どんなクエリを、何回、どのタイミングで」投げるかで決まる。DBを強くする前に頼み方を直す。

DBが重いとき、原因はDBの外、つまりアプリの使い方にあることがほとんどです。クエリの回数が多い(N+1)、1本のクエリが大量の行を読む、接続が多すぎる、同じ行に書き込みが集中する、トランザクションが長くてロックを持ち続ける、の5つの形に分けて考えると、打ち手が見えます。

見るものどこで分かること
1リクエストあたりのクエリ数と時間APMのトレースN+1、無駄なクエリ
DBで時間を使っている上位のSQLPerformance Insights、pg_stat_statements重いクエリ、回数の多いクエリ
待機イベントPerformance InsightsCPU、I/O、ロック待ちのどれか
接続数DBのメトリクス、プールのメトリクス接続の枯渇、接続の作りすぎ
読み書きの比率DBのメトリクスリードレプリカやキャッシュで逃がせるか

打ち手は「DBに来る量を減らす(キャッシュ、まとめて取得、非同期化)」「1回を軽くする(インデックス、必要な行と列だけ)」「分散する(リードレプリカ、書き込みの分割)」の順に考えます。スケールアップは即効性がありますが、原因が残るので、緊急の緩和として使い、根本の修正とセットにします。

DBの負荷は、ほとんどがアプリの「頼み方」で決まるDBが苦しむ形アプリ側の典型的な原因打ち手クエリが多いN+1・キャッシュなしまとめて取得・キャッシュ1本が重いインデックス不足・大量の行インデックス・必要な行だけ接続が多い台数×プール・Lambdaプールの上限・RDS Proxy書き込みが集中同じ行の更新・大量INSERTキュー・まとめて書く・分割ロック待ち長いトランザクション短くする・順番をそろえるまず「1リクエストあたりのクエリ数と時間」と「DBの上位のSQL」を並べて見ると、どの形かが分かる
DB負荷の形と打ち手

DBのチューニングは、アプリのコードとセットで見る。

面接ではこう答える

DBの負荷の多くは、アプリのクエリの回数、1本の重さ、接続数、書き込みの集中、長いトランザクションのどれかです。APMで1リクエストあたりのクエリ数を、Performance Insightsで上位のSQLと待機イベントを見て形を特定し、キャッシュやまとめ取得で量を減らし、インデックスで1回を軽くし、リードレプリカなどで分散します。

症状原因の候補調べ方
突然全体が遅くなった新しいクエリ(デプロイ)、統計情報の変化で実行計画が変わったスロークエリログ、Performance Insights
CPU 100%インデックスのないクエリ、大量のクエリ上位の待機イベントとSQL
接続エラーmax_connections 超過、接続リーク接続数のメトリクス、SHOW PROCESSLIST
書き込みが止まる長いトランザクションのロック、DDLのメタデータロックSHOW ENGINE INNODB STATUS、ロック待ちのビュー
ストレージ急増ログやバイナリログの肥大、PostgreSQLの VACUUM 停止容量の推移、テーブルサイズ

Redis(AWS では ElastiCache・MemoryDB、Valkey も同じ使い方)は、キャッシュ・セッション・レート制限・キューに使われ、止まると全体に響きやすい部品です。SRE としては、シングルスレッドであること・メモリがすべてであること・どこまで失ってよいか(永続化と冗長化)を押さえます。

テーマ要点
シングルスレッドコマンドは1つずつ処理される。KEYS *・大きなキーの DEL・LRANGE 0 -1・重い Lua で全員が待つ。SCAN・UNLINK を使い、SLOWLOG で見つける。CPU は「1コア」の使用率を見る
メモリと追い出しmaxmemory と maxmemory-policy。キャッシュは allkeys-lru/消えて困るデータは noeviction+監視。TTL のないキーが溜まる・大きなキー(big key)・断片化(mem_fragmentation_ratio)
永続化RDB(スナップショット。fork でメモリが一時的に増える)と AOF(追記。appendfsync everysec なら最大1秒失う)。キャッシュだけなら持たないことも
冗長化レプリカ+Sentinel(自動フェイルオーバー)、Cluster(16384 スロットでシャーディング。複数キーの操作は同じスロットに)。レプリケーションは非同期なので、切り替え時に直前の書き込みを失うことがある
接続接続数の上限(maxclients)、接続プール、タイムアウト。フェイルオーバー時にクライアントが新しいプライマリへつなぎ直せるか
監視する指標メモリ使用率・evicted_keys・ヒット率(keyspace_hits / misses)・CPU(1コア)・接続数・レプリケーション遅延・SLOWLOG・latency
キャッシュが落ちたときDB に全部の読み取りが来る(サンダリングハード)。DB がそれに耐えられるか、復旧時に温まるまでの流量制限を考えておく

障害対応の演習:全部の API がときどき止まる(KEYS)・ログインできない(Redis の OOM)

別のページで開く →

DBラボを読み込んでいます…

この章のまとめ

🖨 まとめシート(印刷・PDF)
  • DB設計の進め方と正規化(第1〜第3正規形)、非正規化との使い分けを説明できるようにする
  • インデックスはB+Treeで探索をO(log n)にする。代わりに書き込みが遅くなる
  • 複合インデックスは左端の列から順にしか使えない
  • トランザクションをACID・MVCC・WALの3層で説明できるようにする
  • 分離レベルと異常の対応、レプリケーション遅延の対策を説明できる
  • データベースの種類と選び方:データの形と、どう読み書きするかで選ぶ。迷ったらまずRDB。特定の用途に強いDBを、必要な部分にだけ組み合わせる。
  • DB設計の進め方(概念・論理・物理):DB設計は「業務の言葉を整理する(概念)→ 表に落とす(論理)→ 製品と性能に合わせる(物理)」の3段階。最後に実際のクエリで確かめる。
  • テーブル・キー・リレーションの基本:表は「行(1件のデータ)」と「列(項目)」でできている。行を一意に指すのが主キー、他の表の行を指すのが外部キー。表同士のつながり方は3種類しかない。
  • 正規化(第1〜第3正規形とその先):正規化は、同じ事実を1か所にだけ持つように表を分ける手順。重複がなくなるので、更新したのに一部だけ古い、という矛盾が起きなくなる。
  • 非正規化と「重複ではない複製」:正規化した上で、読み取りの速さのために意図して重複を持つのが非正規化。ただし「注文時の価格」のように、そもそも別の事実であるものは重複ではない。
  • 設計例:小さなECサイト:「ユーザーが商品を注文する」というよくある題材で、エンティティ・関係・キー・その時点の値の持ち方をまとめて確認する。
  • 主キーの設計(自然キー・サロゲートキー・ID生成):主キーは「変わらない・短い・並び順が都合よい」ものを選ぶ。業務の値(メールなど)ではなく、意味のないIDを使うのが基本。
  • データ型・NULL・文字コード:型は後から変えるのが最も大変な部分の1つ。金額・日時・文字列の3つで事故が多い。NULLは「不明」を意味し、比較の結果も不明になる。
  • 削除・履歴・監査の持ち方:「消す」と「履歴を残す」をどう設計するかで、復元のしやすさ、監査、性能、個人情報の扱いが変わる。
  • DB設計のアンチパターン:最初は楽に見えて、データが増えたり要件が変わったりしたときに必ず困る設計のパターン。レビューで見つけたら早めに直す。
  • マルチテナントの設計:複数の顧客企業(テナント)のデータを1つのシステムで扱うとき、どこで分けるかは、安全性・コスト・運用の手間のトレードオフになる。
  • NoSQL(DynamoDB)の設計はRDBと逆の考え方:RDBは「データを正しく分けてから、クエリを考える」。DynamoDBは「読み方を先に決めて、それに合わせてキーとデータを作る」。
  • スキーマ変更の進め方(SREが見るポイント):スキーマ変更は、アプリのデプロイより戻しにくく、大きな表ではロックで止まる危険がある。レビューでは「ロック・時間・戻し方」を確認する。
  • B-TreeとB+Tree:データを「並んだ状態のまま」木に入れて、少ない読み込みで目的の行にたどり着くための構造。DBのインデックスの正体。
  • トランザクションの仕組み(ACID・MVCC・WAL):複数の操作を「全部成功」か「全部なかったこと」のどちらかにまとめる仕組み。裏ではMVCCとWALが支えている。
  • アプリケーションとDB負荷:DBの負荷のほとんどは、アプリが「どんなクエリを、何回、どのタイミングで」投げるかで決まる。DBを強くする前に頼み方を直す。

この章のクイズ

面接ではこう聞かれる

まず答えを見ずに声に出して答え、そのあと開いて比べる。

Q1インデックスを貼るとなぜ速くなるのですか?

模範回答

インデックスがないと条件に合う行を探すために全行を読む必要があり、計算量はO(n)です。B+Treeのインデックスは値の順に並んだ木構造で、根から辿ることでO(log n)で目的の行に届きます。1ノードに数百のキーを持てるため木は浅く、100万行でも3段程度です。リーフ同士がつながっているので範囲検索にも強いです。代わりに書き込みのたびにインデックスも更新するため、書き込みは遅くなり容量も増えます。

深掘りされたら

  • インデックスを貼っても使われないのはどんなとき?
  • カバリングインデックスとは?

見られているポイント仕組み・計算量・デメリットの3点

Q2複合インデックスの列の順番はどう決めますか?

模範回答

左の列から順にしか使えないので、等価条件でよく絞り込む列を先に、範囲条件やソートに使う列を後ろに置きます。範囲条件の列より後ろの列は絞り込みに効きにくくなります。実際のクエリをEXPLAINで確認して決めます。

深掘りされたら

  • カーディナリティの低い列を先頭にするのは悪いことですか?

見られているポイント左端一致のルール

Q3スロークエリをどう見つけて改善しますか?

模範回答

スロークエリログやAPM、Performance Insights で遅いクエリと頻度を特定し、EXPLAIN で実行計画を見ます。フルスキャンになっていないか、見積もり行数が多すぎないか、filesort や一時テーブルが出ていないかを確認し、インデックスの追加、クエリの書き換え、N+1の解消などで改善します。改善前後の数字で効果を確認し、本番に入れる前にインデックス作成時のロックや負荷も考えます。

深掘りされたら

  • 大きなテーブルに安全にインデックスを追加するには?

見られているポイント特定→分析→改善→検証の流れ

Q4トランザクションの分離レベルを説明してください。

模範回答

同時に動くトランザクション同士がどこまで干渉しないかの段階です。Read Uncommittedでは未コミットの変更が見え、Read Committedではコミット済みのみ見えるが同じ行を読み直すと値が変わることがあり、Repeatable Readでは同じ行は同じ値に見え、Serializableでは順番に実行したのと同じ結果になります。MySQLの既定はRepeatable Read、PostgreSQLはRead Committedです。分離を強くするほど安全ですが並行性能は下がります。

深掘りされたら

  • 在庫の二重引き当てを防ぐにはどうしますか?
  • MVCCとは?

見られているポイント異常との対応とトレードオフ

Q5デッドロックが発生しました。どう対処しますか?

模範回答

DBはデッドロックを検知すると片方をロールバックするので、アプリ側でリトライできるようにしておきます。根本対策としては、複数の行や表をロックする順番をそろえる、トランザクションを短くする、適切なインデックスでロック範囲を狭める、を行います。SHOW ENGINE INNODB STATUS などで直近のデッドロックの内容を確認します。

深掘りされたら

  • インデックスがないとロック範囲が広がるのはなぜ?

見られているポイント原因と予防の両方

Q6レプリケーション遅延で困ったことはありますか?どう対策しますか?

模範回答

書き込み直後にリードレプリカから読むと古い値が返り、「更新したのに反映されていない」というバグになります。更新直後の読み取りはプライマリに向ける、セッション単位で一定時間プライマリを使う、などで対策します。遅延そのものは、大量更新の分割、レプリカのスペック、並列レプリケーションで抑え、遅延秒数を監視します。

深掘りされたら

  • フェイルオーバー時に非同期レプリケーションだと何が起きますか?

見られているポイントアプリへの影響まで語れるか

Q7トランザクションのCOMMITが速いのに、データが失われないのはなぜですか?

模範回答

WALの仕組みのおかげです。データ本体を書き換える前に、変更内容をログに順番に追記してディスクに確定させ、その時点でCOMMITの成功を返します。順番に追記するだけなので速く、データ本体への反映は後でまとめて行います。クラッシュしても再起動時にログを読み直せば、コミット済みの変更を復元できます。

深掘りされたら

  • fsyncを無効にするとどうなりますか?
  • レプリケーションとの関係は?

見られているポイント永続性の仕組みの理解

Q8長時間実行中のトランザクションがあると、何が問題になりますか?

模範回答

ロックを持ち続けて他の更新を待たせるだけでなく、MVCCの古い版を掃除できなくなります。PostgreSQLではVACUUMが進まずテーブルが肥大化し、InnoDBではundoが溜まって性能が落ちます。トランザクションの中で外部APIを呼んだり、ユーザーの操作を待ったりしないこと、長時間のトランザクションを監視して検知することが対策です。

深掘りされたら

  • どうやって長時間トランザクションを見つけますか?

見られているポイントMVCCの副作用まで理解しているか

Q9正規化とは何ですか?なぜ必要ですか?

模範回答

同じ事実を1か所にだけ持つように表を分ける手順です。正規化されていないと、同じ情報が複数の行にあるので、一部だけ直して矛盾する更新異常、関係のないデータがないと登録できない挿入異常、ある行を消すと別の情報まで消える削除異常が起きます。第1正規形で1つのマスに1つの値にし、第2正規形で複合主キーの一部だけで決まる列を、第3正規形で主キー以外の列で決まる列を別の表に分けます。実務では第3正規形までを基本にします。

深掘りされたら

  • 第2正規形と第3正規形の違いを例で説明してください。

見られているポイント異常と各正規形の対応を説明できるか

Q10注文明細に商品の価格を持つのは、正規化に反しますか?

模範回答

反しません。商品表の価格は「現在の定価」で、注文明細の価格は「その注文でいくらで売ったか」という別の事実だからです。商品表だけに持つと、定価を改定したときに過去の注文の金額まで変わってしまいます。配送先住所や請求書の税率も同じで、出来事が起きた時点の値は、その出来事の側に保存します。

深掘りされたら

  • では、ユーザーの表示名をコメントに持たせるのは?

見られているポイント現在の値と時点の値の区別

Q11論理削除を使うことの問題は何ですか?

模範回答

すべてのクエリに削除されていない条件が必要になり、付け忘れると削除したデータが表示されます。メールアドレスの一意制約が削除済みの行にも効くので、同じメールで再登録できなくなります。表が太り続けてインデックスも大きくなり、性能が落ちます。個人情報の削除要求にも応えられません。データの種類ごとに、物理削除と監査ログ、履歴テーブルへの移動などを使い分けます。

深掘りされたら

  • それでも論理削除が向いているのはどんなデータですか?

見られているポイント手軽さの裏のコストを説明できるか

Q12多対多の関係はどう表しますか?

模範回答

中間テーブルを作り、両側の表への外部キーを持たせて、1対多の関係を2つに分けます。たとえば注文と商品なら注文明細を作り、注文IDと商品IDを持たせ、その組み合わせに一意制約を張ります。中間テーブルには数量や販売単価のように、その関係そのものの情報を持たせられます。カンマ区切りで1つの列に複数のIDを入れるのは、検索も整合性の確保もできないので避けます。

深掘りされたら

  • 中間テーブルの主キーは複合キーとサロゲートキーのどちらにしますか?

見られているポイント中間テーブルの意味まで説明できるか

Q13マルチテナントのSaaSで、他社のデータが見えてしまう事故をどう防ぎますか?

模範回答

共有テーブル方式なら、すべての表にtenant_idを持たせ、ORMの既定のスコープで必ず条件が付くようにします。それに加えてPostgreSQLの行レベルセキュリティで、DB側でも接続中のテナント以外の行を見えなくし、アプリのバグがあっても漏れないようにします。他テナントのデータが取れないことを自動テストで確認し、キャッシュのキーにもテナントIDを含めます。規制の厳しい大口顧客はDBを分けることも検討します。

深掘りされたら

  • キャッシュやファイルストレージでの分離はどうしますか?

見られているポイントアプリとDBの二重の守り

Q14DynamoDBとRDBでは、設計の進め方はどう違いますか?

模範回答

RDBはデータを正規化してからクエリを考えますが、DynamoDBは先にアクセスパターンを全部書き出し、それぞれが1回のQueryで取れるようにパーティションキーとソートキーを設計します。JOINがないので、読み方に合わせてデータを重複して持ちます。新しい読み方にはGSIを追加し、全件を読むScanは避けます。そのため、要件が固まっていない初期のサービスや柔軟な検索が必要な業務データにはRDBの方が向いています。

深掘りされたら

  • ホットパーティションとは何ですか?

見られているポイント発想の違いを説明できるか

Q15新しいサービスのDBはどう選びますか?

模範回答

まずデータの性質とアクセスパターンを確認します。お金や在庫、予約のように強い整合性が必要なら、RDB(AuroraやRDSのMySQL・PostgreSQL)を基本にします。多くのサービスはRDBとリードレプリカ、キャッシュで十分です。キーで高速に引くだけのデータはDynamoDBやRedis、全文検索はOpenSearchのように、必要な部分にだけ専用のDBを組み合わせます。運用するチームの人数も考え、マネージドサービスに絞り、種類を増やしすぎないようにします。

深掘りされたら

  • DynamoDBを選ぶときに先に決めておくことは?

見られているポイント整合性・アクセスパターン・運用体制で選べるか

Q16主キーに連番とUUIDのどちらを使いますか?

模範回答

単一のDBで完結するなら連番が小さく速く、挿入も末尾に追加されるので効率的です。複数のシステムでIDを作る、IDから件数を推測されたくない、といった理由でUUIDを使うなら、時刻順に並ぶUUIDv7を選びます。ランダムなUUIDv4を主キーにすると、B+Treeの挿入位置がばらけてページ分割が増え、書き込みとキャッシュの効率が落ちやすいからです。

深掘りされたら

  • 外部に見せるIDと内部のIDを分けることはありますか?

見られているポイントB+Treeの仕組みと設計判断のつながり

Q17Redis を本番で運用するときに気をつけることは?

模範回答

まずシングルスレッドなので、KEYS や大きなキーの削除など重いコマンドで全体が止まります。KEYS を禁止して SCAN・UNLINK を使い、SLOWLOG を監視します。次にメモリ。maxmemory と追い出しの方針を用途で決め(キャッシュなら allkeys-lru、セッションなど消えて困るものは noeviction と監視、できれば別のクラスタ)、TTL のないキーが溜まらないようにします。永続化と冗長化は「どこまで失ってよいか」で決め、レプリカと自動フェイルオーバー(Sentinel・ElastiCache のマルチ AZ)を使います。レプリケーションは非同期なので、切り替え時に直前の書き込みを失う可能性があります。最後に、キャッシュが空になったときに DB が耐えられるかを確かめておきます。

深掘りされたら

  • キャッシュのヒット率が急に下がったら何を疑いますか?
  • Redis Cluster で複数のキーを扱うときの注意は?

見られているポイントシングルスレッド・メモリと追い出し・永続化と冗長化・キャッシュ消失時の DB