1. Home
  2. Benchmarks
Data · reproducible

pgvector benchmarks

We built every index ourselves on PostgreSQL 18.6 + pgvector 0.8.7 and measured it against exact search. Raw JSON and the Python scripts are below — rerun them on your hardware.

Tested on pgvector 0.8.7 · PostgreSQL 18.6Updated 2026-10-2714 min read
0.947recall@10 with all-default HNSW settings, 100K rowsmedian 2.9 ms
0.931same settings at 500K rowsmedian 4.1 ms
5.6 minHNSW build for 500K × 1,536 dims on 4 vCPUsindex 3.81 GB
246×faster than exact search at 500K rowsexact: 1016 ms

Method#

SoftwarePostgreSQL 18.6 and pgvector 0.8.7 from the PGDG apt repository, Ubuntu 24.04; FAISS 1.15.1 (faiss-cpu) for the comparison
Hardware4 vCPU Intel Xeon @ 2.10GHz (cloud VM), 15 GB RAM
Postgres settingsshared_buffers = 4GB, maintenance_work_mem = 4GB, max_parallel_maintenance_workers = 3, work_mem = 64MB, jit = off; everything else default
DataDBpedia entity abstracts embedded with OpenAI text-embedding-3-large at 1,536 dims (first 500,000 rows of Qdrant's 1M dataset) and at 3,072 dims (100K dataset, same articles)
Queries1,000 random articles from a shard never loaded into any table (for 3,072 dims: 1,000 rows held out of the 100K)
Ground truthExact top-100 by cosine distance in NumPy; verified identical to pgvector's own exact search
MetricRecall@10 = |returned ∩ true top-10| / 10, averaged over queries
LatencyWall-clock per query from Python (psycopg 3, prepared statements, binary vectors) over a Unix socket; 50 warm-up queries; one query in flight; p50/p95/p99 over 1,000 queries
LoadBinary COPY, then VACUUM ANALYZE, then CREATE INDEX
Build parallelismmax_parallel_maintenance_workers = 3 was set, but the table's parallel_workers option was not, so 100K-row builds ran as a single process (vectors live in TOAST, leaving the heap below the 8 MB threshold). The build chart shows the effect of setting it.

Read these as shapes, not promises. Numbers come from one 4-vCPU cloud VM, single runs for builds, warm caches, one client. Absolute latency on your hardware will differ; the trade-offs — how recall moves with ef_search, what memory does to build time — transfer.

HNSW#

Recall@10 vs ef_search (HNSW, m = 16, ef_construction = 64)

100,000 × 1,536-dim OpenAI embeddings, 1,000 held-out queries. pgvector's default ef_search is 40.

ef_searchRecall@10p50 latencyp95 latencyQPS (1 client)
100.8151.4 ms2.5 ms625
200.8971.9 ms3.2 ms482
400.9472.9 ms5.3 ms304
640.9703.9 ms5.9 ms246
1000.9835.3 ms8.4 ms181
2000.9909.1 ms14 ms106
4000.99516 ms24 ms61
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Effect of m on the recall/latency curve (ef_construction = 64)

Points are ef_search 10 → 400. Higher m lifts the recall ceiling at the same latency.

  • m = 8
  • m = 16
  • m = 32
mef_searchRecall@10p50
8100.6771.1 ms
8200.8111.6 ms
8400.8962.1 ms
8640.9282.8 ms
81000.9533.8 ms
82000.9765.8 ms
84000.98710 ms
16100.8151.4 ms
16200.8971.9 ms
16400.9472.9 ms
16640.9703.9 ms
161000.9835.3 ms
162000.9909.1 ms
164000.99516 ms
32100.8911.8 ms
32200.9462.6 ms
32400.9753.9 ms
32640.9865.4 ms
321000.9917.2 ms
322000.99513 ms
324000.99723 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Effect of ef_construction on the recall/latency curve (m = 16)

Points are ef_search 10 → 400. A better-built graph gives more recall per millisecond.

  • ef_construction = 32
  • ef_construction = 64
  • ef_construction = 128
  • ef_construction = 256
ef_constructionef_searchRecall@10p50
32100.7821.4 ms
32200.8741.9 ms
32400.9252.7 ms
32640.9493.7 ms
321000.9664.9 ms
322000.9798.1 ms
324000.98814 ms
64100.8151.4 ms
64200.8971.9 ms
64400.9472.9 ms
64640.9703.9 ms
641000.9835.3 ms
642000.9909.1 ms
644000.99516 ms
128100.8191.4 ms
128200.9062.0 ms
128400.9602.9 ms
128640.9794.0 ms
1281000.9895.4 ms
1282000.9969.4 ms
1284000.99817 ms
256100.8181.5 ms
256200.9102.0 ms
256400.9643.0 ms
256640.9834.3 ms
2561000.9915.9 ms
2562000.99710 ms
2564000.99819 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

HNSW build time by m and ef_construction

100,000 × 1,536 dims, maintenance_work_mem 4 GB. Single-process builds (no parallel workers launched — see the HNSW guide).

mef_constructionBuild timeIndex size
8642.0 min781 MB
16322.0 min781 MB
16643.4 min781 MB
32645.6 min781 MB
161285.9 min781 MB
1625610.0 min781 MB
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Analysis and recommendations: HNSW guide.

Scaling to 500K rows#

HNSW recall as the table grows (m 16, ef_construction 64)

Same 1,000 queries against 100K, 250K and 500K rows. Recall drops slowly with size at a fixed ef_search.

  • ef_search 40
  • ef_search 100
  • ef_search 200
Rowsef_searchRecall@10p50Build timeIndex size
100,000400.9482.8 ms3.3 min781 MB
100,0001000.9825.4 ms3.3 min781 MB
100,0002000.9908.9 ms3.3 min781 MB
250,000400.9383.1 ms2.6 min1.91 GB
250,0001000.9745.7 ms2.6 min1.91 GB
250,0002000.9869.7 ms2.6 min1.91 GB
500,000400.9314.1 ms5.6 min3.81 GB
500,0001000.9686.2 ms5.6 min3.81 GB
500,0002000.98211 ms5.6 min3.81 GB
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Exact scan vs HNSW: median query latency

Exact = no index, sequential scan (at 100K rows Postgres planned no parallel workers — the same TOAST effect as builds). HNSW at ef_search 40.

  • Exact scan
  • HNSW
RowsExact p50HNSW p50 (ef 40)Speed-up
100,000447 ms2.8 ms160×
250,000680 ms3.1 ms217×
500,0001016 ms4.1 ms246×
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Build settings#

HNSW build time: memory, parallel workers and load order

Same 100,000 × 1,536-dim vectors, same index (m 16, ef_construction 64). Workers launched were counted from pg_stat_activity.

SetupBuild timeParallel workers
Index first, then load 100K rows13.8 min0 workers (row-by-row inserts)
64 MB maintenance_work_mem (default)10.4 min0 workers launched
256 MB maintenance_work_mem8.3 min0 workers launched
4 GB, max_parallel_maintenance_workers = 33.4 min0 workers launched
4 GB + ALTER TABLE SET (parallel_workers = 1)1.8 min1 worker launched
4 GB + ALTER TABLE SET (parallel_workers = 3)56 s3 workers launched
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

IVFFlat#

IVFFlat recall@10 vs probes (100K rows)

pgvector's default is probes = 1. More lists means smaller lists, so each probe covers less of the table.

  • 100 lists
  • 316 lists
  • 1000 lists
listsprobesRecall@10p50p95
10010.6192.7 ms7.1 ms
10020.7634.3 ms9.1 ms
10040.8507.9 ms14 ms
10080.92116 ms27 ms
100160.96528 ms41 ms
100320.98554 ms75 ms
100640.998104 ms131 ms
31610.5501.4 ms2.4 ms
31620.6741.9 ms3.2 ms
31640.7783.1 ms4.9 ms
31680.8575.5 ms8.8 ms
316160.9129.9 ms15 ms
316320.95319 ms26 ms
316640.97835 ms49 ms
100010.4851.4 ms2.0 ms
100020.6131.5 ms2.0 ms
100040.7162.0 ms3.2 ms
100080.7972.7 ms4.1 ms
1000160.8604.4 ms6.9 ms
1000320.9107.4 ms12 ms
1000640.94713 ms18 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

IVFFlat recall vs latency at 500K rows

Points are probes 1 → 64. lists = rows/1000 (500) and √rows (707) are the usual rules of thumb.

  • 500 lists
  • 707 lists
  • 2000 lists
listsprobesRecall@10p50
50010.5535.0 ms
50020.6866.8 ms
50040.78010 ms
50080.85816 ms
500160.91330 ms
500320.95156 ms
500640.976111 ms
70710.5613.5 ms
70720.6764.6 ms
70740.7717.9 ms
70780.84314 ms
707160.90223 ms
707320.94143 ms
707640.97082 ms
200010.4993.8 ms
200020.6234.1 ms
200040.7225.9 ms
200080.7978.0 ms
2000160.85613 ms
2000320.90419 ms
2000640.94232 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Build time and index size: IVFFlat vs HNSW (100K rows)

Same table, maintenance_work_mem 4 GB, single-process builds.

IndexBuild timeSize
IVFFlat 100 lists9.8 s782 MB
IVFFlat 316 lists23 s784 MB
IVFFlat 1000 lists2.1 min789 MB
HNSW m16/efc643.4 min781 MB
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Analysis: IVFFlat guide.

halfvec and binary quantization#

Recall vs latency: vector, halfvec and binary quantization

100K × 1,536 dims, HNSW m 16 / ef_construction 64. Binary points vary the re-rank candidate count (20 → 640).

  • vector (float32)
  • halfvec (float16)
  • binary + re-rank
VariantSettingRecall@10p50
vectoref 100.8151.4 ms
vectoref 200.8971.9 ms
vectoref 400.9472.9 ms
vectoref 640.9703.9 ms
vectoref 1000.9835.3 ms
vectoref 2000.9909.1 ms
vectoref 4000.99516 ms
halfvecef 100.8191.4 ms
halfvecef 200.8961.8 ms
halfvecef 400.9492.5 ms
halfvecef 640.9683.5 ms
halfvecef 1000.9824.8 ms
halfvecef 2000.9917.8 ms
halfvecef 4000.99514 ms
binary + re-rank20 candidates0.8541.8 ms
binary + re-rank40 candidates0.9182.3 ms
binary + re-rank80 candidates0.9663.5 ms
binary + re-rank160 candidates0.9855.7 ms
binary + re-rank320 candidates0.99310 ms
binary + re-rank640 candidates0.99720 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

HNSW index size by representation

100,000 × 1,536 dims. Binary quantization indexes 1 bit per dimension; the full vectors stay in the table for re-ranking.

RepresentationTable sizeIndex sizeIndex build
vector(1536)795 MB781 MB3.4 min
halfvec(1536)400 MB391 MB2.9 min
bit(1536) via binary_quantize(uses vector table)48 MB39 s
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Binary quantization: recall vs re-rank candidates

Fetch N candidates by Hamming distance from the bit index, re-order them by exact cosine distance, keep 10.

  • 1,536 dims
  • 3,072 dims
DimsCandidatesRecall@10p50
1536200.8541.8 ms
1536400.9182.3 ms
1536800.9663.5 ms
15361600.9855.7 ms
15363200.99310 ms
15366400.99720 ms
3072400.9302.9 ms
3072800.9704.9 ms
30721600.9868.0 ms
30723200.99316 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Shortening text-embedding-3-large: top-10 overlap with full 3,072 dims

Exact search on truncated, re-normalised vectors vs exact search on the full vectors. 99,000 rows, 1,000 queries.

DimsTop-10 overlapvector byteshalfvec bytes
25658.4%1,032520
51273.2%2,0561,032
76880.0%3,0801,544
102484.1%4,1042,056
153689.0%6,1523,080
204891.9%8,2004,104
3072100.0%12,2966,152
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Analysis: quantization guide.

Filtered search#

Rows returned for LIMIT 10 with a WHERE filter (HNSW index)

100K rows, ef_search 40, no B-tree on the filter column. Without iterative scans, selective filters starve the result.

  • iterative_scan = off
  • iterative_scan = relaxed_order
Rows matchingModeAvg rowsRecall@10p50
50%off10.000.9392.9 ms
50%relaxed_order10.000.9392.9 ms
10%off3.990.3982.7 ms
10%relaxed_order10.000.9466.3 ms
1%off0.390.0392.7 ms
1%relaxed_order10.000.96737 ms
0.1%off0.030.0032.6 ms
0.1%relaxed_order10.000.79169 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Recall@10 with filters: four strategies

100K rows, HNSW ef_search 40, LIMIT 10. The planner's choice switched to a B-tree + exact sort at 10% and below.

  • HNSW, iterative_scan off
  • HNSW, relaxed_order
  • HNSW, strict_order
  • Planner's choice (B-tree present)
Rows matchingStrategyRecall@10Avg rowsp50p95
50%HNSW, iterative_scan off0.93910.002.9 ms4.3 ms
50%HNSW, relaxed_order0.93910.002.9 ms4.4 ms
50%HNSW, strict_order0.93910.002.9 ms5.5 ms
50%Planner's choice (B-tree present)0.93910.002.9 ms4.1 ms
10%HNSW, iterative_scan off0.3983.992.7 ms4.1 ms
10%HNSW, relaxed_order0.94610.006.3 ms9.7 ms
10%HNSW, strict_order0.89910.006.2 ms10 ms
10%Planner's choice (B-tree present)1.00010.0058 ms77 ms
1%HNSW, iterative_scan off0.0390.392.7 ms4.1 ms
1%HNSW, relaxed_order0.96710.0037 ms59 ms
1%HNSW, strict_order0.90110.0036 ms61 ms
1%Planner's choice (B-tree present)1.00010.006.3 ms12 ms
0.1%HNSW, iterative_scan off0.0030.032.6 ms4.0 ms
0.1%HNSW, relaxed_order0.79110.0069 ms88 ms
0.1%HNSW, strict_order0.77310.0070 ms86 ms
0.1%Planner's choice (B-tree present)1.00010.000.82 ms1.1 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Analysis: filtering guide.

pgvector vs FAISS#

pgvector HNSW vs FAISS HNSW, same data and parameters

100K × 1,536 dims, M 16, efConstruction 64; points are ef_search 10 → 400. pgvector latency includes SQL parsing, planning and the client round trip; FAISS is an in-process library call.

  • pgvector 0.8.7 (SQL, local socket)
  • FAISS 1.15.1 (in-process)
ef_searchpgvector recallpgvector p50FAISS recallFAISS p50
100.8151.4 ms0.7650.25 ms
200.8971.9 ms0.8710.35 ms
400.9472.9 ms0.9370.60 ms
640.9703.9 ms0.9640.85 ms
1000.9835.3 ms0.9771.1 ms
2000.9909.1 ms0.9892.0 ms
4000.99516 ms0.9943.7 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Analysis: comparison.

Raw data and scripts#

Everything on this page is generated from these files. Each JSON file includes the environment it was measured in.

ExperimentResultsScript
HNSW parameter sweep (100K)hnsw_100k.jsone1_hnsw.py, e1b_topup.py
Scaling 100K → 500Kscale.jsone2_scale.py
IVFFlat lists / probesivfflat.jsone3_ivfflat.py
halfvec and binary quantizationquantization.jsone4_quant.py
3,072 dimensions and shorteningdims3072.jsone5_3072.py
Filtering and iterative scansfiltering.jsone6_filter.py, e6b_forced.py
FAISS comparisonfaiss.jsone7_faiss.py
Build memory and parallelismbuild.jsone8_build.py, e8b_repeat.py, e9_scale_build.py
Hybrid search (full-text + vector)hybrid.jsonh1_hybrid.py, h2_queries.py
Home-page embedding mapembedding-map.jsonm1_map.py
Shared helpers, data preparation—common.py, prep.py, e0_gt.py

To reproduce: download the two Hugging Face datasets, run prep.py and e0_gt.py, create a database called bench with CREATE EXTENSION vector, then run each experiment script. Found a flaw in the method? Tell us.