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:
- networkit (C++, multi-threaded) — preferred for closeness and harmonic centrality, where parallel traversal dominates
- igraph (C, single-threaded) — preferred for betweenness, community detection, PageRank, and eigenvector centrality
- 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.