ch12 — PostgreSQL 固有の文法と機能
スライドで見るこの章を AI と学ぶ
演習問題の詳細
この章の演習問題の内容を読めます。実際に手元で解くには教材リポジトリを clone してください。
ドリル 01 — `SERIAL` + `RETURNING` で 1 ステートメント採番
問題
notes テーブルに body を 3 件 INSERT し、INSERT 直後の id を取得して 1 件ずつ次のフォーマットで出力する。
inserted id: <id>
PostgreSQL では INSERT ... RETURNING id が使えるため INSERT と id 取得が 1 ステートメント で済む。MySQL / SQLite には RETURNING が無い (※) ので、INSERT → lastInsertId() の 2 段階になる。
※ MySQL 8 / SQLite 3.35 で
RETURNINGがサポートされたが、ポータビリティのため本ドリルではlastInsertId()で書く想定。
期待される出力
inserted id: 1
inserted id: 2
inserted id: 3
ヒント
- 採用ドライバは
getenv('DOJO_DB_DRIVER') ?: 'sqlite'で判定する - PostgreSQL:
RETURNING idの戻り値は$stmt->fetchColumn()で 1 つの値として取れる - SQLite / MySQL:
INSERT後に$pdo->lastInsertId()で取得する - ループで body を 3 回 INSERT すれば id は 1, 2, 3 と採番される
scripts/shared/db-connect.phpのdojo_db_connect()を使うと PDO を driver 非依存に取得できる
なぜ PostgreSQL の RETURNING が便利か
INSERT した行の (シーケンスで採番された) id や created_at などの DB 側で自動入力された値を 1 往復 で取得できる。MySQL の lastInsertId() は AUTO_INCREMENT カラム 1 つに限定だが、RETURNING は 任意のカラム を返せる:
INSERT INTO notes (body) VALUES ('hello')
RETURNING id, created_at, edit_token;
→ 「INSERT 後に SELECT で取り直す」が不要になる。
🧪 ブラウザで試す
スライドのコードを写経して ▶ 実行、自分で書いて 📝 採点 で答え合わせ。初回のみ PHP ランタイム読込で数秒かかります。
ドリル 02 — タグ配列を扱う (PostgreSQL `TEXT[]`)
問題
articles テーブルから タグに "php" を含む記事の title を id 順で出力する。
PHP入門
Laravel入門
タグの格納形式は driver で異なる:
| driver | カラム型 | 中身 |
|---|---|---|
| PostgreSQL | TEXT[] (配列型) |
ARRAY['php','tutorial','beginner'] |
| MySQL | JSON (配列型は無いので JSON で代替) |
'["php","tutorial","beginner"]' |
| SQLite | TEXT (JSON 文字列) |
'["php","tutorial","beginner"]' |
→ WHERE 句も driver で変わる。
| driver | WHERE 句 |
|---|---|
| PostgreSQL | 'php' = ANY(tags) |
| MySQL | JSON_CONTAINS(tags, '"php"') |
| SQLite | EXISTS (SELECT 1 FROM json_each(tags) WHERE value = 'php') |
期待される出力
PHP入門
Laravel入門
ヒント
getenv('DOJO_DB_DRIVER') ?: 'sqlite'で分岐- SQLite の
json_each(tags)は JSON 配列を行に展開するテーブル値関数 - PostgreSQL の
ANY(配列)は 配列内のどれかと一致 という意味 - 結果は
ORDER BY idで並べる fetchAll(PDO::FETCH_COLUMN)でtitle列だけ取り出すと echo が楽
なぜ配列型を持つ DB は便利か
タグや「複数選択値」を「中間テーブル + JOIN」せずに 1 カラム で表せる。
- 中間テーブル不要 → クエリがシンプル
- GIN インデックスを張れる →
WHERE 'php' = ANY(tags)が高速 - 配列演算子 (
@>,&&,||) で集合演算ができる
ただし「タグごとに属性 (created_by など) を持たせたい」なら中間テーブルが必要なので 使い分け が肝心。
🧪 ブラウザで試す
スライドのコードを写経して ▶ 実行、自分で書いて 📝 採点 で答え合わせ。初回のみ PHP ランタイム読込で数秒かかります。
ドリル 03 — JSON 内の値で検索する (PostgreSQL `JSONB`)
問題
products テーブルから spec.brand が 'acme' の商品を id 順で抽出し、name: brand の形式で出力する。
ノートPC: acme
マウス: acme
driver で JSON 抽出のシンタックスが異なる:
| driver | カラム型 | brand 抽出 |
|---|---|---|
| PostgreSQL | JSONB |
spec->>'brand' |
| MySQL | JSON |
JSON_UNQUOTE(JSON_EXTRACT(spec, '$.brand')) |
| SQLite | TEXT (JSON 文字列) |
json_extract(spec, '$.brand') |
期待される出力
ノートPC: acme
マウス: acme
ヒント
getenv('DOJO_DB_DRIVER') ?: 'sqlite'で分岐- PostgreSQL の
->は JSON のまま 返し、->>は テキスト で返す。比較したいときは->> - SQLite の
json_extract(col, '$.key')は組み込み関数 (3.38 以降はcol -> '$.key'でも OK) - MySQL の
JSON_EXTRACT()は二重引用符付き値を返すのでJSON_UNQUOTE()で剥がす - 出力フォーマット:
name: brand(コロンの後ろにスペース 1 つ) ORDER BY id
JSONB と JSON の違い (PostgreSQL)
| 観点 | JSON |
JSONB |
|---|---|---|
| 内部表現 | テキスト (入力そのまま) | バイナリ (正規化) |
| 入力時の空白 | 保持 | 削除 |
| キーの重複 | 保持 | 最後の値で上書き |
| 順序 | 保持 | 失われる |
| インデックス | 不可 | GIN インデックス可 |
| 検索性能 | 遅い (パース必須) | 速い |
| 推奨 | ログのような「原文保存」用途 | 検索・更新する JSON は JSONB |
→ 実務では基本的に JSONB を選ぶ。
なぜ JSON 型を使うか
「商品ごとに任意のスペックを持つ」ような スキーマレス な要件で、
- 中間テーブルを作るほどでもない
- カラムを増やすほど明確でもない
ようなときに JSONB が便利。ただし「検索する属性が決まったら」普通のカラムに切り出した方が高速。
🧪 ブラウザで試す
スライドのコードを写経して ▶ 実行、自分で書いて 📝 採点 で答え合わせ。初回のみ PHP ランタイム読込で数秒かかります。
演習問題(3問)
-
ドリル 01 — `SERIAL` + `RETURNING` で 1 ステートメント採番
-
ドリル 02 — タグ配列を扱う (PostgreSQL `TEXT[]`)
-
ドリル 03 — JSON 内の値で検索する (PostgreSQL `JSONB`)
サイト内で問題文・雛形・解答例を確認できます。実際に手元で解くには教材リポジトリを clone してください。
完了にすると進捗に反映されます