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

15.37 ms

66.08 ms

74.50 ms

27.85 ms

7.29 ms

6.21 ms

GFQL polars-gpu

q2

36.40 ms

44.41 ms

67.74 ms

29.71 ms

11.30 ms

10.59 ms

GFQL polars-gpu

q3

6.02 ms

4.16 ms

8.31 ms

10.11 ms

4.58 ms

4.56 ms

Memgraph

q4

3.20 ms

6.38 ms

11.54 ms

8.75 ms

3.38 ms

16.41 ms

Kuzu

q5

5.82 ms

4.63 ms

18.33 ms

76.67 ms

3.84 ms

7.34 ms

GFQL polars

q6

8.70 ms

4.85 ms

20.99 ms

77.99 ms

4.81 ms

7.18 ms

GFQL polars

q7

5.31 ms

5.39 ms

15.14 ms

18.16 ms

3.09 ms

8.57 ms

GFQL polars

q8

2.79 ms

198.80 ms

198.71 ms

7.41 ms

2.66 ms

2.17 ms

GFQL polars-gpu

q9

10.69 ms

124.91 ms

422.29 ms

16.20 ms

7.68 ms

6.95 ms

GFQL polars-gpu

The 100,000-person board#

Query

Kuzu

Memgraph

Neo4j

GFQL pandas

GFQL polars

GFQL polars-gpu

Fastest

q1

152.67 ms

655.50 ms

680.72 ms

163.57 ms

26.38 ms

18.85 ms

GFQL polars-gpu

q2

265.01 ms

468.94 ms

632.70 ms

168.17 ms

39.95 ms

29.25 ms

GFQL polars-gpu

q3

34.45 ms

30.17 ms

51.14 ms

69.47 ms

9.92 ms

9.38 ms

GFQL polars-gpu

q4

13.69 ms

37.64 ms

63.76 ms

62.79 ms

8.75 ms

10.11 ms

GFQL polars

q5

12.79 ms

3.95 ms

8.96 ms

400.64 ms

11.37 ms

13.52 ms

Memgraph

q6

21.49 ms

5.14 ms

22.94 ms

400.74 ms

14.04 ms

12.37 ms

Memgraph

q7

9.81 ms

3.74 ms

140.18 ms

113.31 ms

7.10 ms

14.56 ms

Memgraph

q8

9.74 ms

7316.46 ms

3766.74 ms

77.01 ms

14.01 ms

178.58 ms

Kuzu

q9

82.85 ms

6199.80 ms

4843.46 ms

139.95 ms

34.26 ms

23.64 ms

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

11.997 ms

3.921 ms

2.329 ms

3.497 ms

0.592 ms

Memgraph

message content

3.469 ms

1.659 ms

0.717 ms

2.260 ms

0.486 ms

Memgraph

message creator

6.396 ms

2.867 ms

1.433 ms

2.630 ms

0.603 ms

Memgraph

recent replies

35.613 ms

47.675 ms

32.407 ms

7.454 ms

4.342 ms

Memgraph

message replies (GFQL and Kuzu only)

13.138 ms

13.990 ms

23.904 ms

GFQL polars

new topics (GFQL and Kuzu only)

44.467 ms

70.572 ms

47.198 ms

GFQL polars

SF1#

Query

GFQL polars

GFQL pandas

Kuzu

Neo4j

Memgraph

Fastest

seed lookup

10.719 ms

4.042 ms

2.517 ms

2.884 ms

0.523 ms

Memgraph

message content

2.991 ms

1.682 ms

0.674 ms

2.435 ms

0.293 ms

Memgraph

message creator

5.897 ms

2.381 ms

1.509 ms

1.671 ms

0.308 ms

Memgraph

new topics (GFQL and Kuzu only)

162.322 ms

533.559 ms

223.715 ms

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#