Skilore

CRUD操作 — SELECT / INSERT / UPDATE / DELETE 完全ガイド

CRUDはCreate(作成)、Read(読取)、Update(更新)、Delete(削除)の頭文字であり、データベース操作の最も基本的な4つの操作を表す。あらゆるアプリケーションのデータ層はこの4操作の組み合わせで構成される。

86 分で読めます42,769 文字

CRUD操作 — SELECT / INSERT / UPDATE / DELETE 完全ガイド

CRUDはCreate(作成)、Read(読取)、Update(更新)、Delete(削除)の頭文字であり、データベース操作の最も基本的な4つの操作を表す。あらゆるアプリケーションのデータ層はこの4操作の組み合わせで構成される。


この章で学ぶこと

  1. SELECT文の完全な構文と論理的実行順序を正確に理解する
  2. INSERT / UPDATE / DELETE の安全な実行パターンとトランザクション活用法を身につける
  3. RETURNING句、UPSERT、論理削除など実務で頻出する応用パターンを習得する
  4. 各操作のパフォーマンス特性とアンチパターンを把握する

前提知識

  • 00-sql-overview.md — SQL概要、リレーショナルモデルの基礎、RDBMS選定
  • SQLの基本的なデータ型(INTEGER、VARCHAR、DATE、DECIMAL等)の理解
  • ../../security-fundamentals/docs/01-web-security/ — SQLインジェクション対策(推奨)

1. SELECT — データの読み取り

SELECTはSQLの中で最も頻繁に使用される文であり、データベースからデータを取得する唯一の手段である。SELECTの理解はSQLマスタリーの基盤となる。

1.1 SELECT文の論理的実行順序

SELECT文は「書く順序」と「実行される順序」が異なるという点が、多くの初学者が躓くポイントである。

SELECT文の論理的実行順序
書く順序 実行順序
───────── ─────────
SELECT ──────┐ ① FROM / JOIN
FROM ◀─────┤ ② ON (結合条件)
WHERE ──────┤ ③ WHERE (行フィルタ)
GROUP BY ──────┤ ④ GROUP BY (グループ化)
HAVING ──────┤ ⑤ HAVING (グループフィルタ)
SELECT ──────┤ ⑥ SELECT (式の評価、エイリアス)
DISTINCT ──────┤ ⑦ DISTINCT (重複排除)
ORDER BY ──────┤ ⑧ ORDER BY (並べ替え)
LIMIT ──────┘ ⑨ LIMIT / OFFSET (件数制限)
重要な帰結:
- WHERE句でSELECTのエイリアスは使えない
(WHEREが先に実行されるため)
- ORDER BY句ではSELECTのエイリアスが使える
(ORDER BYが後に実行されるため)
- HAVING句では集約関数が使える
(GROUP BYの後に実行されるため)

コード例1: SELECT文の完全な構文と実行順序

-- テーブル準備
CREATE TABLE departments (
    id   INTEGER PRIMARY KEY,
    name VARCHAR(50) NOT NULL
);
 
CREATE TABLE employees (
    id            SERIAL PRIMARY KEY,
    name          VARCHAR(100) NOT NULL,
    department_id INTEGER REFERENCES departments(id),
    salary        DECIMAL(10, 2),
    status        VARCHAR(20) DEFAULT 'active',
    hired_date    DATE NOT NULL DEFAULT CURRENT_DATE
);
 
INSERT INTO departments VALUES (1, '営業'), (2, '開発'), (3, '人事'), (4, '企画');
INSERT INTO employees (name, department_id, salary, status, hired_date) VALUES
    ('田中太郎', 1, 450000, 'active', '2020-04-01'),
    ('鈴木花子', 2, 520000, 'active', '2019-07-15'),
    ('佐藤次郎', 1, 380000, 'active', '2021-01-10'),
    ('高橋三郎', 2, 600000, 'active', '2018-04-01'),
    ('山田四郎', 3, 420000, 'active', '2022-04-01'),
    ('伊藤五郎', 2, 480000, 'inactive', '2017-10-01'),
    ('渡辺六子', 1, 510000, 'active', '2019-01-15');
 
-- SELECT文の完全な構文例
SELECT DISTINCT
    d.name AS department,              -- ⑥ 列を選択・計算
    COUNT(*) AS employee_count,        -- ⑥ 集約関数の評価
    AVG(e.salary) AS avg_salary        -- ⑥ 集約関数の評価
FROM employees e                       -- ① テーブルを特定
    INNER JOIN departments d           -- ① テーブル結合
        ON e.department_id = d.id      -- ② 結合条件の評価
WHERE e.status = 'active'              -- ③ 行レベルのフィルタ
GROUP BY d.name                        -- ④ グループ化
HAVING COUNT(*) >= 2                   -- ⑤ グループレベルのフィルタ
ORDER BY avg_salary DESC               -- ⑧ 並べ替え
LIMIT 10;                              -- ⑨ 件数制限

コード例2: WHERE句の全条件パターン

-- === 比較演算子 ===
SELECT * FROM products WHERE price > 1000;               -- より大きい
SELECT * FROM products WHERE price >= 1000;              -- 以上
SELECT * FROM products WHERE price < 5000;               -- より小さい
SELECT * FROM products WHERE price <= 5000;              -- 以下
SELECT * FROM products WHERE price <> 1000;              -- 等しくない(標準SQL)
SELECT * FROM products WHERE price != 1000;              -- 等しくない(多くのRDBMSで使用可)
 
-- === 範囲 ===
SELECT * FROM products WHERE price BETWEEN 1000 AND 5000;
-- ↑ price >= 1000 AND price <= 5000 と同等(両端を含む)
 
-- === パターンマッチ(LIKE) ===
SELECT * FROM users WHERE name LIKE '田中%';             -- 前方一致
SELECT * FROM users WHERE email LIKE '%@gmail.com';      -- 後方一致
SELECT * FROM users WHERE name LIKE '%太%';              -- 部分一致
SELECT * FROM users WHERE code LIKE 'A_B';               -- _ = 任意の1文字
-- LIKE のエスケープ
SELECT * FROM products WHERE name LIKE '%25\%%' ESCAPE '\';  -- '%'を含む
 
-- === NULL判定 ===
-- 重要: = NULL は動作しない(3値論理のため)
SELECT * FROM users WHERE phone IS NULL;                  -- NULLの行
SELECT * FROM users WHERE phone IS NOT NULL;              -- NULLでない行
 
-- === IN / NOT IN ===
SELECT * FROM orders WHERE status IN ('pending', 'processing', 'shipped');
SELECT * FROM orders WHERE status NOT IN ('cancelled', 'refunded');
 
-- === 複合条件(AND / OR) ===
SELECT * FROM products
WHERE (category = 'electronics' OR category = 'books')
  AND price < 5000
  AND stock > 0;
 
-- === EXISTS ===
SELECT * FROM customers c
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.customer_id = c.id AND o.total > 10000
);
 
-- === ANY / ALL ===
SELECT * FROM employees
WHERE salary > ALL (
    SELECT AVG(salary) FROM employees GROUP BY department_id
);
 
-- === CASE式をWHEREで使用 ===
SELECT * FROM orders
WHERE CASE
    WHEN priority = 'high' THEN total_amount > 0
    WHEN priority = 'low'  THEN total_amount > 1000
    ELSE TRUE
END;

コード例3: SELECT句の応用テクニック

-- === 計算列 ===
SELECT
    name,
    salary,
    salary * 12 AS annual_salary,                    -- 年収
    salary * 12 * 1.1 AS annual_with_bonus,          -- 年収+賞与
    ROUND(salary / 160, 0) AS hourly_rate            -- 時給換算
FROM employees;
 
-- === CASE式(条件分岐) ===
SELECT
    name,
    salary,
    CASE
        WHEN salary >= 600000 THEN 'S'
        WHEN salary >= 500000 THEN 'A'
        WHEN salary >= 400000 THEN 'B'
        ELSE 'C'
    END AS grade,
    -- CASE式の簡略形(単純CASE)
    CASE status
        WHEN 'active'   THEN '在籍'
        WHEN 'inactive' THEN '休職'
        WHEN 'retired'  THEN '退職'
        ELSE '不明'
    END AS status_label
FROM employees;
 
-- === COALESCE(最初の非NULL値) ===
SELECT
    name,
    COALESCE(phone, mobile, email, '連絡先なし') AS primary_contact
FROM employees;
 
-- === NULLIF(2値が等しければNULL) ===
-- 0除算防止パターン
SELECT
    department_id,
    total_revenue,
    total_cost,
    total_revenue / NULLIF(total_cost, 0) AS cost_ratio  -- 0除算を回避
FROM department_financials;
 
-- === 型変換(CAST) ===
SELECT
    CAST(price AS INTEGER) AS rounded_price,
    CAST(created_at AS DATE) AS created_date,
    CAST(quantity AS VARCHAR(10)) || '個' AS quantity_text
FROM products;
 
-- === サブクエリとの組み合わせ ===
SELECT
    e.name,
    e.salary,
    (SELECT AVG(salary) FROM employees) AS company_avg,
    e.salary - (SELECT AVG(salary) FROM employees) AS diff_from_avg,
    ROUND(e.salary / (SELECT AVG(salary) FROM employees) * 100, 1) AS pct_of_avg
FROM employees e
ORDER BY diff_from_avg DESC;

1.2 ORDER BYの詳細

-- 基本的なソート
SELECT * FROM employees ORDER BY salary DESC;                -- 降順
SELECT * FROM employees ORDER BY salary ASC;                 -- 昇順(デフォルト)
SELECT * FROM employees ORDER BY department_id ASC, salary DESC;  -- 複合ソート
 
-- NULLの並び順制御
SELECT * FROM employees ORDER BY department_id NULLS FIRST;  -- NULLを先頭
SELECT * FROM employees ORDER BY department_id NULLS LAST;   -- NULLを末尾
 
-- 列番号によるソート(可読性が低いため非推奨だが知っておくべき)
SELECT name, salary FROM employees ORDER BY 2 DESC;  -- 2番目の列 = salary
 
-- CASE式でカスタムソート
SELECT * FROM orders
ORDER BY
    CASE status
        WHEN 'urgent'     THEN 1
        WHEN 'processing' THEN 2
        WHEN 'pending'    THEN 3
        WHEN 'completed'  THEN 4
        ELSE 5
    END,
    created_at DESC;

1.3 DISTINCT と LIMIT

-- DISTINCT: 重複行の排除
SELECT DISTINCT department_id FROM employees;
 
-- DISTINCT ON (PostgreSQL固有): 各グループの最初の行のみ
SELECT DISTINCT ON (department_id) *
FROM employees
ORDER BY department_id, salary DESC;
-- → 各部署で最も給与の高い社員1名ずつ
 
-- LIMIT / OFFSET: ページネーション
SELECT * FROM products
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;     -- 1ページ目(1-20件)
 
SELECT * FROM products
ORDER BY created_at DESC
LIMIT 20 OFFSET 20;    -- 2ページ目(21-40件)
 
-- 標準SQL: FETCH FIRST
SELECT * FROM products
ORDER BY created_at DESC
OFFSET 20 ROWS
FETCH NEXT 20 ROWS ONLY;

2. INSERT — データの作成

2.1 INSERTの内部動作

INSERTが実行されると、内部的に以下の処理が行われる:

① 構文解析・権限チェック
└→ テーブル存在確認、カラム型チェック
② 制約チェック
├→ NOT NULL 制約
├→ UNIQUE / PRIMARY KEY 制約
├→ FOREIGN KEY 制約(参照先の存在確認)
├→ CHECK 制約
└→ 排他制約(PostgreSQL EXCLUDE)
③ DEFAULT値・GENERATED列の計算
④ トリガー実行(BEFORE INSERT)
⑤ 行の挿入(WALへの書き込み → バッファプール更新)
⑥ インデックスの更新(全関連インデックス)
⑦ トリガー実行(AFTER INSERT)
⑧ RETURNING句の評価(PostgreSQL)
※ これら全体がトランザクション内で原子的に実行される

コード例4: 各種INSERTパターン

-- === 基本的なINSERT(単一行) ===
INSERT INTO employees (name, department_id, salary, hired_date)
VALUES ('山田太郎', 10, 400000, '2024-04-01');
 
-- === 複数行の一括INSERT ===
-- パフォーマンス: 個別INSERTの10〜100倍高速
INSERT INTO employees (name, department_id, salary, hired_date)
VALUES
    ('鈴木花子', 20, 450000, '2024-04-01'),
    ('佐藤次郎', 10, 380000, '2024-04-01'),
    ('高橋三郎', 30, 420000, '2024-04-01');
 
-- === SELECTの結果をINSERT ===
-- データ移行・アーカイブに最適
INSERT INTO employee_archive (name, department_id, salary, archived_at)
SELECT name, department_id, salary, CURRENT_TIMESTAMP
FROM employees
WHERE status = 'retired';
 
-- === RETURNING句で挿入結果を取得(PostgreSQL) ===
-- アプリケーション側で自動生成されたIDを即座に取得可能
INSERT INTO employees (name, department_id, salary)
VALUES ('新人一号', 10, 350000)
RETURNING id, name, created_at;
-- → id=42, name='新人一号', created_at='2024-04-01 10:30:00'
 
-- === DEFAULT VALUES ===
-- 全列がDEFAULT値またはNULL許可の場合
INSERT INTO audit_log DEFAULT VALUES;
 
-- === INSERT with CTE ===
-- 複雑な変換を伴う挿入
WITH source_data AS (
    SELECT
        name,
        department_id,
        salary * 1.05 AS adjusted_salary  -- 5%昇給した金額で挿入
    FROM employees
    WHERE status = 'active' AND hired_date < '2020-01-01'
)
INSERT INTO salary_adjustments (employee_name, dept_id, new_salary, adjusted_at)
SELECT name, department_id, adjusted_salary, CURRENT_TIMESTAMP
FROM source_data;

コード例5: UPSERT(存在すれば更新、なければ挿入)

-- === PostgreSQL: ON CONFLICT ===
-- user_settingsテーブル: (user_id, key) がUNIQUE制約
CREATE TABLE user_settings (
    user_id    INTEGER NOT NULL,
    key        VARCHAR(100) NOT NULL,
    value      TEXT,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (user_id, key)
);
 
-- 存在すればvalueを更新、なければ新規挿入
INSERT INTO user_settings (user_id, key, value)
VALUES (1, 'theme', 'dark')
ON CONFLICT (user_id, key)
DO UPDATE SET
    value = EXCLUDED.value,          -- EXCLUDED = 挿入しようとした値
    updated_at = CURRENT_TIMESTAMP;
 
-- ON CONFLICT DO NOTHING: 重複を無視(エラーを出さない)
INSERT INTO user_settings (user_id, key, value)
VALUES (1, 'theme', 'dark')
ON CONFLICT (user_id, key) DO NOTHING;
 
-- 条件付きUPSERT: 既存値より新しい場合のみ更新
INSERT INTO cache_entries (key, value, version)
VALUES ('user:1', '{"name":"田中"}', 5)
ON CONFLICT (key)
DO UPDATE SET
    value = EXCLUDED.value,
    version = EXCLUDED.version
WHERE cache_entries.version < EXCLUDED.version;  -- バージョンが新しい場合のみ
 
-- === MySQL: ON DUPLICATE KEY UPDATE ===
INSERT INTO user_settings (user_id, setting_key, setting_value)
VALUES (1, 'theme', 'dark')
ON DUPLICATE KEY UPDATE
    setting_value = VALUES(setting_value),
    updated_at = NOW();
 
-- === SQL Server: MERGE ===
MERGE INTO user_settings AS target
USING (VALUES (1, 'theme', 'dark')) AS source (user_id, key, value)
ON target.user_id = source.user_id AND target.key = source.key
WHEN MATCHED THEN
    UPDATE SET value = source.value, updated_at = GETDATE()
WHEN NOT MATCHED THEN
    INSERT (user_id, key, value) VALUES (source.user_id, source.key, source.value);

3. UPDATE — データの更新

3.1 UPDATEの安全な実行手順

本番環境でUPDATEを実行する際は、以下のプロトコルに従うことが推奨される:

Step 1: 対象行の確認
┌──────────────────────────────────────────┐
SELECT * FROM employees
WHERE department_id = 10
AND performance_rating >= 4;
-- → 5行ヒット(期待通りか確認)
└──────────────────────────────────────────┘
Step 2: トランザクション開始
┌──────────────────────────────────────────┐
BEGIN;
└──────────────────────────────────────────┘
Step 3: UPDATE実行
┌──────────────────────────────────────────┐
UPDATE employees
SET salary = salary * 1.05
WHERE department_id = 10
AND performance_rating >= 4;
-- → UPDATE 5 (件数が一致するか確認)
└──────────────────────────────────────────┘
Step 4: 結果確認
┌──────────────────────────────────────────┐
SELECT * FROM employees
WHERE department_id = 10
AND performance_rating >= 4;
-- → 変更後の値が期待通りか確認
└──────────────────────────────────────────┘
Step 5: 確定 or 取消
┌──────────────────────────────────────────┐
COMMIT; -- 問題なければ確定
-- ROLLBACK; -- 問題あれば取り消し
└──────────────────────────────────────────┘

コード例6: 各種UPDATEパターン

-- === 基本的なUPDATE ===
UPDATE employees
SET salary = 500000
WHERE employee_id = 42;
 
-- === 複数列の同時更新 ===
UPDATE employees
SET salary = salary * 1.05,
    updated_at = CURRENT_TIMESTAMP
WHERE department_id = 10
  AND performance_rating >= 4;
 
-- === 計算式を使った更新 ===
-- 全社員の給与を等級に応じて昇給
UPDATE employees
SET salary = CASE
    WHEN grade = 'S' THEN salary * 1.10   -- S等級: 10%昇給
    WHEN grade = 'A' THEN salary * 1.07   -- A等級: 7%昇給
    WHEN grade = 'B' THEN salary * 1.05   -- B等級: 5%昇給
    ELSE salary * 1.03                     -- その他: 3%昇給
END,
updated_at = CURRENT_TIMESTAMP
WHERE status = 'active';
 
-- === JOINを使ったUPDATE(PostgreSQL) ===
-- 禁止顧客の未処理注文をキャンセル
UPDATE orders o
SET status = 'cancelled',
    cancelled_at = NOW(),
    cancel_reason = 'customer_banned'
FROM customers c
WHERE o.customer_id = c.id
  AND c.is_banned = TRUE
  AND o.status = 'pending';
 
-- === JOINを使ったUPDATE(MySQL) ===
-- UPDATE orders o
-- INNER JOIN customers c ON o.customer_id = c.id
-- SET o.status = 'cancelled',
--     o.cancelled_at = NOW()
-- WHERE c.is_banned = TRUE
--   AND o.status = 'pending';
 
-- === サブクエリを使ったUPDATE ===
UPDATE products
SET price = price * 0.9
WHERE category_id IN (
    SELECT id FROM categories WHERE name = 'セール対象'
);
 
-- === RETURNING句で更新結果を確認(PostgreSQL) ===
UPDATE employees
SET salary = salary * 1.10
WHERE department_id = 20
RETURNING id, name, salary AS new_salary;
 
-- === CTEを使った複雑なUPDATE ===
-- 部署平均を下回る社員を最低ラインまで引き上げ
WITH dept_min AS (
    SELECT department_id, AVG(salary) * 0.8 AS min_salary
    FROM employees
    WHERE status = 'active'
    GROUP BY department_id
)
UPDATE employees e
SET salary = dm.min_salary
FROM dept_min dm
WHERE e.department_id = dm.department_id
  AND e.salary < dm.min_salary
  AND e.status = 'active'
RETURNING e.id, e.name, e.salary AS adjusted_salary;

4. DELETE — データの削除

4.1 物理削除 vs 論理削除

物理削除(Hard Delete)
┌─────────────────────────────────────┐
DELETE FROM users WHERE id = 42;
メリット:
- テーブルがシンプルに保たれる
- ストレージを即座に解放
- クエリにフィルタ条件不要
デメリット:
- 復元が困難(バックアップのみ)
- 監査証跡が残らない
- 外部キー制約の連鎖削除リスク
└─────────────────────────────────────┘
論理削除(Soft Delete)
┌─────────────────────────────────────┐
UPDATE users
SET deleted_at = CURRENT_TIMESTAMP,
status = 'deleted'
WHERE id = 42;
メリット:
- 復元が容易(フラグを戻すだけ)
- 監査証跡が残る
- 参照整合性が維持される
デメリット:
- 全クエリにWHERE deleted_at IS NULLが必要
- ストレージが増大し続ける
- UNIQUE制約の設計が複雑化
└─────────────────────────────────────┘
推奨: 論理削除 + 定期的なアーカイブ/パージ

コード例7: 安全なDELETEパターン

-- === 基本的なDELETE ===
DELETE FROM sessions
WHERE expires_at < CURRENT_TIMESTAMP;
 
-- === JOINを使ったDELETE(PostgreSQL: USING句) ===
DELETE FROM order_items oi
USING orders o
WHERE oi.order_id = o.id
  AND o.status = 'cancelled';
 
-- === RETURNING句で削除した行を取得 ===
DELETE FROM notifications
WHERE user_id = 42 AND read_at IS NOT NULL
RETURNING id, message, created_at;
 
-- === 論理削除(ソフトデリート)の実装パターン ===
-- 基本パターン
UPDATE users
SET deleted_at = CURRENT_TIMESTAMP,
    status = 'deleted',
    -- 個人情報の匿名化(GDPR対応)
    email = 'deleted_' || id || '@deleted.example.com',
    name = '削除済みユーザー'
WHERE id = 42;
 
-- 論理削除対応のビュー
CREATE VIEW active_users AS
SELECT * FROM users WHERE deleted_at IS NULL;
 
-- 論理削除 + UNIQUE制約の問題と解決
-- 問題: deleted_atがNULLの行同士でのみUNIQUEを保証したい
-- PostgreSQL: 部分インデックスで解決
CREATE UNIQUE INDEX idx_users_email_active
ON users (email)
WHERE deleted_at IS NULL;
 
-- === TRUNCATE: テーブル全行の高速削除(DDL操作) ===
TRUNCATE TABLE temp_import_data;
-- ※ WHERE句使用不可
-- ※ TRUNCATEはロールバック不可(MySQL)、PostgreSQLでは可能
-- ※ トリガーが発火しない
-- ※ 自動連番がリセットされる
 
-- === CASCADE付きTRUNCATE ===
-- 外部キーで参照されているテーブルも同時にTRUNCATE
TRUNCATE TABLE orders CASCADE;  -- order_itemsも同時にTRUNCATE
 
-- === DELETEの安全な実行(トランザクション) ===
BEGIN;
    -- Step 1: 削除対象の確認
    SELECT COUNT(*) FROM logs WHERE created_at < '2023-01-01';
    -- → 15,000行
 
    -- Step 2: 削除実行
    DELETE FROM logs WHERE created_at < '2023-01-01';
    -- → DELETE 15000
 
    -- Step 3: 確認して確定
    SELECT COUNT(*) FROM logs WHERE created_at < '2023-01-01';
    -- → 0行(期待通り)
COMMIT;
 
-- === 大量データの分割削除(ロック時間を短くする) ===
-- 一度に全件削除するとロック時間が長くなるため、バッチで実行
DO $$
DECLARE
    deleted_count INTEGER;
BEGIN
    LOOP
        DELETE FROM logs
        WHERE id IN (
            SELECT id FROM logs
            WHERE created_at < '2023-01-01'
            LIMIT 10000  -- 1回あたり10,000件ずつ
        );
        GET DIAGNOSTICS deleted_count = ROW_COUNT;
        EXIT WHEN deleted_count = 0;
 
        -- 他のトランザクションに実行機会を与える
        PERFORM pg_sleep(0.1);
    END LOOP;
END $$;

5. CRUD操作のデータフロー全体像

アプリケーション層
┌───────────────────────────────────────────┐
ORM / クエリビルダー / 生SQL
パラメータバインド(SQLインジェクション防止)
└──────────────────┬────────────────────────┘
データベース接続層 ▼
┌───────────────────────────────────────────┐
接続プーリング(PgBouncer等)
プリペアドステートメント
└──────────────────┬────────────────────────┘
データベースエンジン ▼
┌───────────────────────────────────────────┐
INSERT INTO → 制約チェック → 行追加
→ インデックス更新
SELECT FROM → 実行計画生成 → データ取得
→ フィルタ → ソート → 返却
UPDATE SET → 行ロック → 旧バージョン保持
→ 新値書き込み → ロック解放
DELETE FROM → 行ロック → 論理/物理削除
→ 領域管理 → ロック解放
└───────────────────────────────────────────┘
WAL(Write-Ahead Log) → ディスクへの永続化

比較表

DELETE vs TRUNCATE vs DROP 比較表

特徴 DELETE TRUNCATE DROP
操作種別 DML DDL DDL
WHERE句 使用可能 使用不可 使用不可
ロールバック 可能 DB依存(PG可、MySQL不可) DB依存
速度 遅い(行単位ログ) 高速(ページ単位) 高速
トリガー発火 する しない しない
自動連番リセット しない する N/A
領域解放 しない(VACUUM必要) する する
テーブル構造 残る 残る 消える
権限 DML権限 DDL権限 DDL権限
外部キー制約 制約チェックあり CASCADEが必要な場合あり CASCADE指定可

INSERT方式の比較表

方式 用途 速度 安全性 RDBMS
単一行INSERT 個別レコード追加 全DB
複数行INSERT バッチ挿入(〜1000行) 全DB
INSERT...SELECT データ移行/複製 全DB
COPY / LOAD DATA 大量データ投入(万〜億行) 最速 PG / MySQL
UPSERT 冪等な挿入/更新 全DB(構文異なる)
INSERT...RETURNING IDの即時取得 PostgreSQL
バルクINSERT (ORM) アプリ層での最適化 中〜高 全DB

UPDATE/DELETE方式の比較表

方式 用途 ロック影響 推奨場面
単純WHERE 少量行の更新/削除 日常操作
JOIN/FROM付き 関連テーブルに基づく更新 データ連携
サブクエリ付き 複雑な条件の更新 中〜大 条件が複合的
CTE付き 計算結果に基づく更新 集約値での更新
バッチ分割 大量行の更新/削除 小(分割) 本番大量更新
RETURNING付き 更新結果の即時確認 監査・ログ

アンチパターン

アンチパターン1: WHERE句なしのUPDATE/DELETE

-- NG: 全行が更新されてしまう!
UPDATE employees SET salary = 0;
-- → 全社員の給与が0になる
 
-- NG: 全行が削除されてしまう!
DELETE FROM employees;
-- → 全社員のデータが消える
 
-- OK: 必ずWHERE句で対象を限定
UPDATE employees SET salary = 500000 WHERE employee_id = 42;
 
-- 安全策1: 先にSELECTで確認
SELECT COUNT(*), MIN(salary), MAX(salary) FROM employees
WHERE department_id = 99;
-- → 5行、320000〜480000(妥当か確認)
 
DELETE FROM employees WHERE department_id = 99;
 
-- 安全策2: トランザクションで囲む
BEGIN;
    DELETE FROM employees WHERE department_id = 99;
    -- DELETE 5 ← 件数が期待通りか確認
    -- 問題なければ → COMMIT;
    -- 問題あれば → ROLLBACK;
COMMIT;
 
-- 安全策3: 本番DBではsafe_updateモード有効化(MySQL)
-- SET sql_safe_updates = 1;
-- → WHERE句のないUPDATE/DELETEがエラーになる

WHY: WHERE句のないUPDATEやDELETEは、データベースの全行に影響する。本番環境での「うっかり全件削除」は、企業にとって致命的な損失になりうる。この問題は「プログラマーの注意力」ではなく「仕組み」で防ぐべきである。

アンチパターン2: SELECT * の濫用

-- NG: 不要な列まで全て取得
SELECT * FROM orders;
 
-- 問題点:
-- 1. ネットワーク帯域の浪費(BLOB/TEXT列がある場合は特に深刻)
--    例: 100万行 × 不要なBLOB列 = 数GBのネットワーク転送
-- 2. テーブル構造変更時にアプリケーションが壊れる
--    例: 列の追加/削除/順序変更でORMのマッピングが崩れる
-- 3. インデックスオンリースキャン(カバリングインデックス)が効かない
--    例: CREATE INDEX ON orders(status, customer_id)があっても
--         SELECT * では全行ヒープアクセスが必要
-- 4. 実行計画が非効率になる
--    不要な列のデシリアライズ、メモリ消費、ソートコスト増大
 
-- OK: 必要な列だけ明示的に指定
SELECT order_id, customer_id, total_amount, status
FROM orders
WHERE status = 'pending';
 
-- 例外: 対話的な探索やデバッグ時にはSELECT *は許容される
-- ただしLIMITを付ける
SELECT * FROM orders LIMIT 10;

WHY: SELECT *は「必要十分なデータだけを取得する」というデータベース操作の基本原則に反する。特に大規模テーブルでは、不要な列の転送コストが積算されてパフォーマンスに深刻な影響を与える。

アンチパターン3: 文字列連結でSQLを組み立てる

-- NG: SQLインジェクションの脆弱性
-- Python疑似コード
-- query = f"SELECT * FROM users WHERE name = '{user_input}'"
-- user_input = "'; DROP TABLE users; --"
-- → SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
 
-- OK: パラメータバインド(プリペアドステートメント)
-- Python (psycopg2)
-- cursor.execute("SELECT * FROM users WHERE name = %s", (user_input,))
-- Java (JDBC)
-- PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
-- ps.setString(1, userInput);
-- Node.js (pg)
-- client.query('SELECT * FROM users WHERE name = $1', [userInput])
 
-- PostgreSQLでのプリペアドステートメント
PREPARE get_user (TEXT) AS
SELECT * FROM users WHERE name = $1;
 
EXECUTE get_user('田中太郎');
 
DEALLOCATE get_user;

WHY: SQLインジェクションは最も古くから知られ、最も被害の大きいWebアプリケーションの脆弱性の一つ。パラメータバインドを使えば、ユーザー入力がSQL構文として解釈されることを防止できる。


実践演習

演習1(基礎): 従業員データの基本CRUD

以下のテーブル定義に対して、指定されたCRUD操作をSQLで記述せよ。

CREATE TABLE departments (
    id   SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE
);
 
CREATE TABLE employees (
    id            SERIAL PRIMARY KEY,
    name          VARCHAR(100) NOT NULL,
    email         VARCHAR(255) NOT NULL UNIQUE,
    department_id INTEGER REFERENCES departments(id),
    salary        DECIMAL(10, 2) NOT NULL CHECK (salary >= 0),
    status        VARCHAR(20) DEFAULT 'active',
    hired_date    DATE NOT NULL DEFAULT CURRENT_DATE
);
  1. departments テーブルに「営業」「開発」「人事」を挿入せよ
  2. employees テーブルに5名の社員を挿入せよ(複数行INSERT使用)
  3. 開発部門の社員のみを給与の高い順に取得せよ
  4. 営業部門の全社員の給与を5%昇給させよ
  5. 「退職」status の社員を論理削除(status='deleted', deleted_atを設定)せよ
模範解答
-- 1. departments テーブルへの挿入
INSERT INTO departments (name) VALUES ('営業'), ('開発'), ('人事');
 
-- 2. employees テーブルへの5名挿入
INSERT INTO employees (name, email, department_id, salary, status, hired_date) VALUES
    ('田中太郎', 'tanaka@example.com', 1, 450000, 'active', '2020-04-01'),
    ('鈴木花子', 'suzuki@example.com', 2, 520000, 'active', '2019-07-15'),
    ('佐藤次郎', 'sato@example.com',   1, 380000, 'active', '2021-01-10'),
    ('高橋三郎', 'takahashi@example.com', 2, 600000, 'active', '2018-04-01'),
    ('山田四郎', 'yamada@example.com', 3, 420000, 'active', '2022-04-01');
 
-- 3. 開発部門の社員を給与順で取得
SELECT e.id, e.name, e.salary
FROM employees e
    INNER JOIN departments d ON e.department_id = d.id
WHERE d.name = '開発'
ORDER BY e.salary DESC;
 
-- 4. 営業部門の全社員を5%昇給
UPDATE employees
SET salary = salary * 1.05
WHERE department_id = (SELECT id FROM departments WHERE name = '営業');
-- または
UPDATE employees e
SET salary = salary * 1.05
FROM departments d
WHERE e.department_id = d.id AND d.name = '営業';
 
-- 5. 退職者の論理削除
-- まずdeleted_at列を追加(既存テーブルの変更)
ALTER TABLE employees ADD COLUMN deleted_at TIMESTAMP;
 
UPDATE employees
SET status = 'deleted',
    deleted_at = CURRENT_TIMESTAMP
WHERE status = 'retired';

演習2(応用): 安全な大量データ操作

以下のシナリオでSQLを記述せよ。全ての操作はトランザクション内で安全に実行すること。

  1. productsテーブルで、在庫数が0の商品の価格を10%値下げし、結果をRETURNINGで確認せよ
  2. ordersテーブルで、2023年より前に作成され、statusが'completed'の注文をアーカイブテーブルに移動(INSERT...SELECT → DELETE)せよ
  3. user_sessionsテーブルで、24時間以上前に期限切れのセッションを削除する「バッチ削除」を記述せよ(1回の削除は5000件以内)
-- テーブル定義
CREATE TABLE products (
    id    SERIAL PRIMARY KEY,
    name  VARCHAR(100) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INTEGER NOT NULL DEFAULT 0
);
 
CREATE TABLE orders (
    id         SERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    total      DECIMAL(10, 2),
    status     VARCHAR(20),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
 
CREATE TABLE orders_archive (LIKE orders INCLUDING ALL);
 
CREATE TABLE user_sessions (
    id         UUID PRIMARY KEY,
    user_id    INTEGER NOT NULL,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
模範解答
-- 1. 在庫切れ商品の10%値下げ(RETURNING付き)
BEGIN;
    -- 対象確認
    SELECT id, name, price, stock FROM products WHERE stock = 0;
 
    -- 値下げ実行
    UPDATE products
    SET price = ROUND(price * 0.9, 2)
    WHERE stock = 0
    RETURNING id, name, price AS new_price;
COMMIT;
 
-- 2. 古い完了注文のアーカイブ移動
BEGIN;
    -- Step 1: アーカイブテーブルへコピー
    INSERT INTO orders_archive (id, customer_id, total, status, created_at)
    SELECT id, customer_id, total, status, created_at
    FROM orders
    WHERE created_at < '2023-01-01'
      AND status = 'completed';
    -- → INSERT 0 12345
 
    -- Step 2: コピー完了を確認してから元テーブルから削除
    DELETE FROM orders
    WHERE created_at < '2023-01-01'
      AND status = 'completed';
    -- → DELETE 12345(件数がINSERTと一致することを確認)
 
    -- Step 3: 確認
    SELECT COUNT(*) FROM orders_archive WHERE created_at < '2023-01-01';
    -- → 12345
COMMIT;
 
-- 3. 期限切れセッションのバッチ削除
DO $$
DECLARE
    batch_size CONSTANT INTEGER := 5000;
    deleted_count INTEGER;
    total_deleted INTEGER := 0;
BEGIN
    LOOP
        DELETE FROM user_sessions
        WHERE id IN (
            SELECT id FROM user_sessions
            WHERE expires_at < CURRENT_TIMESTAMP - INTERVAL '24 hours'
            LIMIT batch_size
            FOR UPDATE SKIP LOCKED  -- ロック競合を回避
        );
        GET DIAGNOSTICS deleted_count = ROW_COUNT;
        total_deleted := total_deleted + deleted_count;
 
        -- 進捗ログ(RAISE NOTICEはPostgreSQL固有)
        RAISE NOTICE 'Deleted % sessions (total: %)', deleted_count, total_deleted;
 
        EXIT WHEN deleted_count < batch_size;  -- 残りがバッチサイズ未満なら終了
 
        -- 他のトランザクションに実行機会を与える
        PERFORM pg_sleep(0.1);
    END LOOP;
 
    RAISE NOTICE 'Total deleted: % sessions', total_deleted;
END $$;

ポイント:

  • FOR UPDATE SKIP LOCKEDで他トランザクションがロック中の行をスキップ(デッドロック防止)
  • バッチサイズを制限することでロック時間を短縮
  • PERFORM pg_sleep(0.1)で他トランザクションに実行機会を与える

演習3(発展): UPSERTとRETURNINGの組み合わせ

ECサイトのカート機能を実装せよ。以下の要件を満たすSQLを記述すること。

要件:

  1. カートに商品を追加する際、既に同じ商品があれば数量を加算する(UPSERT)
  2. カートの内容を商品名・価格付きで取得する
  3. カートの合計金額を計算する
  4. カートから特定商品を削除し、削除した内容をRETURNINGで返す
  5. カートの有効期限チェック(30日以上前のカートを自動削除)
CREATE TABLE cart_items (
    id         SERIAL PRIMARY KEY,
    user_id    INTEGER NOT NULL,
    product_id INTEGER NOT NULL REFERENCES products(id),
    quantity   INTEGER NOT NULL CHECK (quantity > 0),
    added_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE (user_id, product_id)
);
模範解答
-- 1. カートに商品を追加(UPSERT: 既存なら数量加算)
INSERT INTO cart_items (user_id, product_id, quantity)
VALUES (42, 101, 2)
ON CONFLICT (user_id, product_id)
DO UPDATE SET
    quantity = cart_items.quantity + EXCLUDED.quantity,
    updated_at = CURRENT_TIMESTAMP
RETURNING id, product_id, quantity AS total_quantity;
-- → 既にquantity=3だった場合: total_quantity=5
 
-- 2. カートの内容を商品情報付きで取得
SELECT
    ci.id AS cart_item_id,
    p.name AS product_name,
    p.price AS unit_price,
    ci.quantity,
    p.price * ci.quantity AS subtotal,
    ci.added_at
FROM cart_items ci
    INNER JOIN products p ON ci.product_id = p.id
WHERE ci.user_id = 42
ORDER BY ci.added_at;
 
-- 3. カートの合計金額
SELECT
    ci.user_id,
    COUNT(*) AS item_count,
    SUM(ci.quantity) AS total_quantity,
    SUM(p.price * ci.quantity) AS total_amount
FROM cart_items ci
    INNER JOIN products p ON ci.product_id = p.id
WHERE ci.user_id = 42
GROUP BY ci.user_id;
 
-- 4. カートから特定商品を削除(RETURNING付き)
DELETE FROM cart_items
WHERE user_id = 42 AND product_id = 101
RETURNING id, product_id, quantity;
-- → id=7, product_id=101, quantity=5
 
-- 5. 期限切れカートの自動削除
WITH expired_carts AS (
    DELETE FROM cart_items
    WHERE updated_at < CURRENT_TIMESTAMP - INTERVAL '30 days'
    RETURNING user_id, product_id, quantity
)
SELECT
    user_id,
    COUNT(*) AS items_removed,
    SUM(quantity) AS total_quantity_removed
FROM expired_carts
GROUP BY user_id;
-- → user_id=15, items_removed=3, total_quantity_removed=8
-- → user_id=23, items_removed=1, total_quantity_removed=2
 
-- ボーナス: カート内容の一括更新(数量変更)
UPDATE cart_items
SET quantity = new_data.quantity,
    updated_at = CURRENT_TIMESTAMP
FROM (VALUES
    (42, 101, 3),  -- user_id=42, product_id=101, new_quantity=3
    (42, 102, 1),  -- user_id=42, product_id=102, new_quantity=1
    (42, 103, 5)   -- user_id=42, product_id=103, new_quantity=5
) AS new_data (user_id, product_id, quantity)
WHERE cart_items.user_id = new_data.user_id
  AND cart_items.product_id = new_data.product_id
RETURNING cart_items.product_id, cart_items.quantity;

設計のポイント:

  • UPSERTにより「カートに追加」が冪等操作になる(同じリクエストを何度実行しても安全)
  • RETURNING句により追加のSELECTクエリが不要(ラウンドトリップ削減)
  • CTEとDELETE...RETURNINGの組み合わせで削除と集計を1クエリで実行

トラブルシューティング

よくあるエラーと解決策

エラー 原因 解決策
初期化エラー 設定ファイルの不備 設定ファイルのパスと形式を確認
タイムアウト ネットワーク遅延/リソース不足 タイムアウト値の調整、リトライ処理の追加
メモリ不足 データ量の増大 バッチ処理の導入、ページネーションの実装
権限エラー アクセス権限の不足 実行ユーザーの権限確認、設定の見直し
データ不整合 並行処理の競合 ロック機構の導入、トランザクション管理

デバッグの手順

  1. エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
  2. 再現手順の確立: 最小限のコードでエラーを再現する
  3. 仮説の立案: 考えられる原因をリストアップする
  4. 段階的な検証: ログ出力やデバッガを使って仮説を検証する
  5. 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
 
# ロガーの設定
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
 
def debug_decorator(func):
    """関数の入出力をログ出力するデコレータ"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
        try:
            result = func(*args, **kwargs)
            logger.debug(f"戻り値: {func.__name__} -> {result}")
            return result
        except Exception as e:
            logger.error(f"例外発生: {func.__name__}: {e}")
            logger.error(traceback.format_exc())
            raise
    return wrapper
 
@debug_decorator
def process_data(items):
    """データ処理(デバッグ対象)"""
    if not items:
        raise ValueError("空のデータ")
    return [item * 2 for item in items]

パフォーマンス問題の診断

パフォーマンス問題が発生した場合の診断手順:

  1. ボトルネックの特定: プロファイリングツールで計測
  2. メモリ使用量の確認: メモリリークの有無をチェック
  3. I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
  4. 同時接続数の確認: コネクションプールの状態を確認
問題の種類 診断ツール 対策
CPU負荷 cProfile, py-spy アルゴリズム改善、並列化
メモリリーク tracemalloc, objgraph 参照の適切な解放
I/Oボトルネック strace, iostat 非同期I/O、キャッシュ
DB遅延 EXPLAIN, slow query log インデックス、クエリ最適化

設計判断ガイド

選択基準マトリクス

技術選択を行う際の判断基準を以下にまとめます。

判断基準 重視する場合 妥協できる場合
パフォーマンス リアルタイム処理、大規模データ 管理画面、バッチ処理
保守性 長期運用、チーム開発 プロトタイプ、短期プロジェクト
スケーラビリティ 成長が見込まれるサービス 社内ツール、固定ユーザー
セキュリティ 個人情報、金融データ 公開データ、社内利用
開発速度 MVP、市場投入スピード 品質重視、ミッションクリティカル

アーキテクチャパターンの選択

アーキテクチャ選択フロー
① チーム規模は?
├─ 小規模(1-5人)→ モノリス
└─ 大規模(10人+)→ ②へ
② デプロイ頻度は?
├─ 週1回以下 → モノリス + モジュール分割
└─ 毎日/複数回 → ③へ
③ チーム間の独立性は?
├─ 高い → マイクロサービス
└─ 中程度 → モジュラーモノリス

トレードオフの分析

技術的な判断には必ずトレードオフが伴います。以下の観点で分析を行いましょう:

1. 短期 vs 長期のコスト

  • 短期的に速い方法が長期的には技術的負債になることがある
  • 逆に、過剰な設計は短期的なコストが高く、プロジェクトの遅延を招く

2. 一貫性 vs 柔軟性

  • 統一された技術スタックは学習コストが低い
  • 多様な技術の採用は適材適所が可能だが、運用コストが増加

3. 抽象化のレベル

  • 高い抽象化は再利用性が高いが、デバッグが困難になる場合がある
  • 低い抽象化は直感的だが、コードの重複が発生しやすい
# 設計判断の記録テンプレート
class ArchitectureDecisionRecord:
    """ADR (Architecture Decision Record) の作成"""
 
    def __init__(self, title: str):
        self.title = title
        self.context = ""
        self.decision = ""
        self.consequences = []
        self.alternatives = []
 
    def set_context(self, context: str):
        """背景と課題の記述"""
        self.context = context
        return self
 
    def set_decision(self, decision: str):
        """決定内容の記述"""
        self.decision = decision
        return self
 
    def add_consequence(self, consequence: str, positive: bool = True):
        """結果の追加"""
        self.consequences.append({
            'description': consequence,
            'type': 'positive' if positive else 'negative'
        })
        return self
 
    def add_alternative(self, name: str, reason_rejected: str):
        """却下した代替案の追加"""
        self.alternatives.append({
            'name': name,
            'reason_rejected': reason_rejected
        })
        return self
 
    def to_markdown(self) -> str:
        """Markdown形式で出力"""
        md = f"# ADR: {self.title}\n\n"
        md += f"## 背景\n{self.context}\n\n"
        md += f"## 決定\n{self.decision}\n\n"
        md += "## 結果\n"
        for c in self.consequences:
            icon = "✅" if c['type'] == 'positive' else "⚠️"
            md += f"- {icon} {c['description']}\n"
        md += "\n## 却下した代替案\n"
        for a in self.alternatives:
            md += f"- **{a['name']}**: {a['reason_rejected']}\n"
        return md

FAQ

Q1: UPDATE文で「変更前の値」を参照できるか?

PostgreSQLではRETURNING句で更新後の値を取得できるが、更新前の値は直接取得できない。更新前の値が必要な場合はCTEを使う:

WITH old AS (
    SELECT id, salary FROM employees WHERE id = 42
)
UPDATE employees SET salary = salary * 1.1 WHERE id = 42
RETURNING id,
    (SELECT salary FROM old) AS old_salary,
    salary AS new_salary;
-- → id=42, old_salary=450000, new_salary=495000

別の方法として、監査トリガーを設定する:

CREATE TABLE salary_audit (
    id          SERIAL PRIMARY KEY,
    employee_id INTEGER,
    old_salary  DECIMAL(10, 2),
    new_salary  DECIMAL(10, 2),
    changed_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
 
CREATE OR REPLACE FUNCTION log_salary_change()
RETURNS TRIGGER AS $$
BEGIN
    IF OLD.salary <> NEW.salary THEN
        INSERT INTO salary_audit (employee_id, old_salary, new_salary)
        VALUES (OLD.id, OLD.salary, NEW.salary);
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
 
CREATE TRIGGER trg_salary_audit
    AFTER UPDATE ON employees
    FOR EACH ROW
    EXECUTE FUNCTION log_salary_change();

Q2: 大量データのINSERTを高速化するには?

速度目安と推奨方法:

方法 速度目安(100万行) 推奨場面
個別INSERT 数十分 使用しない
複数行INSERT(1000行/バッチ) 数分 小〜中規模
COPY文(PostgreSQL) 数十秒 大規模データ投入
LOAD DATA(MySQL) 数十秒 大規模データ投入
インデックス無効化 + COPY + 再作成 数秒〜十数秒 初期データ投入
-- PostgreSQL: COPY文(最速)
COPY employees (name, department_id, salary, hired_date)
FROM '/path/to/data.csv'
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',');
 
-- さらに高速化する場合
BEGIN;
    -- 1. インデックスを一時的に無効化
    ALTER TABLE employees DISABLE TRIGGER ALL;
    DROP INDEX IF EXISTS idx_employees_dept;
 
    -- 2. COPY実行
    COPY employees FROM '/path/to/data.csv' WITH (FORMAT CSV, HEADER);
 
    -- 3. インデックスを再作成
    CREATE INDEX idx_employees_dept ON employees(department_id);
    ALTER TABLE employees ENABLE TRIGGER ALL;
 
    -- 4. 統計情報の更新
    ANALYZE employees;
COMMIT;

Q3: 論理削除と物理削除のどちらを使うべきか?

判断基準:

条件 推奨
法規制で保存義務がある 論理削除
ユーザーに「元に戻す」機能を提供 論理削除
監査証跡が必要 論理削除
データ量が多くストレージ制約がある 物理削除 + アーカイブ
GDPR等で「忘れられる権利」に対応 物理削除(匿名化後)
テーブル設計をシンプルに保ちたい 物理削除

実務では論理削除 + 定期的なアーカイブ/パージが最もバランスの良い選択である。論理削除したデータは一定期間後にアーカイブテーブルに移動し、さらに一定期間後に物理削除する。

Q4: RETURNING句はどのRDBMSで使えるか?

RDBMS RETURNING対応 代替手段
PostgreSQL INSERT/UPDATE/DELETE全対応 -
SQLite INSERT/UPDATE/DELETE全対応 (3.35.0+) -
MySQL なし LAST_INSERT_ID() / SELECT
SQL Server OUTPUT句(同等機能) SCOPE_IDENTITY()
Oracle RETURNING INTO(PL/SQL内で使用) シーケンス.CURRVAL

Q5: 1つのINSERT文で挿入できる最大行数は?

RDBMS 最大行数 推奨バッチサイズ
PostgreSQL 制限なし(メモリ依存) 1,000〜10,000行
MySQL 制限なし(max_allowed_packet依存) 1,000行程度
SQLite 500行(コンパイル時設定) 500行
SQL Server 1,000行(リテラル値の場合) 1,000行

まとめ

操作 SQL文 要注意点
CREATE INSERT INTO ... VALUES 制約違反、重複キー、バッチサイズ
READ SELECT ... FROM ... WHERE 実行順序理解、SELECT *回避、インデックス活用
UPDATE UPDATE ... SET ... WHERE WHERE句忘れ防止、トランザクション使用
DELETE DELETE FROM ... WHERE WHERE句忘れ防止、論理/物理削除の選択
UPSERT ON CONFLICT DO UPDATE 方言差が大きい、冪等性の設計
RETURNING INSERT/UPDATE/DELETE ... RETURNING PostgreSQL/SQLiteで使用可能
安全策 BEGIN → 確認 → COMMIT/ROLLBACK 本番操作は必ずトランザクション
パラメータバインド $1 / ? / %s SQLインジェクション防止の必須手段

次に読むべきガイド


参考文献

  1. PostgreSQL Documentation — "Data Manipulation" https://www.postgresql.org/docs/current/dml.html
  2. PostgreSQL Documentation — "INSERT" https://www.postgresql.org/docs/current/sql-insert.html
  3. MySQL Reference Manual — "Data Manipulation Statements" https://dev.mysql.com/doc/refman/8.0/en/sql-data-manipulation-statements.html
  4. Karwin, B. (2010). SQL Antipatterns: Avoiding the Pitfalls of Database Programming. Pragmatic Bookshelf.
  5. Winand, M. (2012). SQL Performance Explained. Markus Winand. https://use-the-index-luke.com/
  6. OWASP — "SQL Injection Prevention Cheat Sheet" https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html