pg_durable - Dauerhafte SQL-Funktionen für PostgreSQL
(github.com/microsoft)- Eine Erweiterung für durable functions, die Retries, Scheduling, paralleles Fan-out und bedingte Verzweigungen innerhalb von PostgreSQL allein mit einer kleinen SQL-DSL verarbeitet
- Läuft ohne Container oder externe Services nur mit Postgres und Background Workern
- Alle Schritte schreiben ihren Status als Checkpoints in PostgreSQL, sodass sie nach Crash, Neustart oder Verbindungsabbruch ab dem Unterbrechungspunkt fortgesetzt werden
- Statt Queue-Management, Statusverfolgung, Crash-Recovery, Schrittkoordination und Retries selbst zu implementieren, schreibt man nur SQL, den Rest übernimmt die Orchestration Engine
- Ersetzt Aufgaben, die bei Eigenimplementierung mehr als 300 Zeilen Boilerplate erfordern würden, durch einen einzelnen DSL-Aufruf und ist mit PostgreSQL 17 sofort als Open Source nutzbar
Überblick und zentraler Nutzen
- Eine in Postgres integrierte crash-proof durable function, die Retries, Scheduling, paralleles Fan-out und bedingte Verzweigungen mit einer kleinen SQL-DSL orchestriert
- Funktioniert ohne zusätzliche Infrastruktur nur mit Postgres + Background Worker, separate Container oder externe Services sind nicht nötig
- Übernimmt als Orchestration Engine Queue-Management, Statusverfolgung, Crash-Recovery, Schrittkoordination und Retries; der Nutzer schreibt nur SQL
Implementierung ohne pg_durable
- Wer 3 Aggregationen parallel ausführen, anschließend ein Dashboard aktualisieren und dabei auch Retries und Crash-Recovery einbauen will, braucht mehr als 300 Zeilen Boilerplate
- Was man selbst bauen muss: Queue-Setup und -Konfiguration, Worker-Management und Polling, Nachrichtenverarbeitung und Statusverfolgung, Fehlerbehandlung und Retries, manuelle Schrittkoordination
- Der Beispielcode enthält zahlreiche Status-Tabellen wie
job_queue,job_results,job_state,workflow_steps,step_variables,scheduled_jobssowie Polling-Worker, Workflow-Fortschritt, Crash-Recovery, einen Koordinator für parallele Ausführung, Variablenweitergabe, Scheduling und Cleanup-Funktionen - Für die Berechnung von
next_runim Scheduling wird zusätzlich eine externe Cron-Parser-Bibliothek benötigt
Implementierung mit pg_durable
- Dieselbe parallele Aggregation plus Dashboard-Aktualisierung lässt sich mit einem einzigen Aufruf von
df.start()ausdrücken; mit dem&-Operator erfolgt das Fan-out, mit~>der Join- Beispiel: Drei Queries verzweigen parallel und laufen anschließend im Schritt
refresh dashboardwieder zusammen, um das Ergebnis zu erzeugen - Im Live-Beispiel ist nach 3 parallel ausgeführten Schritten samt Join bis zu
dashboard readyalles durable in nur 1,9 Sekunden abgeschlossen
- Beispiel: Drei Queries verzweigen parallel und laufen anschließend im Schritt
- Queue-Management, Statusverfolgung, Crash-Recovery, Schrittkoordination und Retries übernimmt vollständig pg_durable
Wichtige Merkmale
-
Standardmäßig durable
- Jeder Schritt schreibt seinen Status als Checkpoint in PostgreSQL, sodass der Workflow auch bei Crash, Neustart oder Verbindungsabbruch überlebt
- Die Ausführung wird exakt an der Unterbrechungsstelle fortgesetzt
-
Automatische Retries
- Eingebaute Retry-Logik für fehleranfällige Aufgaben; bei einem Fehlschlag wird nur der betroffene Schritt erneut ausgeführt, der restliche Workflow läuft weiter
- Manueller Code für Fehlerbehandlung ist nicht nötig
-
Volle Observability in SQL
- Sämtliche Workflow-Zustände werden in Postgres-Tabellen gespeichert; Ausführungshistorie, Schritt-Outputs und Fehleranalyse lassen sich mit Standard-SQL abfragen
- Externe Dashboards sind nicht nötig
-
Parallele Ausführung
- Mit dem
&-Operator oderdf.join()lassen sich unabhängige Aufgaben per Fan-out verzweigen; Aggregationen, API-Aufrufe oder ETL-Schritte werden mit automatischer Koordination parallel ausgeführt
- Mit dem
Muster, die sich damit umsetzen lassen
-
ETL-Pipelines
cleanup → transform → loadlässt sich mit garantierter Reihenfolge verknüpfen; jeder Schritt wartet auf den vorherigen, und bei Fehlern stoppt die Pipeline sauber (~> sequence,|=> variables)
-
Parallele Aggregation
- Nutzerzählung, Umsatzsummierung und Bestandsprüfung laufen gleichzeitig; per Fan-out über mehrere Queries und mit Warten auf den Gesamtabschluss (
&,df.join())
- Nutzerzählung, Umsatzsummierung und Bestandsprüfung laufen gleichzeitig; per Fan-out über mehrere Queries und mit Warten auf den Gesamtabschluss (
-
Auftragsverarbeitung
- Eine Bestell-ID wird erfasst und an die Schritte Validierung, Verarbeitung und Abschluss weitergereicht; Variablen fließen automatisch zwischen den Schritten (
|=> capture,$var substitution,df.sleep())
- Eine Bestell-ID wird erfasst und an die Schritte Validierung, Verarbeitung und Abschluss weitergereicht; Variablen fließen automatisch zwischen den Schritten (
-
Geplante Jobs
- API-Polling, Datensatzarchivierung und Datensynchronisierung per Cron-Schedule; die Schleife läuft dauerhaft weiter und überlebt auch Neustarts (
@> loop,df.wait_for_schedule())
- API-Polling, Datensatzarchivierung und Datensynchronisierung per Cron-Schedule; die Schleife läuft dauerhaft weiter und überlebt auch Neustarts (
-
Bedingte Verzweigungen
- Offene Aufgaben, Zeilenzahl oder Flags werden geprüft, um zu verarbeiten oder zu überspringen; die Verzweigungslogik liegt in SQL statt in der Anwendung (
df.if(),?> conditional)
- Offene Aufgaben, Zeilenzahl oder Flags werden geprüft, um zu verarbeiten oder zu überspringen; die Verzweigungslogik liegt in SQL statt in der Anwendung (
-
Mehrstufige Validierung
- Daten abrufen → Schema validieren → Business-Regeln prüfen → genehmigen/ablehnen; jeder Schritt wird checkpointed, sodass selbst bei Fehlern kein Fortschritt verloren geht
-
Datenbankwartung
- Blocker für autovacuum, Table Bloat und Wraparound-Risiken werden erkannt und zur Prüfung bereitgestellt; nach Freigabe erfolgt die Korrektur auch über Neustarts hinweg durable (
?> conditional,df.wait_for_signal(),@> loop)
- Blocker für autovacuum, Table Bloat und Wraparound-Risiken werden erkannt und zur Prüfung bereitgestellt; nach Freigabe erfolgt die Korrektur auch über Neustarts hinweg durable (
-
Azure Functions & HTTP
- Mit
df.http()lassen sich Azure Functions oder erlaubte HTTPS-Endpunkte direkt aus SQL aufrufen, um Dokument-Chunking, Zeilenanreicherung oder Datensatzklassifizierung inline auszuführen
- Mit
-
Human-in-the-Loop-Freigabe
- Alltägliche Aufgaben werden automatisch genehmigt, während risikoreiche Vorgänge wie große Rechnungen oder destruktive Operationen bis zu einem menschlichen Freigabesignal pausieren (
df.wait_for_signal(),df.if())
- Alltägliche Aufgaben werden automatisch genehmigt, während risikoreiche Vorgänge wie große Rechnungen oder destruktive Operationen bis zu einem menschlichen Freigabesignal pausieren (
KI-gestützte Schreibhilfe
- Beschreibt man den Workflow in einfachem Englisch, erzeugt Copilot das korrekte durable-function-SQL; man muss also nicht erst die Syntax lernen, sondern nur das gewünschte Verhalten formulieren
- Das Repository enthält den wiederverwendbaren Agent-Skill
pg-durable-sql, der GitHub Copilot und anderen Agenten beibringt, korrektes SQL mit Operatoren, Variablenersetzung, Schleifen und parallelen Joins zu erzeugen
Als Open Source verfügbar
- Vollständig als Open Source verfügbar – ohne Warteliste und ohne Lock-in; Repository klonen, bauen und sofort im eigenen PostgreSQL ausführen
- Durable Orchestration lässt sich auf Notebook, Server oder in der Cloud einsetzen
Managed-Option Azure HorizonDB
- Azure HorizonDB ist Microsofts neuer PostgreSQL-Cloud-Service mit integriertem pg_durable, sodass geschriebene durable functions erhalten bleiben und zugleich Enterprise-Skalierung, Sicherheit und zusätzliche KI-Funktionen hinzukommen
- Bis zu 3× höhere Performance, automatische Speicher-Skalierung bis 128 TB, Compute-Scale-out bis 3.072 vCore
- Microsoft Defender zur Echtzeit-Bedrohungserkennung, Microsoft Entra ID für Identitätsmanagement
- Filtered DiskANN-Vektorsuche, semantisches Ranking und In-Database-Kuratierung von KI-Modellen
- Microsoft Fabric mit Beinahe-Echtzeit-Mirroring, VS-Code-Integration und Anbindung an GitHub Copilot
-
Integrierte KI-Pipeline
- HorizonDB schichtet auf die durable Ausführung von pg_durable eine gemanagte End-to-End-KI-Pipeline, bei der jeder Schritt Checkpoints, Retries und Crash-Sicherheit bietet
- Ablauf der Schritte: Ingest (Dokumente/Daten laden) → Chunk (Inhalte aufteilen) → Embed (vektorisieren) → Index (in DiskANN speichern) → Serve (Suche/Ranking)
1 Kommentare
Hacker-News-Kommentare
2026 wird wohl das Jahr der Postgres-Queues: Es gibt Entwicklungen wie DBOS[0] und pgQue[1], und es ist großartig, dass die Community solche Optionen schafft
Aus Sicht eines ehemaligen Application Engineers bevorzuge ich aber, dass die Queue-Logik im Code und in Git liegt. Mit den richtigen Tools könnte ich meine Meinung vielleicht ändern
[0]: https://www.dbos.dev/
[1]: https://github.com/NikolayS/pgque
Ich frage mich, wie Versionierung, Debugging, Tests und Releases funktionieren. Für Datenlokalität und einen einfacheren Stack alles an einem Ort zu haben, klingt gut, aber es fühlt sich so an, als würde dabei viel nützliches Wissen darüber verloren gehen, wie man es „richtig“ macht
Genau deshalb habe ich es auch wirklich gehasst, dass man bei Supabase schon für leicht komplexere Dinge Postgres-Funktionen schreiben musste. In einem früheren Startup haben wir allerdings selbst eine einfache Job-Queue auf Postgres gebaut, und etwas wie pgQue hätte das vermutlich deutlich ausgereifter gemacht
Auch Multi-Master-Erweiterungen sind nicht etwas, das man sofort einsetzen kann und das völlig sicher wäre, daher bin ich vorsichtig, schreibintensive komplexe Aufgaben hineinzupacken, die den Bedarf an Datenbankerweiterungen früher auslösen
Beim lokalen Setup haben wir Trigger teils in Django-Migrationen hineingedrückt, damit sie in der lokalen Datenbank landeten
Das riecht nach Stored Procedures. Unit-Tests und Versionsverwaltung sind schwierig, und Business-Logik versteckt sich in der Datenbank als „verstecktes Gehirn“
Außerdem ist es schwer, laute Workloads zu isolieren, es fehlt an Observability, und der gesamte Skalierungsdruck landet bei Postgres. Besonders bei Ein-/Ausgabe wie API-Aufrufen gibt es Defizite. Für Aufgaben, die nur innerhalb der lokalen Datenbank laufen, ist es okay, aber der Einsatzzweck wirkt eng
Natürlich braucht man dafür ein ordentliches Datenbank-Upgrade-Verfahren. Wenn Teammitglieder als root beliebige SQL-Migrationen ausführen, bekommt man Probleme
Unit-Tests sind genauso möglich wie bei anderen SQL-Tests, man muss nur eine Datenbank hochfahren. Wenn man Stored Procedures nicht testen kann, bedeutet das, dass es generell keinen Weg gibt, SQL zu testen, und das ist das eigentliche Problem
Die Alternative zu Stored Procedures ist nicht, gar keine Business-Logik in die Datenbank zu legen; stattdessen endet SQL oft über die ganze Codebasis verstreut, ist schwer zu testen, schlecht versioniert und gekapselt und unnötig langsam
Beim Thema Observability ist etwas Wahres dran: SQL-Probleme zu untersuchen ist meist mühsamer als in den meisten Programmiersprachen. Aber wenn Stored Procedures Ein-/Ausgabe- und Skalierungsprobleme verursachen, werden sie falsch eingesetzt; richtig eingesetzt reduzieren sie Ein-/Ausgabe oft massiv und verbessern dadurch die Skalierbarkeit
Wenn ich es richtig verstehe, scheint Absurd, gemacht von den Entwicklern des Pi LLM Harness, darauf ausgelegt zu sein, reine Datenbankzugriffe so weit wie möglich zu reduzieren. Ich schaue mir das Thema allerdings gerade erst an
https://github.com/earendil-works/absurd
Natürlich kenne ich nicht alle Details, daher frage ich aus ehrlichem Interesse
Unter „wann man es nicht verwenden sollte“ steht „wenn der Workflow größtenteils außerhalb von Postgres liegt und sich über mehrere heterogene Systeme erstreckt“; dann verstehe ich nicht, wie sich dieses Projekt mit etwas wie Temporal vergleichen lässt
Ich frage mich, ob ich die durch diese Empfehlung angedeutete Einschränkung falsch verstehe
Technisch mag es eine interessante Leistung sein, aber so ein SQL zu lesen ist ziemlich bizarr
SELECT df.start(@> (($$SELECT ... FROM demo.invoices WHERE status = 'pending'$$ |=> 'inv')~> df.if_rows('inv',$$UPDATE ... SET status = 'processing'$$~> (df.http(...) |=> 'resp')~> df.if($$SELECT $r.ok$$,-- klassifizieren, verzweigen, auf Signal warten ...),df.sleep(5))),'invoice-approval-pipeline');Wir sind im Unternehmen an Azure gebunden und warten weiter darauf, dass Azure PostgreSQL bei modernen Features aufholt.
Zum Beispiel kann man das hier nicht nutzen: https://www.paradedb.com/blog/hybrid-search-in-postgresql-th...
Unterstützung für ultrabreite hochdimensionale Vektoren gibt es ebenfalls nicht. Es ist gut, dass pg_durable als Open Source veröffentlicht wird, aber vielleicht sollte man erst einmal die Basisfunktionen einführen, die man bei AWS selbstverständlich bekommt.
Offen gesagt: Ich bin Maintainer von pg_textsearch und inzwischen bei Azure. Die Bemerkung zur Vektorunterstützung habe ich nicht ganz verstanden; gemeint ist also etwas über das von Azure angebotene pgvector + diskann hinaus?
Bei der hybriden Suche (BM25 + Vektor) ist auch ParadeDBs pg_search kein AWS-natives Feature, sondern muss direkt auf EC2 selbst gehostet werden. Für Azure PostgreSQL haben wir pg_textsearch nativ entwickelt, das dasselbe BM25-Ranking-Modell bietet, und der Hauptbeitragende ist jetzt im Azure-Postgres-Team.
Dokumentation: https://learn.microsoft.com/en-us/azure/horizondb/ai/full-te...
Bei hochdimensionalen Vektoren sind wir eher voraus. pgvector mit HNSW hat eine Begrenzung von 2.000 Dimensionen, aber Azure unterstützt pgvector für Vektorspeicherung und -suche und bietet für hochdimensionale, großskalige Workloads pg_diskann, Microsofts graphbasierten Vektorindex. Er unterstützt bis zu 16.000 Dimensionen und bietet außerdem fortgeschrittenes In-Index-Filtering, das WHERE-Bedingungen während der Graph-Traversierung auswertet, sodass bei selektiven Bedingungen kein Recall verloren geht.
pgvector: https://learn.microsoft.com/en-us/azure/horizondb/ai/vector-...
DiskANN-Unterstützung für hohe Dimensionen: https://learn.microsoft.com/en-us/azure/horizondb/ai/vector-...
Diese Funktionen sind derzeit in Azure PostgreSQL verfügbar, insbesondere in Azure HorizonDB Preview. Wenn es einen konkreten Workload gibt, kann man sich das gern genauer ansehen.
Das wirkt auf mich wie eine falsche Lösung für ein altes Problem, das DAG-Scheduler wie Apache Airflow schon vor langer Zeit gelöst haben.
Ich finde es seltsam, den Kontrollfluss in der Datenbank statt im Code speichern zu wollen. Das soll das Projekt nicht herabsetzen, ich verstehe es nur noch nicht ganz.
Dieses Projekt scheint eher ein datenbankspezifischer Anwendungsfall zu sein. Der Vorteil dürfte sein, dass man den exakten Zustand der Aufgaben direkt in der Datenbank selbst nachverfolgen kann, statt Workflow-Logs mit der Codebasis Zeile für Zeile abgleichen zu müssen. Last und Latenz dürften auch geringer sein, und operativ müsste man eine Komponente weniger betreiben.
[1] https://learn.microsoft.com/en-us/azure/durable-task/common/...
Umgekehrt scheint dieser Ansatz zwar derzeit noch nicht so zu funktionieren, hätte aber potenziell die Möglichkeit, sich anhand von nahezu Echtzeit-Performance-Feedback ohne Roundtrip-Latenzkosten selbst zu regulieren.
Auch nach dem Lesen der Dokumentation und Beispiele sind mir einige Dinge nicht klar. Ich frage mich, wie
df.wait_for_schedule()funktioniert.Wenn man es in der Anwendung aufruft: Ist es idempotent? Wenn man es zweimal mit denselben Parametern ausführt, wird der Tick dann zweimal ausgelöst? Oder ruft man es nur einmal manuell in der Query-Konsole auf? Oder als Teil eines Migrationsskripts?
Ich frage mich auch, ob
timed_outim Beispiel[0] eine feste Konstante ist, die bei einem Timeout zurückgegeben wird. Auch wie Fehler- oder Exception-Handling funktioniert, ist nicht direkt ersichtlich.[0] https://github.com/microsoft/pg_durable/blob/main/examples/i...
df.start()aufruft, erstellt man eine durable function und startet ihre Ausführung gleichzeitig. Dieser Aufruf gibt eine Instanz-ID zurück, die diese Ausführung repräsentiert und mit der man später auf sie verweisen kann.Innerhalb dieser durable function wird
df.wait_for_signal()aufgerufen; dieser Aufruf wird innerhalb dieser Funktionsinstanz genau einmal ausgeführt, daher sind Duplikate nicht möglich. Wenn der Aufruf vondf.start()selbst wegen eines Timeouts erneut ausgeführt wird, kann es zu Duplikaten kommen, aber in diesem Fall wird eine andere Funktionsinstanz erstellt.Tritt während der SQL-Ausführung ein unbehandelter Fehler auf, schlägt die Funktionsinstanz fehl, und im Status erscheint der exakt aufgetretene Fehler unverändert.
Kannst du erklären, warum man das statt eines Orchestrierungstools außerhalb der Datenbank verwenden sollte? Auch nach dem Lesen von README und Beispielen verstehe ich es noch nicht ganz.
Man muss Backups nicht mit anderen Komponenten synchronisieren, die zum selben Datenspeicher gehören; das ist gut für ETL-Pipelines oder zustandsmaschinenartige Aufgaben. Wenn das ETL größtenteils aus SQL besteht, hilft es auch, dass die eigentliche Arbeit auf demselben Server ausgeführt wird.
Wenn der gesamte Zustand in einer einzigen Datenbank liegt, ist die Wahrscheinlichkeit höher, konsistente Backups zu erhalten.
Bei https://transport.data.gouv.fr wird Postgres dafür genutzt, und das ist hilfreich in einer Elixir-App, die ziemlich viel Verarbeitung übernimmt. Über pg_durable weiß ich noch nicht viel, aber ich kann das gut nachvollziehen, weil ich ähnliche Lösungen genutzt oder selbst umgesetzt habe.
Ist die Datenbank nicht ohnehin schon eine der am schwierigsten zu skalierenden Infrastrukturkomponenten? Ich verstehe nicht, warum man dort auch noch lang laufende Aufgaben unterbringen möchte.
Letztlich sind solche Workloads Aufgaben, die gegen die Datenbank ausgeführt werden, unabhängig davon, ob sie von einer externen Komponente getriggert werden oder nicht. In Daten- oder AI-Pipelines ist es auch üblicher geworden, HTTP-Requests aus der Datenbank heraus zu senden, um zusätzliche Roundtrips und Fehlerquellen durch weitere Komponenten zu vermeiden. Ob man allerdings die Berechnung zu den Daten bringt oder die Daten zur Berechnung, ist eine große Architekturentscheidung, über die viel diskutiert wird.
Es wirkt wie ein weiteres https://en.wikipedia.org/wiki/Inner-platform_effect, das nicht nötig wäre, wenn populäre Programmiersprachen oder virtuelle Maschinen bereits Determinismus, messbare und kontrollierbare schrittweise Ausführung, das Anhalten des Laufzeitzustands sowie Serialisierung/Deserialisierung und Wiederaufnahme unterstützen würden.