Skip to content

GFQL: pandas Cypher RETURN row-pipeline is ~O(N) — ~95ms to return 1 row at 500k nodes (polars ~80× faster) #1670

Description

@lmeyerov

Summary

The pandas GFQL Cypher RETURN / row-pipeline path is ~O(N) in the graph size even for a 1-row result. Returning a single matched node from a 500K-node graph takes ~95 ms on pandas vs ~1.2 ms on polars (~80×). The cost is in the RETURN/result-postprocess path, not the node filter (which finds the row in <1 ms) and not indexing.

This surfaced while investigating point-lookup competitiveness (a Ladybug/Kuzu comparison): the "slow point lookup" is really this RETURN-path overhead, so it's worth its own issue independent of the CSR adjacency-index work (#1658).

Evidence (local CPU, 500K nodes / 2M edges, id = 1..N)

operation pandas polars
bare node filter gfql([n({'id': X})]) 0.79 ms 0.34 ms
MATCH (a) WHERE a.id = X RETURN a (1 row) 94.7 ms 1.2 ms
MATCH (a) WHERE a.id = X RETURN a.id (1 row) 96.2 ms 1.2 ms

Scales ~linearly with N (1-row RETURN a.id, pandas): 15.6 ms @ 50K → 100 ms @ 500K.

Both RETURN a (whole entity) and RETURN a.id (scalar) are ~equally slow, so it is not whole-entity rendering specifically — it looks like a fixed O(N) pass in the row pipeline / result_postprocess.

Repro

import numpy as np, pandas as pd, graphistry
N, M = 500_000, 2_000_000
rng = np.random.default_rng(0)
ndf = pd.DataFrame({"id": np.arange(1, N + 1, dtype=np.int64)})
edf = pd.DataFrame({"src": rng.integers(1, N + 1, M), "dst": rng.integers(1, N + 1, M)})
g = graphistry.nodes(ndf, "id").edges(edf, "src", "dst")
X = N // 2
%timeit g.gfql(f"MATCH (a) WHERE a.id = {X} RETURN a.id", engine="pandas")  # ~100 ms
%timeit g.gfql(f"MATCH (a) WHERE a.id = {X} RETURN a.id", engine="polars")  # ~1 ms

Hypothesis / where to look

  • An O(N) pass in the pandas RETURN path (compute/gfql/cypher/result_postprocess.py and/or the row pipeline) that runs regardless of result cardinality.
  • polars already avoids it (~1 ms), so a candidate fix is to bring the pandas path in line (or steer lookup-heavy Cypher to polars/cuDF).

Impact

Lookup/point-query workloads on pandas. GFQL already wins full scans / counts / traversals; this is the one shape where a tiny result pays a large fixed cost.

Notes

  • Separate subsystem from the CSR seeded-index PR (feat(gfql): physical adjacency indexes for O(degree) seeded traversal #1658) — the index does not help here (the cost isn't the filter).
  • Numbers are CPU (pandas + polars). cuDF/polars-GPU RETURN-path measurement is deferred to GPU hardware.
  • Full analysis: internal receipts plans/gfql-1658-seeded-index/receipts/lookup-finding.md.

Activity

  1. 12 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions