-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix: UnnestExec preserves relevant equivalence properties of input #16985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
7527f16
to
5838f45
Compare
Tagging @alamb, maybe you can trigger CI? 🙏🏻 |
5838f45
to
751a8ba
Compare
/// For list unnesting, each rows is vertically transformed into multiple rows | ||
/// For struct unnesting, each columns is horizontally transformed into multiple columns, | ||
/// For list unnesting, each row is vertically transformed into multiple rows | ||
/// For struct unnesting, each column is horizontally transformed into multiple columns, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammar fix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
523eefd
to
80567ec
Compare
I discovered I have updated the PR doing that. Equivalence properties are
I am pretty sure that this takes care of 1 and 2, since we now have no equivalence properties for the columns. I am not yet sure about 3, though - if the original expression uses a column that is a primary key, after the unnest we will have multiple rows with the same column. Does that mean we need to remove that constraint from the eq properties? It kinda sounds like yes, but I need to see exactly what it's being used for. |
80567ec
to
a17ec47
Compare
After reading some more I have now updated it so that we remove any constraint from the properties. I've updated the PR description. I think this is semantically sound now. FYI @alamb and @asubiotto |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! This LGTM, I'll leave it to @alamb for a final review and CI kickoff.
79ec7e6
to
f1e889d
Compare
Hi @vegarsti - sorry I didn't see this earlier. I will try and review it over the next day or two Maybe @berkaysynnada or @suremarc has some time to review as well |
Friendly ping @alamb 😄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this contribution @vegarsti
I am sorry for the delayed review -- I am always trying to encourage others to review PRs, but indeed I often function as the reviewer of last resort. Anything you can do to help (like help review PRs yourself) would be most appreciated!
This is definitely the right direction, but when I did some testing of this PR some of the behavior didn't make sense to me
Could you look at the test I provided, as well as add additional cases:
- That unnest a struct (the one I provided unnests a list)
- has multiple list/structs unnested (as the code seems to handle such a case)
Thank you so much for the detailed and gracious review. Thanks for catching the weird behavior, I will address this. And I am happy to start reviewing PRs! |
Marking as draft as I think this PR is no longer waiting on feedback and I am trying to make it easier to find PRs in need of review. Please mark it as ready for review when it is ready for another look |
Indeed, thanks! |
5ab9778
to
c5ebd82
Compare
Figured out why the test @alamb added failed -- the way I was creating the projection mapping was too simplistic, causing indexes to not match. Will add the two requested test cases as well. |
c5ebd82
to
7099d11
Compare
Added two similar test cases:
|
0cf176b
to
95cdb26
Compare
c23e4a9
to
4093afb
Compare
Since CI ran on this one, I'll leave it here without updating the branch until this gets reviewed again 👍🏻 |
@berkaysynnada @suremarc @alamb Gentle ping for a review! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me, the changes and tests make sense. Thanks!
CAVEAT: I am by no means a DataFusion pro. Just trying to learn more while providing some feedback. :)
.iter() | ||
.enumerate() | ||
.filter(|(idx, _)| { | ||
!list_column_indices.contains(idx) && !struct_column_indices.contains(idx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we have had multiple issues with quadratic planning time for a large amount of columns. I think we could get the same problem here as the contains
is another linear scan, thus creating a quadratic runtime depending on the number of columns. I could also be wrong and this doesn't cause an issue.
Maybe we could build a buffer and then simply index into the buffer on whether this is unnested (not tested):
let input_schema = input.schema();
let mut unnested_indices = BooleanBufferBuilder::new(input.len());
unnested_indices.append_n(input.len(), false);
for list_unnest in list_column_indices {
unnested_indices.set_bit(list_unnest.index_in_input_schema, true);
}
for list_unnest in struct_column_indices {
unnested_indices.set_bit(*list_unnest, true)
}
let unnested_indices = unnested_indices.finish();
let non_unnested_indices: Vec<usize> = (0..input_schema.fields().len())
.filter(|idx| !unnested_indices.value(*idx))
.collect();
Otherwise, I think changing the iterator to (0..input_schema.fields().len())
would help with readability as you don't seem to be using the actual field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise, I think changing the iterator to (0..input_schema.fields().len()) would help with readability as you don't seem to be using the actual field.
Definitely doing this! Thank you.
Good idea to build a buffer and index into it. I'll give that a shot and see how it turns out!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'd only change that if its easy to do with similar complexity. I think the quadratic behavior only makes a problem if we have many many columns and most of them use unnest
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This worked very well! Added in c42c8c1. I think the buffer approach you gave is more readable as well, so it's win win! Thanks a lot
Maybe one additional note: I think the resulting sort properties can be improved for unnesting structs if we know that the struct columns themselves are ordered. If that makes sense we could also somehow expand the But as this is already an improvement I think that tracking this in a separate issue is fine. |
Thank you so much @tobixdev! |
Great idea! |
I took a look and it seems all good to me but given there's already been a lot of review on it I think the existing reviewers need to approve for it to be mergeable, so I will defer to them. Consider this my token ✅ |
4093afb
to
c42c8c1
Compare
Thanks a lot everyone! @alamb ready for the stamp now ;) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this contribution @vegarsti
I think this is very close. I think it should have:
- Some additional tests / comments cleanup (see comments)
- Avoid
unwrap
/expect
to minimize the severity of symptoms
physical_plan | ||
01)ProjectionExec: expr=[array_agg(unnested.ar)@1 as array_agg(unnested.ar)] | ||
02)--AggregateExec: mode=FinalPartitioned, gby=[generated_id@0 as generated_id], aggr=[array_agg(unnested.ar)], ordering_mode=Sorted | ||
03)----SortExec: expr=[generated_id@0 ASC NULLS LAST], preserve_partitioning=[true] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this plan shows the data being sorted, but the comment suggests it should not be 🤔
Could you please explain in more detail what you expect this explain plan to be showing? Given there is no ORDER BY
in the query (or in the OVER
clause) it is not clear why this is testing ordering
3 400 | ||
1 400 | ||
|
||
# Explain should not have a SortExec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also please add two additional tests:
- a negative test case here. order by the output of the unnest and verify that it is in fact sorted correctly
- A case with the ordering column as the first index (e.g. tuples like (
100, [3,2,1], 'a')
and then order by 100
Which issue does this PR close?
What changes are included in this PR?
UnnestExec
'scompute_properties
we now construct itsEquivalenceProperties
using what we can from the input plan, so that we preserve sort ordering of unrelated columns (and avoid unnecessary sorts further up in the plan).Are these changes tested?
UnnestExec
inunnest.slt
Are there any user-facing changes?
No
Explanation
Given a struct or array value
col
,unnest(col)
takes the N elements ofcol
and "spreads" these onto N rows, where all other columns in the statement are preserved. Said another way, when we unnest a column we are inserting a lateral cross-join against its elements, which by construction:E.g. (from
unnest.slt
):datafusion/datafusion/sqllogictest/test_files/unnest.slt
Lines 699 to 712 in 6d9b76e
The
EquivalenceProperties
struct has three types of properties:In this PR we construct the
UnnestExec
node'sEquivalenceProperties
by using the input plan's equivalence properties for the columns that are not transformed - except for table constraints, which we discard entirely. The reasoning for discarding constraints is that because we're duplicating the other columns across rows, we are invalidating any uniqueness or primary-key constraint. We also need to some twiddling with the mapping of the projection (indices change due to the unnesting).