GFQL Performance: Measured Against Graph Databases#
This page holds GFQL’s measured performance results. Every number renders from a committed pyg-bench artifact; the Measurement block at the end names the runs, hosts, and commits. Losses appear next to wins.
Choose an engine#
GFQL runs the same query on pandas (the default), polars (CPU), cudf
(NVIDIA GPU), or polars-gpu, and every engine returns the same rows. On the q1–q9
boards below, the
Polars engine is faster than pandas on 9 of 9
queries at 100,000 people, by up to 28.5x
(q6). See Choosing a GFQL Engine: pandas, Polars, cuDF, Polars-GPU for the selection guide.
g.gfql(query) # engine='pandas' (default)
g.gfql(query, engine='polars') # columnar CPU execution
The q1–q9 board: GFQL, Kuzu, Memgraph, and Neo4j#
Nine Cypher queries from prrao87/graph-benchmark rank nodes by degree, group and
filter records, and count two-hop paths on synthetic social graphs with 20,000 and
100,000 people. Every cell passed result-row validation against every other engine.
Times are milliseconds; lower is better.
GFQL binds the graph cold inside every timed run; the polars-gpu column runs the
same fused plan on the GPU. Kuzu compiles the query text on each call. Memgraph and
Neo4j answer over Bolt with their default plan caches. These are direct times under
those profiles, not cross-engine speedup ratios. At these sizes the queries are
millisecond-scale, so the GPU engine wins some and loses others to the CPU engine:
5 of 9 at 100,000 people. Its widest loss is
q8 at 100,000 people, 178.58 ms against
14.01 ms on the CPU.
At 20,000 people, GFQL Polars is faster than Kuzu on 8 of 9 queries, than Memgraph on 8 of 9, and than Neo4j on 9 of 9. At 100,000 people the counts are 8 of 9 (Kuzu), 6 of 9 (Memgraph), and 8 of 9 (Neo4j). Kuzu wins q4 at 20,000 people and q8 at 100,000 people; the artifact’s compare tables classify both as ties because the per-slot medians overlap. Memgraph wins q3 and q6 at 20,000 people and q5, q6, and q7 at 100,000 people, where Neo4j also wins q5: their planners start from the ten-node interest side, which GFQL’s Cypher path does not yet do.
The 20,000-person board#
Query |
Kuzu |
Memgraph |
Neo4j |
GFQL pandas |
GFQL polars |
GFQL polars-gpu |
Fastest |
|---|---|---|---|---|---|---|---|
q1 |
|
|
|
|
|
|
GFQL polars-gpu |
q2 |
|
|
|
|
|
|
GFQL polars-gpu |
q3 |
|
|
|
|
|
|
Memgraph |
q4 |
|
|
|
|
|
|
Kuzu |
q5 |
|
|
|
|
|
|
GFQL polars |
q6 |
|
|
|
|
|
|
GFQL polars |
q7 |
|
|
|
|
|
|
GFQL polars |
q8 |
|
|
|
|
|
|
GFQL polars-gpu |
q9 |
|
|
|
|
|
|
GFQL polars-gpu |
The 100,000-person board#
Query |
Kuzu |
Memgraph |
Neo4j |
GFQL pandas |
GFQL polars |
GFQL polars-gpu |
Fastest |
|---|---|---|---|---|---|---|---|
q1 |
|
|
|
|
|
|
GFQL polars-gpu |
q2 |
|
|
|
|
|
|
GFQL polars-gpu |
q3 |
|
|
|
|
|
|
GFQL polars-gpu |
q4 |
|
|
|
|
|
|
GFQL polars |
q5 |
|
|
|
|
|
|
Memgraph |
q6 |
|
|
|
|
|
|
Memgraph |
q7 |
|
|
|
|
|
|
Memgraph |
q8 |
|
|
|
|
|
|
Kuzu |
q9 |
|
|
|
|
|
|
GFQL polars-gpu |
SNB-derived point and small-result queries: the databases win#
Matched query shapes derived from the LDBC Social Network Benchmark (SNB) Interactive workload, run on the SF0.1 and SF1 datasets without the official LDBC driver. This is internal evidence, not an official LDBC result. All four engines ran under one timing contract with exact result parity. Times are milliseconds.
Kuzu, Neo4j, and Memgraph are faster than GFQL on every point-lookup row, and Memgraph
is fastest on most. The GFQL columns run with resident indexes built once before the
timed runs (gfql_index_all plus node property indexes), the same footing as the
databases’ primary-key and label indexes, and the GFQL arm runs native op lists, not
Cypher text. A seeded lookup, a seeded typed hop, and a node-only lookup now resolve
through the resident node-id, adjacency, and node-property indexes on every CPU engine,
so the SF0.1 point rows sit in the low single-digit milliseconds on pandas and under
about ten milliseconds on polars, against a database’s sub-millisecond index probe. The
hop-shaped rows (message replies, recent replies, new topics) are unchanged by that work
and remain GFQL’s slowest cells here. GFQL’s strengths are the bulk shapes above and on
the speedup case study; choose a database when the
workload is dominated by point lookups.
Open items behind the remaining gaps: CPU PageRank spends most of its time converting to
igraph (#2032); a polars
native chain whose edge alias collides with the edge column its own filter uses raises
where pandas serves it (#2039);
rows(table=nodes, source=alias) multiplies rows for duplicate node ids
(#2034). The SF0.1 and SF1 GFQL
cells are the release measurement on landed master; the Measurement block below carries the
provenance.
SF0.1#
Query |
GFQL polars |
GFQL pandas |
Kuzu |
Neo4j |
Memgraph |
Fastest |
|---|---|---|---|---|---|---|
seed lookup |
|
|
|
|
|
Memgraph |
message content |
|
|
|
|
|
Memgraph |
message creator |
|
|
|
|
|
Memgraph |
recent replies |
|
|
|
|
|
Memgraph |
message replies (GFQL and Kuzu only) |
|
|
|
— |
— |
GFQL polars |
new topics (GFQL and Kuzu only) |
|
|
|
— |
— |
GFQL polars |
SF1#
Query |
GFQL polars |
GFQL pandas |
Kuzu |
Neo4j |
Memgraph |
Fastest |
|---|---|---|---|---|---|---|
seed lookup |
|
|
|
|
|
Memgraph |
message content |
|
|
|
|
|
Memgraph |
message creator |
|
|
|
|
|
Memgraph |
new topics (GFQL and Kuzu only) |
|
|
|
— |
— |
GFQL polars |
Neo4j and Memgraph use a reduced adapter for one query, and one parameter returns zero rows; those cells are excluded rather than estimated. SF10 was not run.
Lookups from known nodes#
A query that starts from known node ids (a watchlist, a session) scans every edge by default. The opt-in adjacency index turns that scan into a gather over the seeds’ neighbors, so its cost tracks the seeds rather than the graph, on every engine. This lane has not yet been measured under the provenance-carrying harness used above, so this page prints no figure for it; see Adjacency Index: Fast Lookups from Known Nodes for the design and Indexing Guide: Build Once, Query Faster for the lifecycle.
How GFQL is fast, and when it is not#
GFQL joins tables of nodes and edges in batches instead of following one path at a time, over columnar frames based on Apache Arrow. Polars fuses the operations into one lazy plan and collects once; cuDF and Polars GPU run the same columnar operations on NVIDIA GPUs. That favors bulk work: multi-join analytics, frontier expansion from many seeds, and full-graph aggregation. It does not favor single-row point lookups: the resident indexes bring a seeded lookup to a few milliseconds, and an indexed database still answers in well under a millisecond, as the SNB tables show.
Start on CPU with no special hardware, and move to a GPU engine by changing one keyword when the graph or result becomes large. The speedup case study measures a full filter, PageRank, filter pipeline on CPU and GPU against Neo4j + GDS, and GFQL vs Apache Spark GraphFrames on One Node measures filters, traversals, and PageRank against Spark GraphFrames.
Note
Same-path constraints (where) can be more expensive on dense graphs.
Prefer selective per-step predicates and see GFQL WHERE (Same-Path Constraints) for details.
Provenance#
Every figure on this page is printed from docs/source/_data/gfql_benchmarks.json,
which pyg-bench publishes. The documentation build and docs/test_bench_numbers.py
reject missing, stale, or unpublished values.
Measurement
- Measured:
2026-09-06 and 2026-09-02 and 2026-09-05
- Host:
dgx-spark (NVIDIA GB10), 20 CPU
- Repetitions:
2026-09-06: GFQL and Kuzu: 5 warmups then 51 timed runs per slot, 4 position-balanced slots per engine (K/G/GPU), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included); the polars-gpu slots run under dgx-guard/safe_run.sh. Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board. GFQL/Kuzu slots re-measured at pygraphistry master (release close-out); Memgraph and Neo4j arms unchanged.
2026-09-06: GFQL and Kuzu: 5 warmups then 51 timed runs per slot, 4 position-balanced slots per engine (K/G/GPU), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included); the polars-gpu slots run under dgx-guard/safe_run.sh. Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board. GFQL/Kuzu slots re-measured at pygraphistry master (release close-out); Memgraph and Neo4j arms unchanged.
2026-09-02: 2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs; median of the three run medians
2026-09-05: 2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs; median of the three run medians
- Runtime:
2026-09-06: RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2, cudf-polars for polars-gpu; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars
2026-09-06: RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2, cudf-polars for polars-gpu; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars
2026-09-02: GFQL Polars on CPU in a uv Python 3.12 container; Kuzu 0.11.3; Memgraph 2.18.1 (Docker); Neo4j 5 community (Docker); all on the same host
2026-09-05: GFQL polars / pandas on CPU in a uv Python 3.12 container, gfql_index: use with node property indexes (auto) and per-query engagement verification; same host, lock and contract as the 2026-09-02 four-engine board
- Dataset:
2026-09-06: prrao87 graph-benchmark, 20,000 persons / 199,939 FOLLOWS / 260,107 edges
2026-09-06: prrao87 graph-benchmark, 100,000 persons / 2,417,738 FOLLOWS / 2,775,195 edges
2026-09-02: LDBC SNB Interactive SF0.1 and SF1, derived matched query shapes (SF10 is a capability boundary and is excluded)
2026-09-05: LDBC SNB Interactive SF0.1 and SF1, derived matched query shapes (the release board’s parameters)
- Result validation:
2026-09-06: every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor’s on every slot
2026-09-06: every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor’s on every slot
2026-09-02: exact result parity for every measured query across all engines; cells with zero rows, reduced adapters, or missing queries are excluded
2026-09-05: exact result parity per query against the suite’s expected rows in every run; a cell is comparable only when its cardinalities equal the release GFQL arm’s for the same query
- Caveats:
GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline’s per-call floor (pygraphistry #2027).
Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell.
Next steps#
Choose an engine: Choosing a GFQL Engine: pandas, Polars, cuDF, Polars-GPU.
Lookups from known nodes: Adjacency Index: Fast Lookups from Known Nodes and Indexing Guide: Build Once, Query Faster.
Speedup case study: Speedup Case Study: Cypher + PageRank, GFQL vs Neo4j + GDS.
Explore GFQL: 10 Minutes to GFQL. Get started: 10 Minutes to PyGraphistry.