All four layouts for these rows and dimensions
Table and index size, MB. Bars missing = can't be indexed at this dimension.
- Table
- Index
How the estimate works#
PostgreSQL stores everything in 8 KB pages, and pgvector index tuples can't span pages. That makes size a packing problem, not just "rows × bytes":
- Value size:
vector= 4 × dims + 8 bytes;halfvec= 2 × dims + 8; binary-quantizedbit= dims / 8 + 8. - HNSW: each row is an element tuple (72-byte header + value) plus a neighbour tuple of 6-byte pointers — 2 × m slots on layer 0 and m more per extra layer. pgvector keeps the two together on one page when they fit. A 1,536-dim
vectorelement is ~6.4 KB with its neighbours, so only one fits per page and ~1.7 KB per page is unused. Halving tohalfvecfits two per page. - IVFFlat: one index tuple per row (8-byte header + value), grouped by list, plus one list tuple holding each centre.
- Table: values over ~2 KB are moved out of line into TOAST in 1,996-byte chunks, four chunks per page; the heap row keeps an 18-byte pointer.
Accuracy against real indexes#
The model is the same JavaScript file this page runs. At build time we execute it against every table and index from our benchmarks and print the difference:
| Relation | Measured | Model | Error |
|---|---|---|---|
| HNSW, vector(1536), m 8, 100K rows | 781 MB | 781 MB | +0.0% |
| HNSW, vector(1536), m 16, 100K rows | 781 MB | 781 MB | +0.0% |
| HNSW, vector(1536), m 32, 100K rows | 781 MB | 781 MB | +0.0% |
| HNSW, halfvec(1536), m 16, 100K rows | 391 MB | 391 MB | +0.0% |
| HNSW, bit(1536), m 16, 100K rows | 48 MB | 47 MB | -2.7% |
| HNSW, halfvec(3072), m 16, 99K rows | 773 MB | 773 MB | +0.0% |
| HNSW, bit(3072), m 16, 99K rows | 66 MB | 65 MB | -0.4% |
| HNSW, vector(1536), m 16, 100,000 rows | 781 MB | 781 MB | +0.0% |
| HNSW, vector(1536), m 16, 250,000 rows | 1.91 GB | 1.91 GB | +0.0% |
| HNSW, vector(1536), m 16, 500,000 rows | 3.81 GB | 3.81 GB | +0.0% |
| IVFFlat, vector(1536), 100 lists, 100,000 rows | 782 MB | 783 MB | +0.1% |
| IVFFlat, vector(1536), 316 lists, 100,000 rows | 784 MB | 786 MB | +0.3% |
| IVFFlat, vector(1536), 1000 lists, 100,000 rows | 789 MB | 797 MB | +1.0% |
| IVFFlat, vector(1536), 500 lists, 500,000 rows | 3.82 GB | 3.82 GB | +0.1% |
| IVFFlat, vector(1536), 707 lists, 500,000 rows | 3.82 GB | 3.83 GB | +0.1% |
| IVFFlat, vector(1536), 2000 lists, 500,000 rows | 3.83 GB | 3.85 GB | +0.4% |
| Table, vector(1536), 100K rows | 795 MB | 795 MB | -0.1% |
| Table, halfvec(1536), 100K rows | 400 MB | 400 MB | -0.1% |
| Table, vector(3072), 99K rows | 1.28 GB | 1.34 GB | +4.9% |
| Table, vector(1536), 100,000 rows | 795 MB | 795 MB | -0.1% |
| Table, vector(1536), 250,000 rows | 1.94 GB | 1.94 GB | -0.1% |
| Table, vector(1536), 500,000 rows | 3.88 GB | 3.88 GB | -0.1% |
Measured with pg_relation_size() (indexes) and pg_table_size() (tables, including TOAST) on PostgreSQL 18.6 + pgvector 0.8.7. Real tables also carry your other columns, dead tuples between vacuums, and fill-factor slack — budget 10–20% on top.
Memory: building and serving#
Building. HNSW builds fastest when the whole graph fits in maintenance_work_mem. When it doesn't, pgvector logs a notice and continues much more slowly — 2.9× slower in our test. The calculator's memory figure is scaled from that measurement: at 64 MB the graph held 9,753 1,536-dim elements, about 6,881 bytes each.
Serving. HNSW search hops between random pages, so latency stays low only while the index is cached in memory. Plan RAM for the index size (via shared_buffers plus the OS page cache), and more if queries also read full vectors for re-ranking.
Frequently asked questions
How big is an HNSW index in pgvector?
For 1,536-dimension vector embeddings with default settings, about 8 KB per row: each element tuple (~6.2 KB) plus its neighbour list fills most of a page on its own. We measured 781 MB for 100,000 rows. A halfvec index fits two elements per page (391 MB), and a binary-quantized index many more (48 MB).
How much maintenance_work_mem do I need to build an HNSW index?
Enough to hold the whole graph in memory. Our build with the 64 MB default reported the graph no longer fit after 9,753 of 100,000 tuples — roughly 6,881 bytes per 1,536-dim element. The calculator scales that to your row count and dimensions.