env.dev

pgvector: Using Postgres as a Vector Database

pgvector adds vector similarity search to Postgres: HNSW indexes, working SQL for RAG, benchmark numbers vs Pinecone, and when a dedicated store wins.

By env.dev Updated

pgvector (22,000+ GitHub stars, available on every major managed-Postgres provider) turns the database you already run into a vector store: an embedding vector(1536) column, an HNSW index, and nearest-neighbor search with an ORDER BY embedding <=> query. The architectural win is colocation — embeddings live in the same rows as the data they describe, so a RAG query is one SQL statement with ordinary WHERE filters instead of a fan-out to a second system plus the pipeline that keeps them in sync. The performance story holds up too: on 50 million 768-dimension embeddings, Postgres with the pgvectorscale extension benchmarked 28× lower p95 latency and 16× higher throughput than Pinecone's storage-optimized index at 99% recall, at 75% lower monthly cost ($835 vs $3,241 self-hosted) — vendor-run numbers, but with published methodology.

TL;DR

  • CREATE EXTENSION vector, add a vector(n) column, build an HNSW index — vector search with no new infrastructure.
  • Colocation removes the sync pipeline: similarity search joins directly against tenant filters, permissions, and metadata in one query.
  • pgvector supports up to 16,000 dimensions, six distance operators, and both HNSW (query speed) and IVFFlat (build speed) indexes.
  • pgvectorscale adds StreamingDiskANN (from Microsoft's DiskANN research) for Pinecone-class performance at a fraction of the cost.
  • Dedicated vector databases start making sense at billions of vectors or extreme multi-tenant sharding — not at the scale of a typical RAG application.

Why keep embeddings in Postgres?

Because embeddings are column data, not a separate universe. Every vector you store describes a row you already have — a document chunk, a product, a support ticket. Put it in a dedicated vector database and you inherit the classic two-system problems: a pipeline to keep vectors in sync with source rows, tombstones for deleted content that still ranks in search, and permission filters reimplemented in a second query language. Keep it in Postgres and a deleted row's embedding disappears in the same transaction, and "only documents this user may see" is the WHERE clause it always was. This is the Just Use Postgres argument at its strongest, because vector workloads are almost never standalone — they are joins waiting to happen.

How do you set up pgvector?

Three statements: enable the extension, add the column sized to your embedding model, index it. HNSW is the default index choice — slower to build than IVFFlat, faster and more accurate to query:

pgvector setup for OpenAI text-embedding-3-small (1536 dims)
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE chunks (
  id        bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  doc_id    bigint NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
  content   text NOT NULL,
  embedding vector(1536) NOT NULL
);

CREATE INDEX chunks_embedding_idx ON chunks
  USING hnsw (embedding vector_cosine_ops);

-- 10 nearest chunks by cosine distance
SELECT content, embedding <=> $1 AS distance
FROM chunks
ORDER BY embedding <=> $1
LIMIT 10;

The operator must match the index opclass: <=> is cosine distance (vector_cosine_ops), <-> is L2 (vector_l2_ops), and <#> is negative inner product. Standard vectors go up to 16,000 dimensions; halfvec halves storage for models that tolerate it. ON DELETE CASCADE is the entire "keep the index consistent" story — there is no second system to reconcile.

What does a real RAG query look like?

Production retrieval is never "nearest ten vectors, period" — it is nearest ten among rows this tenant may see, from documents that are published, weighted toward recent content. In SQL that is one statement:

Filtered retrieval — the query dedicated vector stores make hard
SELECT c.content, d.title,
       c.embedding <=> $1 AS distance
FROM chunks c
JOIN documents d ON d.id = c.doc_id
WHERE d.tenant_id = $2
  AND d.status = 'published'
ORDER BY c.embedding <=> $1
LIMIT 10;

One gotcha worth knowing: an HNSW index scan walks the graph first and applies your filter afterwards, so a highly selective WHERE can leave fewer than LIMIT results. Recent pgvector releases added iterative index scans that keep walking until the limit is satisfied — if filtered recall matters, test on your real filter selectivity. For keyword-plus-semantic hybrid search, combine this with the tsvector machinery and merge the two ranked lists — both live in the same database, so the merge is SQL, not application glue.

How does pgvector performance compare to Pinecone?

The reference numbers come from Timescale's published benchmark: 50 million Cohere embeddings (768 dimensions) on a single r6id.4xlarge EC2 instance running Postgres with pgvectorscale, whose StreamingDiskANN index descends from Microsoft's DiskANN research. Against Pinecone's storage-optimized index at 99% recall: 28× lower p95 latency, 16× higher throughput, $835 vs $3,241 per month. Against Pinecone's performance-optimized tier at 90% recall the gap narrows to 1.4×/1.5× — still ahead, still 75–79% cheaper. Treat vendor benchmarks with the usual skepticism, but the direction is corroborated by how many providers now ship pgvector by default: the "Postgres can't do vectors seriously" claim is a 2023 take.

For plain pgvector HNSW, the tuning surface is small: m and ef_construction at build time, hnsw.ef_search per session to trade recall against speed. Most applications never touch the defaults.

When do you need a dedicated vector database?

  • Billions of vectors — index build times and memory pressure on a single Postgres node become the constraint; purpose-built distributed stores shard this transparently.
  • Extreme multi-tenant isolation — thousands of tenants each needing isolated indexes and per-tenant scaling is an operational pattern serverless vector platforms sell directly.
  • Vector search with no relational data at all — if there is genuinely nothing to join against and no Postgres in the stack, colocation buys you nothing.

The typical RAG application — thousands to low millions of chunks, filters on every query, embeddings tied to rows that change — sits squarely in Postgres territory. Same rule as the rest of the series: escalate on a measured limit, not a hypothetical one.

References

This closes the Postgres is Enough series: the consolidation argument, queues, search, and vectors — one database for all four.

Was this helpful?
Series · Part 4 of 4

Postgres is Enough

A series on Postgres as the default for queues, caching, search, vectors, and more.

  1. Part 1

    Just Use Postgres: One Database for Almost Everything

  2. Part 2

    Postgres as a Message Queue with SKIP LOCKED

  3. Part 3

    Postgres Full-Text Search vs Elasticsearch

  4. Part 4 · You are here

    pgvector: Using Postgres as a Vector Database

Frequently Asked Questions

Is pgvector production-ready for AI applications?

Yes. pgvector has 22,000+ GitHub stars, ships on AWS RDS, Google Cloud SQL, Azure, Supabase, and Neon, and supports HNSW indexes with six distance operators. For most RAG and semantic-search workloads — thousands to low millions of embeddings with metadata filters — it is the boring, well-supported choice.

Should I use HNSW or IVFFlat?

HNSW for almost everything: better query speed and recall, and it handles data that changes over time gracefully. IVFFlat builds faster and uses less memory, but needs its lists rebalanced as data grows and trades away query performance. Start with HNSW and only revisit if index build time becomes a real problem.

How many dimensions can pgvector handle, and what should I use?

The vector type supports up to 16,000 dimensions (halfvec up to 4,000, binary vectors up to 64,000). In practice you match the embedding model: 1536 for OpenAI text-embedding-3-small, 768 for many open-source models. Smaller dimensions mean faster search and smaller indexes — shortening embeddings via a model that supports it often beats tuning the index.

Can I combine vector search with SQL filters like tenant_id?

Yes — that is the main reason to keep vectors in Postgres. Filters, joins, and permissions apply in the same query as the similarity ranking. Watch the recall gotcha: HNSW scans apply filters after walking the graph, so very selective filters can return fewer rows than LIMIT; recent pgvector versions mitigate this with iterative index scans.

Is pgvector cheaper than Pinecone?

Usually, substantially. Timescale's 50M-vector benchmark ran Postgres with pgvectorscale at $835/month self-hosted against $3,241/month for Pinecone's storage-optimized index — 75% less, while outperforming it at 99% recall. Below a few million vectors the difference is starker: pgvector rides on the database you already pay for.

Stay up to date

Get notified about new guides, tools, and cheatsheets.