PostgreSQL UPDATE CASE完全ガイド – 条件分岐で賢くデータ更新

データベース・SQL

「条件によって異なる値で更新したい」

「複数のUPDATE文を実行するのは非効率的…」

「IF文のような条件分岐をUPDATEで使いたい」

「一括で複数パターンの更新を行いたい」

そんな要望を叶えるのがUPDATE文でのCASE式です!

CASE式を使えば、1つのUPDATE文で複雑な条件分岐を実現でき、パフォーマンスも大幅に向上します。

この記事では、UPDATE CASEの基本から実務で使える高度なテクニックまで、豊富な実例とともに解説していきます!

スポンサーリンク

UPDATE CASEの基本構文

基本的な書き方

UPDATE テーブル名
SET カラム名 = CASE
    WHEN 条件1 THEN 値1
    WHEN 条件2 THEN 値2
    WHEN 条件3 THEN 値3
    ELSE デフォルト値
END
WHERE 更新対象の条件;

シンプルな例で理解する

-- 商品テーブルの価格を条件によって更新
UPDATE products
SET price = CASE
    WHEN category = '食品' THEN price * 1.08  -- 8%値上げ
    WHEN category = '家電' THEN price * 0.95  -- 5%値下げ
    WHEN category = '衣類' THEN price * 1.10  -- 10%値上げ
    ELSE price  -- その他は変更なし
END;

-- 成績によって評価を設定
UPDATE students
SET grade = CASE
    WHEN score >= 90 THEN 'A'
    WHEN score >= 80 THEN 'B'
    WHEN score >= 70 THEN 'C'
    WHEN score >= 60 THEN 'D'
    ELSE 'F'
END
WHERE exam_id = 101;

実践!よく使うUPDATE CASEパターン

パターン1:ステータス更新

-- 注文テーブル
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    order_date DATE,
    shipped_date DATE,
    delivered_date DATE,
    status VARCHAR(20),
    total_amount DECIMAL(10,2)
);

-- 日付に基づいてステータスを自動更新
UPDATE orders
SET status = CASE
    WHEN delivered_date IS NOT NULL THEN '配送完了'
    WHEN shipped_date IS NOT NULL AND delivered_date IS NULL THEN '配送中'
    WHEN order_date < CURRENT_DATE - INTERVAL '7 days' AND shipped_date IS NULL THEN '処理遅延'
    WHEN shipped_date IS NULL THEN '処理中'
    ELSE '不明'
END
WHERE status IS NULL OR status = '';

-- 金額によって配送優先度を設定
UPDATE orders
SET priority = CASE
    WHEN total_amount >= 10000 THEN 'HIGH'
    WHEN total_amount >= 5000 THEN 'MEDIUM'
    ELSE 'LOW'
END
WHERE shipped_date IS NULL;

パターン2:段階的な値の調整

-- 在庫テーブル
CREATE TABLE inventory (
    product_id INTEGER PRIMARY KEY,
    stock_quantity INTEGER,
    reorder_level INTEGER,
    max_stock INTEGER,
    stock_status VARCHAR(20)
);

-- 在庫状況に応じた発注数量の設定
UPDATE inventory
SET reorder_quantity = CASE
    WHEN stock_quantity = 0 THEN max_stock  -- 在庫切れは最大まで発注
    WHEN stock_quantity < reorder_level * 0.5 THEN max_stock - stock_quantity  -- 危険水準
    WHEN stock_quantity < reorder_level THEN (max_stock - stock_quantity) * 0.7  -- 通常発注
    ELSE 0  -- 発注不要
END,
stock_status = CASE
    WHEN stock_quantity = 0 THEN '在庫切れ'
    WHEN stock_quantity < reorder_level * 0.5 THEN '至急発注'
    WHEN stock_quantity < reorder_level THEN '要発注'
    WHEN stock_quantity > max_stock * 0.9 THEN '過剰在庫'
    ELSE '正常'
END;

パターン3:複数カラムの同時更新

-- 従業員テーブル
CREATE TABLE employees (
    employee_id SERIAL PRIMARY KEY,
    salary DECIMAL(10,2),
    bonus DECIMAL(10,2),
    performance_score INTEGER,
    department VARCHAR(50),
    years_of_service INTEGER
);

-- 評価に基づいて給与とボーナスを同時更新
UPDATE employees
SET 
    salary = CASE
        WHEN performance_score >= 90 THEN salary * 1.10
        WHEN performance_score >= 70 THEN salary * 1.05
        WHEN performance_score >= 50 THEN salary * 1.02
        ELSE salary
    END,
    bonus = CASE
        WHEN performance_score >= 90 THEN salary * 2.0
        WHEN performance_score >= 70 THEN salary * 1.5
        WHEN performance_score >= 50 THEN salary * 1.0
        ELSE salary * 0.5
    END,
    salary_grade = CASE
        WHEN salary * CASE 
            WHEN performance_score >= 90 THEN 1.10
            WHEN performance_score >= 70 THEN 1.05
            WHEN performance_score >= 50 THEN 1.02
            ELSE 1
        END > 100000 THEN 'A'
        WHEN salary * CASE 
            WHEN performance_score >= 90 THEN 1.10
            WHEN performance_score >= 70 THEN 1.05
            WHEN performance_score >= 50 THEN 1.02
            ELSE 1
        END > 70000 THEN 'B'
        ELSE 'C'
    END
WHERE department IN ('営業部', '開発部');

高度なCASE式の活用法

他のテーブルを参照する条件

-- カテゴリマスタを参照して価格を更新
UPDATE products p
SET price = CASE
    WHEN c.category_type = 'premium' THEN p.price * 1.20
    WHEN c.category_type = 'standard' THEN p.price * 1.00
    WHEN c.category_type = 'discount' THEN p.price * 0.80
    ELSE p.price
END
FROM categories c
WHERE p.category_id = c.category_id
  AND p.last_updated < CURRENT_DATE - INTERVAL '30 days';

-- 売上実績に基づいた在庫調整
UPDATE inventory i
SET reorder_level = CASE
    WHEN s.total_sold > 1000 THEN 200
    WHEN s.total_sold > 500 THEN 100
    WHEN s.total_sold > 100 THEN 50
    ELSE 20
END
FROM (
    SELECT product_id, SUM(quantity) as total_sold
    FROM sales
    WHERE sale_date >= CURRENT_DATE - INTERVAL '3 months'
    GROUP BY product_id
) s
WHERE i.product_id = s.product_id;

サブクエリを使った複雑な条件

-- 平均値との比較による評価更新
UPDATE students s
SET relative_grade = CASE
    WHEN s.score > (SELECT AVG(score) * 1.2 FROM students WHERE class_id = s.class_id) THEN '優秀'
    WHEN s.score > (SELECT AVG(score) FROM students WHERE class_id = s.class_id) THEN '平均以上'
    WHEN s.score > (SELECT AVG(score) * 0.8 FROM students WHERE class_id = s.class_id) THEN '平均'
    ELSE '要努力'
END
WHERE exam_id = 2023;

-- ランキングに基づくボーナス計算
UPDATE sales_staff ss
SET bonus_rate = CASE
    WHEN (
        SELECT COUNT(*) 
        FROM sales_staff ss2 
        WHERE ss2.sales_amount > ss.sales_amount
          AND ss2.department = ss.department
    ) < 3 THEN 0.20  -- 部門TOP3
    WHEN (
        SELECT COUNT(*) 
        FROM sales_staff ss2 
        WHERE ss2.sales_amount > ss.sales_amount
          AND ss2.department = ss.department
    ) < 10 THEN 0.10  -- 部門TOP10
    ELSE 0.05
END
WHERE period = '2024Q1';

日付や時間による条件分岐

-- 時間帯による料金設定
UPDATE parking_fees
SET fee = CASE
    -- 平日
    WHEN EXTRACT(DOW FROM entry_time) BETWEEN 1 AND 5 THEN
        CASE
            WHEN EXTRACT(HOUR FROM entry_time) BETWEEN 7 AND 9 THEN 500   -- 朝ラッシュ
            WHEN EXTRACT(HOUR FROM entry_time) BETWEEN 17 AND 19 THEN 500 -- 夕ラッシュ
            ELSE 300  -- 通常時間
        END
    -- 週末
    ELSE
        CASE
            WHEN EXTRACT(HOUR FROM entry_time) BETWEEN 10 AND 20 THEN 400
            ELSE 200
        END
END
WHERE fee IS NULL;

-- 季節による価格調整
UPDATE products
SET seasonal_price = CASE
    WHEN category = 'エアコン' THEN
        CASE
            WHEN EXTRACT(MONTH FROM CURRENT_DATE) IN (6,7,8) THEN price * 1.2  -- 夏
            WHEN EXTRACT(MONTH FROM CURRENT_DATE) IN (12,1,2) THEN price * 1.1 -- 冬
            ELSE price
        END
    WHEN category = 'ストーブ' THEN
        CASE
            WHEN EXTRACT(MONTH FROM CURRENT_DATE) IN (11,12,1,2) THEN price * 1.15
            ELSE price * 0.9
        END
    ELSE price
END;

CASE式の種類と使い分け

Simple CASE vs Searched CASE

-- Simple CASE(単純な等価比較)
UPDATE orders
SET shipping_cost = CASE region
    WHEN '北海道' THEN 1000
    WHEN '沖縄' THEN 1200
    WHEN '関東' THEN 500
    WHEN '関西' THEN 600
    ELSE 700
END;

-- Searched CASE(複雑な条件)
UPDATE orders
SET shipping_cost = CASE
    WHEN region IN ('北海道', '沖縄') AND weight > 10 THEN 2000
    WHEN region IN ('北海道', '沖縄') THEN 1000
    WHEN distance > 500 THEN 800
    WHEN weight > 20 THEN 900
    ELSE 500
END;

複数CASE式の組み合わせ

-- ネストしたCASE式
UPDATE products
SET final_price = 
    base_price * 
    CASE  -- カテゴリによる係数
        WHEN category = 'luxury' THEN 1.5
        WHEN category = 'standard' THEN 1.0
        ELSE 0.8
    END *
    CASE  -- 在庫による係数
        WHEN stock < 10 THEN 1.2
        WHEN stock > 100 THEN 0.9
        ELSE 1.0
    END *
    CASE  -- 季節による係数
        WHEN is_seasonal AND EXTRACT(MONTH FROM CURRENT_DATE) IN (12,1,2) THEN 1.1
        ELSE 1.0
    END;

パフォーマンス最適化のテクニック

インデックスを活用した高速化

-- インデックスを作成
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_orders_status ON orders(status) WHERE status IS NULL;

-- インデックスが効く書き方
UPDATE products
SET price = CASE
    WHEN category = '家電' THEN price * 0.9
    ELSE price
END
WHERE category = '家電';  -- WHERE句で絞り込み

-- 非効率な書き方(全行スキャン)
UPDATE products
SET price = CASE
    WHEN category = '家電' THEN price * 0.9
    ELSE price  -- 変更なしでも全行更新
END;

バッチ処理での効率化

-- 一時テーブルを使った大量更新
CREATE TEMP TABLE update_batch AS
SELECT 
    product_id,
    CASE
        WHEN total_sales > 1000 THEN 'ベストセラー'
        WHEN total_sales > 100 THEN '人気商品'
        ELSE '通常商品'
    END as new_status
FROM (
    SELECT product_id, SUM(quantity) as total_sales
    FROM sales
    GROUP BY product_id
) s;

-- インデックスを作成してから更新
CREATE INDEX ON update_batch(product_id);

-- 効率的な更新
UPDATE products p
SET status = u.new_status
FROM update_batch u
WHERE p.product_id = u.product_id;

よくあるエラーと対処法

データ型の不一致

-- エラーになる例
UPDATE users
SET status = CASE
    WHEN age >= 20 THEN 1  -- 数値
    ELSE 'minor'  -- 文字列(型が違う!)
END;

-- 正しい例(型を統一)
UPDATE users
SET status = CASE
    WHEN age >= 20 THEN 'adult'
    ELSE 'minor'
END;

-- または型変換を使用
UPDATE users
SET status_code = CASE
    WHEN age >= 20 THEN 1
    ELSE 0
END::INTEGER;

NULL値の扱い

-- NULLの比較は要注意
UPDATE accounts
SET category = CASE
    WHEN balance = NULL THEN 'unknown'  -- 動作しない!
    WHEN balance IS NULL THEN 'unknown'  -- 正しい
    WHEN balance > 10000 THEN 'premium'
    ELSE 'standard'
END;

-- COALESCEと組み合わせる
UPDATE accounts
SET fee = CASE
    WHEN COALESCE(balance, 0) >= 100000 THEN 0
    WHEN COALESCE(balance, 0) >= 10000 THEN 100
    ELSE 300
END;

更新対象の絞り込み忘れ

-- 危険:全レコードが更新される
UPDATE orders
SET status = CASE
    WHEN shipped_date IS NOT NULL THEN '発送済み'
    ELSE status  -- 元の値のままだが、更新は実行される
END;

-- 安全:必要なレコードのみ更新
UPDATE orders
SET status = '発送済み'
WHERE shipped_date IS NOT NULL
  AND status != '発送済み';  -- 既に更新済みは除外

実践的な応用例

在庫と価格の動的調整

-- 在庫連動型の価格調整システム
WITH stock_levels AS (
    SELECT 
        product_id,
        stock_quantity,
        AVG(stock_quantity) OVER (PARTITION BY category_id) as avg_stock,
        daily_sales_avg
    FROM inventory
)
UPDATE products p
SET 
    dynamic_price = p.base_price * CASE
        -- 在庫切れ間近
        WHEN sl.stock_quantity < sl.daily_sales_avg * 3 THEN 1.15
        -- 在庫過多
        WHEN sl.stock_quantity > sl.avg_stock * 2 THEN 0.85
        -- 通常
        ELSE 1.0
    END,
    price_updated_at = CURRENT_TIMESTAMP,
    price_reason = CASE
        WHEN sl.stock_quantity < sl.daily_sales_avg * 3 THEN '在庫僅少'
        WHEN sl.stock_quantity > sl.avg_stock * 2 THEN '在庫調整セール'
        ELSE '通常価格'
    END
FROM stock_levels sl
WHERE p.product_id = sl.product_id
  AND p.auto_pricing = true;

ポイント計算システム

-- 購買履歴に基づくポイント付与
UPDATE customer_points cp
SET 
    points = cp.points + CASE
        -- VIP会員
        WHEN c.membership_level = 'VIP' THEN
            CASE
                WHEN purchase_amount >= 10000 THEN purchase_amount * 0.05
                WHEN purchase_amount >= 5000 THEN purchase_amount * 0.03
                ELSE purchase_amount * 0.02
            END
        -- 一般会員
        ELSE
            CASE
                WHEN purchase_amount >= 10000 THEN purchase_amount * 0.02
                ELSE purchase_amount * 0.01
            END
    END,
    tier = CASE
        WHEN cp.points + (calculated_points) >= 100000 THEN 'Platinum'
        WHEN cp.points + (calculated_points) >= 50000 THEN 'Gold'
        WHEN cp.points + (calculated_points) >= 10000 THEN 'Silver'
        ELSE 'Bronze'
    END
FROM customers c, 
     purchases p
WHERE cp.customer_id = c.customer_id
  AND cp.customer_id = p.customer_id
  AND p.purchase_date = CURRENT_DATE;

トランザクション管理のベストプラクティス

-- 安全な更新処理
BEGIN;

-- 更新前の状態を確認
SELECT COUNT(*), status 
FROM orders 
WHERE order_date < CURRENT_DATE - INTERVAL '30 days'
GROUP BY status;

-- 条件付き更新を実行
UPDATE orders
SET status = CASE
    WHEN status = 'pending' AND order_date < CURRENT_DATE - INTERVAL '30 days' THEN 'cancelled'
    WHEN status = 'processing' AND order_date < CURRENT_DATE - INTERVAL '7 days' THEN 'delayed'
    ELSE status
END
WHERE order_date < CURRENT_DATE - INTERVAL '7 days';

-- 更新結果を確認
SELECT COUNT(*), status 
FROM orders 
WHERE order_date < CURRENT_DATE - INTERVAL '30 days'
GROUP BY status;

-- 問題なければコミット、問題があればロールバック
COMMIT;  -- または ROLLBACK;

まとめ – UPDATE CASEで効率的なデータ更新を実現!

PostgreSQLのUPDATE CASE文について、基本から応用まで詳しく解説してきました。

重要ポイント:

  • CASE式で複雑な条件分岐を1つのUPDATE文で実現
  • 複数カラムの同時更新も可能
  • サブクエリや他テーブルとの結合も活用できる
  • パフォーマンスを考慮したWHERE句の使用が重要
  • トランザクション管理で安全な更新を

使いどころ:

  • ステータスの一括更新
  • 条件による価格調整
  • 評価やランクの自動計算
  • 在庫管理の自動化
  • ポイントやボーナスの計算

UPDATE CASE文をマスターすれば、複雑なビジネスロジックもSQLだけで実装でき、アプリケーション側の処理を大幅に簡略化できます。

ぜひ実際のプロジェクトで活用してみてください!

コメント

タイトルとURLをコピーしました