RAG & Retrievalvector-db

Why pgvector similarity search is slow, and how to fix it without leaving Postgres

Error
pgvector query taking seconds instead of milliseconds

Also appears as

  • SELECT ... ORDER BY embedding <=> query_vector LIMIT 10 is slow
  • pgvector index not being used
  • sequential scan on vector column instead of index scan

Short answer

pgvector queries are almost always slow because of an index and operator class mismatch (an index built for one distance function while queries use a different operator), a missing index entirely so Postgres falls back to a sequential scan, or search-time parameters (ef_search, probes) set too low. Confirm EXPLAIN ANALYZE shows an index scan, match the operator class to your distance function, and tune maintenance_work_mem before building large HNSW indexes.

Affects: pgvector on any Postgres version, most noticeable once a table passes a few hundred thousand rows without a properly matched index.

Fastest path to fast pgvector queries

  1. 1Run EXPLAIN ANALYZE on the slow query and confirm it shows an index scan on the vector column, not a sequential scan.
  2. 2Confirm the operator class used to build the index (vector_l2_ops, vector_cosine_ops, or vector_ip_ops) matches the distance operator used in the query (<->, <=>, or <#>); a mismatch silently disables the index.
  3. 3For HNSW indexes, raise hnsw.ef_search for the session to trade a small amount of latency for recall.
  4. 4For IVFFlat indexes, raise ivfflat.probes since the default of 1 probe searches only a single list and misses most of the table.
  5. 5Run VACUUM ANALYZE on the table if it has had heavy inserts/updates/deletes, since bloat degrades both scan types.
  6. 6If building an HNSW index fails or is extremely slow, raise maintenance_work_mem for the session before running CREATE INDEX.

How to confirm this is your problem

  • A similarity query that should return in milliseconds takes hundreds of milliseconds to several seconds
  • EXPLAIN ANALYZE shows Seq Scan instead of Index Scan on the embedding column
  • Query latency degrades sharply as the table grows past a few hundred thousand rows
  • Recall looks fine on a small test set but real queries frequently miss obviously relevant rows
  • CREATE INDEX for HNSW runs for a very long time or fails with a memory-related error

Root causes and fixes

Most common

No vector index exists, or it was never built successfully

Without an HNSW or IVFFlat index, pgvector falls back to a brute-force sequential scan that computes distance against every row in the table. This works fine for a few thousand rows but scales linearly and becomes unacceptably slow once the table reaches tens or hundreds of thousands of rows.

Fix: Create an appropriate index: an HNSW index with vector_cosine_ops is the best default choice, or an IVFFlat index with lists set to roughly the square root of the row count.

Commands
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);
EXPLAIN ANALYZE SELECT id FROM items ORDER BY embedding <=> '[...]' LIMIT 10;
Common

Index built with the wrong operator class for the distance function used at query time

pgvector indexes are built for a specific operator class: vector_l2_ops for Euclidean distance, vector_cosine_ops for cosine distance, and vector_ip_ops for inner product. If the index was built with one operator class but the query uses a different operator, Postgres cannot use that index for the query and silently falls back to a sequential scan with no error message.

Fix: Check the index definition and confirm the operator class matches the operator used in ORDER BY; if it does not, drop and rebuild the index with the correct operator class rather than changing the query, since the operator class should match the distance metric your embeddings were normalized for.

Commands
SELECT indexdef FROM pg_indexes WHERE tablename = 'items';
Occasional

ef_search or probes set too low for the workload

HNSW's ef_search and IVFFlat's probes control the search-time speed/recall tradeoff. Low default values prioritize speed over recall, which can look like slow queries when what is actually happening is fast-but-wrong results that require the application layer to retry or over-fetch to compensate.

Fix: Increase ef_search (HNSW) or probes (IVFFlat) incrementally and measure both latency and recall against a labeled test set to find the setting that meets your recall target without unnecessary latency.

Commands
SET hnsw.ef_search = 100;
SET ivfflat.probes = 10;
Occasional

Table bloat from heavy insert/update/delete activity

Postgres does not immediately reclaim space from updated or deleted rows; dead tuples accumulate and both sequential scans and, to a lesser extent, index scans have to skip over them, adding overhead that grows with churn and shows up as gradually worsening query times.

Fix: Run VACUUM ANALYZE regularly on high-churn vector tables, and consider more aggressive autovacuum settings for that table specifically if updates are frequent.

Commands
VACUUM ANALYZE items;
Rare

Not enough maintenance_work_mem to build the HNSW index efficiently

Building an HNSW index is memory-intensive; if maintenance_work_mem is too low, Postgres has to spill graph-building work to disk, which can make index creation extremely slow or, in constrained environments, fail outright partway through.

Fix: Raise maintenance_work_mem for the session running CREATE INDEX, then reset it afterward if it was raised globally.

Commands
SET maintenance_work_mem = '2GB';
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);

Diagnostic commands

Confirm whether the index is actually being used

EXPLAIN ANALYZE SELECT id FROM items ORDER BY embedding <=> '[0.1, 0.2]' LIMIT 10;

If the plan shows Seq Scan, the index either does not exist, uses the wrong operator class for this query's operator, or the planner has chosen not to use it; if it shows Index Scan using an hnsw or ivfflat index, the index is working and the bottleneck is elsewhere.

List existing indexes and their operator classes

SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'items';

Compare the operator class in the index definition against the operator your application actually uses in ORDER BY; any mismatch means the index is silently unused for that query.

Check table bloat and dead tuple count

SELECT relname, n_dead_tup, n_live_tup FROM pg_stat_user_tables WHERE relname = 'items';

A high ratio of dead to live tuples indicates bloat; run VACUUM ANALYZE and re-test query latency to see how much of the slowdown was bloat versus indexing.

Measure recall and latency across a range of ef_search/probes values

for v in 40 80 120 200; do echo ef_search=$v; done

Use this sweep against a labeled query set to plot the recall/latency tradeoff curve for your data, since the right value is workload-specific and not a single universal default.

Stopping it from happening again

  • Always build an explicit HNSW or IVFFlat index immediately after loading a vector table; never rely on the default sequential scan.
  • Standardize on one distance metric across embedding generation, index operator class, and query operator, and document it so it cannot silently drift.
  • Schedule regular VACUUM ANALYZE on high-churn vector tables rather than relying solely on autovacuum defaults.
  • Set maintenance_work_mem appropriately before any large index build, and treat index rebuilds as a planned maintenance operation for large corpora.
  • Track query latency and recall together in monitoring, since optimizing one without measuring the other leads to silently degraded retrieval quality.

When this becomes an architecture problem

If query latency is still unacceptable after confirming index usage, operator class match, tuned ef_search/probes, and a clean vacuum, pgvector on a single Postgres instance may simply be undersized for the corpus and query volume. Consider read replicas, partitioning, or migrating high-QPS collections to a dedicated vector database, which is worth validating against measured load rather than more parameter tuning.

Frequently asked questions

Should I use HNSW or IVFFlat for pgvector?

HNSW is the better default for most workloads: it gives better recall at a given latency and does not require retraining lists as data grows, at the cost of higher memory usage and slower index builds. IVFFlat can be a reasonable choice for very large, mostly-static collections where memory is constrained.

Why does EXPLAIN ANALYZE show a sequential scan even though I created an index?

The most common reason is an operator class mismatch between the index and the query's distance operator. Verify with pg_indexes that the operator class matches exactly what your query uses.

Is pgvector fast enough for production RAG at scale?

Yes, for most enterprise RAG workloads, up to tens of millions of vectors, pgvector with a properly configured HNSW index and tuned ef_search performs competitively with dedicated vector databases, while keeping data in the same Postgres instance, which simplifies operations for on-prem and air-gapped deployments.

What is a good starting value for ef_search or probes?

Start with ef_search around 100 for HNSW or probes around 10 for IVFFlat with roughly 100 lists, then measure recall against a labeled query set and adjust. There is no universal correct value; it depends on embedding dimensionality, corpus size, and recall requirements.

Related problems

Vector index memory usage is too high (HNSW blowing up RAM)

Vector index memory usage exceeds raw embedding size because HNSW stores a graph of neighbor connections on top of every vector, typically adding 1.5 to 3 times the raw vector size in overhead depending on the M parameter, while IVFFlat has lower overhead but worse recall at the same speed. Estimate memory as dimension times row count times 4 bytes for raw vectors, then add graph overhead, and consider quantization if the total does not fit in available RAM.

Vector database connection errors under load

Vector database connection errors under production load are almost always pool exhaustion, either too many application processes each opening their own connections, or a pool sized for development traffic rather than real concurrent RAG query volume, not an actual network or database outage. Use a connection pooler sized for your real concurrency, add retry logic with exponential backoff for transient failures, and separate TLS/auth failures from timeout and pool errors since they need different fixes.

Embedding dimension mismatch after switching models

Different embedding models produce vectors of different fixed dimensions, so swapping models without re-embedding the entire corpus produces a hard dimension mismatch error or, worse, silently meaningless similarity scores if the column is resized without re-indexing. There is no shortcut: changing the embedding model requires re-embedding every document and rebuilding the vector index from scratch.

Guide

pgvector vs Dedicated Vector Databases: An Honest Comparison

pgvector vs Milvus, Qdrant, and Weaviate for enterprise RAG: real tradeoffs on scale, latency, operational overhead, and when Postgres is genuinely enough.

Guide

Enterprise RAG Architecture: The Full 2026 Blueprint

A practitioner's blueprint for enterprise RAG in 2026: ingestion, chunking, embedding, retrieval, rerank, generation, and the eval loop that keeps it honest.

Guide

RAG Evaluation Metrics: Recall@k, MRR, Faithfulness, and More

The RAG evaluation metrics that matter: recall@k and MRR for retrieval, faithfulness and answer relevance for generation, and how to build the eval loop.

Still stuck, or tired of fighting your own infrastructure?

Netray deploys and operates on-prem AI for regulated manufacturers and defense suppliers. We have debugged this stack in production, on air-gapped networks, at scale.