データを変える
INSERT / UPDATE / DELETE。WHERE を忘れると全行が変わる。影響行数を先に数える習慣と、制約が守ってくれること。
- 行を足す・変える・消すにはどう書くか
- WHERE を忘れると何が起きるか
- 間違った変更から、DB は何を守ってくれるか
この章はデータを変える。壊したら npm run seed:basics で元に戻る。
実物: 変えると「影響した行数」が返る
表は返らず、計器に「影響した行数 1」と出る。UPDATE テーブル SET 列 = 値, … WHERE 条件 で、条件に合う行の列を書き換える。 この「影響した行数」が、これからずっと最初に見る数字になる。
quantity = quantity - 1 のように、右辺に今の値を使える。「在庫を 1 減らす」はこう書く。
驚き: WHERE のない UPDATE
WHERE を書き忘れて実行するとどうなるか。エラーになるだろうか。影響した行数を予測してから実行する。 50 行の小さな表なので、ここで一度起こしておく。
影響した行数 50 のはずだ。エラーにはならない。全行が配達済みになった。 UPDATE と DELETE は、WHERE がなければ全行に効く。これは仕様であって、DB は「本当に全行ですか」とは聞かない。 本番で「全注文が配達済みになった」「全顧客が退会した」は、この一行で起きる。
防ぐ習慣は 3 つ。
- UPDATE / DELETE を書く前に、同じ WHERE で
SELECT COUNT(*)して行数を見る。 - 本番の手作業は必ずトランザクションの中で行い、影響した行数を見てから確定する(第 7 章)。
- 手作業を減らす。データの修正はスクリプトにしてレビューを通す。
壊したので戻す。ターミナルで npm run seed:basics を実行し、上のパネルの「更新」を押してから続ける。
習慣 1 を実際にやる
上の COUNT と影響した行数が一致すれば、意図したとおりだ。一致しなければ WHERE を疑う。数えた 4 行と書き換わった 4 行が同じ行だということを、表の上で見る。
INSERT: 足す
LAST_INSERT_ID() は、同じ接続で直前に採番された id。アプリは「今作った注文の id」をこれで得て、明細を入れるときに使う。
INSERT INTO … SELECT … は、SELECT の結果を行として入れる。カテゴリ 6 は「セール対象」。 ただし、もともとセール対象だった商品が含まれていると、主キー(product_id, category_id)の重複でエラーになる。 エラーになったなら、次の UPSERT で解決する。
UPSERT: あれば何もしない、なければ入れる
categories.name にはユニーク制約があり、同じ名前は 2 つ入らない。この制約を利用して「なければ入れる」を 1 文で書ける。
INSERT IGNORE は、主キーやユニーク制約に当たった行を飛ばして残りを入れる。「あれば更新したい」なら ON DUPLICATE KEY UPDATE、「あれば何もしない」なら INSERT IGNORE、と使い分ける。 「先に SELECT で存在確認 → なければ INSERT」と 2 文に分けると、2 つのリクエストが同時に来たとき両方が INSERT して失敗する(第 7 章)。 ユニーク制約 + ON DUPLICATE KEY UPDATE なら DB が 1 文で片付ける。
DELETE: 消す
商品 1 は order_items から指されている。消すと、明細が存在しない商品を指す(孤児になる)。第 4 章の外部キー制約がこれを止める。 消したいなら先に明細を消すか、宣言時に ON DELETE CASCADE(親を消したら子も消す)を付ける。 指されている行を消そうとしたとき何が起きるかを、矢印で見る。
実務では、消さずに deleted_at 列に日時を入れて「消えたことにする」(論理削除)ことも多い。履歴が残り、取り消せ、参照整合性で悩まない。 代わりに全クエリに WHERE deleted_at IS NULL が付き、忘れると消したはずのものが表示される。
変更の文は「影響した行数」を返す。WHERE を忘れれば全行に効き、DB は聞き返さない。制約(NOT NULL、外部キー、ユニーク)は間違った変更を止める。
確認
WHERE を忘れた UPDATE が全行を変えてしまうのはなぜか。
変更の文を打つ前に、同じ WHERE で数えておくのはなぜか。
先に存在を確かめてから INSERT する 2 文より、ユニーク制約と UPSERT のほうが安全なのはなぜか。
次の章へ
ここまでの SELECT は 1 つの表(または JOIN した表)から取り出すものだった。次の章では、SELECT の結果をもう一つの SELECT の中で使う。 「平均より高い明細」「gold 会員の注文」のように、条件そのものが別の問い合わせで決まるときの書き方だ。