Skip to content

DuckDB plans views that share reads in time exponential in their depth β€” draft for upstreamΒ #292

Open

Description

@alxmrs

πŸŒ€ Claude: DuckDB's planning time grows exponentially when views read the previous view several times, although the plan it finds stays small. The GraphCast demo (#270) writes each processor round as a view of the round before. On it, DuckDB plans one round in 45 s, and doesn't finish planning two within 10 minutes. This is a draft for an upstream DuckDB issue.

Reproduction

Each view reads the one before three times: as each edge's sender, as its receiver, and as a residual. This is one round of message passing in a graph neural network, with a layer norm's windows. The script needs only duckdb, and runs in seconds up to round 5:

import sys
import time
import duckdb

nodes, edges, f = 100, 400, 4
con = duckdb.connect()
con.execute(f"CREATE TABLE node AS SELECT i // {f} AS id, i % {f} AS f, random() AS val FROM range({nodes * f}) t(i)")
con.execute(f"CREATE TABLE edge AS SELECT i AS edge, (i * 7) % {nodes} AS sender, (i * 13) % {nodes} AS receiver FROM range({edges}) t(i)")
con.execute(f"CREATE TABLE w AS SELECT i // {2*f} AS f, i % {2*f} AS g, random() AS w FROM range({2 * f * 2 * f}) t(i)")
con.execute("CREATE VIEW mesh_0 AS SELECT * FROM node")
print("duckdb", duckdb.__version__)
for r in range(int(sys.argv[1])):
    con.execute(f"""CREATE VIEW mesh_{r + 1} AS
WITH edge_in AS (
  SELECT e.edge AS id, s.f, s.val FROM edge e JOIN mesh_{r} s ON e.sender = s.id
  UNION ALL
  SELECT e.edge AS id, x.f + {f} AS f, x.val FROM edge e JOIN mesh_{r} x ON e.receiver = x.id),
message AS (
  SELECT i.id, w.g AS f, SUM(i.val * w.w) AS val
  FROM edge_in i JOIN w ON i.f = w.f GROUP BY i.id, w.g),
normed AS (
  SELECT id, f, (val - AVG(val) OVER (PARTITION BY id))
               / sqrt(VAR_POP(val) OVER (PARTITION BY id) + 1e-5) AS val
  FROM message),
summed AS (
  SELECT e.receiver AS id, m.f, SUM(m.val) AS val
  FROM normed m JOIN edge e ON m.id = e.edge WHERE m.f < {f} GROUP BY e.receiver, m.f)
SELECT x.id, x.f, x.val + coalesce(s.val, 0) AS val
FROM mesh_{r} x LEFT JOIN summed s ON x.id = s.id AND x.f = s.f""")
    start = time.perf_counter()
    plan = con.sql(f"EXPLAIN SELECT * FROM mesh_{r + 1}").fetchall()[0][1]
    print(f"mesh_{r + 1}: planned in {time.perf_counter() - start:6.2f} s, {plan.count('β”Œ')} boxes", flush=True)

python view_plan_repro.py 6, with DuckDB 1.5.6 on an Intel i7-8700:

View Planning (EXPLAIN) Boxes in the plan
mesh_1 0.00 s 28
mesh_2 0.01 s 57
mesh_3 0.05 s 79
mesh_4 0.45 s 81
mesh_5 6.71 s 83
mesh_6 111 s 85

Planning time grows about 15Γ— per view, while the plan grows by two boxes. So DuckDB finds that each view's reads are shared, and computes them once, but only after doing work exponential in the depth. It looks as if the views are expanded inline before the shared reads are found.

In the GraphCast demo

The same shape, with GraphCast_small's real tables: 308 operators for the encoder (mesh_0), planned in 0.21 s; 504 for one processor round (mesh_1), planned in 45 s; mesh_2 didn't finish planning in 10 minutes. A forecast has 16 rounds. DataFusion doesn't plan it either, for a different reason: it inlines every read, so its plan grows 4.7Γ— per round (#267).

For upstream

  • Search DuckDB's issues for planning time with nested views or CTEs, before filing.
  • If there's no existing issue, file this script and its table.
  • The demo keeps its SQL as written; it will run on DuckDB once planning is linear in depth.

Activity

  1. added
    mlGradients in SQL, the ml extra and its demos
    on Oct 31, 2026
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

    mlGradients in SQL, the ml extra and its demos

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions