この記事の要点
• SQL/JSON標準(JSON_TABLE等)の本格導入でJSONと関係モデルの両立が強化
• インクリメンタルバックアップでバックアップのコスト・時間を大幅圧縮
• VACUUMの高速化、I/O統計の刷新、BRINインデックス改善でエンジン効率化
PostgreSQL 17の概要
PostgreSQL 17は、JSONBのSQL/JSON標準準拠、インクリメンタルバックアップ、大幅なパフォーマンス改善を含むメジャーリリースです。オープンソースデータベースのデファクトスタンダードとして、エンタープライズから個人開発まで幅広く使われているPostgreSQLは、このリリースで「JSONと関係モデルの両立」「運用の省力化」「大規模データ処理」のすべてで前進しました。
背景 - なぜPostgreSQL 17が重要か
ここ数年のデータベーストレンドは、半構造化データ(JSON)と強い型システムの共存、クラウド時代のバックアップ/リストア戦略、そして解析系ワークロードの高速化に向かっています。PostgreSQL 17はまさにこのニーズに応えるリリースで、次の三本柱で進化しました。
- SQL/JSON標準準拠:
JSON_TABLE、JSON_VALUE、JSON_QUERYなどISO SQL標準の関数群を本格導入 - 運用の抜本改善:
pg_basebackupによるインクリメンタルバックアップでコスト・時間を圧縮 - エンジン効率化: VACUUMの高速化、I/O統計の刷新、BRINインデックスの改善
これにより「PostgreSQLならドキュメントDBと専用OLAPの代替も一定までカバーできる」という立ち位置がさらに強化されました。
SQL/JSON標準のサポート
JSON_TABLE
JSONデータをリレーショナルテーブルとして扱えます。
SELECT *
FROM JSON_TABLE(
'[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]',
'$[*]'
COLUMNS (
id INT PATH '$.id',
name TEXT PATH '$.name'
)
) AS users;
-- 結果:
-- id | name
-- ---+------
-- 1 | Alice
-- 2 | Bob
JSON_QUERY / JSON_VALUE
-- JSON_VALUE: スカラー値を取得
SELECT JSON_VALUE(data, '$.user.name') as name
FROM events;
-- JSON_QUERY: JSON値(配列・オブジェクト)を取得
SELECT JSON_QUERY(data, '$.user.tags' WITH WRAPPER) as tags
FROM events;
JSON_EXISTS
-- 条件に一致するJSONが存在するかチェック
SELECT *
FROM products
WHERE JSON_EXISTS(attributes, '$.colors[*] ? (@ == "red")');
ネストパスとエラーハンドリング
JSON_TABLEはネストしたパスにも対応しており、親子関係を1つのクエリで平坦化できます。
SELECT t.*
FROM orders o,
JSON_TABLE(
o.payload,
'$' COLUMNS (
order_id INT PATH '$.id',
customer TEXT PATH '$.customer.name',
NESTED PATH '$.items[*]' COLUMNS (
sku TEXT PATH '$.sku',
qty INT PATH '$.quantity',
price NUMERIC(10,2) PATH '$.price'
)
)
) AS t;
エラー時の振る舞いはON ERROR句で制御できます。
SELECT JSON_VALUE(
data,
'$.price'
RETURNING NUMERIC
DEFAULT 0 ON EMPTY
DEFAULT -1 ON ERROR
) FROM events;
インクリメンタルバックアップ
前回のバックアップからの変更分のみをバックアップできます。
# フルバックアップを取得
pg_basebackup -D /backup/base --checkpoint=fast
# インクリメンタルバックアップを取得
pg_basebackup -D /backup/incr1 \
--incremental=/backup/base/backup_manifest
# 復元時にマージ
pg_combinebackup /backup/base /backup/incr1 -o /restore
利点
| 項目 | フルバックアップ | インクリメンタル |
|---|---|---|
| 時間 | 長い | 短い |
| 容量 | 大きい | 小さい |
| 復元 | 簡単 | マージが必要 |
運用シナリオ例
日次でフル、時間ごとにインクリメンタルを取得する構成。
#!/usr/bin/env bash
set -euo pipefail
BASE=/backup/base
INCR_DIR=/backup/incr
DATE=$(date +%Y%m%d_%H%M)
if [ ! -d "$BASE" ]; then
pg_basebackup -D "$BASE" --checkpoint=fast --wal-method=stream
else
pg_basebackup -D "$INCR_DIR/$DATE" \
--incremental="$BASE/backup_manifest" \
--checkpoint=fast
fi
リストアは時系列順にマージ。
pg_combinebackup \
/backup/base \
/backup/incr/20251210_0100 \
/backup/incr/20251210_0200 \
-o /restore/target
パフォーマンス改善
VACUUMの高速化
PostgreSQL 16 vs 17:
- 大規模テーブルのVACUUM: 最大20倍高速化
- メモリ使用量: 最大20倍削減
PostgreSQL 17ではVACUUMの内部で、dead tupleのIDを保持するデータ構造がradix treeベースに刷新され、メモリ効率と走査性能が大きく改善しました。特に数億行を超えるテーブルの自動VACUUMで効果が顕著です。
I/O最適化
-- 新しいI/O統計
SELECT * FROM pg_stat_io;
-- 結果にはバックエンドタイプごとのI/O統計が含まれる
-- ワーカー種別ごとの読み書き傾向を可視化
SELECT backend_type, object, context,
reads, writes, extends, evictions
FROM pg_stat_io
ORDER BY writes DESC
LIMIT 10;
インデックスの改善
-- BRIN(Block Range Index)の改善
CREATE INDEX ON events USING BRIN (created_at)
WITH (pages_per_range = 32);
-- より効率的なマルチカラムBRIN
CREATE INDEX ON sensor_data USING BRIN (tenant_id, recorded_at);
B-treeも重複値の多い列での探索が改善され、IN句の大量展開や= ANY(array)がより高速になりました。
新しい機能
MERGE文の拡張
MERGE INTO inventory t
USING new_items s
ON t.product_id = s.product_id
WHEN MATCHED THEN
UPDATE SET quantity = t.quantity + s.quantity
WHEN NOT MATCHED THEN
INSERT (product_id, quantity)
VALUES (s.product_id, s.quantity)
RETURNING *; -- PostgreSQL 17で追加
WHEN NOT MATCHED BY SOURCEもサポートされ、削除系のアップサートが1文で書けるようになりました。
MERGE INTO inventory t
USING current_snapshot s ON t.product_id = s.product_id
WHEN MATCHED THEN
UPDATE SET quantity = s.quantity
WHEN NOT MATCHED BY TARGET THEN
INSERT (product_id, quantity) VALUES (s.product_id, s.quantity)
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
部分的な集計プッシュダウン
-- パーティションテーブルでの集計が高速化
SELECT date_trunc('month', created_at), COUNT(*)
FROM events -- パーティション化されたテーブル
GROUP BY 1;
新しいシステムビュー
-- 実行中のクエリの進捗
SELECT * FROM pg_stat_progress_copy;
SELECT * FROM pg_stat_progress_basebackup;
論理レプリケーションの改善
フェイルオーバースロット
-- スタンバイサーバーでレプリケーションスロットを同期
ALTER SUBSCRIPTION my_sub
SET (failover = true);
サブスクライバーでの競合解決
-- 競合発生時の動作を設定
ALTER SUBSCRIPTION my_sub
SET (disable_on_error = false);
pg_createsubscriberによる物理→論理変換
物理レプリカを論理レプリケーションのサブスクライバーへ変換できるツールが追加されました。これにより巨大DBのコピーを高速に作成し、移行・分割に活用できます。
pg_createsubscriber \
-D /var/lib/postgresql/17/replica \
-P "host=primary port=5432 dbname=app user=repl" \
-d app \
-n my_subscription
実践的なサンプルコード
Node.jsからpgドライバで利用
// db.ts
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30_000,
});
export async function upsertInventory(
items: Array<{ productId: number; quantity: number }>,
) {
const sql = `
MERGE INTO inventory t
USING (SELECT unnest($1::int[]) AS product_id,
unnest($2::int[]) AS quantity) s
ON t.product_id = s.product_id
WHEN MATCHED THEN
UPDATE SET quantity = t.quantity + s.quantity
WHEN NOT MATCHED THEN
INSERT (product_id, quantity) VALUES (s.product_id, s.quantity)
RETURNING *;
`;
const ids = items.map((i) => i.productId);
const qty = items.map((i) => i.quantity);
const { rows } = await pool.query(sql, [ids, qty]);
return rows;
}
JSON_TABLEとORMの併用
const result = await pool.query(`
SELECT j.order_id, j.customer, j.sku, j.qty
FROM orders o,
JSON_TABLE(o.payload, '$'
COLUMNS (
order_id INT PATH '$.id',
customer TEXT PATH '$.customer.name',
NESTED PATH '$.items[*]' COLUMNS (
sku TEXT PATH '$.sku',
qty INT PATH '$.quantity'
)
)
) j
WHERE o.created_at > $1
`, [since]);
バージョン比較表
| 機能 | PostgreSQL 15 | PostgreSQL 16 | PostgreSQL 17 |
|---|---|---|---|
| SQL/JSON標準関数 | 一部 | 一部 | 主要関数を完備 |
| インクリメンタルバックアップ | なし | なし | あり |
| MERGE RETURNING | 不可 | 不可 | 可 |
| pg_stat_io | なし | あり | 拡張 |
| VACUUM高速化 | 従来 | 改善 | 大幅改善 |
| 論理レプリフェイルオーバー | 手動 | 手動 | 宣言的 |
| 物理→論理変換 | 手動 | 手動 | pg_createsubscriber |
競合DBとの簡易比較
| 観点 | PostgreSQL 17 | MySQL 8.4 | MongoDB 7 |
|---|---|---|---|
| リレーショナル整合性 | 強力 | 強力 | 弱(スキーマレス) |
| JSON操作 | SQL/JSON標準 | JSON関数群 | ネイティブ |
| トランザクション | 強力 | 強力 | レプリカセットで対応 |
| 拡張性 | 拡張モジュール豊富 | 限定的 | プラグイン |
| ライセンス | PostgreSQLライセンス | GPL/商用 | SSPL |
ベストプラクティス
1. JSONBは「スキーマレス部分」に限定
頻繁にクエリ条件となるフィールドは通常の列に昇格し、GINインデックスを貼る対象を絞りましょう。
CREATE INDEX idx_events_payload_gin
ON events USING GIN (payload jsonb_path_ops);
2. autovacuum設定の見直し
PostgreSQL 17の高速VACUUMを活かすため、maintenance_work_memを十分に確保(例: 1GB以上)し、大規模テーブルにはautovacuum_vacuum_scale_factorを個別に設定。
ALTER TABLE events SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.01
);
3. バックアップは世代管理
インクリメンタルバックアップは世代管理を誤るとマージ不能になります。backup_manifestファイルを厳密に追跡し、保持ポリシーをスクリプトで制御しましょう。
4. EXPLAIN (ANALYZE, BUFFERS, WAL)の活用
PostgreSQL 17ではEXPLAIN出力がさらに詳細になり、pg_stat_ioと組み合わせることでI/Oボトルネックの特定が容易になります。
5. レプリケーションスロットの監視
SELECT slot_name, active, restart_lsn,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots;
注意点・落とし穴
- 拡張モジュール非互換: メジャーアップグレードでは拡張機能(pg_cron、TimescaleDBなど)の対応バージョンを先に確認
pg_basebackup --incrementalは9.5以前形式非対応:summarize_wal設定をONにしておく必要あり- MERGE文のデッドロック: 高並行のUPSERTではMERGEよりも
INSERT ... ON CONFLICTの方が安定する場合あり - JSON_TABLEのエラーハンドリング:
ON ERROR NULLを明示しないと想定外のエラーで失敗する - 論理レプリケーションのスキーマ変更: DDLは自動複製されないため、両側で適用順序を管理
導入・移行手順
# アップグレード前のチェック
pg_upgrade --check \
-b /usr/lib/postgresql/16/bin \
-B /usr/lib/postgresql/17/bin \
-d /var/lib/postgresql/16/main \
-D /var/lib/postgresql/17/main
# pg_upgradeでのアップグレード
pg_upgrade \
-b /usr/lib/postgresql/16/bin \
-B /usr/lib/postgresql/17/bin \
-d /var/lib/postgresql/16/main \
-D /var/lib/postgresql/17/main
推奨手順
- ステージング環境で
pg_upgrade --check - 拡張モジュールの17対応状況を確認(不足があればPR待ち or 代替検討)
- バックアップを取得(フル + WALアーカイブ)
- メンテナンスウィンドウで
pg_upgrade --link(高速) ANALYZEで統計情報を再生成pg_stat_statementsをリセットして新バージョンでの傾向をモニタ
-- アップグレード後に必ず実行
VACUUM ANALYZE;
パフォーマンス/ベンチマーク
公表されているワークロードから、PostgreSQL 17で改善が大きい領域は次のとおりです。
| ワークロード | PG16比 |
|---|---|
| 大規模テーブルのVACUUM | 最大20倍高速 |
| VACUUMのメモリ使用量 | 最大20倍削減 |
| パーティションGROUP BY | 大幅改善 |
| 高並行の書き込み | 若干改善 |
| WALスループット | 10-20%改善 |
実際の数値はハードウェアとワークロード次第のため、自環境でpgbenchやHammerDBを使った検証を推奨します。
pgbench -i -s 100 appdb
pgbench -c 32 -j 4 -T 300 -P 10 appdb
FAQ
Q1: PostgreSQL 17のインクリメンタルバックアップは、pgBackRestの代わりになりますか?
A: 基本機能は内蔵されましたが、世代管理・暗号化・遠隔保存・並列処理などの運用機能はpgBackRestやBarmanの方が豊富です。小規模ならpg_basebackupで十分、中〜大規模なら専用ツールの併用を推奨します。
Q2: JSONBから通常カラムに戻すべきタイミングは?
A: そのフィールドが(1) WHERE句で頻繁に使われる、(2) 型制約をかけたい、(3) 統計情報で選択率を正確に扱いたい、のいずれかに該当するなら通常カラムへ切り出しましょう。
Q3: VACUUM改善の恩恵を受けるには設定変更が必要?
A: 基本的には自動で効きますが、maintenance_work_memを大きめに設定するとより効果的です。従来の上限16MBを撤廃しているため、1GB程度まで割り当てて問題ありません。
Q4: MERGEとINSERT ... ON CONFLICT、どちらを使うべき?
A: 複数のアクション(挿入・更新・削除)を1文で扱いたい場合はMERGE、単純な重複回避UPSERTにはINSERT ... ON CONFLICTがシンプルで高速です。
Q5: 拡張機能の対応は?
A: 主要なものはPostgreSQL 17リリースから数週間〜数か月で対応が進みます。TimescaleDB、Citus、pgvectorなど主要拡張は対応済み。本番導入前に各拡張のChangelogを確認してください。
まとめ
PostgreSQL 17は、SQL/JSON標準への対応、インクリメンタルバックアップ、大幅なパフォーマンス改善により、エンタープライズでの利用がさらに容易になりました。特にJSONデータを扱うアプリケーションでは、新しいJSON関数が大きな恩恵をもたらします。
- 開発者にとっての嬉しさ: SQL/JSON標準関数でアプリ側のJSON操作が簡素化
- DBAにとっての嬉しさ: VACUUM/バックアップ/レプリの運用負荷が軽減
- 経営にとっての嬉しさ: 解析系とOLTPを同じDBで回せる範囲が広がり、TCO削減に寄与
バージョンアップはpg_upgradeで比較的簡単に実施できるので、ステージングで検証のうえ積極的に採用を検討しましょう。