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
Balázs Csorba··10 Min. Lesezeit
- Vector search
- Postgres
- HNSW
- RAG
- Quantisation

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:
vectormit 4 Byte pro Dimension,halfvecmit 2,bitmit einem Bit pro Dimension undsparsevecfü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.
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.
Gefilterte Suche
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_orderein: Der Graph wird so lange abgelaufen, bis die LIMIT voll ist, undstrict_ordersteht bereit, wenn die Distanzreihenfolge exakt sein muss. - Begrenze die Arbeit.
hnsw.max_scan_tupleshat Standard 20.000 undhnsw.scan_mem_multiplierein Vielfaches vonwork_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.
| Typ | Byte pro Dimension | Indexierbare Grenze | Was es kostet |
|---|---|---|---|
vector | 4 | 2.000 Dims | die Basis; exakte Suche darüber ist exakter Recall |
halfvec | 2 | 4.000 Dims | halb so großer Index, in AWS-Tests nahezu kein Recall-Verlust |
bit | 1/8 | 64.000 Dims | Hamming über Vorzeichenbits, braucht Reranking für Recall |
sparsevec | 8 pro non-zero | 1.000 non-zero | dü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_memvor 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 ProduktionCREATE 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.
| Alternative | Betriebsform | Betriebsaufwand | Wo sie besser ist |
|---|---|---|---|
| Qdrant | Ein eigener Rust-Server oder die Cloud des Herstellers | Noch ein Cluster zu patchen, sichern und absichern | Payload-Filterung und Quantisierung auf Recall bei Skala optimiert |
| Weaviate | Ein eigener Server mit GraphQL-API oder die Cloud des Herstellers | Dasselbe noch einmal, plus eigene Modulkonfiguration | Hybrid-Suche und Vektorisierung an einem Ort konfiguriert |
| Chroma | Eingebettet im Prozess oder als kleiner eigenständiger Server | Fast keiner, aber auch kein Postgres | Der 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.
- 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.
- 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.
- 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.
- Nimm es nicht, wenn eine einzelne Anfrage einen mehrere hundert Gigabyte großen Graphen plus den Arbeitspeicher der Anwendung halten muss, wenn nicht
halfvecoder Binärquantisierung auf den echten Embeddings schon gemessen wurde. - 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
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.