Why pgvector similarity search is slow, and how to fix it without leaving Postgres
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
- 1Run EXPLAIN ANALYZE on the slow query and confirm it shows an index scan on the vector column, not a sequential scan.
- 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.
- 3For HNSW indexes, raise hnsw.ef_search for the session to trade a small amount of latency for recall.
- 4For IVFFlat indexes, raise ivfflat.probes since the default of 1 probe searches only a single list and misses most of the table.
- 5Run VACUUM ANALYZE on the table if it has had heavy inserts/updates/deletes, since bloat degrades both scan types.
- 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
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.
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops); EXPLAIN ANALYZE SELECT id FROM items ORDER BY embedding <=> '[...]' LIMIT 10;
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.
SELECT indexdef FROM pg_indexes WHERE tablename = 'items';
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.
SET hnsw.ef_search = 100; SET ivfflat.probes = 10;
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.
VACUUM ANALYZE items;
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.
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.
Size it properly next time
Free calculators that prevent this class of failure before you provision hardware.
Vector Database Sizing Calculator
Convert vector count, embedding dimensions, and precision into a real storage footprint, including index overhead and replica factor, before you pick a vector database.
Free ToolRAG Context Window Budget Calculator
Allocate your context window across system prompt, retrieved chunks, and conversation history, then see window utilization and the real cost of every RAG query.
Free ToolDocument Ingestion Pipeline Estimator
Estimate total pipeline time from document count, OCR share, and embedding throughput, so ingestion timelines stop being a guess in the project plan.
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.
Guidepgvector 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.
GuideEnterprise 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.
GuideRAG 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.