Skip to content

Performance issue list based on LDBC benchmark #122

Open
1 of 4 issues completed
Open

Description

@prrao87

I'm creating this issue to uncover some deeper performance issues I've noticed in the LDBC SNB-SF1 benchmark summary table - it's clear that there are more fundamental reasons why lance-graph is ~10x slower than other alternatives for nearly all the queries.

Why this dataset?

LDBC SNB SF1 is a reasonably large and complex dataset with several wide tables as opposed to the earlier benchmark on an artificially generated social network dataset, which had fewer, narrower tables. This means that we're uncovering issues with the LDBC benchmark that are more likely to surface in users' real-world workloads, so it's worth investigating deeper from a query parsing and planning perspective.

Summary of issues (high-level)

  • The previous version of lance-graph used the execute API, which rebuilt the catalog each time it was called (since the dataset was passed as an input parameter). In v0.5.1, lance-graph introduces a new CypherEngine API, which caches the catalog, so we can compare the pure query execution times with other engines in the benchmark repo.

  • Currently, there doesn't exist a short‑circuit for existence checks due to a parser limitation in lance-graph. The parser rejects COUNT(DISTINCT x) > 0, which blocks simple boolean returns as required by Q20-30 of the LDBC benchmark, and this forces full counts + Python post‑processing overheads. That removes the chance to stop early on LIMIT 1 / EXISTS, which other engines can exploit this pattern. This is addressed by Parser rejects boolean comparison after COUNT(DISTINCT ...) during projection #123.

  • There are query optimizer limitations around multi‑MATCH + WHERE. Per Multi-match query with WHERE predicate downstream filter/projection fails #117, we must express many queries as a single MATCH followed by the WHERE clause in order to be able to filter, which restricts the flexibility with which users can write their own queries using these patterns. From a performance perspective, it makes join‑order/alias‑pruning inefficient and risks huge intermediate joins if predicate pushdown or reorder isn't considered during planning. In general, from a DevEx perspective, the user should be able to write all kinds of patterns (including multi-match + WHERE queries) without facing a performance penalty.

  • No parameter binding / plan cache. Parameters are manually inlined (string replace), so each query is parsed and planned from scratch and can’t be cached as a prepared statement. File: query.py (apply_params + execute_query). This would be fixed by feat: Properly support parameter placeholders in Python/Rust #103.

  • Self-referencing alias in a Cypher query pattern doesn't work, meaning that the most expensive query in the LDBC benchmark (Q30) cannot be run against other tools. This is addressed by Self-pointing Cypher pattern causes lance-graph to hang indefinitely for high cardinality datasets #111.

  • LDBC queries typically involve large fan‑out traversals, which can become expensive quickly. In the DataFusion engine, graph traversal is implemented as joins over edge tables. If the engine doesn’t reorder joins aggressively, high‑degree steps (e.g., Comment ↔ Post ↔ Tag) can become expensive.

Note

Some of the above may not be query planning issues, but I've been explaining and inspecting the query plans with GPT 5.2 Codex to the best of my ability to see what could explain the performance gap. Experienced human eyes are needed to look through the plans and identify performance gaps with other query plans from Kuzu/Ladybug and Neo4j.

We can expand/contract this list as new issues become apparent, and link them to existing sub-issues in this repo.

Activity

  1. changed the title [-]List of query planner performance issues based on LDBC benchmark[/-] [+]List of parser/planner performance issues based on LDBC benchmark[/+] on Feb 2, 2026
  2. changed the title [-]List of parser/planner performance issues based on LDBC benchmark[/-] [+]Performance issue list based on LDBC benchmark[/+] on Feb 2, 2026
  3. pinned this issue on Feb 2, 2026
  4. added theissue type on Feb 2, 2026
  5. ChunxuTang commented on Feb 4, 2026

    @ChunxuTang
    Collaborator

    Hi @prrao87, thanks for proposing and running the benchmark!
    I checked your latest benchmark results. Through profiling, I found that catalog rebuilding on every query execution could be the major bottleneck. Unlike Neo4j and KuzuDB, which cache catalogs in memory, calling lance-graph query.execute() will rebuild the catalog every time. The catalog rebuilding begins to dominate the query execution time when the dataset schema becomes more complicated. Cc @beinan

    To address that, I exposed a CypherEngine API from this #124 to reuse the catalog. It's now included in the latest version v0.5.1. Could you try the new API and re-run the benchmark?

  6. prrao87 commented on Feb 4, 2026

    @prrao87
    ContributorAuthor

    Hi @ChunxuTang, many thanks! I rebuilt 0.5.1 and reran the benchmark with the latest version, it's barely any improvement. The table of results is below. I suspected something else other than caching is the problem, because we'd have seen some sign of the cache missing in my first benchmark (with the simpler schema). I took a look at my code in much more detail over the last week, and I can't think of what else I'm doing wrong, so I think we should work our way through each of the other issues listed above. I think LDBC is very a high cardinality scenario, exposing other issues in the query plan that the previous benchmark didn't.

    I also think yours and @beinan's eyes on the query plan for just Q1 of LDBC would help. This query doesn't materialize any unnecessary results, and doesn't do any downstream processing in Python either. I can't understand why it would be 4x slower than Neo4j, even though I created a scalar index on the id column in lance-graph (just like I created a uniqueness constraint in Neo4j). For such a simple query, I'd expect that lance-graph would do really well.

    Here's the latest results from the LDBC repo.

    Query neo4j-2025.12.1 (ms) kuzu-0.11.3 (ms) ladybug-0.14.1 (ms) lance-graph-0.5.1 (ms)
    q1 6ms 2ms (2.4x) 2ms (2.7x) 24ms (0.2x)
    q2 6ms 1ms (4.4x) 2ms (4.2x) 25ms (0.3x)
    q3 3ms 1ms (2.1x) 1ms (2.2x) 26ms (0.1x)
    q4 5ms 1ms (4.6x) 1ms (4.3x) 25ms (0.2x)
    q5 5ms 4ms (1.3x) 4ms (1.4x) 24ms (0.2x)
    q6 4ms 1ms (5.1x) 1ms (4.6x) 23ms (0.2x)
    q7 2ms 31ms (0.1x) 31ms (0.1x) 35ms (0.1x)
    q8 16ms 3ms (5.6x) 3ms (5.5x) 23ms (0.7x)
    q9 3ms 2ms (1.3x) 2ms (1.2x) 25ms (0.1x)
    q10 5ms 2ms (2.7x) 2ms (2.6x) 48ms (0.1x)
    q11 13ms 8ms (1.6x) 8ms (1.5x) 26ms (0.5x)
    q12 5ms 18ms (0.3x) 18ms (0.3x) 40ms (0.1x)
    q13 9ms 50ms (0.2x) 52ms (0.2x) 31ms (0.3x)
    q14 2ms 2ms (1.0x) 2ms (0.9x) 25ms (0.1x)
    q15 3ms 3ms (1.2x) 3ms (1.2x) 25ms (0.1x)
    q16 2ms 2ms (1.0x) 2ms (1.0x) 27ms (0.1x)
    q17 5ms 3ms (1.8x) 3ms (1.8x) 25ms (0.2x)
    q18 4ms 2ms (2.3x) 2ms (2.3x) 25ms (0.2x)
    q19 6ms 13ms (0.5x) 14ms (0.5x) 42ms (0.1x)
    q20 422ms 12ms (35.2x) 11ms (38.1x) 25ms (16.9x)
    q21 2ms 1ms (2.7x) 1ms (2.7x) 24ms (0.1x)
    q22 4ms 20ms (0.2x) 21ms (0.2x) 35ms (0.1x)
    q23 4ms 1ms (2.9x) 1ms (2.8x) 25ms (0.2x)
    q24 2ms 2ms (1.0x) 2ms (1.0x) 25ms (0.1x)
    q25 3ms 2ms (1.9x) 2ms (1.8x) 24ms (0.1x)
    q26 2ms 4ms (0.5x) 4ms (0.5x) 26ms (0.1x)
    q27 3ms 13ms (0.3x) 16ms (0.2x) 46ms (0.1x)
    q28 4ms 2ms (2.6x) 2ms (2.3x) 26ms (0.2x)
    q29 3ms 1ms (2.4x) 1ms (2.4x) 26ms (0.1x)
  7. ChunxuTang commented on Feb 5, 2026

    @ChunxuTang
    Collaborator

    @prrao87 You'll need to use the new CypherEngine API in the v0.5.1 to avoid duplicate catalog building when executing the queries. We're actually using lance-graph's catalog building time + query execution time to compare with pure query execution times of Neo4j and Kuzu/Ladybug. That's why lance-graph looks much slower than others.

    Let me create a PR to your benchmark repo.

  8. prrao87 commented on Feb 5, 2026

    @prrao87
    ContributorAuthor

    Ah, I misunderstood your earlier statement about how to use the new Cypher engine. Looking forward to your PR. Thanks!

  9. prrao87 commented on Feb 5, 2026

    @prrao87
    ContributorAuthor

    Seems to help! I tried the new engine approach on the first benchmark (artificial dataset, 9 queries), and it's much faster 😁. Will update that repo and hopefully we can see a significant improvement here, too!

    Image
  10. ChunxuTang commented on Feb 5, 2026

    @ChunxuTang
    Collaborator

    @prrao87 I've just sent a PR to your repo. Would you mind taking a look? I think this would significantly improve the performance.

  11. prrao87 commented on Feb 5, 2026

    @prrao87
    ContributorAuthor

    Thanks a lot @ChunxuTang! Indeed, this significantly improves performance 😁. I just reran the benchmark, and lance-graph is now the fastest in quite a few of the queries. Q10 and Q27 ares interesting, because lance-graph is the slowest there, and Q20 is also interesting, because lance-graph is way faster than anything else 😅. Hopefully the benchmark is helpful to you and Beinan too as you optimize the planner going forward! 🚀

    Query neo4j-2025.12.1 (ms) kuzu-0.11.3 (ms) ladybug-0.14.1 (ms) lance-graph-0.5.1 (ms)
    q1 6ms 2ms (2.4x) 2ms (2.7x) 1ms (3.9x)
    q2 6ms 1ms (4.4x) 2ms (4.2x) 2ms (2.8x)
    q3 3ms 1ms (2.1x) 1ms (2.2x) 3ms (0.8x)
    q4 5ms 1ms (4.6x) 1ms (4.3x) 3ms (1.5x)
    q5 5ms 4ms (1.3x) 4ms (1.4x) 2ms (2.4x)
    q6 4ms 1ms (5.1x) 1ms (4.6x) 1ms (5.0x)
    q7 2ms 31ms (0.1x) 31ms (0.1x) 13ms (0.2x)
    q8 16ms 3ms (5.6x) 3ms (5.5x) 1ms (12.4x)
    q9 3ms 2ms (1.3x) 2ms (1.2x) 2ms (1.2x)
    q10 5ms 2ms (2.7x) 2ms (2.6x) 25ms (0.2x)
    q11 13ms 8ms (1.6x) 8ms (1.5x) 3ms (4.0x)
    q12 5ms 18ms (0.3x) 18ms (0.3x) 18ms (0.3x)
    q13 9ms 50ms (0.2x) 52ms (0.2x) 9ms (1.0x)
    q14 2ms 2ms (1.0x) 2ms (0.9x) 3ms (0.6x)
    q15 3ms 3ms (1.2x) 3ms (1.2x) 2ms (1.4x)
    q16 2ms 2ms (1.0x) 2ms (1.0x) 4ms (0.5x)
    q17 5ms 3ms (1.8x) 3ms (1.8x) 3ms (2.0x)
    q18 4ms 2ms (2.3x) 2ms (2.3x) 2ms (2.0x)
    q19 6ms 13ms (0.5x) 14ms (0.5x) 19ms (0.3x)
    q20 422ms 12ms (35.2x) 11ms (38.1x) 2ms (175.8x)
    q21 2ms 1ms (2.7x) 1ms (2.7x) 2ms (0.9x)
    q22 4ms 20ms (0.2x) 21ms (0.2x) 12ms (0.3x)
    q23 4ms 1ms (2.9x) 1ms (2.8x) 3ms (1.5x)
    q24 2ms 2ms (1.0x) 2ms (1.0x) 2ms (0.8x)
    q25 3ms 2ms (1.9x) 2ms (1.8x) 1ms (2.1x)
    q26 2ms 4ms (0.5x) 4ms (0.5x) 3ms (0.6x)
    q27 3ms 13ms (0.3x) 16ms (0.2x) 23ms (0.1x)
    q28 4ms 2ms (2.6x) 2ms (2.3x) 3ms (1.5x)
    q29 3ms 1ms (2.4x) 1ms (2.4x) 3ms (1.2x)
    Image
  12. ChunxuTang commented on Feb 5, 2026

    @ChunxuTang
    Collaborator

    @prrao87 Thanks for re-running the benchmark!

    For your questions on Query 10 and Query 20:

    1. Query 10: This query requires complex relationship traversal. Currently, lance-graph unrolls this into multiple SQL selects with UNION. In contrast, Neo4j and kuzu use more graph-native traversal approaches (e.g. BFS). That's why lance-graph runs slowest here.
    2. Query 20: This query is actually a pure analytical query. Since lance-graph uses SQL optimization and columnar storage from lance, it is expected to run much faster than normal graphDB.

    In summary, the benchmark result demonstrates the advantages of lance-graph for GraphRAG scenarios (most queries with base property filtering and only shallow traversal).

    @beinan and I are also exploring some hybrid queries (graph + vector) for benchmarking, which are very common in GraphRAG. That would also be useful for the evaluation.

  13. ChunxuTang commented on Feb 5, 2026

    @ChunxuTang
    Collaborator

    @prrao87 BTW, for your benchmark results, could you show the data rounded to one decimal place (e.g., 3.1 ms instead of 3ms)?

  14. prrao87 commented on Feb 5, 2026

    @prrao87
    ContributorAuthor

    Sounds good, will update the numbers and show them with 1 decimal rounding. I agree that these two benchmarks are analytical/traversal queries only, but they're important to see if the low-hanging fruit is addressed (which I think it is). Any further gains would require some special optimizations, that can be a longer term goal. It's still admirable how well Lance does at these kinds of queries that are traditionally the realm of pure graph DBs, so this was all in all, a very illuminating exercise for me.

    I think more benchmarks that study Lance's vector/multimodal capabilities would be very helpful. I'd love to see your benchmarks with graph + vector queries (lance-graph will definitely be at an advantage in those). Do let me know if there are datasets and benchmarks you have in mind! Feel free to tag me on those issues as and when you create them. Happy to help you with creating new benchmarks too.

  15. ChunxuTang commented on Feb 10, 2026

    @ChunxuTang
    Collaborator

    Hi @prrao87, the last query (query 30) should be unblocked now by the latest release (0.5.2). Would you like to retry the query 30?

    Just the query needs to be rewritten to

    MATCH (c:Comment)-[:commentHasCreator]->(creator:Person),
          (c)-[:replyOfPost]->(post:Post)-[:postHasCreator]->(creator)
    RETURN COUNT(DISTINCT c.ID) AS has_self_reply;

    since we don't support the self-reference query yet.

  16. prrao87 commented on Feb 10, 2026

    @prrao87
    ContributorAuthor

    Hi @ChunxuTang, this is awesome! It not only works, but lance-graph is now the fastest solution among them all for Q30 😎.

    Query neo4j-2025.12.1 (ms) kuzu-0.11.3 (ms) ladybug-0.14.1 (ms) lance-graph-0.5.2 (ms)
    q30 1255.2ms 158.7ms (7.9x) 154.4ms (8.1x) 31.3ms (40.1x)

    Full table of updated results here 👇🏽
    https://github.com/prrao87/graph-benchmark-ldbc?tab=readme-ov-file#high-level-results

    Thanks a lot!

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions