基礎LESSON 02

この教材で学ぶこと 結合・集約・サブクエリを使うSQLの基本を固めます。

SQL 実践 + 応用情報午後対策

対象読者: ORMの利用経験を、生SQLの理解とデータベース問題の解決へつなげたい人。


1. Room の知識を生 SQL に対応させる

// Room(Kotlin)でやっていたこと
@Query("SELECT * FROM books WHERE status = :status ORDER BY updatedAt DESC")
fun getBooksByStatus(status: String): Flow<List<BookEntity>>

// 生 SQL で書くと
SELECT * FROM books 
WHERE status = 'reading' 
ORDER BY updated_at DESC;

2. 応用情報頻出の SQL パターン

GROUP BY + 集計関数

-- 各ステータスの書籍数と平均評価を取得
SELECT 
    status,
    COUNT(*)        AS book_count,
    AVG(rating)     AS avg_rating,
    MAX(updated_at) AS last_updated
FROM books
GROUP BY status
HAVING COUNT(*) >= 5  -- HAVING: GROUP BY後のフィルタ(WHERE とは違う)
ORDER BY book_count DESC;

JOIN(応用情報で最頻出)

-- 本・棚・タグを扱うサンプルアプリの構造をSQLで表現
-- BookShelfCrossRef, BookTagCrossRef は中間テーブル

-- INNER JOIN: 両方に存在するもの
SELECT b.title, s.shelf_name
FROM books b
INNER JOIN book_shelf_cross_ref bsc ON b.id = bsc.book_id
INNER JOIN shelves s                ON bsc.shelf_id = s.id;

-- LEFT JOIN: 左テーブルは全件(棚未登録の本も表示)
SELECT b.title, s.shelf_name
FROM books b
LEFT JOIN book_shelf_cross_ref bsc ON b.id = bsc.book_id
LEFT JOIN shelves s                ON bsc.shelf_id = s.id;

-- 棚に登録されていない本を探す(NOT EXISTS)
SELECT b.title
FROM books b
WHERE NOT EXISTS (
    SELECT 1 FROM book_shelf_cross_ref bsc 
    WHERE bsc.book_id = b.id
);

サブクエリ

-- 平均評価より高い本の一覧
SELECT title, rating
FROM books
WHERE rating > (SELECT AVG(rating) FROM books)
ORDER BY rating DESC;

-- 各プラットフォームで最も最近更新された本
SELECT b.*
FROM books b
WHERE b.updated_at = (
    SELECT MAX(updated_at) 
    FROM books b2 
    WHERE b2.platform = b.platform
);

-- WITH句(CTE)で可読性を高める
WITH monthly_stats AS (
    SELECT 
        DATE_TRUNC('month', completed_at) AS month,
        COUNT(*) AS completed_count
    FROM books
    WHERE status = 'completed'
    GROUP BY DATE_TRUNC('month', completed_at)
)
SELECT month, completed_count,
       SUM(completed_count) OVER (ORDER BY month) AS cumulative  -- ウィンドウ関数
FROM monthly_stats;

3. トランザクションと整合性

-- ACID特性を保証するトランザクション
BEGIN;

-- 本の移動(棚Aから棚Bへ)
DELETE FROM book_shelf_cross_ref 
WHERE book_id = 1 AND shelf_id = 10;

INSERT INTO book_shelf_cross_ref (book_id, shelf_id) 
VALUES (1, 20);

-- エラーがなければコミット
COMMIT;
-- エラー時: ROLLBACK;

分離レベル(応用情報頻出)

分離レベル ダーティリード 反復不能読み取り ファントムリード
READ UNCOMMITTED 発生 発生 発生
READ COMMITTED 防止 発生 発生
REPEATABLE READ 防止 防止 発生(MySQLは防止)
SERIALIZABLE 防止 防止 防止
  • ダーティリード: コミット前のデータを読む
  • 反復不能読み取り: 同一トランザクション内で同じ行が変わる
  • ファントムリード: 同一トランザクション内で行数が変わる

4. インデックス設計

-- 通常のBツリーインデックス
CREATE INDEX idx_books_status ON books(status);
CREATE INDEX idx_books_platform_status ON books(platform, status);  -- 複合インデックス

-- 検索がどのインデックスを使っているか確認
EXPLAIN ANALYZE
SELECT * FROM books WHERE platform = 'kindle' AND status = 'reading';

-- インデックスを使わない書き方(注意)
SELECT * FROM books WHERE LOWER(title) = 'clean code';  -- ❌ 関数でラップするとインデックス不使用
SELECT * FROM books WHERE title LIKE '%clean%';          -- ❌ 前方一致でないとBツリーは使えない
SELECT * FROM books WHERE title LIKE 'Clean%';           -- ✅ 前方一致はBツリー使用可能

5. 正規化(応用情報必須)

第1〜第3正規形

【非正規形(問題あり)】
books: id, title, author1, author2, tag1, tag2, tag3  ← 繰り返しグループ

【第1正規形(繰り返しを排除)】
books: id, title
book_authors: book_id, author_name
book_tags: book_id, tag_name

【第2正規形(部分関数従属を排除)】
複合キー(book_id, tag_id) がある場合、
tag_nameがtag_idのみに従属 → tagsテーブルに分離

【第3正規形(推移的関数従属を排除)】
book_id → publisher_id → publisher_name の推移的従属
→ publishersテーブルに分離

6. 応用情報 午後 DB 問題の解き方

よく出るパターン

1. E-R図を読んでSQL を書く
   → リレーションシップを JOIN 条件に変換する練習

2. 依存関係を見てテーブル設計の問題点を指摘する
   → 正規化の第2・第3正規形の違いを即答できるようにする

3. トランザクションの問題(デッドロック等)
   → ロック順序を統一することで防止できると答えられるようにする

4. インデックスの効果を答える
   → ORDER BY, WHERE, JOIN の条件列にインデックスが効く

練習問題例

-- 問: 以下のテーブルで「2026年に完読した本の著者ごとの冊数を多い順に表示」するSQL

-- テーブル: books(id, title, status, completed_at)
--           book_authors(book_id, author_id)  
--           authors(id, name)

SELECT 
    a.name AS author_name,
    COUNT(*) AS completed_count
FROM books b
INNER JOIN book_authors ba ON b.id = ba.book_id
INNER JOIN authors a ON ba.author_id = a.id
WHERE b.status = 'completed'
  AND EXTRACT(YEAR FROM b.completed_at) = 2026
GROUP BY a.id, a.name
ORDER BY completed_count DESC;

7. PostgreSQL / MySQL 実践(RDSで使う)

-- PostgreSQL でよく使う機能
-- JSONB 型(半構造データ)
ALTER TABLE books ADD COLUMN metadata JSONB;
SELECT * FROM books WHERE metadata->>'isbn' = '9784873115658';
CREATE INDEX idx_metadata_isbn ON books ((metadata->>'isbn'));

-- 全文検索
CREATE INDEX idx_books_fts ON books USING gin(to_tsvector('japanese', title));
SELECT * FROM books WHERE to_tsvector('japanese', title) @@ to_tsquery('japanese', 'クリーン');

-- パーティショニング(大規模テーブル)
CREATE TABLE logs (
    id BIGSERIAL,
    created_at TIMESTAMPTZ NOT NULL,
    message TEXT
) PARTITION BY RANGE (created_at);

CREATE TABLE logs_2026 PARTITION OF logs
    FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');

🔗 次のステップ