SQL Scale Lab
第一部 DB 基礎編 · 第 5 章 · 約 25 分

データを変える

INSERT、UPDATE、DELETE の基本。WHERE 条件を書き忘れるリスク、実行前に影響行数を見積もる習慣、制約によるデータ保護を体感します。

この章で答える問い
  1. 行を足す・変える・消すにはどう書くか
  2. WHERE を忘れると何が起きるか
  3. 間違った変更から、DB は何を守ってくれるか

この章はデータを変える。壊したら npm run seed:basics で元に戻る。

更新クエリを実行: 「影響を受けた行数」を確認する

注文 44 を配達済みにする
期待: 影響した行数 1。結果の表はない

SELECT とは異なり結果の表は返らず、実行結果パネルに「影響した行数: 1」と表示されます。UPDATE テーブル名 SET 列名 = 新しい値, … WHERE 条件 という構文で、条件に合致した行のデータを書き換えます。 この「影響した行数(Affected rows)」は、更新処理が意図通りに行われたかを確認する極めて重要な指標となります。

確認
期待: 1 行。status が delivered
今の値を使って更新する
期待: 影響した行数 1

quantity = quantity - 1 のように、現在の列の値を参照して計算結果をセットすることも可能です。「在庫数を 1 減らす」といった処理は実務でもこのように記述します。

最大の落とし穴: WHERE 句のない UPDATE

もし WHERE 句を書き忘れたまま UPDATE を実行してしまうと何が起きるでしょうか。構文エラーで止まってくれるでしょうか。 何行が影響を受けるか、実行して確かめてみてください。 この教材のデータベースは 50 行の検証用環境ですので、安全に失敗を体験できます。

WHERE を忘れた UPDATE

影響した行数は 50、全行が「delivered(配達済み)」に書き換わってしまいました。エラーにはなりません。 SQL の仕様上、WHERE 句を指定しない UPDATE や DELETE は「全行を対象とする」とみなされます。 データベースは「本当に全行を更新して良いですか?」と確認ダイアログを出してくれるわけではありません。 実務において「全注文のステータスが変わってしまった」「全ユーザーが誤って退会処理されてしまった」という大事故は、まさにこの一行で引き起こされます。

このような致命的な人為ミスを防ぐための 3 つの習慣を紹介します。

  1. UPDATE や DELETE を実行する前に、全く同じ WHERE 句を使って SELECT COUNT(*) を実行し、対象行数を必ず事前に確認する。
  2. 本番環境での手作業メンテナンスでは必ずトランザクション(第 7 章)を開始し、影響行数を確認した上でコミットする。
  3. 手作業による直接更新を極力減らし、データ修正はスクリプト化してコードレビューを通す運用にする。

データを破損させてしまったので初期状態に戻しましょう。ターミナルで npm run seed:basics を実行し、画面上の「更新」ボタンを押してから次へ進んでください。

安全な更新手順(事前確認)の実践

先に数える
期待: 1 行。7 月より前の shipped(発送済み・未配達)の件数
同じ WHERE で更新する
期待: 影響した行数が上と一致する

事前にカウントした件数と、UPDATE で影響を受けた行数が完全に一致していれば、想定通りの行だけが更新されたと確信できます。WHERE 句による対象行の抽出ロジックは SELECT でも UPDATE でも完全に共通です。

orders(50 行のうち 6 行)idstatusordered_at3delivered2026-06-046shipped2026-06-089shipped2026-06-1510shipped2026-06-1618shipped2026-06-2729shipped2026-07-13………id 3: status が違うWHERE status = 'shipped'  AND ordered_at < '2026-07-01'この 4 行だけid 29: 7 月なので外SELECT COUNT(*) …→ 4UPDATE orders SET …→ 影響した行数 4同じ WHERE なら、数えた行数と影響した行数は一致する。WHERE がなければ、外の枠(50 行すべて)に効く。
WHERE 句が選択する行は SELECT でも UPDATE でも同一です。事前にカウントした件数と更新件数が一致していれば安全です。

INSERT: データの追加

注文を 1 件足す
期待: 影響した行数 1
採番された id
期待: 51

LAST_INSERT_ID() は、直前の INSERT 文で自動採番された AUTO_INCREMENT の主キー値を取得する関数です。Web アプリケーションでは「今登録した注文の ID」を取得し、続けて注文明細を登録する際によく利用されます。

SELECT の結果をそのまま入れる
期待: 影響した行数 = 在庫 30 未満の商品数。ただし、すでにセール対象の商品が含まれていれば Duplicate entry のエラー

INSERT INTO … SELECT … 構文を使うと、SELECT クエリで取得した結果をそのまま別のテーブルに一括登録できます(ここでは在庫 30 個未満の商品を「セール対象」カテゴリ(ID: 6)に一括登録しています)。 ただし、すでに対象カテゴリに登録済みの商品が含まれていた場合、主キー制約(重複禁止)違反でエラーになります。 このようなケースをスマートに解決するのが次の UPSERT です。

UPSERT: 既存データがあれば更新し、なければ新規挿入する

ユニーク制約に当たる
期待: エラー。Duplicate entry

categories.name 列にはユニーク(一意性)制約が設定されているため、既存の「文具」と同じ名前は登録できずエラーになります。 この制約を活用することで、「存在しなければ新規登録し、すでに存在していれば更新またはスキップする」という処理を 1 つの SQL 文で安全に表現できます。

重複したら何もしない
期待: 影響した行数 0(もうある)
セール対象を、重複を無視して入れる
期待: 影響した行数 = 新しく入った数(重複した行は黙って飛ばされる)

INSERT IGNORE は、重複キー違反が発生した行だけをスキップして残りの行を挿入します。 既存データを上書き更新したい場合は ON DUPLICATE KEY UPDATE、重複を単に無視したい場合は INSERT IGNORE を使い分けます。 「先に SELECT で存在チェックをして、なければ INSERT する」というように処理を 2 回に分けると、同時にリクエストが来た際に両方が「存在しない」と判断して同時に INSERT を試み、衝突エラーを引き起こします。 ユニーク制約と UPSERT 構文を組み合わせることで、データベース側で 1 つの不可分な処理として安全に完結させることができます。

DELETE: データの削除と参照制約

さっき足した注文を消す
期待: 影響した行数 1
参照されている行は消せない
期待: エラー。a foreign key constraint fails

商品 ID 1 の商品は、すでに注文明細(order_items)から参照されています。 もしこの商品を直接削除してしまうと、注文明細が「存在しない架空の商品」を指すことになり、整合性が破綻してしまいます。 第 4 章で学んだ外部キー制約が働き、データベースが DELETE を拒否してデータを保護しています。

order_items(product_id = 1 の 7 行のうち 3 行)idorder_idproduct_idquantity9311581512772011……1…productsidnameprice1歯ブラシ 3本セット480← DELETEDELETE FROM products WHERE id = 1→ エラー: a foreign key constraint failsDELETE FROM orders WHERE id = 51→ 影響した行数 1(誰にも指されていない)7 行の明細が商品 1 を指している。消すと明細は指す先を失う(孤児)。だから DB が拒む。
外部キー制約は参照の整合性を保護します。親行(商品1)を削除すると子行(注文明細7行)の参照先が失われるため、データベースが DELETE をブロックします。

実務では、物理的にレコードを削除する代わりに deleted_at 列に削除日時を記録して「削除されたものとみなす」論理削除という手法も広く用いられます。 履歴が残り、誤操作時に復元が容易である一方、すべてのクエリに WHERE deleted_at IS NULL を記述する必要があり、書き忘れると削除済みデータが表示されてしまうという別のリスクを抱えることになります。

データ変更文は「影響した行数」を返します。WHERE 句を省略すると全行に適用されてしまうため、事前の SELECT 確認やトランザクションの活用が不可欠です。

確認

WHERE を忘れた UPDATE が全行を変えてしまうのはなぜか。
WHERE のない UPDATE と DELETE は全行に効くのが仕様だから。エラーにはならず、DB は「本当に全行か」と聞き返さない。
変更の文を打つ前に、同じ WHERE で数えておくのはなぜか。
WHERE が選ぶ行は、SELECT でも UPDATE でも同じだから。数えた行数と影響した行数が一致すれば意図どおりで、一致しなければ WHERE を疑える。
先に存在を確かめてから INSERT する 2 文より、ユニーク制約と UPSERT のほうが安全なのはなぜか。
2 文の間に別の接続が同じ行を入れると、確認をすり抜けて両方が INSERT してしまう。ユニーク制約があれば DB が 1 文の中で重複を弾き、あれば更新するのか何もしないのかを書き分けられる。

次の章へ

ここまでは 1 つのクエリで完結するデータ操作を学んできました。 次の章では、クエリの実行結果を別のクエリの中で活用する「サブクエリ」の仕組みを学びます。 「平均単価より高い商品」や「特定条件の会員による注文」など、動的な条件判定を行う強力な記法を身につけましょう。