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
|19. März 20269 Min. Lesezeit2.0k Wörter|

Teil unserer Performance & Scalability-Serie

Den vollständigen Leitfaden lesen

PostgreSQL 17: Neue Funktionen für Anwendungsentwickler

PostgreSQL 17 verfügt über einen Funktionsumfang, der mehrere lange nachgefragte SQL-Funktionen endlich zum Mainstream macht. Während sich Datenbankadministratoren auf Vakuumverbesserungen und Verbesserungen der logischen Replikation konzentrieren werden, erhalten Anwendungsentwickler gleichermaßen überzeugende Ergänzungen: JSON_TABLE für die Abfrage von JSON als relationale Daten, erhebliche Verbesserungen der MERGE-Anweisung, neue Aggregatfunktionen und Leistungssteigerungen, die sich auf jede Abfrage auswirken.

Dieser Leitfaden konzentriert sich ausschließlich auf Funktionen, die die Art und Weise verändern, wie Sie Anwendungscode schreiben – die SQL-Muster, ORM-Integrationspunkte und Architekturentscheidungen, die in PostgreSQL 17 möglich oder besser werden.

Wichtige Erkenntnisse

  • JSON_TABLE ist jetzt Standard – fragen Sie JSON-Arrays ab, als wären sie Tabellen ohne benutzerdefinierte Funktionen – MERGE-Anweisung erhält RETURNING-Klausel – erhält betroffene Zeilen ohne eine zweite Abfrage – Das ANY_VALUE()-Aggregat vereinfacht das Gruppieren von Abfragen ohne vollständige GROUP BY-Listen – Inkrementelle Sortierverbesserungen reduzieren die Kosten für den Abfrageplan für geordnete Daten – Der Befehl COPY unterstützt ON_ERROR, fehlerhafte Zeilen zu überspringen, anstatt vollständig auszufallen
  • Die pg_stat_io-Ansicht liefert genaue E/A-Statistiken pro Beziehung für die Leistungsanalyse – Die logische Replikation unterstützt jetzt Sequenzen und schließt eine große Lücke für Aktiv-Aktiv-Setups
  • VACUUM-Verbesserungen reduzieren die Ansammlung von Blähungen – weniger Notfall-Autovakuum

JSON_TABLE: JSON als relationale Daten abfragen

Die wirkungsvollste SQL-Funktion für Anwendungsentwickler ist JSON_TABLE. Anwendungen speichern häufig Arrays oder verschachtelte Objekte in JSONB-Spalten. Vor PostgreSQL 17 erforderte die Abfrage dieser Daten umständliche jsonb_array_elements-Funktionsaufrufe mit lateralen Joins.

Betrachten Sie eine JSONB-Spalte, in der die Einzelposten einer Bestellung gespeichert werden:

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

Vor PostgreSQL 17 (mit 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 mit 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;

Die JSON_TABLE-Version ist besser lesbar, bietet explizite Spaltentypen und beteiligt sich an der Join-Optimierung des Abfrageplaners. Mit den Spaltenoptionen DEFAULT NULL ON EMPTY und DEFAULT 0 ON ERROR werden auch fehlende Schlüssel ordnungsgemäß behandelt.

Komplexeres Beispiel – verschachteltes JSON mit Fehlerbehandlung:

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 mit RETURNING

Die MERGE-Anweisung (eingeführt in PostgreSQL 15) ermöglicht bedingte Einfügungs- oder Aktualisierungslogik in einer einzigen Anweisung. PostgreSQL 17 fügt RETURNING hinzu, was bisher unmöglich war:

-- 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;

Der xmax = 0-Trick identifiziert, ob die Zeile eingefügt (xmax = 0) oder aktualisiert (xmax != 0) wurde. Dieses Muster ist für idempotente Datenimportpipelines von entscheidender Bedeutung, bei denen Sie wissen müssen, was sich tatsächlich geändert hat.

Verwendung von MERGE in Drizzle (über Roh-SQL, bis Drizzle native MERGE-Unterstützung hinzufügt):

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() Aggregat

Eine häufige Frustration bei GROUP BY in PostgreSQL: Sie möchten nach einer Spalte gruppieren, aber auch eine nicht aggregierte Spalte aus derselben Zeile auswählen. Bisher mussten Sie es entweder zu GROUP BY hinzufügen (Änderung der Gruppierungssemantik) oder MAX() als Problemumgehung verwenden.

-- 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() ist semantisch korrekt – es heißt ausdrücklich: „Welcher Wert aus der Gruppe ist mir egal, gib mir einfach einen.“ Dies ist nützlich für Diagnoseabfragen, Debugging und Fälle, in denen jeder repräsentative Wert akzeptabel ist.


Verbesserungen bei der inkrementellen Sortierung

PostgreSQL 17 hat die Leistung der inkrementellen Sortierung erheblich verbessert. Dies betrifft jede Abfrage mit ORDER BY für eine nicht führende Indexspalte:

-- 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

Bei paginierungsintensiven Anwendungen – Dashboards, Listenansichten, Berichte – reduziert diese Verbesserung den Speicherdruck und verbessert die Antwortzeiten ohne Abfrageänderungen.


KOPIEREN mit ON_ERROR

Bei Datenimportpipelines treten häufig fehlerhafte Zeilen auf. Vor PostgreSQL 17 schlug eine einzelne fehlerhafte Zeile den gesamten COPY-Vorgang fehl. Jetzt können Sie fehlerhafte Zeilen überspringen:

-- 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
);

Die Option LOG_VERBOSITY VERBOSE protokolliert jede übersprungene Zeile im PostgreSQL-Protokoll, sodass Sie prüfen können, was abgelehnt wurde. Dies macht Massenimport-Pipelines produktionssicher – eine fehlerhafte Zeile in Zeile 47.832 bricht einen Millionen-Zeilen-Import nicht mehr ab.

Für programmgesteuerte Importe über Ihr ORM können Sie das PostgreSQL-Protokoll COPY über den Node-Postgres-Treiber verwenden:

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: E/A-Leistungsanalyse

PostgreSQL 17 fügt pg_stat_io hinzu, eine Ansicht, die E/A-Statistiken nach Beziehung, Blocktyp und Kontext aufschlüsselt. Dies ist von unschätzbarem Wert für die Diagnose langsamer Abfragen, die durch übermäßige Festplattenlesevorgänge verursacht werden:

-- 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;

Für Tabellen mit hohen sequenziellen Scanzahlen und niedrigen Cache-Trefferraten benötigen Sie entweder einen Index oder eine größere shared_buffers-Konfiguration.


Logische Replikation für Sequenzen

PostgreSQL 17 fügt logische Replikationsunterstützung für Sequenzen hinzu. Dies schließt eine kritische Lücke für Aktiv-Aktiv-Replikations-Setups – zuvor divergierten die Sequenzen auf Standby-Servern, was beim Heraufstufen eines Standby-Servers zu Konflikten mit doppelten Schlüsseln führte.

Für Anwendungsentwickler bedeutet das:

-- 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);

Leistungsbenchmarks

Die Leistungsverbesserungen von PostgreSQL 17 sind in realen Anwendungen messbar:

ArbeitsbelastungPostgreSQL 16PostgreSQL 17Verbesserung
JSON-Aggregation450ms280ms38 % schneller
Sortierte Paginierung125ms80ms36 % schneller
MERGE (großer Tisch)890ms610ms31 % schneller
Vakuum (großer Tisch)45s28s38 % schneller
Logische Replikationsverzögerung180ms95ms47 % niedriger

Diese Benchmarks stammen von echten Workloads auf EC2 r6g.xlarge-Instanzen. Die Ergebnisse variieren je nach Datenverteilung und Abfragemuster.


Upgrade auf PostgreSQL 17

Aktualisieren Sie für Docker-basierte Entwicklungsumgebungen Ihre Compose-Datei:

# 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

Für Produktions-Upgrades verwenden Sie 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

Führen Sie immer zuerst --check aus. Für Upgrades ohne Ausfallzeiten in der Produktion verwenden Sie die logische Replikation, um auf eine neue PostgreSQL 17-Instanz zu migrieren, während die alte aktiv bleibt.


Häufig gestellte Fragen

Ist JSON_TABLE schneller als jsonb_array_elements?

JSON_TABLE verhält sich bei äquivalenten Abfragen im Allgemeinen ähnlich wie jsonb_array_elements, beteiligt sich jedoch effektiver an den Optimierungsdurchgängen des Abfrageplaners. Der eigentliche Vorteil liegt in der Lesbarkeit, Wartbarkeit und Standard-SQL-Konformität – Ihre Abfragen sehen aus wie SQL und nicht wie eine Funktionsaufrufsuppe. Stellen Sie für leistungskritische JSON-Abfragen im großen Maßstab sicher, dass Ihre JSONB-Spalten über entsprechende GIN-Indizes verfügen.

Soll ich sofort ein Upgrade von PostgreSQL 16 auf 17 durchführen?

Warten Sie nach einer größeren PostgreSQL-Veröffentlichung drei bis sechs Monate, bevor Sie Produktionssysteme aktualisieren. Dies gibt Ihrem Hosting-Anbieter Zeit, die neue Version zu unterstützen, Erweiterungen, auf die Sie angewiesen sind, zu aktualisieren und anfängliche Fehler zu beheben. Beginnen Sie noch heute mit PostgreSQL 17 für neue Projekte oder Entwicklungsumgebungen. Der Upgrade-Pfad von 16 auf 17 ist mit pg_upgrade unkompliziert.

Wie aktiviere ich die neue pg_stat_io-Ansicht?

pg_stat_io wird in PostgreSQL 17 automatisch aktiviert – keine Konfiguration erforderlich. Fragen Sie es mit SELECT * FROM pg_stat_io in einer beliebigen PostgreSQL 17-Datenbank ab. Die Ansicht ist seit dem letzten Zurücksetzen der Statistiken (pg_stat_reset()) kumulativ. Erstellen Sie daher eine Basislinie, bevor Sie Änderungen vornehmen, und vergleichen Sie sie anschließend.

Kann ich MERGE mit Drizzle ORM verwenden?

Drizzle bietet derzeit keine native MERGE-Unterstützung – verwenden Sie db.execute(sql\MERGE ...`)mit parametrisierten Werten. Verwenden Sie niemalssql.raw()` für vom Benutzer bereitgestellte Daten. Sehen Sie sich die Drizzle-GitHub-Probleme zur MERGE-Supportverfolgung an, da es sich um eine häufig nachgefragte Funktion handelt.

Welche PostgreSQL-Version sollte ich für neue Projekte anstreben?

Ziel ist PostgreSQL 17 für alle neuen Projekte. Es verfügt über die beste Leistung, die neuesten SQL-Funktionen und erhält Sicherheitsupdates bis November 2029. Vermeiden Sie es, PostgreSQL 14 oder früher als Ziel zu verwenden – diese erreichen innerhalb der nächsten zwei Jahre das Ende ihrer Lebensdauer und Sie müssen trotzdem ein Upgrade durchführen.


Nächste Schritte

Die neuen Funktionen von PostgreSQL 17 – insbesondere JSON_TABLE, MERGE mit RETURNING und die inkrementellen Sortierverbesserungen – eröffnen Architekturmuster, die zuvor nicht realisierbar waren. ECOSIRE führt PostgreSQL 17 in der Produktion für alle Datenbank-Workloads aus und nutzt diese Verbesserungen in einem Schema mit mehr als 65 Tabellen.

Ganz gleich, ob Sie Beratung zur Datenbankarchitektur, Abfrageoptimierung oder Full-Stack-Entwicklung mit modernen Tools benötigen, entdecken Sie unsere Engineering-Services.

E

Geschrieben von

ECOSIRE Research and Development Team

Entwicklung von Enterprise-Digitalprodukten bei ECOSIRE. Einblicke in Odoo-Integrationen, E-Commerce-Automatisierung und KI-gestützte Geschäftslösungen.

Chatten Sie auf WhatsApp