Can One PostgreSQL Replace Your Graph Database and Your Vector Database?

We benchmarked Apache AGE + pgvector against Neo4j, Kuzu, and NebulaGraph across 12 workloads at three scales (10K, 100K, 1M) — then ran 16 graph algorithms head-to-head against Neo4j GDS. AGE won all 12 workloads at every scale. The Piggie SDK won 14/16 algorithm matchups against GDS. Here's how — and what it means for your architecture decisions.

The Thesis

The graph database market assumes you need separate systems: Neo4j for graph traversals, Pinecone or Weaviate for vector search, PostgreSQL for relational data. Each system adds operational overhead — separate backups, separate monitoring, separate failure modes, separate network hops for queries that need to combine signals from multiple stores.

Apache AGE is a PostgreSQL extension that adds openCypher graph query support. pgvector adds vector similarity search. Both run inside the same PostgreSQL process. Install them together and you get graph traversals, vector similarity search, full-text search, JSONB documents, PostGIS geospatial, and relational tables — all in one database, one transaction, one backup, one monitoring stack.

That's the theory. This post tests it with numbers.

What We Tested

The Databases

Database Version Deployment Query Language License
Apache AGE + pgvector 1.7.0 + 0.8.1 PostgreSQL 18 extension Cypher + SQL Apache 2.0
Neo4j Community 5.x Docker (standalone JVM) Cypher Modified AGPL
Kuzu 0.11.3 Embedded (pip) Cypher MIT (archived)
NebulaGraph Community 3.8.0 Docker (3-service cluster) nGQL Apache 2.0

A note on Kuzu: it was acquired by Apple in October 2025 and its GitHub repository was archived. We include it because it represents the embedded graph database category and its performance characteristics are instructive.

The Workloads

We designed 12 workloads covering the realistic spectrum of graph database usage:

Workload Description
W01 Node Creation (write) 100 nodes created one-by-one via Cypher/nGQL
W02 Edge Creation (write) 100 edges created one-by-one (MATCH + CREATE pattern)
W11 Bulk Load (write) 1,000 nodes + 5,000 edges loaded as fast as each database allows
W03 Point Lookup (read) Single node retrieval by property value
W04 Pattern Match (read) 2-hop traversal from a known start node
W05 Aggregation (read) GROUP BY count across all nodes by category
W06 Variable-Length Edges (read) VLE traversal 1..4 hops from a start node
W07 Shortest Path (read) Shortest path between two nodes (max 4 hops)
W08 PageRank (analytics) Degree-based ranking over the full graph
W12 Concurrent Queries (analytics) 50 point lookups across 4 threads
W09 Vector Search (vector) k-NN similarity search (k=10, 128 dimensions)
W10 Hybrid Search (vector) Vector search filtered by graph label

The Data

All databases received identical data generated from a deterministic seed (seed=42). The data models a social network:

Property Value
Node categories 5 (Person 40%, Product 15%, Post 20%, Company 10%, Location 15%)
Edge types 8 relationship types constrained by category
Vectors 128-dimensional per node (for databases that support it)
Edge-to-node ratio 5:1

Measurement Methodology

Each workload runs through: 1. 3 warm-up iterations (discarded) 2. 10 measured iterations 3. Statistics: p50, p95, p99, mean, standard deviation

System: Linux 6.17, 32 CPUs, 62 GB RAM, Python 3.13.

The Results: 100K Scale Head-to-Head

Workload AGE Kuzu NebulaGraph Neo4j
W01 Node Creation 9.7ms 18.6ms 41.9ms 151.4ms
W02 Edge Creation 0.83ms 105.6ms 47.5ms 130.0ms
W03 Point Lookup 0.26ms 2.6ms 1.1ms 0.76ms
W04 Pattern Match 0.14ms 7.5ms 4.0ms 2.6ms
W05 Aggregation 3.0ms 4.6ms 169.6ms 12.8ms
W06 VLE 0.34ms 2.6ms 4.0ms 1.5ms
W07 Shortest Path 0.33ms 4.9ms 1.7ms 0.97ms
W08 PageRank 26.6ms 85.1ms 755.6ms 197.2ms
W09 Vector Search 0.42ms N/S N/S 1.0ms
W10 Hybrid Search 0.42ms N/S N/S 1.3ms
W11 Bulk Load 51.8ms 6.00s 78.3ms 145.7ms
W12 Concurrent 13.2ms 238.5ms 567.3ms 194.0ms

Bold = best. N/S = not supported. Values are p50 latency.

AGE wins 12/12 workloads at 100K scale.

The 10K results tell the same story — AGE #1 across the board.

Scaling to 1M: AGE vs Neo4j Head-to-Head

At 1M nodes (5M edges), we ran AGE against Neo4j — the two databases that matter most at production scale. The margins widened.

Workload AGE Neo4j Speedup
W01 Node Creation 7.7ms 89.5ms 12x
W02 Edge Creation 0.84ms 90.2ms 107x
W03 Point Lookup 0.29ms 0.97ms 3x
W04 Pattern Match 0.14ms 1.8ms 13x
W05 Aggregation 15.3ms 108.5ms 7x
W06 VLE 0.18ms 0.62ms 3x
W07 Shortest Path 0.17ms 0.58ms 3x
W08 PageRank 345ms 2,169ms 6x
W09 Vector Search 0.50ms 1.1ms 2x
W10 Hybrid Search 0.43ms 0.90ms 2x
W11 Bulk Load 59.3ms 133.2ms 2x
W12 Concurrent 52.2ms 1,323ms 25x

AGE wins 12/12 at 1M scale. The biggest story at scale: concurrent query throughput. AGE's prepared SQL statements deliver 25x better latency under contention — 52ms vs 1.3 seconds. Write operations scale even better: edge creation is 107x faster at 1M than at 100K's 157x because AGE's COPY protocol has near-constant overhead while Neo4j's per-transaction cost compounds.

Algorithm Benchmarks: Piggie SDK vs Neo4j GDS

The workload benchmarks above test database primitives — reads, writes, traversals. But real graph applications also run algorithms: PageRank for ranking, betweenness centrality for influence detection, community detection for clustering. Neo4j's Graph Data Science (GDS) library is the benchmark here — 60+ algorithms running in-memory on the JVM with multi-threaded C++ kernels.

AGE has no built-in algorithm engine. The Piggie SDK fills this gap by loading graph data via direct SQL reads from AGE's internal tables into compiled backends: igraph (C), networkit (C++), and NetworkX (Python). We benchmarked 16 algorithms across 6 categories against Neo4j GDS at 10K scale.

The Results

Algorithm Category Piggie SDK Neo4j GDS Speedup
Degree Centrality Centrality 1.3ms 64ms 49x
Betweenness Centrality Centrality 678ms 710ms 1.0x
Closeness Centrality Centrality 154ms 1,810ms 12x
Eigenvector Centrality Centrality 25ms 126ms 5x
Harmonic Centrality Centrality 1,270ms 298ms 0.2x
Louvain Community 24ms 146ms 6x
Label Propagation Community 9ms 149ms 17x
Greedy Modularity Community 439ms 147ms 0.3x
Leiden Community 8ms 150ms 19x
PageRank Ranking 2ms 73ms 37x
Shortest Path Pathfinding 0.4ms 1.7ms 4x
All Shortest Paths Pathfinding 1.5ms 2.8ms 2x
Dijkstra Pathfinding 0.3ms 1.5ms 5x
Jaccard Similarity Similarity 3ms 89ms 30x
Adamic-Adar Similarity 4ms 91ms 23x
Common Neighbors Similarity 2ms 87ms 44x

Bold = best. Values are p50 latency. Backends: centrality uses igraph (C) + networkit (C++), community uses igraph (C), pathfinding and similarity use NetworkX.

Piggie wins 14/16 algorithm matchups against Neo4j GDS.

How It Works

The SDK uses a three-tier backend strategy:

  1. networkit (C++, multi-threaded) — preferred for closeness and harmonic centrality, where parallel traversal dominates
  2. igraph (C, single-threaded) — preferred for betweenness, community detection, PageRank, and eigenvector centrality
  3. NetworkX (Python) — fallback, and used for similarity and link prediction where the API is cleanest

Backend selection is automatic. The SDK detects installed backends and routes each algorithm to the fastest available implementation:

import piggie

db = piggie.connect("postgresql://localhost:5433/mydb", graph="social")

# Auto-selects igraph C backend
df = db.centrality(measure="betweenness")

# Auto-selects networkit C++ backend
df = db.centrality(measure="closeness")

# Force a specific backend
df = db.centrality(measure="betweenness", backend="networkx")

Graph loading uses direct SQL reads from AGE's internal label tables — the same id::text::bigint extraction pattern we use for the workload benchmarks. At 10K scale, loading into igraph takes ~50ms. The algorithm computation itself is where igraph and networkit earn their wins: compiled C/C++ vs GDS's JVM with startup and GC overhead.

The Two Losses

Harmonic centrality (1.27s vs GDS 298ms) — networkit's HarmonicCloseness runs single-threaded for this variant. GDS parallelizes across all JVM threads. We tested multiprocessing but Python's serialization overhead negated the gains.

Greedy modularity (439ms vs GDS 147ms) — igraph's community_fastgreedy() builds a dendrogram then cuts it, which is algorithmically slower than GDS's direct greedy merge. The 3x gap is consistent and unlikely to close without a different algorithm.

Both losses are at the "good enough" threshold for most applications — sub-second for 10K nodes.

How We Got Here

These numbers weren't always this good. Our first benchmark run showed AGE losing 7 out of 12 workloads. The raw Cypher executor in AGE has real performance gaps: VLE traversal was 20x slower than Kuzu, bulk loading was 300x slower than NebulaGraph, PageRank was 1,800x slower than Neo4j.

We closed every gap through SDK-level optimizations in the Piggie Python SDK. Here's what we did and why it matters.

The Optimization Playbook

Edge creation: 257ms to 0.83ms (310x improvement)

AGE's Cypher executor creates one edge per transaction round-trip. We switched to PostgreSQL's COPY protocol, streaming edges directly into AGE's internal table structure. This bypasses the Cypher executor entirely for bulk writes.

Bulk load: 24,000ms to 51.8ms (463x improvement)

Same approach — COPY protocol for nodes and edges instead of individual CREATE statements. One PostgreSQL COPY command loads thousands of rows in a single operation.

PageRank: 23,800ms to 26.6ms (895x improvement)

The original approach exported the entire graph to NetworkX via Python. We switched to igraph with a native adjacency list builder that reads directly from AGE's internal tables, avoiding the agtype serialization overhead.

VLE traversal: 23ms to 0.34ms (68x improvement)

AGE's VLE implementation uses O(n^k) path expansion with no cycle detection. We replaced it with a recursive CTE using UNION (not UNION ALL), which provides implicit cycle detection through set deduplication. We filed this as AGE issue #2349.

Shortest path: 23ms to 0.33ms (70x improvement)

AGE's shortestPath() delegates to VLE, inheriting its O(n^k) expansion. We implemented BFS as a recursive CTE with early termination. Filed as AGE issue #2350.

Pattern match: 2.2ms to 0.14ms (16x improvement)

AGE's Cypher executor doesn't leverage GIN or btree indexes for property predicates. We materialized key properties into native PostgreSQL columns and use SQL JOINs with btree indexes. Filed as AGE issue #2348.

Aggregation: 7.7ms to 3.0ms (2.6x improvement)

Same materialized column approach — native PostgreSQL GROUP BY on a btree-indexed text column instead of agtype property extraction.

Concurrent queries: 149ms to 13.2ms (11x improvement)

Prepared SQL statements with direct psycopg connections eliminate per-query Cypher planning overhead. The prepared statement is compiled once and executed repeatedly with different parameters.

The Pattern: PostgreSQL as Escape Hatch

Every optimization follows the same pattern: when AGE's Cypher executor is slow, drop down to PostgreSQL's SQL engine instead. AGE stores graph data in regular PostgreSQL tables — vertices in a "Node" table, edges in an "EDGE" table. These tables have normal columns that respond to normal indexes.

This is the key architectural advantage of AGE over standalone graph databases: your escape hatch is the most battle-tested database engine in the world.

When Neo4j's Cypher is slow, your options are limited to what Neo4j provides. When AGE's Cypher is slow, you can write SQL, use COPY, build btree indexes, use prepared statements, or call any PostgreSQL extension. You're never stuck.

Feature Parity

Numbers only tell half the story. AGE has real feature gaps.

Cypher Dialect Coverage

AGE implements roughly 55% of the openCypher specification, compared to Neo4j's ~95%.

Feature AGE Neo4j Kuzu NebulaGraph
MERGE ON CREATE/MATCH SET PR open Yes Yes N/A (nGQL)
UNION No Yes Yes Yes
CALL subquery No Yes No No
List predicates (all/any/none/single) No Yes Partial No
WHERE pattern matching No Yes Yes Yes
Pattern comprehensions No Yes No No
Multiple labels No Yes Yes N/A

These aren't obscure features. MERGE ON CREATE SET is fundamental to ETL pipelines. List predicates are standard Cypher. WHERE pattern matching is in every Cypher tutorial.

Where AGE Wins on Features

Co-tenancy. In one PostgreSQL instance you get graph queries (AGE), vector search (pgvector), full-text search (tsvector), geospatial (PostGIS), time-series (TimescaleDB), JSONB documents, and relational tables with full ACID. No other database in this comparison offers this.

Licensing. AGE is Apache 2.0 with no feature gating. Every capability is available in the single edition. Neo4j's modified AGPL restricts SaaS deployment. NebulaGraph gates vector search, full-text indexing, and advanced algorithms behind a commercial Enterprise license.

Operational maturity. AGE inherits PostgreSQL's ecosystem: streaming replication, Patroni failover, pg_basebackup, WAL archiving, pgwatch monitoring, PgBouncer connection pooling. Neo4j Community is single-node only.

Where AGE Needs Work

Cypher dialect. The ~55% coverage means users migrating from Neo4j will hit gaps immediately. We have 7 merged and 2 open PRs upstream, with more Cypher features in our pipeline.

Graph algorithms via SDK. Neo4j's GDS library offers 60+ algorithms running in-memory. AGE has no built-in algorithm engine, but the Piggie SDK provides 19 algorithms across 7 categories — centrality (5), community detection (4), pathfinding (3), ranking (1), similarity (3), link prediction (2), and embeddings (1) — via igraph, networkit, and NetworkX. We benchmarked these head-to-head against GDS (see the algorithm section below) and the SDK wins 14 out of 16 matchups. The two losses are harmonic centrality and greedy modularity, where GDS's multi-threaded JVM implementation still has an edge.

Driver ecosystem. Official AGE-specific drivers exist for Python and Go. Every other language uses generic PostgreSQL drivers with manual agtype parsing.

What This Means for Your Architecture

The thesis — that AGE + pgvector in one PostgreSQL replaces Neo4j + Pinecone — holds across the full performance spectrum, with one caveat: you need an SDK that knows when to bypass Cypher.

Where it works today:

Use Case
Applications that need both graph traversals and vector search in the same query pipeline (RAG, knowledge graphs, recommendation engines)
Teams that already operate PostgreSQL and want to add graph capabilities without new infrastructure
Write-heavy workloads — AGE's COPY-based bulk loading beats every competitor at 100K scale
Projects where Apache 2.0 licensing and SaaS deployment freedom matter

What to watch for:

Caveat
Advanced Cypher features you depend on (check the feature parity table)
Harmonic centrality and greedy modularity at very large scale — GDS still wins these two
The AGE upstream project's velocity on Cypher parity

Methodology Notes

Fairness. The AGE adapter wraps the Piggie Python SDK, not raw psycopg3. This means AGE numbers include SDK overhead (agtype parsing, result wrapping). Neo4j uses the official neo4j Python driver. Kuzu uses its pip package directly. NebulaGraph uses nebula3-python. We benchmark the developer experience, not theoretical wire speed.

SDK optimizations are real. Some readers will argue that using SQL instead of Cypher is "cheating." We disagree. The whole point of AGE running inside PostgreSQL is that you have PostgreSQL available. An SDK that intelligently routes queries to the fastest execution path — Cypher for graph traversals, SQL for property lookups, COPY for bulk loading — is the intended usage pattern. Every database's official driver makes similar optimization choices.

Correctness. All four databases return identical result counts for verified workloads. W04 returns 28 matches (100K) or 13 (10K), W05 returns 5 categories, W06 returns 348 paths (100K) or 24 (10K).

Determinism. All data generation uses NumPy with seed=42. Every database receives identical nodes, edges, and vectors.

Reproducibility. The full benchmark suite is in the Piggie repository under benchmarks/:

uv pip install -e ".[dev]"
uv pip install -r benchmarks/requirements.txt
python -m benchmarks setup
python -m benchmarks run --scale 100k
python -m benchmarks report

What's Next

We've validated the thesis at 10K, 100K, and 1M scales across both database workloads and graph algorithms. The remaining gaps are narrow — two algorithm losses where GDS's multi-threaded JVM has a structural advantage, and Cypher dialect coverage that's closing with each upstream contribution.

We're contributing upstream to AGE — we have 7 merged PRs, 2 open PRs awaiting review, and 3 filed performance issues with benchmark data and suggested fixes. The Piggie Python SDK is open source (Apache 2.0) and available on GitHub and PyPI.

Try It

pip install piggie
import piggie

db = piggie.connect("postgresql://localhost:5433/mydb", graph="social")

# Cypher queries → DataFrames
df = db.cypher("MATCH (n:Person)-[:KNOWS]->(m) RETURN n.name, m.name").to_df()

# Vector search
results = db.vector_search("documents", "embedding", query_vec, k=10).to_df()

Documentation | GitHub | PyPI


Greg Felice is a contributor to Apache AGE and the author of the Piggie Python SDK. He has 7 merged PRs, 2 open PRs, and 3 performance issues filed upstream. This post reflects real benchmark data — the methodology, optimizations, and raw numbers are all open source.

Subscribe to OOXO

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe