結論から言うと、1つのUPDATE文の中で行ごとに更新値を変えるならCASE式を使い、UPDATE文そのものを実行するか、別の処理へ分けるかを判断するならDBMSごとのIF制御文を使います。『SQLのIF文』は製品共通の構文ではありません。MySQLとSQL Serverでは書き方も使える場所も異なるため、同じコードをそのまま流用しないでください。
条件付きUPDATEで最も重大な事故は、WHERE句の漏れによる全行更新です。次に多いのが、対象件数の想定違い、NULLの扱い、再実行による二重加算、SELECTとUPDATEの間に別セッションが値を変える競合です。この記事の例は、最初に対象をSELECTし、トランザクション内で更新し、変更前後と件数を確認したうえでROLLBACKする検証手順を基本にします。
CASEとIFの使い分け
| やりたいこと | MySQL 8.4 | SQL Server | 推奨 |
|---|---|---|---|
| 同じUPDATEで条件ごとに代入値を変える | CASE式またはIF()関数 | CASE式 | まずCASE式 |
| 条件により別のSQL文を実行する | ストアドプログラム内のIF … THEN | IF … ELSE | 製品別に記述 |
| 複数列を異なる条件で更新する | 列ごとにCASE式 | 列ごとにCASE式 | ELSEで既存値を明示 |
| 更新前後を確認する | トランザクション内でSELECTとROW_COUNT() | OUTPUTと@@ROWCOUNT | 初回はROLLBACK |
| 更新元表と別表を結合する | 複数テーブルUPDATEの仕様を確認 | UPDATE … FROMの一意性を確認 | 結合結果を先にSELECT |
CASEは値を返す式です。SQL Serverの公式資料も、CASEは文、文ブロック、ストアドプロシージャの実行フローを制御するものではないと説明しています。対してIFは、条件により実行する文を分ける制御構文です。この違いを押さえるだけで、CASEとIFを無理に置き換える混乱を避けられます。
UPDATE前の安全確認
- 対象DBMSとバージョンを確認します。ここではMySQL 8.4と、SQL Serverまたは対応するAzure SQLのTransact-SQLを扱います。
- 本番と同じ条件のSELECTを実行し、対象主キーと件数を保存します。
- WHERE条件に主キーや一意キーを含め、意図しない行が混ざらないことを確認します。
- NULL、境界値、すでに更新済みの値、同時更新の可能性を確認します。
- 初回はトランザクションを開始し、結果を確認してもCOMMITせずROLLBACKします。
- 本番実行用は検証用スクリプトと分け、レビュー後にだけCOMMITへ変更します。
バックアップがあることと、無条件にUPDATEしてよいことは同じではありません。復元には停止時間や他処理との整合性確認が必要です。WHEREを外した全行更新は、意図した一括メンテナンスでない限り実行しないでください。
MySQL 8.4:CASE式で条件付きUPDATEを行う
employees表のemployee_idが12345の一行だけを対象にし、salaryが50000未満なら55000へ、それ以外なら5000加算する例です。検証用なので最後は必ずROLLBACKします。
START TRANSACTION;
SELECT employee_id, salary
FROM employees
WHERE employee_id = 12345
FOR UPDATE;
UPDATE employees
SET salary = CASE
WHEN salary < 50000 THEN 55000
ELSE salary + 5000
END
WHERE employee_id = 12345;
SELECT ROW_COUNT() AS affected_rows;
SELECT employee_id, salary
FROM employees
WHERE employee_id = 12345;
ROLLBACK;
START TRANSACTIONでautocommitの外に入り、FOR UPDATEで対象行を確認してから更新しています。ROW_COUNT()が1であること、二回目のSELECTで新しい値が期待どおりであることを確認し、それでも初回はROLLBACKします。MySQLは通常autocommitが有効なので、トランザクション外で成功したUPDATEは後からROLLBACKできません。
この例はInnoDBなどトランザクションをサポートするストレージエンジンが前提です。非トランザクション表が混ざるとROLLBACKできない変更が残る場合があります。実際のアプリケーションでは、クライアントライブラリが提供するトランザクションAPIを使う設計も検討してください。
複数列を同時に更新する
START TRANSACTION;
UPDATE products
SET price = CASE
WHEN stock_level < 10 THEN base_price * 1.20
WHEN stock_level BETWEEN 10 AND 50 THEN base_price * 1.10
ELSE base_price
END,
discount_rate = CASE
WHEN stock_level < 10 THEN 0.05
WHEN stock_level BETWEEN 10 AND 50 THEN 0.10
ELSE discount_rate
END
WHERE product_id = 987;
SELECT ROW_COUNT() AS affected_rows;
SELECT product_id, base_price, price, discount_rate
FROM products
WHERE product_id = 987;
ROLLBACK;
価格を現在のpriceへ繰り返し掛けるのではなく、変更されないbase_priceから計算しています。これにより、同じ条件で再実行したときに価格が1.20倍、さらに1.20倍と膨らむ事故を防ぎやすくなります。discount_rateのELSEでは既存値を明示しており、条件不一致をNULLへ変えません。
MySQLのIF文はストアドプログラム内で使う
MySQLのIF … THEN … ELSE … END IFは、ストアドプロシージャ、ストアドファンクション、トリガーなどの複合文内で使う制御構文です。通常のSQLクライアントで、UPDATEの前へIFだけを書いて実行する構文ではありません。また、値を返すIF()関数とは別です。
IF search_condition THEN
statement_list;
ELSEIF another_condition THEN
statement_list;
ELSE
statement_list;
END IF;
単一行の現在値に応じて更新値を変えるだけなら、前のCASE式による一回のUPDATEのほうが競合を減らせます。先にSELECTで値を変数へ読み、その後に別UPDATEを実行すると、二つの文の間に他セッションが値を変更する可能性があります。複数の異なる文を実行する必要がある場合だけ、ストアドプロシージャ内のIFを検討してください。
SQL Server:CASE、OUTPUT、@@ROWCOUNTで検証する
SQL ServerではOUTPUT句で変更前後の値を返せます。次のスクリプトは成功してもROLLBACKする検証用です。アプリケーションから値を渡す場合は文字列を連結せず、パラメーターを使ってください。
DECLARE @EmployeeId int = 12345;
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
UPDATE dbo.Employees
SET Salary = CASE
WHEN Salary < 50000 THEN 55000
ELSE Salary + 5000
END
OUTPUT
deleted.EmployeeId,
deleted.Salary AS OldSalary,
inserted.Salary AS NewSalary
WHERE EmployeeId = @EmployeeId;
IF @@ROWCOUNT <> 1
THROW 50001, N'Expected exactly one row.', 1;
ROLLBACK TRANSACTION;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0
ROLLBACK TRANSACTION;
THROW;
END CATCH;
SET XACT_ABORT ONにより、実行時エラーが起きたトランザクションを安全に終了しやすくします。OUTPUTでoldとnewを比較し、@@ROWCOUNTが想定の一行でなければTHROWします。検証が終わったら、このスクリプトをそのまま編集して即実行するのではなく、本番用コピーでROLLBACKをCOMMITへ変更し、対象値と承認内容をもう一度レビューしてください。
SQL Serverで文そのものを分岐するIF…ELSE
IF EXISTS (
SELECT 1
FROM dbo.Employees
WHERE EmployeeId = @EmployeeId
AND Salary < 50000
)
BEGIN
UPDATE dbo.Employees
SET Salary = 55000
WHERE EmployeeId = @EmployeeId;
END
ELSE
BEGIN
UPDATE dbo.Employees
SET Salary = Salary + 5000
WHERE EmployeeId = @EmployeeId;
END;
IF…ELSEは条件に応じて異なるTransact-SQL文やBEGIN…ENDブロックを実行します。ただし、この例のように同じ行を先に調べて後から更新すると、分離レベルや同時実行によっては判定後に値が変わる可能性があります。単一UPDATEのCASE式で表現できる処理はCASEを優先し、IFが必要ならトランザクションと適切なロック設計を含めてレビューします。
よくある失敗と修正方針
| 失敗 | 起こること | 修正方針 |
|---|---|---|
| WHEREを省略 | 対象表の全行を更新 | 同じ条件のSELECT、主キー条件、件数検証 |
| CASEのELSEを省略 | 条件不一致がNULLになる | 既存値または明示した既定値を返す |
| 現在値へ倍率を繰り返し掛ける | 再実行で値が累積変化 | 基準値から計算し、冪等性を確認 |
| SELECT後に別UPDATE | 文の間で他セッションが変更 | 単一UPDATE、トランザクション、必要なロック |
| UPDATE … FROMの結合元が複数行 | SQL Serverでどの値を使うか未定義 | 更新対象ごとに結合元を一意にする |
| MySQLトリガーから同じ表をUPDATE | 実行元が使用中の表を変更できずエラー | 元UPDATEへCASEを組み込むか設計を見直す |
| 影響行数を見ない | 0件または想定以上でも処理続行 | ROW_COUNT()または@@ROWCOUNTを検証 |
MySQLの同一テーブル更新トリガーを使わない
MySQLでは、ストアドファンクションまたはトリガーを呼び出した文がすでに読み書きしている表を、そのファンクションやトリガーから変更できません。したがって、products表のAFTER UPDATEトリガー内で再びproductsをUPDATEする例は、安全性以前に公式制限へ抵触します。customers表についても同じです。
さらに、在庫更新のたびに現在価格へ倍率を掛ける処理は冪等ではありません。同じデータを再送しただけで価格が再び変わります。単純な派生値なら元のUPDATEにCASE式を含め、監査や履歴が必要なら別の履歴表へ記録します。トリガーが本当に必要な場合は、実行順序、OLD/NEW値、複数行更新、レプリケーション、エラー時のロールバックまで含めて別途設計してください。
NULLと境界値を確認する
SQLの比較でNULLは通常の値と同じように真偽判定されません。salaryがNULLならsalary < 50000はTRUEにならず、ELSE側のsalary + 5000もNULLになります。NULLを0とみなす、更新対象外にする、エラーとして止めるなど、業務ルールを先に決めてください。安易なCOALESCEは欠損値を正常値へ見せかけるため、採用理由を明記します。
50000ちょうど、在庫10、在庫50などの境界もテストします。BETWEENは両端を含むため、条件の順番が重なると先に一致したWHENが使われます。CASEは上から評価し、最初にTRUEとなった結果を返すので、広い条件を先に置いて狭い条件が到達不能になっていないか確認してください。
本番実行のチェックリスト
- 検証環境またはROLLBACK前提の本番トランザクションで、対象件数と変更値を確認した。
- WHERE条件とJOINが対象行ごとに一意である。
- NULL、境界値、0件、複数件、再実行をテストした。
- トランザクションを長時間開いたままにせず、ロックの影響範囲を確認した。
- 変更前後を監査でき、必要なら復元できるバックアップや履歴がある。
- 本番用スクリプトは検証用と分け、COMMITを含む版を別の担当者がレビューした。
よくある質問
CASEとIF()はどちらが速いですか?
構文名だけでは決まりません。対象行数、索引、WHERE条件、実行計画、式の複雑さが影響します。移植性と読みやすさを優先するならCASEを基準にし、実データに近い環境で実行計画と時間を比較してください。
CASEのELSEは必須ですか?
文法上は省略できますが、一致しないとNULLを返します。既存値を残すならELSE 列名を明示し、NULLへ変える仕様ならその意図をテストとコメントで残してください。
IFの中に複数のUPDATEを書けますか?
書けますが、MySQLではストアドプログラム内、SQL ServerではTransact-SQLのIF…ELSEとして製品別の構文になります。複数文の一部だけ成功しないよう、トランザクションと例外処理を設計してください。
UPDATE前のSELECTだけで安全ですか?
十分ではありません。SELECT後にデータが変わる可能性があり、UPDATEの条件を写し間違えることもあります。同じトランザクション、同じ条件、必要なロック、影響件数の検証を組み合わせます。
大量更新を一度に実行してよいですか?
大量更新はログ量、ロック、レプリケーション、業務処理への影響が大きくなります。主キー範囲などで安全に分割できるか、各バッチの再実行性と中断位置を管理できるかをDBAと確認してください。
トリガーならアプリ改修なしで安全ですか?
安全とは限りません。暗黙に実行されるため影響範囲が見えにくく、MySQLには同一テーブル変更などの制限もあります。単一UPDATEで表現できるならCASEを優先し、トリガー採用時は複数行、再実行、障害、監査を含めて設計します。
公式リファレンス
- MySQL 8.4 IF Statement
- MySQL 8.4 UPDATE Statement
- MySQL 8.4 START TRANSACTION, COMMIT, and ROLLBACK
- MySQL 8.4 Restrictions on Stored Programs
- ELSE (IF…ELSE) (Transact-SQL)
- UPDATE (Transact-SQL)
- CASE (Transact-SQL)
- BEGIN TRANSACTION (Transact-SQL)
まとめ
条件ごとに更新値を変えるだけならCASE式、実行する文やブロックを分けるならMySQLまたはSQL Server固有のIFを使います。どちらを選んでも、同じ条件のSELECT、主キーまたは一意条件、トランザクション、変更前後の確認、影響件数、初回ROLLBACKを一組にしてください。旧来の同一テーブル更新トリガーや、現在値へ倍率を重ねる非冪等な例は使わず、再実行しても意図しない変化が増えない更新を設計することが重要です。

コメント