PostgreSQLで複数カラムを一発UPDATE!効率的な更新テクニック完全マスター

データベース・SQL

「複数のカラムを更新するのに、何回もUPDATE文を実行している…」 「サブクエリの結果を複数カラムに反映させたい」 「他のテーブルのデータで一括更新する方法が分からない」

こんな悩みを抱えていませんか?

PostgreSQLなら、1つのUPDATE文で複数カラムを効率的に更新できます。しかも、思っているより簡単なんです!

この記事を読めば、複雑な更新処理もスマートに書けるようになります。一緒に、UPDATE文の達人を目指しましょう!


スポンサーリンク

基本構文:複数カラム更新の3つの書き方

方法1:カンマ区切りで列挙(最も基本的)

UPDATE テーブル名
SET 
    カラム1 = 値1,
    カラム2 = 値2,
    カラム3 = 値3
WHERE 条件;

実例:

-- 従業員情報を更新
UPDATE employees
SET 
    salary = 350000,
    position = 'シニアエンジニア',
    updated_at = CURRENT_TIMESTAMP
WHERE employee_id = 101;

方法2:括弧を使った記法(PostgreSQL特有)

UPDATE テーブル名
SET (カラム1, カラム2, カラム3) = (値1, 値2, 値3)
WHERE 条件;

実例:

UPDATE employees
SET (salary, position, updated_at) = (350000, 'シニアエンジニア', CURRENT_TIMESTAMP)
WHERE employee_id = 101;

方法3:サブクエリを使った一括更新

UPDATE テーブル名
SET (カラム1, カラム2) = (
    SELECT 計算値1, 計算値2
    FROM 別テーブル
    WHERE 条件
)
WHERE 条件;

どの方法を選ぶべきか?次のセクションで詳しく見ていきましょう!


実践例1:基本的な複数カラム更新

サンプルテーブルの準備

-- 商品テーブルの作成
CREATE TABLE products (
    product_id SERIAL PRIMARY KEY,
    product_name VARCHAR(100),
    price DECIMAL(10,2),
    stock_quantity INTEGER,
    category VARCHAR(50),
    is_active BOOLEAN DEFAULT true,
    last_updated TIMESTAMP,
    updated_by VARCHAR(50)
);

-- サンプルデータの挿入
INSERT INTO products (product_name, price, stock_quantity, category, last_updated, updated_by) VALUES
('ノートPC', 120000, 50, '電子機器', '2024-01-01', 'admin'),
('ワイヤレスマウス', 3500, 200, '周辺機器', '2024-01-01', 'admin'),
('USBメモリ 32GB', 2000, 150, '記憶媒体', '2024-01-01', 'admin'),
('モニター 27インチ', 45000, 30, '周辺機器', '2024-01-01', 'admin'),
('キーボード', 8000, 100, '周辺機器', '2024-01-01', 'admin');

基本的な更新:価格改定と在庫調整

-- 特定商品の価格と在庫を同時更新
UPDATE products
SET 
    price = price * 1.1,  -- 10%値上げ
    stock_quantity = stock_quantity - 10,  -- 在庫を10個減らす
    last_updated = CURRENT_TIMESTAMP,
    updated_by = 'sales_team'
WHERE product_id = 1;

条件付き一括更新

-- カテゴリごとに異なる更新を実行
UPDATE products
SET 
    price = CASE 
        WHEN category = '電子機器' THEN price * 1.15
        WHEN category = '周辺機器' THEN price * 1.08
        ELSE price * 1.05
    END,
    stock_quantity = CASE
        WHEN stock_quantity < 50 THEN stock_quantity + 100  -- 在庫補充
        ELSE stock_quantity
    END,
    last_updated = CURRENT_TIMESTAMP,
    updated_by = 'system'
WHERE is_active = true;

実践例2:サブクエリを使った高度な更新

他テーブルのデータで更新

-- 売上統計テーブル
CREATE TABLE sales_stats (
    product_id INTEGER,
    total_sales INTEGER,
    average_rating DECIMAL(3,2),
    last_sale_date DATE
);

-- 売上データを基に商品情報を更新
UPDATE products p
SET 
    stock_quantity = stock_quantity - s.total_sales,
    is_active = CASE 
        WHEN s.average_rating < 3.0 THEN false 
        ELSE true 
    END,
    last_updated = CURRENT_TIMESTAMP
FROM sales_stats s
WHERE p.product_id = s.product_id
  AND s.last_sale_date >= CURRENT_DATE - INTERVAL '30 days';

サブクエリで複数カラムを一括取得

-- 価格調整テーブル
CREATE TABLE price_adjustments (
    product_id INTEGER,
    new_price DECIMAL(10,2),
    discount_percentage INTEGER,
    effective_date DATE
);

-- PostgreSQL特有の書き方(ROW型を使用)
UPDATE products
SET (price, last_updated, updated_by) = (
    SELECT 
        new_price * (1 - discount_percentage/100.0),
        CURRENT_TIMESTAMP,
        'pricing_system'
    FROM price_adjustments
    WHERE price_adjustments.product_id = products.product_id
      AND effective_date = CURRENT_DATE
)
WHERE EXISTS (
    SELECT 1 FROM price_adjustments
    WHERE price_adjustments.product_id = products.product_id
      AND effective_date = CURRENT_DATE
);

実践例3:JOINを使った複雑な更新

USING句を使った更新

-- 部門情報テーブル
CREATE TABLE departments (
    dept_id INTEGER PRIMARY KEY,
    dept_name VARCHAR(100),
    budget DECIMAL(12,2),
    manager_id INTEGER
);

-- 従業員テーブル
CREATE TABLE employees (
    emp_id SERIAL PRIMARY KEY,
    emp_name VARCHAR(100),
    dept_id INTEGER,
    salary DECIMAL(10,2),
    bonus DECIMAL(10,2),
    performance_score INTEGER
);

-- 部門予算に基づいてボーナスを計算・更新
UPDATE employees e
SET 
    bonus = d.budget * 0.01 * e.performance_score / 100,
    salary = salary * CASE
        WHEN e.performance_score >= 90 THEN 1.10
        WHEN e.performance_score >= 70 THEN 1.05
        ELSE 1.00
    END,
    last_modified = CURRENT_TIMESTAMP
FROM departments d
WHERE e.dept_id = d.dept_id
  AND d.budget > 1000000;

複数テーブルの情報を統合して更新

-- 顧客の購入履歴を基にステータスを更新
UPDATE customers c
SET 
    customer_level = CASE
        WHEN total_purchase >= 1000000 THEN 'プラチナ'
        WHEN total_purchase >= 500000 THEN 'ゴールド'
        WHEN total_purchase >= 100000 THEN 'シルバー'
        ELSE 'ブロンズ'
    END,
    discount_rate = CASE
        WHEN total_purchase >= 1000000 THEN 15
        WHEN total_purchase >= 500000 THEN 10
        WHEN total_purchase >= 100000 THEN 5
        ELSE 0
    END,
    last_purchase_date = latest_date,
    updated_at = CURRENT_TIMESTAMP
FROM (
    SELECT 
        customer_id,
        SUM(amount) as total_purchase,
        MAX(purchase_date) as latest_date
    FROM orders
    WHERE purchase_date >= CURRENT_DATE - INTERVAL '1 year'
    GROUP BY customer_id
) AS purchase_summary
WHERE c.customer_id = purchase_summary.customer_id;

RETURNING句で更新結果を確認

更新した値を即座に取得

-- 更新前後の値を比較
UPDATE products
SET 
    price = price * 1.2,
    stock_quantity = stock_quantity + 50,
    last_updated = CURRENT_TIMESTAMP
WHERE category = '電子機器'
RETURNING 
    product_id,
    product_name,
    price AS new_price,
    price / 1.2 AS old_price,  -- 計算で旧価格を表示
    stock_quantity AS new_stock;

更新件数と詳細を同時に確認

WITH updated_rows AS (
    UPDATE employees
    SET 
        salary = salary * 1.05,
        bonus = 10000,
        review_date = CURRENT_DATE
    WHERE performance_score >= 80
    RETURNING *
)
SELECT 
    COUNT(*) AS 更新件数,
    AVG(salary) AS 平均給与,
    SUM(bonus) AS ボーナス総額
FROM updated_rows;

パフォーマンスを考慮した更新テクニック

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

-- 更新対象を絞るためのインデックス作成
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_active ON products(is_active) WHERE is_active = true;

-- インデックスを活用した効率的な更新
UPDATE products
SET 
    price = price * 1.05,
    last_updated = CURRENT_TIMESTAMP
WHERE category = '周辺機器'
  AND is_active = true;

バッチ更新で大量データを処理

-- 大量データを分割して更新(メモリ使用量を抑える)
DO $$
DECLARE
    batch_size INTEGER := 1000;
    offset_val INTEGER := 0;
    updated_count INTEGER;
BEGIN
    LOOP
        WITH batch AS (
            SELECT product_id
            FROM products
            WHERE needs_update = true
            ORDER BY product_id
            LIMIT batch_size
            OFFSET offset_val
        )
        UPDATE products p
        SET 
            price = price * 1.1,
            needs_update = false,
            updated_at = CURRENT_TIMESTAMP
        FROM batch b
        WHERE p.product_id = b.product_id;
        
        GET DIAGNOSTICS updated_count = ROW_COUNT;
        
        EXIT WHEN updated_count < batch_size;
        offset_val := offset_val + batch_size;
        
        -- 負荷軽減のため少し待機
        PERFORM pg_sleep(0.1);
    END LOOP;
END $$;

不要なトリガーを一時的に無効化

-- トリガーの無効化(大量更新時)
ALTER TABLE products DISABLE TRIGGER ALL;

-- 大量更新実行
UPDATE products
SET 
    price = price * 1.05,
    category = UPPER(category),
    last_updated = CURRENT_TIMESTAMP;

-- トリガーの有効化
ALTER TABLE products ENABLE TRIGGER ALL;

よくある間違いと対処法

間違い1:WHERE句の書き忘れ

-- 危険!全レコードが更新される
UPDATE products
SET price = 0;  -- WHERE句なし!

-- 安全対策:トランザクションを使う
BEGIN;
UPDATE products
SET price = price * 1.1
WHERE category = '電子機器';
-- 確認
SELECT * FROM products WHERE category = '電子機器';
-- 問題なければコミット、問題があればロールバック
COMMIT; -- または ROLLBACK;

間違い2:サブクエリが複数行を返す

-- エラーになる例
UPDATE products
SET price = (
    SELECT price FROM price_list  -- 複数行返される可能性
    WHERE category = products.category
);

-- 正しい書き方(集約関数を使う)
UPDATE products
SET price = (
    SELECT AVG(price) FROM price_list
    WHERE category = products.category
);

-- またはLIMIT 1を使う
UPDATE products
SET price = (
    SELECT price FROM price_list
    WHERE category = products.category
    ORDER BY effective_date DESC
    LIMIT 1
);

間違い3:NULL値の扱い

-- NULL値で上書きしてしまう危険
UPDATE products p
SET (price, stock_quantity) = (
    SELECT new_price, new_stock
    FROM temp_updates t
    WHERE t.product_id = p.product_id
);

-- NULL値を防ぐ安全な書き方
UPDATE products p
SET 
    price = COALESCE(t.new_price, p.price),
    stock_quantity = COALESCE(t.new_stock, p.stock_quantity)
FROM temp_updates t
WHERE t.product_id = p.product_id;

実用的なユースケース集

在庫管理システム

-- 注文処理時の在庫更新
UPDATE products p
SET 
    stock_quantity = p.stock_quantity - o.quantity,
    reserved_quantity = p.reserved_quantity + o.quantity,
    last_sold = CURRENT_TIMESTAMP,
    total_sales = p.total_sales + o.quantity
FROM order_items o
WHERE p.product_id = o.product_id
  AND o.order_id = 12345
  AND p.stock_quantity >= o.quantity;

ユーザーステータス管理

-- 最終ログイン時刻とアクティブ状態を更新
UPDATE users
SET 
    last_login = CURRENT_TIMESTAMP,
    login_count = login_count + 1,
    is_active = true,
    session_token = encode(gen_random_bytes(32), 'hex'),
    ip_address = '192.168.1.1'
WHERE email = 'user@example.com'
  AND password_hash = crypt('password', password_hash)
RETURNING user_id, session_token;

定期バッチ処理

-- 月次の統計情報更新
UPDATE monthly_summary
SET 
    total_revenue = subq.revenue,
    order_count = subq.orders,
    average_order_value = subq.revenue / NULLIF(subq.orders, 0),
    top_product = subq.best_seller,
    calculated_at = CURRENT_TIMESTAMP
FROM (
    SELECT 
        DATE_TRUNC('month', order_date) as month,
        SUM(total_amount) as revenue,
        COUNT(*) as orders,
        MODE() WITHIN GROUP (ORDER BY product_name) as best_seller
    FROM orders
    WHERE order_date >= DATE_TRUNC('month', CURRENT_DATE)
    GROUP BY DATE_TRUNC('month', order_date)
) AS subq
WHERE monthly_summary.month = subq.month;

まとめ:複数カラム更新をマスターしよう!

今日学んだポイントを振り返りましょう:

3つの基本構文(カンマ区切り、括弧記法、サブクエリ) ✅ FROM句でJOINして他テーブルのデータを利用 ✅ CASE文で条件付き更新を実現 ✅ RETURNING句で更新結果を即座に確認 ✅ トランザクションで安全に更新 ✅ バッチ処理で大量データに対応 ✅ COALESCEでNULL対策

複数カラムの更新は、最初は「複雑そう…」と感じるかもしれません。

でも、基本パターンを理解すれば、効率的で読みやすいSQLが書けるようになります。1つずつUPDATE文を書いていた時間が、嘘のように短縮されるはずです。

今日から、スマートなUPDATE文で、データベース操作を効率化しましょう!

Happy Updating!


次のステップにおすすめ:

  • PostgreSQLのMERGE文(UPSERT)の使い方
  • CTEを使った複雑な更新処理
  • トリガーによる自動更新の実装

コメント

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