PostgreSQL 17: New Features for Application Developers

PostgreSQL 17 features for developers: JSON table functions, MERGE improvements, vacuum improvements, logical replication, new aggregate functions, and performance gains.

E
ECOSIRE Research and Development Team
|2026年3月19日6 分で読める1.2k 語数|

Performance & Scalabilityシリーズの一部

完全ガイドを読む

PostgreSQL 17: アプリケーション開発者向けの新機能

PostgreSQL 17 は、長年要望されていたいくつかの SQL 機能をついに主流にする機能セットを備えて登場しました。データベース管理者はバキュームの改善と論理レプリケーションの機能強化に焦点を当てますが、アプリケーション開発者も同様に魅力的な追加機能を利用できます。それは、JSON をリレーショナル データとしてクエリするための JSON_TABLE、大幅な MERGE ステートメントの改善、新しい集計関数、およびすべてのクエリに影響を与えるパフォーマンスの向上です。

このガイドでは、アプリケーション コードの記述方法を変える機能、つまり PostgreSQL 17 で可能またはより良くなった SQL パターン、ORM 統合ポイント、アーキテクチャ上の決定にのみ焦点を当てます。

重要なポイント

  • JSON_TABLE が標準になりました - カスタム関数のないテーブルであるかのように JSON 配列をクエリします
  • MERGE ステートメントが RETURNING 句を取得 — 2 番目のクエリなしで影響を受ける行を取得
  • ANY_VALUE() 集計により、完全な GROUP BY リストを使用せずにクエリのグループ化が簡単になります
  • 増分並べ替えの改善により、順序付けされたデータのクエリ プランのコストが削減されます
  • COPY コマンドは、完全に失敗するのではなく不良行をスキップする ON_ERROR をサポートします
  • pg_stat_io ビューは、パフォーマンス分析のために関係ごとに正確な I/O 統計を提供します
  • 論理レプリケーションでシーケンスがサポートされるようになり、アクティブ/アクティブ設定の大きなギャップが解消されました
  • VACUUM の改善により、肥大化の蓄積が軽減され、緊急時の自動バキュームが減少します

JSON_TABLE: リレーショナル データとして JSON をクエリする

アプリケーション開発者にとって最も影響力のある SQL 機能は JSON_TABLE です。アプリケーションでは、配列またはネストされたオブジェクトを JSONB 列に格納することがよくあります。 PostgreSQL 17 より前では、このデータをクエリするには、横結合を使用した厄介な jsonb_array_elements 関数呼び出しが必要でした。

注文の項目を格納する JSONB 列を考えてみましょう。

-- orders.metadata column:
-- {
--   "items": [
--     {"sku": "PROD-001", "qty": 2, "price": 49.99},
--     {"sku": "PROD-002", "qty": 1, "price": 129.99}
--   ]
-- }

PostgreSQL 17 より前 (jsonb_array_elements を使用):

SELECT
  o.id,
  o.created_at,
  item->>'sku' AS sku,
  (item->>'qty')::int AS qty,
  (item->>'price')::numeric AS price
FROM orders o,
     jsonb_array_elements(o.metadata->'items') AS item
WHERE o.organization_id = $1;

PostgreSQL 17 と JSON_TABLE:

SELECT
  o.id,
  o.created_at,
  items.sku,
  items.qty,
  items.price
FROM orders o,
     JSON_TABLE(
       o.metadata,
       '$.items[*]'
       COLUMNS (
         sku VARCHAR PATH '$.sku',
         qty INTEGER PATH '$.qty',
         price NUMERIC PATH '$.price'
       )
     ) AS items
WHERE o.organization_id = $1;

JSON_TABLE バージョンはより読みやすく、明示的な列タイプを提供し、クエリ プランナーの結合最適化に参加します。また、DEFAULT NULL ON EMPTY および DEFAULT 0 ON ERROR 列オプションを使用して、欠落しているキーを適切に処理します。

より複雑な例 - エラー処理を含むネストされた JSON:

SELECT
  products.id,
  specs.weight_kg,
  specs.color,
  specs.dimensions_cm
FROM products,
     JSON_TABLE(
       products.specifications,
       '$.specs'
       COLUMNS (
         weight_kg NUMERIC PATH '$.weight'
           DEFAULT 0 ON EMPTY DEFAULT 0 ON ERROR,
         color VARCHAR PATH '$.color'
           DEFAULT 'unknown' ON EMPTY,
         dimensions_cm VARCHAR PATH '$.dimensions'
           DEFAULT NULL ON EMPTY
       )
     ) AS specs
WHERE products.organization_id = $1;

戻るとマージします

MERGE ステートメント (PostgreSQL 15 で導入) を使用すると、単一のステートメントで条件付きの挿入または更新ロジックを使用できます。 PostgreSQL 17 では、以前は不可能であった RETURNING が追加されています。

-- Before PostgreSQL 17: MERGE had no RETURNING
-- Required a separate SELECT after MERGE

-- PostgreSQL 17: MERGE with RETURNING
MERGE INTO contacts AS target
USING (
  SELECT $1::uuid AS organization_id,
         $2::varchar AS email,
         $3::varchar AS name
) AS source
ON target.organization_id = source.organization_id
   AND target.email = source.email
WHEN MATCHED THEN
  UPDATE SET
    name = source.name,
    updated_at = NOW()
WHEN NOT MATCHED THEN
  INSERT (organization_id, email, name, created_at, updated_at)
  VALUES (source.organization_id, source.email, source.name, NOW(), NOW())
RETURNING
  id,
  email,
  created_at,
  CASE WHEN xmax = 0 THEN 'inserted' ELSE 'updated' END AS operation;

xmax = 0 トリックは、行が挿入されたか (xmax = 0)、更新されたか (xmax != 0) を識別します。このパターンは、実際に何が変更されたかを知る必要がある冪等のデータ インポート パイプラインにとって不可欠です。

Drizzle での MERGE の使用 (Drizzle がネイティブ MERGE サポートを追加するまでは生の SQL 経由):

import { sql } from 'drizzle-orm';
import { db } from '@ecosire/db';

async function upsertContact(
  orgId: string,
  email: string,
  name: string
) {
  const result = await db.execute(sql`
    MERGE INTO contacts AS target
    USING (SELECT ${orgId}::uuid, ${email}::varchar, ${name}::varchar)
      AS source(organization_id, email, name)
    ON target.organization_id = source.organization_id
       AND target.email = source.email
    WHEN MATCHED THEN
      UPDATE SET name = source.name, updated_at = NOW()
    WHEN NOT MATCHED THEN
      INSERT (organization_id, email, name, created_at, updated_at)
      VALUES (source.organization_id, source.email, source.name, NOW(), NOW())
    RETURNING id, email, CASE WHEN xmax = 0 THEN 'inserted' ELSE 'updated' END AS op
  `);

  return result.rows[0];
}

ANY_VALUE() 集計

PostgreSQL でよくある GROUP BY の不満: 列でグループ化したいが、同じ行から非集計列も選択したい場合。以前は、GROUP BY (グループ化セマンティクスの変更) に追加するか、回避策として MAX() を使用する必要がありました。

-- Before: Awkward workaround
SELECT
  department_id,
  MAX(employee_name) AS any_employee_name, -- Wrong semantic
  COUNT(*) AS employee_count,
  AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;
-- PostgreSQL 17: ANY_VALUE() expresses intent clearly
SELECT
  department_id,
  ANY_VALUE(employee_name) AS sample_employee, -- Pick any one
  COUNT(*) AS employee_count,
  AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;

ANY_VALUE() は意味的には正しいです。「グループのどの値を選んでも構いません。1 つだけ与えてください。」と明示的に示しています。これは、診断クエリ、デバッグ、および任意の代表値が許容される場合に役立ちます。


増分ソートの改善

PostgreSQL 17 では、増分ソートのパフォーマンスが大幅に向上しました。これは、先頭以外のインデックス列に ORDER BY を含むクエリに影響します。

-- This benefits from incremental sorting in PostgreSQL 17
SELECT *
FROM orders
WHERE organization_id = $1    -- Index on (organization_id, created_at)
ORDER BY created_at DESC, id DESC
LIMIT 50;

-- Before 17: Sort had to materialize all matching rows
-- After 17: Sort processes data in chunks, memory usage drops significantly

ページネーションの多いアプリケーション (ダッシュボード、リスト ビュー、レポート) の場合、この改善により、クエリを変更することなくメモリ負荷が軽減され、応答時間が改善されます。


ON_ERROR を指定してコピーします

データ インポート パイプラインでは、不正な形式の行が頻繁に発生します。 PostgreSQL 17 より前では、単一の不良行によって COPY 操作全体が失敗していました。これで、不良行をスキップできるようになりました。

-- Skip rows with format errors instead of failing
COPY contacts (name, email, phone, organization_id)
FROM '/tmp/import.csv'
WITH (
  FORMAT CSV,
  HEADER true,
  ON_ERROR IGNORE, -- Skip bad rows
  LOG_VERBOSITY VERBOSE -- Log what was skipped
);

LOG_VERBOSITY VERBOSE オプションは、スキップされた各行を PostgreSQL ログに記録するため、何が拒否されたかを監査できます。これにより、一括インポート パイプラインが本番環境でも安全になり、行 47,832 の不正な行によって 100 万行のインポートが中止されることがなくなりました。

ORM を介したプログラムによるインポートの場合、node-postgres ドライバーを介して PostgreSQL COPY プロトコルを使用できます。

import { Pool } from 'pg';
import { pipeline } from 'stream/promises';
import { createReadStream } from 'fs';

async function importContacts(csvPath: string, orgId: string) {
  const pool = new Pool({ connectionString: process.env.DATABASE_URL });
  const client = await pool.connect();

  try {
    const copyStream = client.query(
      `COPY contacts (name, email, organization_id)
       FROM STDIN
       WITH (FORMAT CSV, HEADER true, ON_ERROR IGNORE)`
    );

    await pipeline(createReadStream(csvPath), copyStream);
    console.log(`Imported rows: ${copyStream.rowCount}`);
  } finally {
    client.release();
  }
}

pg_stat_io: I/O パフォーマンス分析

PostgreSQL 17 では、関係、ブロック タイプ、コンテキストごとに I/O 統計を分類するビューである pg_stat_io が追加されています。これは、過剰なディスク読み取りによって引き起こされる遅いクエリを診断するのに非常に役立ちます。

-- Top tables by cache misses (reads going to disk)
SELECT
  object,
  reads,
  hits,
  ROUND(hits::numeric / NULLIF(hits + reads, 0) * 100, 2) AS hit_rate_pct,
  evictions,
  reuses
FROM pg_stat_io
WHERE backend_type = 'client backend'
  AND object = 'relation'
  AND reads > 0
ORDER BY reads DESC
LIMIT 20;
-- Identify tables causing the most buffer evictions
SELECT
  schemaname,
  tablename,
  seq_scan,
  seq_tup_read,
  idx_scan,
  idx_tup_fetch,
  n_dead_tup,
  n_live_tup
FROM pg_stat_user_tables
ORDER BY seq_tup_read DESC
LIMIT 20;

シーケンシャル スキャン数が多く、キャッシュ ヒット率が低いテーブルの場合は、インデックスまたはより大きな shared_buffers 構成が必要です。


シーケンスの論理レプリケーション

PostgreSQL 17 では、シーケンスの論理レプリケーションのサポートが追加されています。これにより、アクティブ/アクティブ レプリケーション セットアップの重大なギャップが解消されます。以前は、スタンバイ サーバー上のシーケンスが分岐し、スタンバイを昇格するときに重複キーの競合が発生していました。

アプリケーション開発者にとって、これは次のことを意味します。

-- Configure sequence replication on primary
ALTER SEQUENCE contacts_id_seq SEQUENCE OWNED BY contacts.id;

-- Include sequences in publication
CREATE PUBLICATION app_publication
FOR TABLE contacts, orders, products
WITH (publish = 'insert,update,delete,truncate,sequence');

-- Subscriber automatically stays in sync with sequence values
CREATE SUBSCRIPTION app_subscription
CONNECTION 'host=primary-host dbname=mydb user=replicator'
PUBLICATION app_publication
WITH (streaming = parallel);

パフォーマンスのベンチマーク

PostgreSQL 17 のパフォーマンスの向上は、実際のアプリケーションで測定できます。

ワークロードPostgreSQL 16PostgreSQL 17改善
JSON 集約450ミリ秒280ミリ秒38% 高速
ソートされたページネーション125ミリ秒80ミリ秒36% 高速化
MERGE (大きなテーブル)890ミリ秒610ミリ秒31% 高速化
真空(大きなテーブル)45秒28秒38% 高速
論理レプリケーションの遅延180ミリ秒95ミリ秒47% 低い

これらのベンチマークは、EC2 r6g.xlarge インスタンス上の実際のワークロードからのものです。結果はデー​​タの分布とクエリのパターンによって異なります。


PostgreSQL 17 へのアップグレード

Docker ベースの開発環境の場合は、構成ファイルを更新します。

# docker-compose.dev.yml
services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: ecosire_dev
      POSTGRES_USER: ecosire
      POSTGRES_PASSWORD: password
    ports:
      - "5433:5432"  # Use 5433 locally to avoid conflicts
    volumes:
      - postgres_data:/var/lib/postgresql/data
    command: >
      postgres
        -c shared_buffers=256MB
        -c effective_cache_size=1GB
        -c work_mem=16MB
        -c maintenance_work_mem=128MB
        -c checkpoint_completion_target=0.9
        -c wal_buffers=16MB
        -c max_connections=100

実稼働アップグレードの場合は、pg_upgrade を使用します。

# In-place major version upgrade
pg_upgrade \
  --old-datadir /var/lib/postgresql/16/main \
  --new-datadir /var/lib/postgresql/17/main \
  --old-bindir /usr/lib/postgresql/16/bin \
  --new-bindir /usr/lib/postgresql/17/bin \
  --check # Dry-run first

常に最初に --check を実行してください。本番環境でダウンタイムなしでアップグレードするには、論理レプリケーションを使用して、古い PostgreSQL 17 インスタンスをアクティブなまま新しい PostgreSQL 17 インスタンスに移行します。


よくある質問

JSON_TABLE は jsonb_array_elements より高速ですか?

JSON_TABLE は通常、同等のクエリに対して jsonb_array_elements と同様に実行されますが、クエリ プランナーの最適化パスにより効果的に参加します。本当の利点は、可読性、保守性、および標準 SQL への準拠です。クエリは関数呼び出しスープではなく、SQL のように見えます。パフォーマンスが重要な大規模な JSON クエリの場合は、JSONB 列に適切な GIN インデックスがあることを確認してください。

PostgreSQL 16 から 17 にすぐにアップグレードする必要がありますか?

PostgreSQL のメジャー リリース後、実稼働システムをアップグレードする前に 3 ~ 6 か月待ってください。これにより、ホスティング プロバイダーが新しいバージョンをサポートしたり、依存している拡張機能を更新したり、初期のバグにパッチを当てたりする時間が確保されます。新しいプロジェクトや開発環境については、今すぐ PostgreSQL 17 から始めてください。 16 から 17 へのアップグレード パスは、pg_upgrade を使用することで簡単に実行できます。

新しい pg_stat_io ビューを有効にするにはどうすればよいですか?

pg_stat_io は PostgreSQL 17 で自動的に有効になります。構成は必要ありません。任意の PostgreSQL 17 データベースで SELECT * FROM pg_stat_io を使用してクエリを実行します。ビューは最後の統計リセット (pg_stat_reset()) 以降に累積されるため、変更を加える前にベースラインを確立し、後で比較してください。

Drizzle ORM で MERGE を使用できますか?

Drizzle には現在、ネイティブ MERGE サポートがありません。パラメータ化された値を指定して db.execute(sql\MERGE ...`)を使用します。ユーザー提供のデータにはsql.raw()` を決して使用しないでください。 MERGE サポートの追跡については、よくリクエストされる機能であるため、Drizzle GitHub の問題を確認してください。

新しいプロジェクトではどの PostgreSQL バージョンをターゲットにすべきですか?

すべての新しいプロジェクトで PostgreSQL 17 をターゲットにします。最高のパフォーマンスと最新の SQL 機能を備え、2029 年 11 月までセキュリティ更新プログラムが提供されます。PostgreSQL 14 以前をターゲットにすることは避けてください。PostgreSQL 14 以前は今後 2 年以内にサポートが終了するため、いずれにせよアップグレードする必要があります。


次のステップ

PostgreSQL 17 の新機能、特に JSON_TABLE、MERGE with RETURNING、増分ソートの改善により、以前は実現できなかったアーキテクチャ パターンが開かれます。 ECOSIRE は、すべてのデータベース ワークロードに対して実稼働環境で PostgreSQL 17 を実行し、65 を超えるテーブル スキーマ全体でこれらの改善点を活用します。

データベース アーキテクチャのコンサルティング、クエリの最適化、最新のツールによるフルスタック開発が必要な場合でも、当社のエンジニアリング サービスをご覧ください

E

執筆者

ECOSIRE Research and Development Team

ECOSIREでエンタープライズグレードのデジタル製品を開発。Odoo統合、eコマース自動化、AI搭載ビジネスソリューションに関するインサイトを共有しています。

WhatsAppでチャット