postgres=# SELECT index FROM reality WHERE claims = 'benchmarked';

Choosing an Index for pgvector: HNSW vs. IVFFlat, With Numbers

Two ANN indexes, one SQL surface, and a decision most teams make by copying a blog post instead of reading their own recall curve. Here's the actual trade-off.

Vimal Maheedharan11 min readPostgres · pgvector

pgvector gives you exactly one thing standalone vector databases spend a lot of marketing budget convincing you that you need: approximate nearest neighbor search, inside the database you're already running. You get there through a vector column type, a distance operator in your ORDER BY, and a choice between two index types. That last part — the choice — is where most of the actual engineering judgment lives, and it's also the part most write-ups skip in favor of "just use HNSW."

This post doesn't skip it. Distance operators first, then what HNSW and IVFFlat are actually doing structurally, then the tuning knobs, then real benchmark numbers pulled from published sources — not vibes.

Distance operators: what you're actually asking for

Every pgvector query is a nearest-neighbor search expressed through one of three operators in your ORDER BY clause. Pick the wrong one and your index won't even get used correctly — the operator has to match how the index was built.

<->
L2 Distance
Straight-line Euclidean distance. Default choice for most embedding models unless told otherwise.
<=>
Cosine Distance
Direction, not magnitude. Standard for OpenAI / most sentence-transformer embeddings.
<#>
Inner Product
Negative inner product — used when your model was trained to maximize dot-product similarity.
-- cosine distance, the common case for text embeddings SELECT id, content FROM documents ORDER BY embedding <=> '[0.012, -0.034, ...]' LIMIT 10;

The index you build has to be created against the same operator you plan to query with — vector_l2_ops, vector_cosine_ops, or vector_ip_ops. Build an HNSW index with the wrong ops class and your queries will silently fall back to a sequential scan, which at any real row count means a query that used to take milliseconds now takes seconds.

What the two indexes are actually doing

Both indexes solve the same problem — avoid comparing your query vector against every row — but they get there through structurally different approaches, and that difference is what drives every trade-off below.

IVFFlat — cluster first, search fewer clusters

Inverted File with Flat compression partitions your vectors into a fixed number of clusters using k-means, called lists. At query time, it identifies the nearest few clusters to your query vector and only searches inside those — controlled by probes. It needs a training step to place those clusters, which means it wants data in the table before you build the index, and it means the clustering quality degrades as your data drifts away from what it was trained on.

CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); -- query-time tuning SET ivfflat.probes = 10;

HNSW — a navigable graph, no training required

Hierarchical Navigable Small World builds a multi-layer proximity graph at index time, where each vector is a node connected to its approximate neighbors. Queries enter at the top, sparse layer and descend, narrowing in on the right neighborhood in roughly logarithmic time. There's no clustering step and no training data requirement — every insert incrementally extends the graph, which is why HNSW tolerates write-heavy workloads far better than IVFFlat.

CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); -- query-time tuning SET hnsw.ef_search = 40;
The parameters that actually matter: for HNSW, m controls how many connections each graph node keeps (higher = better recall, more memory), and ef_construction controls how thorough the build-time search is (higher = better graph quality, slower build). For IVFFlat, lists is commonly set to rows / 1000 up to a million rows and sqrt(rows) beyond that, with probes starting around sqrt(lists) and tuned up from there.

The numbers

Synthetic benchmarks never perfectly match your workload — dimensionality, dataset size, and hardware all move these figures. But the direction of every trade-off below is consistent across independent published sources, and the magnitude is worth knowing before you guess.

Build time

IVFFlat — 58K rows, 1536 dims, db.r5.large~15s
HNSW — same dataset, same hardware~81s

AWS's published pgvector 0.5.0 benchmark. Independent testing across multiple datasets puts IVFFlat's build-time advantage anywhere from roughly 5× to over 40× faster than HNSW, depending on dataset size and dimensionality — the AWS figures above sit toward the lower end of that range.

Index size on disk

HNSW's graph structure costs memory: independent benchmarking found HNSW indexes running roughly 1.3× to 4.5× larger than IVFFlat indexes on the same data, with the gap widening on higher-dimensional datasets. Broader estimates put HNSW's overall memory footprint at 2–5× that of IVFFlat once you account for query-time working memory too.

Query throughput and recall

MetricHNSWIVFFlat
Out-of-box recall 95%+ typical Depends heavily on lists/probes tuning
Search time scaling ~O(log n) Grows linearly with probes
QPS / p99 latency vs. IVFFlat at 99% recall ~30× better (pgvector 0.7.0) baseline
Recall stability as data grows Stable, no retrain needed Drifts — centroids need periodic REINDEX
Insert behavior Incremental, no rebuild Degrades until next REINDEX
Quantization changes the math. Since pgvector 0.7.0, binary and scalar quantization for HNSW have cut build times by roughly 50–150× on some benchmark datasets versus the earliest HNSW release, while keeping query throughput gains over IVFFlat at similar recall targets. If you're comparing index types on an old pgvector version, you're comparing outdated numbers — update first, then benchmark.

The actual decision

Strip away the benchmark noise and the decision comes down to two questions: does your data change after the index is built, and does build time or query time matter more to you?

Reach for HNSW when

  • Your table gets continuous writes (RAG ingestion, live chat history, growing corpora)
  • Query latency and recall matter more than build time
  • Dataset is under roughly 10M vectors
  • You want good defaults with less tuning

Reach for IVFFlat when

  • Dataset is large, mostly static, and rebuilt in batch
  • Memory footprint is the binding constraint
  • Sub-second build time on ingest matters more than top-tier recall
  • You're willing to REINDEX on a schedule as data drifts

For most RAG pipelines and semantic search workloads — the common case for anyone reading this — HNSW with sensible defaults (m = 16, ef_construction = 64–200) is the safer starting point. It costs more to build and more to store, but it tolerates the write patterns real applications actually have, and it doesn't quietly lose recall while you're not looking.

IVFFlat earns its place at a specific point on the curve: very large, mostly-static datasets where build time or memory is the binding constraint and you're prepared to own the REINDEX schedule that keeps its clustering honest.

Either way — benchmark on your actual data, your actual dimensionality, and your actual query patterns before you trust a number from this post or anyone else's. The direction of these trade-offs is stable. The magnitude, for your workload, is something only your own EXPLAIN ANALYZE output can tell you.

Benchmark figures referenced above are drawn from published pgvector project benchmarks and independent community testing (AWS pgvector benchmarks, pgvector 0.7.0 release benchmarking, and independent index-comparison studies across multiple ANN datasets), current as of 2026. Numbers vary by hardware, dataset, and pgvector version — treat the directions as reliable and the magnitudes as a starting estimate, not a guarantee for your workload.
← Return to Home LinkedIn ↗