Close Menu
Techy101 –

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    In this age of pervasive slop maybe we should be grateful for Shrek 2 Remastered

    October 21, 2026

    What Can You Use Your Apple Watch Ultra 4 Action Button For?

    October 21, 2026

    pgvector vs. Dedicated Vector Databases: A Production Benchmark

    October 21, 2026
    Facebook X (Twitter) Instagram
    Trending
    • In this age of pervasive slop maybe we should be grateful for Shrek 2 Remastered
    • What Can You Use Your Apple Watch Ultra 4 Action Button For?
    • pgvector vs. Dedicated Vector Databases: A Production Benchmark
    • This ESP32 CYD project turns your display into a Flock camera scanner
    • Ubisoft’s Extreme Sports Playground Launches On Switch 2 This December
    • Spotify Launches Paid Fan Memberships for Podcast Creators
    • This robot vacuum washes, dries and empties itself, and it’s £200 off
    • SpaceX Q3 Revenue Rises 161% as Super Intelligence Swings to Profit – Unite.AI
    Facebook X (Twitter) Instagram Pinterest YouTube LinkedIn TikTok
    Techy101 –Techy101 –
    • Home
    • Laptops
    • Mobiles
    • Gaming
    • Gadgets
    • Apps
    • AI
    • How To
    • Reviews
    Techy101 –
    Home»AI»pgvector vs. Dedicated Vector Databases: A Production Benchmark
    AI

    pgvector vs. Dedicated Vector Databases: A Production Benchmark

    By RepublisherOctober 21, 2026No Comments13 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Reddit Telegram Email
    pgvector vs. Dedicated Vector Databases: A Production Benchmark
    Share
    Facebook Twitter LinkedIn Pinterest Email


    In this article, you will learn how pgvector compares to dedicated vector databases in terms of real, measured performance, and how to decide which approach suits your production workload.

    Topics we will cover include:

    • What pgvector and dedicated vector databases are, and how they differ architecturally.
    • How to set up and run a hands-on benchmark comparing pgvector and Qdrant on your own machine.
    • What production-scale benchmark data actually shows about throughput, latency, and the tradeoffs between the two approaches.

    A team adds pgvector to their existing Postgres database on a Tuesday afternoon, embeddings and relational data sitting in the same table, one query doing both the filter and the similarity search. Six months later, a vendor pitch lands in their inbox insisting they need to migrate to a “real” vector database before they hit a wall. No benchmark accompanies the claim in either direction, just confidence. That gap — strong opinions with no numbers behind them — is exactly what this article exists to close.

    What pgvector Actually Is

    pgvector is an open-source Postgres extension, currently at version 0.8.6 and supporting Postgres 13 and later, according to its official GitHub repository. It adds a native vector column type to Postgres, along with exact and approximate nearest-neighbor search, directly inside the database you’re likely already running.

    What that means in practice: a vector embedding lives in the same table as the record it describes, in the same row if you want, queried with the same SQL you already know. There’s no separate service to stand up, no second system to keep in sync, no network hop between your relational data and your vector data. pgvector supports single-precision, half-precision, binary, and sparse vector types, and five distance functions — L2, inner product, cosine, L1, and Hamming — covering essentially every distance metric a real embedding model uses.

    What a Dedicated Vector Database Is

    A dedicated vector database is a separate, purpose-built service, run as its own process with its own storage engine, designed from the ground up around vector search rather than having it added onto a general-purpose database. It’s a genuinely different architectural choice, not just a different product, since it means a new system to deploy, monitor, back up, and keep synchronized with whatever holds your actual source of truth.

    Here’s a plain rundown of the five most commonly reached for:

    Database
    Best for

    Pinecone
    A fully managed, zero-setup cloud database, best for low-ops enterprise scale

    Qdrant
    A high-performance Rust-based database, best for fast queries with complex JSON payload filtering

    Weaviate
    An open-source, AI-native system, best for built-in vectorization and hybrid BM25 search

    Milvus
    A distributed, GPU-accelerated database, best for massive, billion-scale enterprise workloads

    ChromaDB
    A lightweight, embedded database, best for local prototyping and Python development

    Prerequisites

    Everything in this tutorial runs entirely on your own machine — no cloud account required. You’ll need Docker and Docker Compose installed, Python 3.10 or later, roughly 8GB of free RAM, and basic comfort reading SQL and Python. The hands-on benchmark below is sized to run comfortably on a laptop.

    Setting Up pgvector Locally

    Start with a Docker Compose file that runs Postgres with pgvector already built in:

    # docker-compose.yml
    services:
    postgres:
    image: pgvector/pgvector:pg17
    ports:
    – “5432:5432″
    environment:
    – POSTGRES_PASSWORD=benchmark
    – POSTGRES_DB=vectorbench

    # docker-compose.yml

    services:

      postgres:

        image: pgvector/pgvector:pg17

        ports:

          – “5432:5432”

        environment:

          – POSTGRES_PASSWORD=benchmark

          – POSTGRES_DB=vectorbench

    Run docker compose up -d, then connect and set up the extension and table:

    import psycopg2

    conn = psycopg2.connect(
    host=”localhost”, port=5432, dbname=”vectorbench”,
    user=”postgres”, password=”benchmark”,
    )
    conn.autocommit = True
    cur = conn.cursor()

    # Enables the vector type and index access methods for this database
    cur.execute(“CREATE EXTENSION IF NOT EXISTS vector;”)

    # 1536 dimensions matches OpenAI’s text-embedding-3-small, a realistic
    # stand-in for a genuine production embedding model
    cur.execute(“””
    CREATE TABLE IF NOT EXISTS items (
    id bigserial PRIMARY KEY,
    embedding vector(1536)
    );
    “””)

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    import psycopg2

     

    conn = psycopg2.connect(

        host=“localhost”, port=5432, dbname=“vectorbench”,

        user=“postgres”, password=“benchmark”,

    )

    conn.autocommit = True

    cur = conn.cursor()

     

    # Enables the vector type and index access methods for this database

    cur.execute(“CREATE EXTENSION IF NOT EXISTS vector;”)

     

    # 1536 dimensions matches OpenAI’s text-embedding-3-small, a realistic

    # stand-in for a genuine production embedding model

    cur.execute(“”“

        CREATE TABLE IF NOT EXISTS items (

            id bigserial PRIMARY KEY,

            embedding vector(1536)

        );

    ““”)

    pgvector/pgvector:pg17 is the official Docker image that bundles the extension with Postgres 17, so there’s no separate compile step. CREATE EXTENSION IF NOT EXISTS vector is the one-time step that registers the vector type and its index methods in this specific database, per the pgvector documentation.

    Setting Up Qdrant Locally

    For the dedicated-database side of the comparison, run Qdrant with a single Docker command:

    docker run -d –name qdrant-bench -p 6333:6333 -p 6334:6334 qdrant/qdrant

    docker run –d —name qdrant–bench –p 6333:6333 –p 6334:6334 qdrant/qdrant

    Then connect from Python and create a matching collection:

    from qdrant_client import QdrantClient
    from qdrant_client.models import Distance, VectorParams

    qdrant = QdrantClient(url=”http://localhost:6333″)

    qdrant.create_collection(
    collection_name=”items”,
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
    )

    from qdrant_client import QdrantClient

    from qdrant_client.models import Distance, VectorParams

     

    qdrant = QdrantClient(url=“http://localhost:6333”)

     

    qdrant.create_collection(

        collection_name=“items”,

        vectors_config=VectorParams(size=1536, distance=Distance.COSINE),

    )

    Qdrant is worth picking specifically for this comparison because it’s the subject of the most detailed, methodologically transparent public benchmark against pgvector available, which this article draws on directly in a later section. VectorParams(size=1536, distance=Distance.COSINE) mirrors the same dimensionality and distance metric used in the pgvector table above, which matters — a fair comparison needs both systems measuring the same thing.

    Generating and Loading a Real Test Dataset

    With both systems running, generate a realistic set of synthetic vectors and load them into each:

    import numpy as np

    NUM_VECTORS = 100_000
    DIM = 1536

    rng = np.random.default_rng(seed=42)
    vectors = rng.normal(size=(NUM_VECTORS, DIM)).astype(np.float32)

    # Real embedding models produce normalized vectors, so normalize
    # these to length 1 for a realistic cosine-distance comparison
    vectors /= np.linalg.norm(vectors, axis=1, keepdims=True)

    import numpy as np

     

    NUM_VECTORS = 100_000

    DIM = 1536

     

    rng = np.random.default_rng(seed=42)

    vectors = rng.normal(size=(NUM_VECTORS, DIM)).astype(np.float32)

     

    # Real embedding models produce normalized vectors, so normalize

    # these to length 1 for a realistic cosine-distance comparison

    vectors /= np.linalg.norm(vectors, axis=1, keepdims=True)

    Load into pgvector using pgvector-python’s adapter, which lets psycopg2 accept NumPy arrays directly:

    from pgvector.psycopg2 import register_vector
    from psycopg2.extras import execute_values

    register_vector(conn)

    data = [(v,) for v in vectors]
    execute_values(cur, “INSERT INTO items (embedding) VALUES %s”, data, template=”(%s)”)

    from pgvector.psycopg2 import register_vector

    from psycopg2.extras import execute_values

     

    register_vector(conn)

     

    data = [(v,) for v in vectors]

    execute_values(cur, “INSERT INTO items (embedding) VALUES %s”, data, template=“(%s)”)

    Load the same vectors into Qdrant in batches:

    from qdrant_client.models import PointStruct
    batch_size = 1000
    for start in range(0, NUM_VECTORS, batch_size):
    batch = vectors[start:start + batch_size]
    points = [
    PointStruct(id=start + i, vector=batch[i].tolist())
    for i in range(len(batch))
    ]
    qdrant.upsert(collection_name=”items”, points=points)

    from qdrant_client.models import PointStruct

    batch_size = 1000

    for start in range(0, NUM_VECTORS, batch_size):

        batch = vectors[start:start + batch_size]

        points = [

            PointStruct(id=start + i, vector=batch[i].tolist())

            for i in range(len(batch))

        ]

        qdrant.upsert(collection_name=“items”, points=points)

    register_vector(conn) is the key line on the pgvector side — it teaches psycopg2 how to convert a NumPy array into the format pgvector expects, so execute_values can insert raw arrays without manual string formatting. On the Qdrant side, upsert is batched deliberately at 1,000 vectors per call rather than one giant request, since Qdrant’s client, like most vector database clients, performs better with reasonably sized batches than either single inserts or one massive payload.

    Building the Indexes

    Create an HNSW index in pgvector, straight from pgvector’s own indexing documentation:

    cur.execute(
    “CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops) “
    “WITH (m = 16, ef_construction = 64);”
    )

    cur.execute(

        “CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops) “

        “WITH (m = 16, ef_construction = 64);”

    )

    Apply matching HNSW parameters in Qdrant, so both systems are running the same underlying algorithm with the same settings:

    from qdrant_client.models import HnswConfigDiff

    qdrant.update_collection(
    collection_name=”items”,
    hnsw_config=HnswConfigDiff(m=16, ef_construct=64),
    )

    from qdrant_client.models import HnswConfigDiff

     

    qdrant.update_collection(

        collection_name=“items”,

        hnsw_config=HnswConfigDiff(m=16, ef_construct=64),

    )

    m controls the maximum number of connections per node in the HNSW graph, and ef_construction controls how thorough the search is while building it. Both are set to 16 and 64 here — pgvector’s own defaults — deliberately mirrored on the Qdrant side. This matters more than it might seem: comparing two systems running different index parameters isn’t a benchmark, it’s two separate, incomparable results. Matching them is what makes the numbers in the next section mean anything.

    Running the Benchmark

    Here’s the actual measurement script, querying both systems with a fresh, held-out set of query vectors that weren’t part of the loaded dataset:

    import time
    import statistics

    query_vectors = rng.normal(size=(200, DIM)).astype(np.float32)
    query_vectors /= np.linalg.norm(query_vectors, axis=1, keepdims=True)

    def benchmark_pgvector(cur, query_vectors, ef_search=64, limit=10):
    cur.execute(f”SET hnsw.ef_search = {ef_search};”)
    latencies = []
    for qv in query_vectors:
    start = time.perf_counter()
    cur.execute(
    “SELECT id FROM items ORDER BY embedding %s LIMIT %s”,
    (qv, limit),
    )
    cur.fetchall()
    latencies.append((time.perf_counter() – start) * 1000) # ms
    return latencies

    def benchmark_qdrant(client, query_vectors, limit=10):
    latencies = []
    for qv in query_vectors:
    start = time.perf_counter()
    client.query_points(collection_name=”items”, query=qv.tolist(), limit=limit)
    latencies.append((time.perf_counter() – start) * 1000)
    return latencies

    def summarize(name, latencies, total_time):
    latencies_sorted = sorted(latencies)
    p50 = statistics.median(latencies_sorted)
    p95 = latencies_sorted[int(len(latencies_sorted) * 0.95)]
    p99 = latencies_sorted[int(len(latencies_sorted) * 0.99)]
    qps = len(latencies) / total_time
    print(f”{name}: p50={p50:.2f}ms p95={p95:.2f}ms p99={p99:.2f}ms QPS={qps:.1f}”)

    start = time.perf_counter()
    pg_latencies = benchmark_pgvector(cur, query_vectors)
    summarize(“pgvector”, pg_latencies, time.perf_counter() – start)

    start = time.perf_counter()
    qdrant_latencies = benchmark_qdrant(qdrant, query_vectors)
    summarize(“Qdrant”, qdrant_latencies, time.perf_counter() – start)

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    import time

    import statistics

     

    query_vectors = rng.normal(size=(200, DIM)).astype(np.float32)

    query_vectors /= np.linalg.norm(query_vectors, axis=1, keepdims=True)

     

    def benchmark_pgvector(cur, query_vectors, ef_search=64, limit=10):

        cur.execute(f“SET hnsw.ef_search = {ef_search};”)

        latencies = []

        for qv in query_vectors:

            start = time.perf_counter()

            cur.execute(

                “SELECT id FROM items ORDER BY embedding %s LIMIT %s”,

                (qv, limit),

            )

            cur.fetchall()

            latencies.append((time.perf_counter() – start) * 1000)  # ms

        return latencies

     

    def benchmark_qdrant(client, query_vectors, limit=10):

        latencies = []

        for qv in query_vectors:

            start = time.perf_counter()

            client.query_points(collection_name=“items”, query=qv.tolist(), limit=limit)

            latencies.append((time.perf_counter() – start) * 1000)

        return latencies

     

    def summarize(name, latencies, total_time):

        latencies_sorted = sorted(latencies)

        p50 = statistics.median(latencies_sorted)

        p95 = latencies_sorted[int(len(latencies_sorted) * 0.95)]

        p99 = latencies_sorted[int(len(latencies_sorted) * 0.99)]

        qps = len(latencies) / total_time

        print(f“{name}: p50={p50:.2f}ms  p95={p95:.2f}ms  p99={p99:.2f}ms  QPS={qps:.1f}”)

     

    start = time.perf_counter()

    pg_latencies = benchmark_pgvector(cur, query_vectors)

    summarize(“pgvector”, pg_latencies, time.perf_counter() – start)

     

    start = time.perf_counter()

    qdrant_latencies = benchmark_qdrant(qdrant, query_vectors)

    summarize(“Qdrant”, qdrant_latencies, time.perf_counter() – start)

    A few things worth understanding here. The 200 query vectors are freshly generated, not pulled from the 100,000 already loaded, since querying with a vector that’s literally already in the index trivially returns itself at zero distance, which would make the benchmark meaningless.

    SET hnsw.ef_search = 64 controls how thorough pgvector’s search is at query time — higher values trade speed for better recall, and setting it explicitly rather than relying on the default makes the test reproducible. embedding %s is pgvector’s cosine distance operator, matching the Distance.COSINE setting used on the Qdrant collection. And client.query_points() is Qdrant’s current recommended search method, replacing the older search() call in recent versions of the client. The summarize function computes the same three percentiles — p50, p95, p99 — plus queries-per-second for both systems, so the output is directly comparable line for line.

    A simple bar chart mockup showing p50, p95, and p99 latency for pgvector and Qdrant (click to enlarge)

    What the Local Results Actually Show

    Run this on a typical laptop and both systems will report latencies in the low single-digit to tens of milliseconds range, with the exact numbers depending heavily on your CPU, available RAM, and whatever else is running on your machine at the time. That variability is the honest point of this section: 100,000 vectors on a laptop is a fine way to learn the mechanics of both systems and confirm your setup works correctly, but it’s genuinely too small a scale to draw a real production conclusion from. Both systems are fast enough at this size that the difference between them barely matters yet. The real question — which one holds up as data and query load actually grow — needs numbers gathered at real production scale, not a laptop benchmark.

    How This Holds at Production Scale

    This is where Tiger Data’s published benchmark becomes genuinely useful, since it’s the most detailed, methodology-transparent public comparison available. They tested 50 million real Cohere embeddings at 768 dimensions, on matched AWS r6id.4xlarge hardware (16 vCPUs, 128GB RAM) for both systems, using a modified fork of the industry-standard ANN-Benchmarks tool.

    One important accuracy note before the numbers: this specific benchmark used Postgres with pgvector plus pgvectorscale — Timescale’s own additional extension that adds a different index type called StreamingDiskANN — not vanilla pgvector alone. That distinction gets blurred in a lot of secondary coverage of this benchmark, and it matters, since the numbers below reflect that combined setup.

    Metric
    pgvector + pgvectorscale
    Qdrant

    p50 latency @ 99% recall
    31.07 ms
    30.75 ms

    p95 latency @ 99% recall
    60.42 ms
    36.73 ms

    p99 latency @ 99% recall
    74.60 ms
    38.71 ms

    Throughput @ 99% recall
    471.57 QPS
    41.47 QPS

    p50 latency @ 90% recall
    9.54 ms
    4.74 ms

    Throughput @ 90% recall
    1,589.79 QPS
    360.81 QPS

    Index build time (50M vectors)
    11.1 hours
    3.3 hours

    The pattern that emerges is genuinely two-sided, not a clean win for either system. At 99% recall, Postgres with pgvectorscale delivered 11.4 times the query throughput of Qdrant — 471.57 QPS against 41.47. Qdrant, in turn, held a real, measurable advantage on tail latency: 48% better p99 (38.71ms vs. 74.60ms), meaning its slowest queries stayed noticeably faster even though its overall capacity was lower. Qdrant also built its index roughly 3.4 times faster. Both systems kept every measured percentile under 100 milliseconds at 99% recall on a 50-million-vector dataset, which is itself worth sitting with — since it directly undercuts the assumption that a general-purpose database can’t keep up at real scale.

    When pgvector Is Good Enough

    Scale under roughly 5 to 10 million vectors is comfortably within pgvector’s range on reasonable hardware. A genuine need for transactional consistency between your relational data and its embeddings — updating a document and its vector in the same atomic transaction — is a real structural advantage pgvector has that a separate database simply can’t offer. An existing Postgres investment, a team that already knows how to operate, monitor, and back it up, means adding vector search costs an extension and an index, not a new system. And moderate query concurrency, rather than thousands of simultaneous queries per second, keeps you well within what a single well-tuned Postgres instance handles comfortably.

    When to Reach for a Dedicated Vector Database

    The other side of the line is equally concrete. Genuinely massive scale — hundreds of millions to billions of vectors — is where purpose-built distributed architectures earn their operational cost. A hard tail-latency SLA under heavy concurrent filtered search, the kind of requirement where a p99 above 25 to 40 milliseconds is a real problem, favors Qdrant’s demonstrated tail-latency advantage specifically. GPU-accelerated indexing needs point toward Milvus, the one system on the list built around that from the ground up. And a genuine organizational preference for zero-operations managed infrastructure — no database expertise on the team, no appetite for running and tuning anything — is exactly what a fully managed service like Pinecone exists to remove from your plate entirely.

    Conclusion

    The benchmark data in this article doesn’t support either of the clean stories people tend to reach for — “pgvector is always fine” or “dedicated databases always win.” It shows two systems with genuinely different tradeoffs measured under matched, transparent conditions: one wins decisively on throughput, the other wins decisively on tail latency, and both comfortably clear the bar most real applications actually need. The honest answer to “which one should I use” was never going to be a single name. It’s whichever side of that tradeoff your actual workload — not a vendor’s pitch — actually sits on.



    Source link

    Benchmark Databases dedicated pgvector Production Vector
    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Previous ArticleThis ESP32 CYD project turns your display into a Flock camera scanner
    Next Article What Can You Use Your Apple Watch Ultra 4 Action Button For?
    Republisher
    • Website

    Related Posts

    AI

    SpaceX Q3 Revenue Rises 161% as Super Intelligence Swings to Profit – Unite.AI

    October 21, 2026
    AI

    OpenAI Acquires Orchestra Bio to Expand Rosalind Life Sciences Tools – Unite.AI

    October 20, 2026
    AI

    NVIDIA Releases Cosmos3-H-Dreams Real-Time Surgical World Model – Unite.AI

    October 20, 2026
    Add A Comment
    Leave A Reply Cancel Reply

    Top Posts

    In this age of pervasive slop maybe we should be grateful for Shrek 2 Remastered

    October 21, 2026

    AMD is apparently gearing up to raise GPU prices right after Nvidia’s steep hike

    August 1, 2026

    LanceDB Vector Database Guide: Features anndPython Demo

    August 1, 2026
    Stay In Touch
    • Facebook
    • YouTube
    • TikTok
    • WhatsApp
    • Twitter
    • Instagram
    Latest Reviews

    Subscribe to Updates

    Get the latest tech news from FooBar about tech, design and biz.

    Latest Post

    In this age of pervasive slop maybe we should be grateful for Shrek 2 Remastered

    October 21, 2026

    AMD is apparently gearing up to raise GPU prices right after Nvidia’s steep hike

    August 1, 2026

    LanceDB Vector Database Guide: Features anndPython Demo

    August 1, 2026
    Recent Posts
    • In this age of pervasive slop maybe we should be grateful for Shrek 2 Remastered
    • What Can You Use Your Apple Watch Ultra 4 Action Button For?
    • pgvector vs. Dedicated Vector Databases: A Production Benchmark
    • This ESP32 CYD project turns your display into a Flock camera scanner
    • Ubisoft’s Extreme Sports Playground Launches On Switch 2 This December

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    Facebook X (Twitter) Instagram Pinterest YouTube LinkedIn TikTok
    • About Us
    • Contact Us
    • Privacy Policy
    • Terms & Conditions
    • Disclaimer
    © 2026 techy101. Designed by Pro.

    Type above and press Enter to search. Press Esc to cancel.