Skip to content

Conversation

@mgree
Copy link
Contributor

@mgree mgree commented Apr 18, 2025

Introduces new default syntax for EXPLAIN, such that now (1) EXPLAIN by default explains LIR plans, which have unambiguous interpretations (unlike MIR plans), and (2) EXPLAIN shows information in a Postgres-like syntax, and significantly less information than it used to for EXPLAIN PHYSICAL PLAN FOR (i.e., for LIR).

You can still explain MIR plans with the old syntax using EXPLAIN OPTIMIZED PLAN FOR.

You can still explain LIR plans with the old, very verbose syntax using EXPLAIN PHYSICAL PLAN AS VERBOSE TEXT FOR.

Remaining TODOs:

Motivation

Tips for reviewer

Checklist

  • This PR has adequate test coverage / QA involvement has been duly considered. (trigger-ci for additional test/nightly runs)
  • This PR has an associated up-to-date design doc, is a design doc (template), or is sufficiently small to not require a design.
  • If this PR evolves an existing $T ⇔ Proto$T mapping (possibly in a backwards-incompatible way), then it is tagged with a T-proto label.
  • If this PR will require changes to cloud orchestration or tests, there is a companion cloud PR to account for those changes that is tagged with the release-blocker label (example).
  • If this PR includes major user-facing behavior changes, I have pinged the relevant PM to schedule a changelog post.

@mgree mgree force-pushed the explain-new-default-syntax branch 2 times, most recently from ed61038 to bcb046d Compare April 30, 2025 20:05
@mgree mgree force-pushed the explain-new-default-syntax branch 3 times, most recently from 99c9204 to 9322754 Compare May 14, 2025 14:06
@mgree mgree force-pushed the explain-new-default-syntax branch 8 times, most recently from 4f8da05 to 394276a Compare May 19, 2025 20:40
@mgree mgree marked this pull request as ready for review May 23, 2025 20:22
@mgree mgree requested review from a team as code owners May 23, 2025 20:22
@mgree mgree requested review from ParkMyCar, ggevay and kay-kim May 23, 2025 20:22
@mgree mgree force-pushed the explain-new-default-syntax branch from 1dc2362 to 3a5f0ba Compare June 4, 2025 15:50
Copy link
Contributor

@ggevay ggevay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm quickly typing some preliminary comments from looking at the new output. I need to sign off now for ~2 hours, but I'll review it a bit more later today. I think this new output format is eventually gonna be great!

For Map/Filter/Project, did we decide to not show the scalar expressions? I'm a bit concerned that this would make it hard to map it back to SQL (which is already harder for us than for Postgres, because of our plan operators being so far away from SQL, e.g., no outer joins, subqueries, window functions, ...).

Indexed l0 -- This appears both for Gets to global IDs and to local IDs, where the referred CTE ends with an arrangement. I'm not sure whether it's ok to use the word "index" in the latter case, since an index is a somewhat different concept from an arrangement. Maybe it would confuse users into thinking that it's using an index that they created. Maybe just somehow use the word "arranged" when it's a local ID?

Delta joins are extremely verbose. How about wiring up the existing WITH (JOIN IMPLEMENTATIONS) flag to the new explain, and show the verbose stuff only when it's set?

(IndexUsageType is not shown inline in the plan (only at the end, separately), but this is not so easy to fix, so ok if this will be a follow-up.)

"Consolidating Monotonic Hierarchical GroupAggregate" -- I'm thinking that maybe we should drop the word Hierarchical when it's also Monotonic, since there is actually no hierarchy rendered in this case. (This is a confusing thing also in the code, which we should eventually refactor.)

For a "Map/Filter/Project", I think "Input key" is important only in some extremely rare cases that are kinda deprecated (literal constraints discovered in MIR-to-LIR lowering), so let's not show it.

There might be some fused MFPs that are not shown. E.g., for TPC-H 04 I don't see the 1993-07-01 filter anywhere. This should be somewhere above or inside the join.

TPC-H 06: the Indexed ... Filter looks like as if we were using the index to do a range lookup, but this is not the case.

Literal constraints: Fast path looks good, but the slow path got much more verbose: we are now showing the underlying Differential join. Not sure we can do something with this in this PR, just something to keep in mind. E.g.:

create table t(x int, y int);
explain select * from t as t1 join t as t2 on t1.y = t2.y where t1.x=5;

Physical Plan
Explained Query:
  →Differential Join %0 » %1
    Join stage %0: Lookup key #1{y} in %1
    →Arrange
      Keys: 1 arrangement available, plus raw stream
        Arrangement 0: #1{y}
      →Differential Join %1 » %0
        Join stage %0: Lookup key #0{x} in %0
        →Indexed materialize.public.t
        →Arrange
          Keys: 1 arrangement available, plus raw stream
            Arrangement 0: #0
          →Constant (1 row)
    →Arrange
      Keys: 1 arrangement available, plus raw stream
        Arrangement 0: #1{y}
      →Indexed materialize.public.t
        Filter: (#1{y}) IS NOT NULL
        Key: (#0{x})
 
Used Indexes:
  - materialize.public.i1 (*** full scan ***, lookup)
 
Target cluster: quickstart

@mgree mgree force-pushed the explain-new-default-syntax branch from 3a5f0ba to ea2aa8f Compare June 4, 2025 18:12
@mgree
Copy link
Contributor Author

mgree commented Jun 4, 2025

I'm quickly typing some preliminary comments from looking at the new output. I need to sign off now for ~2 hours, but I'll review it a bit more later today. I think this new output format is eventually gonna be great!

Thank you!

For Map/Filter/Project, did we decide to not show the scalar expressions? I'm a bit concerned that this would make it hard to map it back to SQL (which is already harder for us than for Postgres, because of our plan operators being so far away from SQL, e.g., no outer joins, subqueries, window functions, ...).

We do show them---just not projects. Check out test/sqllogictest/explain/default.slt for many examples.

Indexed l0 -- This appears both for Gets to global IDs and to local IDs, where the referred CTE ends with an arrangement. I'm not sure whether it's ok to use the word "index" in the latter case, since an index is a somewhat different concept from an arrangement. Maybe it would confuse users into thinking that it's using an index that they created. Maybe just somehow use the word "arranged" when it's a local ID?

Sure, changed!

Delta joins are extremely verbose. How about wiring up the existing WITH (JOIN IMPLEMENTATIONS) flag to the new explain, and show the verbose stuff only when it's set?

Thoughts on what to trim/condense?

(IndexUsageType is not shown inline in the plan (only at the end, separately), but this is not so easy to fix, so ok if this will be a follow-up.)

"Consolidating Monotonic Hierarchical GroupAggregate" -- I'm thinking that maybe we should drop the word Hierarchical when it's also Monotonic, since there is actually no hierarchy rendered in this case. (This is a confusing thing also in the code, which we should eventually refactor.)

Sure, changed!

For a "Map/Filter/Project", I think "Input key" is important only in some extremely rare cases that are kinda deprecated (literal constraints discovered in MIR-to-LIR lowering), so let's not show it.

Is this true for the input_key we see in FlatMap, Reduce, and ArrangeBy? I'm happy to drop more stuff on the floor!

There might be some fused MFPs that are not shown. E.g., for TPC-H 04 I don't see the 1993-07-01 filter anywhere. This should be somewhere above or inside the join.

It's getting pushed down:

explain as verbose text SELECT
    o_orderpriority,
    count(*) AS order_count
FROM
    orders
WHERE
    o_orderdate >= DATE '1993-07-01'
    AND o_orderdate < DATE '1993-07-01' + INTERVAL '3' month
    AND EXISTS (
        SELECT
            *
        FROM
            lineitem
        WHERE
            l_orderkey = o_orderkey
            AND l_commitdate < l_receiptdate
    )
GROUP BY
    o_orderpriority
ORDER BY
    o_orderpriority;
                                               Physical Plan                                               
-----------------------------------------------------------------------------------------------------------
 Explained Query:                                                                                         +
   Finish order_by=[#0 asc nulls_last] output=[#0, #1]                                                    +
     Reduce::Accumulable                                                                                  +
       simple_aggrs[0]=(0, 0, count(*))                                                                   +
       val_plan                                                                                           +
         project=(#1)                                                                                     +
         map=(true)                                                                                       +
       key_plan=id                                                                                        +
       Join::Linear                                                                                       +
         linear_stage[0]                                                                                  +
           closure                                                                                        +
             project=(#1)                                                                                 +
           lookup={ relation=0, key=[#0] }                                                                +
           stream={ key=[#0], thinning=() }                                                               +
         source={ relation=1, key=[#0] }                                                                  +
         ArrangeBy                                                                                        +
           raw=true                                                                                       +
           arrangements[0]={ key=[#0], permutation=id, thinning=(#1) }                                    +
           types=[bigint, text]                                                                           +
           Get::Collection materialize.public.orders                                                      +
             raw=true                                                                                     +
         Reduce::Distinct                                                                                 +
           val_plan                                                                                       +
             project=()                                                                                   +
           key_plan=id                                                                                    +
           Get::Collection materialize.public.lineitem                                                    +
             raw=true                                                                                     +
                                                                                                          +
 Source materialize.public.lineitem                                                                       +
   project=(#0)                                                                                           +
   filter=((#11{l_commitdate} < #12{l_receiptdate}))                                                      +
 Source materialize.public.orders                                                                         +
   project=(#0, #5)                                                                                       +
   filter=((date_to_timestamp(#4{o_orderdate}) < 1993-10-01 00:00:00) AND (#4{o_orderdate} >= 1993-07-01))+
                                                                                                          +
 Target cluster: quickstart                                                                               +
 

TPC-H 06: the Indexed ... Filter looks like as if we were using the index to do a range lookup, but this is not the case.

What do you mean? This is what I get:

explain SELECT
    sum(l_extendedprice * l_discount) AS revenue
FROM
    lineitem
WHERE
    l_quantity < 24
    AND l_shipdate >= DATE '1994-01-01'
    AND l_shipdate < DATE '1994-01-01' + INTERVAL '1' year
    AND l_discount BETWEEN 0.06 - 0.01 AND 0.07;
                                                                                         Physical Plan                                                                                         
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Explained Query:                                                                                                                                                                             +
   →With                                                                                                                                                                                      +
     cte l0 =                                                                                                                                                                                 +
       →Accumulable GroupAggregate                                                                                                                                                            +
         Simple aggregates: sum((#0{l_extendedprice} * #1{l_discount}))                                                                                                                       +
         →Read materialize.public.lineitem                                                                                                                                                    +
   →Return                                                                                                                                                                                    +
     →Union                                                                                                                                                                                   +
       →Unarranged Raw Stream                                                                                                                                                                 +
         Input key: ()                                                                                                                                                                        +
         →Indexed l0                                                                                                                                                                          +
       →Map/Filter/Project                                                                                                                                                                    +
         Map: null                                                                                                                                                                            +
           →Consolidating Union                                                                                                                                                               +
             →Negate Diffs                                                                                                                                                                    +
               →Indexed l0                                                                                                                                                                    +
                 Key: ()                                                                                                                                                                      +
             →Constant (1 row)                                                                                                                                                                +
                                                                                                                                                                                              +
 Source materialize.public.lineitem                                                                                                                                                           +
   project=(#5, #6)                                                                                                                                                                           +
   filter=((#4{l_quantity} < 24) AND (#6{l_discount} <= 0.07) AND (#6{l_discount} >= 0.05) AND (date_to_timestamp(#10{l_shipdate}) < 1995-01-01 00:00:00) AND (#10{l_shipdate} >= 1994-01-01))+
                                                                                                                                                                                              +
 Target cluster: quickstart                                                                                                                                                                   +
 
(1 row)

Literal constraints: Fast path looks good, but the slow path got much more verbose: we are now showing the underlying Differential join. Not sure we can do something with this in this PR, just something to keep in mind. E.g.:

create table t(x int, y int);
explain select * from t as t1 join t as t2 on t1.y = t2.y where t1.x=5;

Physical Plan
Explained Query:
  →Differential Join %0 » %1
    Join stage %0: Lookup key #1{y} in %1
    →Arrange
      Keys: 1 arrangement available, plus raw stream
        Arrangement 0: #1{y}
      →Differential Join %1 » %0
        Join stage %0: Lookup key #0{x} in %0
        →Indexed materialize.public.t
        →Arrange
          Keys: 1 arrangement available, plus raw stream
            Arrangement 0: #0
          →Constant (1 row)
    →Arrange
      Keys: 1 arrangement available, plus raw stream
        Arrangement 0: #1{y}
      →Indexed materialize.public.t
        Filter: (#1{y}) IS NOT NULL
        Key: (#0{x})
 
Used Indexes:
  - materialize.public.i1 (*** full scan ***, lookup)
 
Target cluster: quickstart

Weird, I get something different:

create table t(x int, y int);
explain select * from t as t1 join t as t2 on t1.y = t2.y where t1.x=5;
                    Physical Plan                     
------------------------------------------------------
 Explained Query:                                    +
   →Differential Join %0 » %1                        +
     Join stage %0: Lookup key #1{y} in %1           +
     →Arrange                                        +
       Keys: 1 arrangement available, plus raw stream+
         Arrangement 0: #1{y}                        +
       →Read materialize.public.t                    +
         Filter: (#0{x} = 5)                         +
     →Arrange                                        +
       Keys: 1 arrangement available, plus raw stream+
         Arrangement 0: #1{y}                        +
       →Read materialize.public.t                    +
                                                     +
 Source materialize.public.t                         +
   filter=((#1{y}) IS NOT NULL)                      +
                                                     +
 Target cluster: quickstart                          +

@antiguru antiguru requested review from antiguru and removed request for ParkMyCar June 4, 2025 19:10

## Which part of my query runs slowly or uses a lot of memory?

{{< public-preview />}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove the public-preview annotation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


You can [`EXPLAIN`](/sql/explain-plan/) a query to see how it will be run as a
dataflow. In particular, `EXPLAIN PHYSICAL PLAN` will show the concrete, fully
dataflow. In particular, `EXPLAIN PHYSICAL PLAN` (the default) will show the concrete, fully
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's in the next graf... should I move it up?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: I added a reference to EXPLAIN ANALYZE towards the bottom of EXPLAIN PLAN.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ... got it! Thank you!!!

Copy link
Member

@antiguru antiguru left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks fine, with the nit that the arrow doesn't render nicely in the font Github choses on Linux. I'm good with merging as-is and iterating if needed.

image

@sjwiesman
Copy link
Contributor

Format LGTM

@ggevay
Copy link
Contributor

ggevay commented Jun 7, 2025

For Map/Filter/Project, did we decide to not show the scalar expressions? I'm a bit concerned that this would make it hard to map it back to SQL (which is already harder for us than for Postgres, because of our plan operators being so far away from SQL, e.g., no outer joins, subqueries, window functions, ...).

We do show them---just not projects. Check out test/sqllogictest/explain/default.slt for many examples.

Ah, ok, sorry! Why not show Projects? Because we don't have col names anyway for those currently?

Delta joins are extremely verbose. How about wiring up the existing WITH (JOIN IMPLEMENTATIONS) flag to the new explain, and show the verbose stuff only when it's set?

Thoughts on what to trim/condense?

Maybe by default it would be enough to know that it's a delta join and the arrangement keys for each input? And WITH (JOIN IMPLEMENTATIONS) could show the whole thing, i.e., all the paths. But then the question is where to put those MFPs that are pushed inside the paths (but not all the way through the join), i.e., the closure in DeltaStagePlan and initial_closure in DeltaPathPlan. Maybe when the are non-trivial then fall back to showing all the paths regardless of the flag? I'm not sure.

For a "Map/Filter/Project", I think "Input key" is important only in some extremely rare cases that are kinda deprecated (literal constraints discovered in MIR-to-LIR lowering), so let's not show it.

Is this true for the input_key we see in FlatMap, Reduce, and ArrangeBy? I'm happy to drop more stuff on the floor!

I'd say we can also hide it for FlatMap: The only thing we do with the input_key is we read the arrangement that is specified by the key and simply turn it into a stream. This adds a slight overhead compared to when input_key is None, but it's probably not interesting enough for users. Same for Reduce. ArrangeBy is a slightly bit more complex, but I'd say we can also hide it there.

There might be some fused MFPs that are not shown. E.g., for TPC-H 04 I don't see the 1993-07-01 filter anywhere. This should be somewhere above or inside the join.

It's getting pushed down:

Ah, yes, sorry! I typed that comment too quickly.

TPC-H 06: the Indexed ... Filter looks like as if we were using the index to do a range lookup, but this is not the case.

What do you mean? This is what I get:

This is my TPC-H setup:
https://docs.google.com/document/d/1eumASOfCjl9YRAne1W-EOiupNm59rC3b-Mqsp9cDaL8/edit?usp=sharing
With this, I get:

                                                                                            Physical Plan                                                                                             
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Explained Query:                                                                                                                                                                                    +
   →With                                                                                                                                                                                             +
     cte l0 =                                                                                                                                                                                        +
       →Accumulable GroupAggregate                                                                                                                                                                   +
         Simple aggregates: sum((#0{l_extendedprice} * #1{l_discount}))                                                                                                                              +
         →Arranged materialize.public.lineitem                                                                                                                                                       +
           Filter: (#4{l_quantity} < 24) AND (#6{l_discount} <= 0.07) AND (#6{l_discount} >= 0.05) AND (#10{l_shipdate} >= 1994-01-01) AND (date_to_timestamp(#10{l_shipdate}) < 1995-01-01 00:00:00)+
           Key: (#0{l_orderkey}, #3{l_linenumber})                                                                                                                                                   +
   →Return                                                                                                                                                                                           +
     →Union                                                                                                                                                                                          +
       →Unarranged Raw Stream                                                                                                                                                                        +
         Input key: ()                                                                                                                                                                               +
         →Arranged l0                                                                                                                                                                                +
       →Map/Filter/Project                                                                                                                                                                           +
         Map: null                                                                                                                                                                                   +
           →Consolidating Union                                                                                                                                                                      +
             →Negate Diffs                                                                                                                                                                           +
               →Arranged l0                                                                                                                                                                          +
                 Key: ()                                                                                                                                                                             +
             →Constant (1 row)                                                                                                                                                                       +
                                                                                                                                                                                                     +
 Used Indexes:                                                                                                                                                                                       +
   - materialize.public.pk_lineitem_orderkey_linenumber (*** full scan ***)                                                                                                                          +
                                                                                                                                                                                                     +
 Target cluster: quickstart                                                                                                                                                                          +
 
(1 row)

I think when I wrote my above comment on Wednesday, it was slightly different, saying Indexed instead of Arranged.

There was also:

Indexed l0 -- This appears both for Gets to global IDs and to local IDs, where the referred CTE ends with an arrangement. I'm not sure whether it's ok to use the word "index" in the latter case, since an index is a somewhat different concept from an arrangement. Maybe it would confuse users into thinking that it's using an index that they created. Maybe just somehow use the word "arranged" when it's a local ID?

Sure, changed!

But the Arranged materialize.public.lineitem is reading a global id, so I was thinking it could say Indexed in this case. But there is also an argument to be made for making Get uniform for local and global ids, so not sure.

In any case, maybe just make it clear that the Filter is not part of the arrangement lookup itself, but happens afterwards. Maybe something like FilterAfter.

Btw. a more general issue, which is also relevant for the above, is that there are several cases where a field of an LIR node is (semantically, not necessarily physically) applied after the main thing that the node does. (E.g., Get reading an arrangement, and then applying a filter; Reduce doing its thing and then applying an MFP; or any other time when there is an mfp_after field.) In these cases, the bottom-to-top data flow is broken when reading the EXPLAIN output, because we put the field under the node, so then the reader's eye has to jump around. We could maybe cheat, and render these fields as a separate node above the original node? But it's tricky because it's also important whether an MFP is fused from above into some other operator or it's executed separately. E.g., Reduce and FlatMap not simply apply their mfp_after after their main thing is done, but do smart fusion stuff. So if we end up rendering the mfp_after as a separate node, we could indicate the fusion on it with an extra word: e.g., writing (fused downward) somewhere in that line. So, for example, the above

         →Arranged materialize.public.lineitem                                                                                                                                                       +
           Filter: (#4{l_quantity} < 24) AND (#6{l_discount} <= 0.07) AND (#6{l_discount} >= 0.05) AND (#10{l_shipdate} >= 1994-01-01) AND (date_to_timestamp(#10{l_shipdate}) < 1995-01-01 00:00:00)+
           Key: (#0{l_orderkey}, #3{l_linenumber})    

could instead be

         →Filter (fused downward): (#4{l_quantity} < 24) AND (#6{l_discount} <= 0.07) AND (#6{l_discount} >= 0.05) AND (#10{l_shipdate} >= 1994-01-01) AND (date_to_timestamp(#10{l_shipdate}) < 1995-01-01 00:00:00)
           →Arranged materialize.public.lineitem
             Key: (#0{l_orderkey}, #3{l_linenumber})

(Although, I think ArrangeBy specifically doesn't do smart fusion stuff, so here the fusion just avoids the overhead of a separate operator. Reduce's smart fusion thing is that sometimes it builds the output arrangement after the MFP, and FlatMap's smart fusion thing is that it applies the projection before constructing the output rows to avoid copying the input many times when e.g. unnesting a big list.)

Edit: And the same with write!(f, " (fused unnest_list)")?;: This could be also shown as a fake node above the Reduce, but with indicating that it's fused downward.

[literal constraints]

Weird, I get something different

Sorry, I forgot the index!

materialize=> 
create table t(x int, y int);
create index on t(x);
explain select * from t as t1 join t as t2 on t1.y = t2.y where t1.x = 5;
 
CREATE TABLE
CREATE INDEX
                       Physical Plan                        
------------------------------------------------------------
 Explained Query:                                          +
   →Differential Join %0 » %1                              +
     Join stage %0: Lookup key #1{y} in %1                 +
     →Arrange                                              +
       Keys: 1 arrangement available, plus raw stream      +
         Arrangement 0: #1{y}                              +
       →Differential Join %1 » %0                          +
         Join stage %0: Lookup key #0{x} in %0             +
         →Arranged materialize.public.t                    +
         →Arrange                                          +
           Keys: 1 arrangement available, plus raw stream  +
             Arrangement 0: #0                             +
           →Constant (1 row)                               +
     →Arrange                                              +
       Keys: 1 arrangement available, plus raw stream      +
         Arrangement 0: #1{y}                              +
       →Arranged materialize.public.t                      +
         Filter: (#1{y}) IS NOT NULL                       +
         Key: (#0{x})                                      +
                                                           +
 Used Indexes:                                             +
   - materialize.public.t_x_idx (*** full scan ***, lookup)+
                                                           +
 Target cluster: quickstart                                +
 
(1 row)

Edit: This is the same situation as in the "Test an IndexedFilter join." in the slt.

Map: null, null
→Consolidating Union
→Negate Diffs
→Differential Join %1 » %0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we are not rendering the equivalences. At the LIR level, they are hidden in ready_equivalences. We could show them inside the stages, and/or also at the top level of the Join node (like in MIR). I think showing them at the top level would be important! (When collecting them to the top level, we'll need to depermute col refs in them, because in ready_equivalences the col refs are in terms of what inputs are already available at the corresponding stage.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding them stage-by-stage.

Copy link
Contributor

@ggevay ggevay Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, there is a bigger problem here than I thought: ready_equivalences contains only those equivalences that need to be applied after executing a lookup, but not those that are implicitly applied by the lookup itself. For example, looking at

WHERE a = c and d = e and b = f
the EXPLAIN output only shows #3{f} = #2{b}. Contrast this with the MIR EXPLAIN, which shows
Join on=(#0{a} = #2{c} AND #1{b} = #5{f} AND #3{d} = #4{e}) type=delta.
The problem is that not seeing the equivalences makes it harder to attribute back the plan to SQL. (Especially if the inputs are not Gets of global things, like materialize.public.t here, but other plan fragments.)

I'm not sure whether there is a straightforward way to recover this info from LIR. We might have to put this information explicitly there during the lowering from MIR, i.e., just copy over the MIR Join node's equivalences. Edit: Or get it from stream_key and lookup_key.

Input key: (#0{a})
Keys: 2 arrangements available, no raw stream
Arrangement 0: #0{a}
Arrangement 1: #1{b}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm now super confused: I thought that ArrangeBy's forms are just the new arrangements that are getting created right here. But shouldn't we be creating only one new arrangement here? Is this a lowering bug maybe? Also, does rendering do the correct thing (create just one new arrangement), or it creates two new arrangements? Or what's going on?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're just showing every arrangement we have.

Copy link
Contributor

@ggevay ggevay Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, that's what I originally thought, but then it turned out that this is not the case in other situations, see #32262 (comment)

Copy link
Contributor

@ggevay ggevay Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I was so confused about how this works. So, the lowering does seem to include all the output arrangements in an LIR ArrangeBy, even those that already exist.

But wait, then I'm sad, because I thought that the #1 benefit of basing our new EXPLAIN format on LIR would be that ArrangeBys will show the arrangements that are created right there. If this is not true, then figuring out how many arrangements are getting created will still involve arcane knowledge about what operators produce what output arrangements in what cases.

Couldn't we just add an invariant that LIR ArrangeBy lists only not-yet-existing arrangements? This shouldn't be hard, as during the lowering we already know which arrangements exist when going into an ArrangeBy.

@mgree mgree force-pushed the explain-new-default-syntax branch from 88d3537 to 44643cb Compare June 12, 2025 14:04
@mgree
Copy link
Contributor Author

mgree commented Jun 12, 2025

For Map/Filter/Project, did we decide to not show the scalar expressions? I'm a bit concerned that this would make it hard to map it back to SQL (which is already harder for us than for Postgres, because of our plan operators being so far away from SQL, e.g., no outer joins, subqueries, window functions, ...).

We do show them---just not projects. Check out test/sqllogictest/explain/default.slt for many examples.

Ah, ok, sorry! Why not show Projects? Because we don't have col names anyway for those currently?>

I just don't think it's that important, especially if we have column names.

Delta joins are extremely verbose. How about wiring up the existing WITH (JOIN IMPLEMENTATIONS) flag to the new explain, and show the verbose stuff only when it's set?

Thoughts on what to trim/condense?

Maybe by default it would be enough to know that it's a delta join and the arrangement keys for each input? And WITH (JOIN IMPLEMENTATIONS) could show the whole thing, i.e., all the paths. But then the question is where to put those MFPs that are pushed inside the paths (but not all the way through the join), i.e., the closure in DeltaStagePlan and initial_closure in DeltaPathPlan. Maybe when the are non-trivial then fall back to showing all the paths regardless of the flag? I'm not sure.
[snip]
I'd say we can also hide it for FlatMap: The only thing we do with the input_key is we read the arrangement that is specified by the key and simply turn it into a stream. This adds a slight overhead compared to when input_key is None, but it's probably not interesting enough for users. Same for Reduce. ArrangeBy is a slightly bit more complex, but I'd say we can also hide it there.

Hidden.

But the Arranged materialize.public.lineitem is reading a global id, so I was thinking it could say Indexed in this case. But there is also an argument to be made for making Get uniform for local and global ids, so not sure.

I'm going to leave it alone for now. We should aim for simpler terminology---the difference between "arranged" and "index" is subtle.

Btw. a more general issue, which is also relevant for the above, is that there are several cases where a field of an LIR node is (semantically, not necessarily physically) applied after the main thing that the node does.
[snip]
Edit: And the same with write!(f, " (fused unnest_list)")?;: This could be also shown as a fake node above the Reduce, but with indicating that it's fused downward.

Done! I used Fused X to indicate an operator was fused with the operator below. I was tempted to write Downward Fused X, but that feels too verbose. Updated docs to reflect this.

@mgree mgree force-pushed the explain-new-default-syntax branch from b2043a0 to 926fc4a Compare June 12, 2025 16:07
@mgree mgree enabled auto-merge (squash) June 12, 2025 20:29
@mgree mgree merged commit 09152d2 into MaterializeInc:main Jun 12, 2025
83 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants