Tools/RAG & Retrieval

pgvector im Review: die Vektordatenbank, die du nicht betreiben musst

Ein Review von pgvector 0.8.7: iterative Index-Scans für gefilterte Suche, HNSW und IVFFlat, Binärquantisierung bei 100 Millionen Vektoren und das CVE im Indexaufbau.

Art
Vector database extension
Preis
PostgreSQL licence

··10 Min. Lesezeit

  • Vector search
  • Postgres
  • HNSW
  • RAG
  • Quantisation
Eine Anfrage teilt sich oben in einen exakten Sequenzscan, einen HNSW-Graphlauf und eine IVFFlat-Sonde; ein Band unten zeigt einen iterativen Scan, der weiterläuft, bis die LIMIT-Zahl erreicht ist.

Das Wichtigste in Kürze

  • pgvector ist eine PostgreSQL-Erweiterung unter der PostgreSQL-Lizenz, Vektoren liegen also in normalen Tabellen, und Mandantenfilter, JOINs und kaskadierende Löschungen bleiben transaktionell.
  • Iterative Index-Scans, eingeführt in 0.8.0, sind die Lösung für gefilterte Suche: ohne sie liefert ein Filter, der 10 % der Zeilen trifft, bei Standard ef_search 40 etwa vier Zeilen.
  • AWS maß einen 367 GB großen HNSW-Index für 100 Millionen Vektoren mit 768 Dimensionen gegen einen 38 GB großen binärquantisierten Index, der in 1,1 statt 16,1 Stunden entstand.
  • CVE-2026-3172, behoben in 0.8.2 im Februar 2026, war ein Pufferüberlauf beim parallelen HNSW-Indexaufbau, der Daten anderer Relationen leaken konnte; der Fix braucht keinen Reindex.
  • Die Grenze ist Speicher, nicht die API: Passt der Index nicht mehr in shared_buffers, bleiben halfvec, Binärquantisierung, Partitionierung oder ein zweites System.

pgvector ist eine PostgreSQL-Erweiterung, die Vektoren in eine normale Tabelle legt und mit SQL durchsucht. Es ist keine Vektordatenbank mit angehängter Abfragesprache: Es ergänzt vier Spaltentypen, sechs Distanzoperatoren und zwei Indextypen zu einer Datenbank, die die meisten Teams ohnehin betreiben, unter der PostgreSQL-Lizenz, die so liberal ist, wie Lizenzierung wird. Die hier vertretene Position: Das ist der richtige Standard für nahezu jede Retrieval-Last bis zu einigen zehn Millionen Vektoren, und ein Team, das in dieser Größenordnung eine eigene Vektordatenbank aufbaut, kauft Betriebsaufwand statt Fähigkeit.

Es konkurriert mit Qdrant, Weaviate, Chroma und den vollständig betreuten Vektordiensten, und mit jedem gehosteten Postgres, das die Erweiterung inzwischen mitbringt. Was es eliminiert, ist eine ganze Klasse Infrastruktur: kein zweiter Dienst, kein zweites Protokoll zu authentifizieren, kein zweiter Backup-Zeitplan, keine Konsistenzlücke zwischen den Zeilen und den Embeddings, die sie beschreiben. Was bleibt, sind alle Postgres-Grenzen: einer Node Speicher, ein Vacuum, ein Schreibdurchsatz. Und genau die werden zu Designparametern, sobald der Index nicht mehr in den RAM passt.

Was es ist

Die erste Version 0.1.0 erschien am 20. April 2021, die aktuelle Version 0.8.7 am 1. Oktober 2026; im Repository waren zum Prüfzeitpunkt 23.300 Sterne und 1.300 Forks verzeichnet. Unterstützt werden PostgreSQL 13 und neuer; ausgeliefert als Docker-Image, über PGXN, APT, Yum, Homebrew und conda-forge, und vorinstalliert bei einer wachsenden Liste gehosteter Anbieter. Das ist relevant, weil ein betreuter Anbieter auf alter Version der häufigste Weg ist, auf dem ein Team meint, einen Sicherheitsfix zu haben, den es nicht hat.

  • Lizenz: die PostgreSQL-Lizenz, derselbe liberale Text, den PostgreSQL selbst verwendet. Kein Open Core, keine kommerzielle Stufe, keine Funktion hinter einem Bezahltarif.
  • Version 0.8.7 vom 1. Oktober 2026; die 0.8-Reihe kennt iterative Index-Scans seit 0.8.0 im Oktober 2024, und genau diese Funktion hat gefilterte Suche berechenbar gemacht.
  • Vier Typen: vector mit 4 Byte pro Dimension, halfvec mit 2, bit mit einem Bit pro Dimension und sparsevec für dünne Vektoren mit bis zu 1.000 nicht null Elementen.
  • Zwei Indextypen: HNSW für das beste Verhältnis aus Tempo und Recall, ohne Trainingsschritt, IVFFlat für schnellere Indexaufbauten und weniger Speicherbedarf.
  • Sechs Operatoren, die direkt in ORDER BY stehen können: L2 (<->), Inneres Produkt (<#>), Kosinus (<=>), L1 (<+>), Hamming (<~>) und Jaccard (<%>).
  • Speicherung und Zugriff kommen von Postgres selbst: ACID, Replikation über WAL, Point-in-Time-Recovery, JOINs und Row-Level-Security in derselben Tabelle wie die Embeddings.

Wie es funktioniert

Ohne Index ist eine Vektoranfrage ein Sequenzscan mit ORDER BY über die Distanzfunktion: exakte Ergebnisse, perfekter Recall, Kosten proportional zur Tabelle. Ein approximativer Index ändert diesen Pakt. HNSW baut über den Vektoren einen mehrschichtigen Graphen und läuft ihn ab und tauscht etwas Recall gegen Kosten, die nicht mehr mit der Tabelle wachsen; IVFFlat gruppiert Vektoren in Listen und befragt einen Teil davon. Das entscheidende Detail: Der Planner greift nur dann auf einen Index zu, wenn die Anfrage so aussieht wie ORDER BY embedding <=> $1 LIMIT n. Dieselbe Anfrage als ORDER BY 1 - (embedding <=> $1) DESC notiert verwendet laut README keinen Index.

Drei Ausführungspfade für eine VektoranfrageEine Anfrage mit ORDER BY über einen Distanzoperator und LIMIT teilt sich oben in drei Wege. Links, kein Index: ein Sequenzscan mit perfekter Recall, dessen Kosten mit jeder Zeile wachsen. Mitte, HNSW: ein Lauf über einen mehrschichtigen Graphen mit m = 16 und ef_search = 40 als Standard, wobei jede WHERE-Klausel erst nach dem Index-Scan auf die Kandidaten angewendet wird. Rechts, IVFFlat: Vektoren in Listen gruppiert, nur die nächstgelegenen Listen werden abgefragt, gebaut nach dem Laden und über ivfflat.probes justiert, mit schlechterem Speed-Recall-Verhältnis. Ein Band unten: iterative Index-Scans, eingeführt in 0.8.0, laufen weiter, solange die Filter Zeilen verwerfen, in strict_order oder relaxed_order, begrenzt durch hnsw.max_scan_tuples mit Standard 20.000.One query, three execution pathsthe ORDER BY decidesQueryORDER BY distance, LIMIT 10Exact scansequential scanperfect recallcost grows per rowno index neededHNSWmultilayer graph walkm = 16, ef_search = 40filter runs after the scanthe default indexIVFFlatlists, then probesbuild it after the loadtune ivfflat.probesfaster build, less recallIterative scan, added in 0.8.0the scan continues when the filter drops rows: strict_order keeps exact distance orderrelaxed_order trades order for recall; bounded by hnsw.max_scan_tuples, default 20,000
Der Filter greift erst, wenn derapproximative Index seine Arbeit getan hat. Deshalb widersprechen sich ein selektiver WHERE-Index und ein approximativer Index, bis der Scan fortdarf.

Das zweite Detail ist, wo die WHERE-Klausel greift. Ein approximativer Index erzeugt Kandidaten, und der Filter wird danach auf sie angewendet; Selektivität und Recall wirken also zusammen: bei hnsw.ef_search mit Standard 40 und einer Bedingung, die 10 % der Zeilen trifft, kommen etwa vier Zeilen zurück. Nichts ist kaputt, der Index hat die verworfenen Zeilen schlicht nie gesehen. Dieses Verhalten ist die häufigste Ursache für Meldungen, die Erweiterung liefere zu wenige Ergebnisse, und darauf gibt es eine dokumentierte Lösung statt eines Workarounds.

Filterung ist hier kein Nachgedanke, sondern ein Problem erster Klasse, und die Dokumentation arbeitet vier Schritte in der Reihenfolge ab, in der ein Reviewer sie testen würde. Welcher gilt, hängt davon ab, wie selektiv der Filter ist, wie viel Recall die Anwendung wirklich braucht und wie viele Ausprägungen der Filter hat: Eine Mandantennummer mit 50.000 Werten verhält sich nicht wie eine Landeskennung mit acht.

  • Indexiere die Filter-Spalte zuerst mit einem einfachen B-Tree. Trifft die Bedingung nur einen kleinen Anteil der Zeilen, liefert das exakte Nachbarn, ohne den approximativen Index anzusprechen, und das README nennt genau das als Ausgangspunkt.
  • Bleibt der Filter breit, schalte iterative Index-Scans mit SET hnsw.iterative_scan = relaxed_order ein: Der Graph wird so lange abgelaufen, bis die LIMIT voll ist, und strict_order steht bereit, wenn die Distanzreihenfolge exakt sein muss.
  • Begrenze die Arbeit. hnsw.max_scan_tuples hat Standard 20.000 und hnsw.scan_mem_multiplier ein Vielfaches von work_mem, ein selektiver Filter wird also zu einem begrenzten Scan statt zu einem unendlichen.
  • Bei vielen Ausprägungen nutzt man Partial-Indizes pro Wert oder list-Partitionierung der Tabelle. Das README weist außerdem darauf hin, dass Mandanten, die sich einen approximativen Index teilen, sich gegenseitig den Recall beeinflussen, was ein Partitionierungs- und kein Tuning-Argument ist.

Erste Schritte

Die gesamte Schnittstelle ist SQL, und genau das ist der Grund, ihr einen eigenen API-Restsystemen vorzuziehen. Der Ausschnitt unten hat die Form einer Produktionstabelle: eine Spalte mit fester Dimension, ein exakter Index auf den Filter, ein approximativer Index auf den Vektor und die eine Einstellung, die entscheidet, ob eine gefilterte Anfrage zu kurz ausfällt.

CREATE EXTENSION IF NOT EXISTS vector;

-- The dimension is part of the type, so every row has to match it.
CREATE TABLE chunks (
  id         bigserial PRIMARY KEY,
  tenant_id  text       NOT NULL,
  embedding  vector(1536)
);

-- Exact index on the filter first: for a selective tenant it answers the
-- whole query and the approximate index is never consulted.
CREATE INDEX ON chunks (tenant_id);

-- Bulk load with COPY, then build the approximate index on top of the data.
CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

-- A filter matching 10% of rows with ef_search = 40 returns about four rows,
-- so the scan has to be allowed to continue past its first pass.
SET hnsw.iterative_scan = relaxed_order;
SET hnsw.ef_search = 100;

SELECT id
FROM chunks
WHERE tenant_id = 'acme'
ORDER BY embedding <=> (SELECT embedding FROM chunks WHERE id = 42)
LIMIT 10;

Drei Entscheidungen in diesem Ausschnitt sind verteidigenswert. Die Dimension ist Teil des Typs, eine Zeile eines anderen Embedding-Modells fällt also schon bei INSERT auf, nicht erst zur Laufzeit. Der approximative Index wird nach den Daten gebaut, denn ein HNSW-Graph über eine leere Tabelle hat nichts zu verlinken und müsste ohnehin neu gebaut werden. Und der Probe-Vektor kommt aus einem Subselect, der Form, die der Planner akzeptiert; dieselbe Anfrage mit einem Ausdruck im ORDER BY fällt ohne Warnung auf einen Sequenzscan zurück.

Performance

Speicher ist die gesamte Performance-Geschichte. Eine Vektorspalte kostet 4 Byte pro Dimension plus 8 Byte Kopfzeile, 1.536 Dimensionen sind also rund 6 KB pro Zeile, noch bevor ein Index existiert. Ein HNSW-Index voller Präzision über 100 Millionen Vektoren mit 768 Dimensionen maß AWS im Benchmark mit 367 GB, etwa 3,7 GB pro Million. Die alternativen Typen existieren, um genau diese Zahl anzugreifen.

TypByte pro DimensionIndexierbare GrenzeWas es kostet
vector42.000 Dimsdie Basis; exakte Suche darüber ist exakter Recall
halfvec24.000 Dimshalb so großer Index, in AWS-Tests nahezu kein Recall-Verlust
bit1/864.000 DimsHamming über Vorzeichenbits, braucht Reranking für Recall
sparsevec8 pro non-zero1.000 non-zerodünne Embeddings, L2, Kosinus, inneres Produkt und L1

AWS hat die klarsten öffentlichen Zahlen dazu veröffentlicht: VectorDBBench v0.3.4 bei top_k=100 auf Aurora PostgreSQL 18.4 mit pgvector 0.8.0. Bei LAION 100M mit 768 Dimensionen hielt ein r8g.4xlarge mit 128 GB einen 367 GB großen HNSW-Index voller Präzision, den es nicht im Cache behalten konnte: 3,4 Anfragen pro Sekunde kalt bei Parallelität 10, 3.336 warm, Recall 0,965, 16,1 Stunden Aufbauzeit. Binärquantisierung mit Reranking brachte den Index auf 38 GB und den Aufbau auf 1,1 Stunden, erreichte 13,5 kalte und 895 warme Anfragen pro Sekunde, und bezahlte das mit Recall: 0,931.

Derselbe Benchmark enthält das Gegenbeispiel, weshalb seine Zahlen mit ihrer Methodik gelesen werden müssen. Bei Cohere 10M, dessen 768-dimensionale Embeddings in der Nähe von null clustern, brauchte Binärquantisierung ein Reranking von 3.000 Kandidaten für 0,93 Recall und brach auf 16 Anfragen pro Sekunde bei p99 1.640 ms ein, während HNSW voller Präzision auf einer 384-GB-Instanz 6.930 Anfragen pro Sekunde bei 0,952 Recall lieferte. Quantisierung hängt von der Verteilung ab: Recall auf den eigenen Embeddings validieren, oder halfvec nehmen und den Index halbieren, statt zu raten.

  • Erhöhe maintenance_work_mem vor dem HNSW-Aufbau; Postgres meldet per Notice, sobald der Graph nicht mehr hineinpasst, und das README warnt davor, es bis zum Speichermangel des Servers anzuheben.
  • Laden mit COPY, danach indexieren, und in Produktion CREATE INDEX CONCURRENTLY, damit der Aufbau Schreibvorgänge nicht blockiert.
  • VACUUM auf einem HNSW-Index kann lange dauern; die dokumentierte Abkürzung ist zuerst REINDEX INDEX CONCURRENTLY, danach VACUUM.
  • Horizontale Skalierung wird geliehen statt gebaut: Replikation und Point-in-Time-Recovery kommen aus dem WAL, und das README verweist für Sharding auf Citus, PgDog oder List-Partitionierung.

Wo es hakt

Die Schwächen sind strukturell, nicht unvollendet. Alles läuft auf einem Postgres-Node, Index, Heap und Buffer-Cache konkurrieren also um denselben Speicher, und ein Index, der nicht mehr hineinpasst, wird zuerst zum I/O-Problem und erst danach zum Recall-Problem. Approximative Suche und selektive Filter streiten sich auch mit iterativen Scans, denn ein begrenzter Scan bleibt begrenzt. Vacuum und Indexpflege sind die Aufgaben dieser Datenbank statt fremder. Und eingebautes Sharding gibt es nicht: horizontale Skalierung bedeutet Replikate, Partitionierung oder eine Erweiterung.

AlternativeBetriebsformBetriebsaufwandWo sie besser ist
QdrantEin eigener Rust-Server oder die Cloud des HerstellersNoch ein Cluster zu patchen, sichern und absichernPayload-Filterung und Quantisierung auf Recall bei Skala optimiert
WeaviateEin eigener Server mit GraphQL-API oder die Cloud des HerstellersDasselbe noch einmal, plus eigene ModulkonfigurationHybrid-Suche und Vektorisierung an einem Ort konfiguriert
ChromaEingebettet im Prozess oder als kleiner eigenständiger ServerFast keiner, aber auch kein PostgresDer kürzeste Weg vom Prototyp zum laufenden System

Die ehrliche Grenze: pgvector gewinnt, solange die Vektoren eine Spalte der Daten sind, die das Team ohnehin speichert, und verliert, sobald eine Anfrage einen großen Graphen, einen gefilterten Scan und den Rest des Arbeitspeichers der Anwendung in denselben Speicher legen muss. AWS hat diese Grenze bei 367 GB Index für 100 Millionen Vektoren gemessen und sie mit Quantisierung und Partitionierung umgangen. Teams, die diesen Tausch nicht besitzen wollen, haben vier Auswege: halfvec, Binärquantisierung mit Reranking, Partitionierung nach Mandant oder ein eigenes System, und die ersten beiden sind billig genug, bevor über das vierte gesprochen wird.

Urteil

pgvector sollte die Standardantwort darauf sein, wohin die Embeddings gehören, für jedes Team, das bereits Postgres betreibt, und eine eigene Vektordatenbank sollte sich ihren Weg freistritten müssen. Die Erweiterung hat die ungewöhnliche Eigenschaft, dass ihre Fehlermodi die Fehlermodi einer Datenbank sind, die das Team bereits versteht: Speicherdruck, Wartungsfenster, Schreibdurchsatz eines Nodes. Wähle etwas anderes bewusst, mit einer benennbaren Skala oder einem benennbaren Latenzziel.

  1. Nimm pgvector, wenn die Vektoren Zeilen beschreiben, die das Team ohnehin speichert, und Mandantentrennung, kaskadierende Löschungen oder ein JOIN mit der Quelltabelle transaktionell sein müssen.
  2. Nimm es, wenn der Bestand bis zu einigen zehn Millionen Vektoren umfasst und der Filter so selektiv ist, dass ein B-Tree auf der Filterspalte den Großteil der Anfrage trägt.
  3. Nimm es, wenn die Alternative ein zweites Produktionssystem ist: Die Erweiterung erbt Backup, Replikation, Monitoring und Zugriffskontrolle, die es schon gibt, und fügt nichts Neues hinzu, was zu betreiben wäre.
  4. Nimm es nicht, wenn eine einzelne Anfrage einen mehrere hundert Gigabyte großen Graphen plus den Arbeitspeicher der Anwendung halten muss, wenn nicht halfvec oder Binärquantisierung auf den echten Embeddings schon gemessen wurde.
  5. Nimm es nicht, wenn der Anspruch dauerhafter Schreibdurchsatz über mehrere Nodes oder latenzarmer gefilterter Recall über mehrere hundert Millionen Vektoren ist; das ist Partitionierung oder ein eigenes System, und Aufschieben kostet später eine Migration.
Nicht alle Embedding-Modelle erzeugen Vektoren, die sich gut quantisieren lassen. Vor der Entscheidung mit den eigenen Daten validieren. — AWS Database Blog, 18. August 2026

Quellen

  1. pgvector-README: Typen, Indexierung, Filterung und Skalierung
  2. pgvector-Changelog, 0.1.0 bis 0.8.7
  3. PostgreSQL-News: pgvector 0.8.2 erschienen (CVE-2026-3172)
  4. AWS: pgvector mit Binärquantisierung auf Aurora PostgreSQL skalieren
  5. pgvector-Lizenz: die PostgreSQL-Lizenz

Häufige Fragen

Ist pgvector kostenlos nutzbar?

Ja. Es steht unter der PostgreSQL-Lizenz, ohne Gebühr, ohne Open-Core-Split und ohne Bezahltarif, und wird von einer wachsenden Zahl gehosteter Postgres-Anbieter vorinstalliert. Wichtig ist die gelieferte Version: Für iterative Index-Scans braucht es 0.8.0 oder neuer.

Wann HNSW statt IVFFlat verwenden?

HNSW liefert das bessere Verhältnis aus Tempo und Recall, kann auf einer leeren Tabelle angelegt werden, weil es keinen Trainingsschritt braucht, ist aber langsamer zu bauen und speicherhungriger. IVFFlat baut schneller und braucht zuerst Daten; das README empfiehlt lists = rows / 1000 bis einer Million Zeilen, darüber sqrt(rows), und probes beginnend bei sqrt(lists).

Funktioniert pgvector mit einer WHERE-Klausel?

Ja, aber bei einem approximativen Index läuft der Filter nach dem Index-Scan, eine selektive Bedingung kann also weniger Zeilen liefern als die LIMIT verlangt. Dokumentiert sind ein B-Tree auf der Filterspalte, iterative Index-Scans über hnsw.iterative_scan, Partial-Indizes pro Wert und List-Partitionierung bei vielen Ausprägungen.

Wie viele Vektoren verarbeitet pgvector?

Eine einzelne Tabelle ist durch die 32-TB-Grenze von PostgreSQL begrenzt und davon, wie viel Index in den Speicher passt; AWS maß 367 GB Index für 100 Millionen Vektoren mit 768 Dimensionen und empfiehlt Partitionierung ab Milliarden-Größen. Darüber hinaus verweist das README auf Replikate, Citus, PgDog oder List-Partitionierung statt auf eine eingebaute Shard-Ebene.

Klingt nach dem, was du suchst?

Erzähl mir von deinem Projekt oder deiner Stelle – ich freue mich, von dir zu hören.