Prune Nested Fields for Parquet Columns#5547
Prune Nested Fields for Parquet Columns#5547zhenxiao wants to merge 1 commit intoprestodb:masterfrom
Conversation
presto-hive/pom.xml
Outdated
There was a problem hiding this comment.
The hive connector shouldn't have a dependency on presto-main. In fact, the classes in presto-main are not guaranteed to be available to plugins (due to classloader isolation)
a3a8c62 to
da929c1
Compare
There was a problem hiding this comment.
@cberner we need to get RowType's Fields, and use the RowField name to look up the corresponding parquet schema in parquet files
There was a problem hiding this comment.
Ya, you should be able to just use the TypeSignature all the names will be in there
|
@nezihyigitbasi do you have time to take a first look at this? |
|
@cberner sure, will do. |
|
@zhenxiao I gave this patch a try and I guess hit a bug. Once you fix the issue I will continue reviewing. I first enabled the new optimizer in the config file and then created a test table then when I query the table it fails: But if I disable the new optimizer I can query the table fine. |
|
@nezihyigitbasi Thank you, get it fixed. Your comments are appreciated. |
There was a problem hiding this comment.
do you need this as a field? This is not used anywhere, the constructor arg typeManager is passed to the createGroupConverter() call.
|
thank you, @mbasmanova |
mbasmanova
left a comment
There was a problem hiding this comment.
@zhenxiao Some more comments.
There was a problem hiding this comment.
since this method returns a list, extractDereferenceExpressions might be a better name
There was a problem hiding this comment.
this variable contains a list of dereference expressions used in the predicate, but the expressions themselves are not predicates, e.g. a.b < 10 is a predicate, but a.b is not. Since this variable is used only once, it can be inlined which removes the need to come up with a better name.
There was a problem hiding this comment.
as a general rule, method names start with a verb, e.g. pushDownDereferences
There was a problem hiding this comment.
This variable is not needed. You can swap key and value in pushdownExpressions instead.
Map<Symbol, DereferenceExpression> pushdownExpressions = expressions.entrySet().stream()
.filter(entry -> !isCastRowType(entry.getKey()))
.filter(entry -> outputSymbols.contains(getOnlyElement(extractAll(entry.getKey()))))
.collect(toImmutableMap(Map.Entry::getValue, Map.Entry::getKey));
...
ImmutableMap.Builder<Symbol, Symbol> symbolsBuilder = ImmutableMap.builder();
pushdownExpressions.entrySet().stream()
.forEach(entry -> symbolsBuilder.put(getOnlyElement(extractAll(entry.getValue())), entry.getKey()));
Map<Symbol, Symbol> symbolsMap = symbolsBuilder.build();
...
DereferenceExpression targetDereference = pushdownExpressions.get(targetSymbol);
There was a problem hiding this comment.
similar to other rules, this variable is not needed
There was a problem hiding this comment.
consider
pushdownExpressions.entrySet().stream()
.forEach(entry -> symbolsBuilder.put(getOnlyElement(extractAll(entry.getValue())), entry.getKey()));
There was a problem hiding this comment.
EqualJoinClause can only reference symbols; it cannot contain dereference expressions, can it?
...in/src/main/java/com/facebook/presto/sql/planner/iterative/rule/DereferencePushDownSort.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
I think it would be clearer if dereferencePushDown returned a Result. Then this check would be simplified to if (result.isEmpty())
57cfc92 to
2023fff
Compare
zhenxiao
left a comment
There was a problem hiding this comment.
thank you, @mbasmanova
I get comments addressed
There was a problem hiding this comment.
you are correct. this never happens. It is dead code. I will fix
...in/src/main/java/com/facebook/presto/sql/planner/iterative/rule/DereferencePushDownSort.java
Outdated
Show resolved
Hide resolved
mbasmanova
left a comment
There was a problem hiding this comment.
@zhenxiao A few questions.
There was a problem hiding this comment.
dereferences is a set of DereferenceExpression and base is an instance of SymbolReference; will this ever return true? I don't think so. Looks like this whole if statement can be deleted and the outer loop simplified to
while (base instanceof DereferenceExpression) {
if (dereferences.contains(base)) {
return true;
}
base = ((DereferenceExpression) base).getBase();
}
There was a problem hiding this comment.
baseExists might be a better name
There was a problem hiding this comment.
node.getAssignments().getExpressions() is equivalent but easier to understand
...in/src/main/java/com/facebook/presto/sql/planner/iterative/rule/DereferencePushDownRule.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Simplify using Assignments::rewrite:
Assignments assignments = node.getAssignments().rewrite(new DereferenceReplacer(expressions));
There was a problem hiding this comment.
Reformat for readability (capitalize SQL keywords, split into multiple lines, replace t1 with t(msg)):
assertPlan("WITH t(msg) AS (SELECT * FROM (VALUES ROW(CAST(ROW(1, 2.0) AS ROW(x BIGINT, y DOUBLE))))) " +
"SELECT msg.x FROM t WHERE msg.x > 10",
output(ImmutableList.of("x"),
project(ImmutableMap.of("x", expression("msg.x")),
filter("msg.x > BIGINT '10'",
values("msg")))));
However, I'd expect msg.x projection to be pushed down, e.g. I'd think the plan would look like
Output(x)
Filter(x > 10)
Project(x: msg.x)
Values
Also, I removed the new rules from the optimizer and re-ran the tests. All tests but testDereferencePushdownJoin succeeded. Could you modify the tests to use queries that are affected by the new rules?
There was a problem hiding this comment.
let me rework the testcases
predicatePushDown will push down filter just on top of tablescan or values. It is OK we have dereference just on top of filter, as long as ScanFilterAndProject has dereferences, so that virtual nested columns could be generated by leveraging dereferences in connector. What do you think?
There was a problem hiding this comment.
@zhenxiao That's right. This is a subtle point that's not easy to get from reading the rules code. Perhaps, modify the rules to not do anything for project(filter(tablescan)) and project(filter(values)). It would also help to update the PR description to give examples of queries where dereference pushdown will be successful and show queries where new rules don't make a difference. For the individual rules, consider adding documentation. See com.facebook.presto.sql.planner.iterative.rule.PushProjectionThroughExchange for an example.
2023fff to
f7f969f
Compare
zhenxiao
left a comment
There was a problem hiding this comment.
thank you, @mbasmanova
Let me rework on the testcases. other comments are addressed.
...in/src/main/java/com/facebook/presto/sql/planner/iterative/rule/DereferencePushDownRule.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
let me rework the testcases
predicatePushDown will push down filter just on top of tablescan or values. It is OK we have dereference just on top of filter, as long as ScanFilterAndProject has dereferences, so that virtual nested columns could be generated by leveraging dereferences in connector. What do you think?
mbasmanova
left a comment
There was a problem hiding this comment.
@zhenxiao Some further questions and comments.
There was a problem hiding this comment.
nit: inline this variable
There was a problem hiding this comment.
requireNonNull: this.expressions = requireNonNull(expressions, "expressions is null");
There was a problem hiding this comment.
This is a bit verbose. How do you feel about creating a top-level PushDownDereferences class and covert all these rules into inner classes?
dereferencePushDownRules = new PushDownDereferences(metadata, sqlParser).rules();
dereferencePushDownRules variable can then be inlined
There was a problem hiding this comment.
Why is it guaranteed that extractAll(entry.getKey()) returns just one element? I'm thinking of a dereference expression f(a, b).c which contains two symbols: a and b.
There was a problem hiding this comment.
What's the logic behind checking for cast to row?
There was a problem hiding this comment.
Why exit here? What if the predicate has dereferences that can be pushed down?
There was a problem hiding this comment.
This line is too long. It might be easier to read if rewritten to
return Result.ofPlanNode(
new JoinNode(
context.getIdAllocator().getNextId(),
joinNode.getType(),
leftChild,
rightChild,
joinNode.getCriteria(),
ImmutableList.<Symbol>builder()
.addAll(leftChild.getOutputSymbols())
.addAll(rightChild.getOutputSymbols())
.build(),
joinNode.getFilter().map(expression -> replaceDereferences(expression, expressions)),
joinNode.getLeftHashSymbol(),
joinNode.getRightHashSymbol(),
joinNode.getDistributionType()));
There was a problem hiding this comment.
this method is not used
There was a problem hiding this comment.
inline this variable and rightOutputs
Assignments.Builder leftBuilder = Assignments.builder();
leftBuilder.putIdentities(left.getOutputSymbols().stream()
.filter(symbol -> !symbolsMap.containsKey(symbol))
.collect(toImmutableList()));
Assignments.Builder rightBuilder = Assignments.builder();
rightBuilder.putIdentities(right.getOutputSymbols().stream()
.filter(symbol -> !symbolsMap.containsKey(symbol))
.collect(toImmutableList()));
There was a problem hiding this comment.
This can be simplified as
Map<Symbol, Symbol> symbolsMap = pushdownExpressions.entrySet().stream()
.collect(toImmutableMap(entry -> getOnlyElement(extractAll(entry.getValue())), Map.Entry::getKey));
f7f969f to
2bb0926
Compare
|
thank you, @mbasmanova A few updates:
|
mbasmanova
left a comment
There was a problem hiding this comment.
@zhenxiao Some further questions and comments.
There was a problem hiding this comment.
This is confusing because join's equi-clause can't contain expressions. The following might be clearer:
* Transforms:
* <pre>
* Project(a_x := a.msg.x)
* Join(a_y = b_y) => [a]
* Project(a_y := a.msg.y)
* Source(a)
* Project(b_y := b.msg.y)
* Source(b)
* </pre>
* to:
* <pre>
* Join(a_y = b_y) => [a_x]
* Project(a_x := a.msg.x, a_y := a.msg.y)
* Source(a)
* Project(b_y := b.msg.y)
* Source(b)
* </pre>
There was a problem hiding this comment.
PushDownDereferenceThroughJoin might be a better name
presto-main/src/test/java/com/facebook/presto/sql/planner/TestDereferencePushDown.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
field, field1, left and right names and not easy to follow. How about msg, a_y, b_y, etc.?
assertPlan("WITH t(msg) AS (SELECT * FROM (VALUES ROW(CAST(ROW(1, 2.0) AS ROW(x BIGINT, y DOUBLE))))) " +
"SELECT b.msg.x FROM t a, t b WHERE a.msg.y = b.msg.y",
output(ImmutableList.of("b_x"),
join(INNER, ImmutableList.of(equiJoinClause("a_y", "b_y")),
anyTree(
project(ImmutableMap.of("a_y", expression("msg.y")),
values("msg"))
), anyTree(
project(ImmutableMap.of("b_y", expression("msg.y"), "b_x", expression("msg.x")),
values("msg"))))));
2bb0926 to
c4ae7b2
Compare
zhenxiao
left a comment
There was a problem hiding this comment.
thank you, @mbasmanova
I get comments addressed
will add more testcase coverage. could you please review when u are free?
There was a problem hiding this comment.
no need to pushdown (Cast As Row).field dereference, since lower level does not have this as Row.
Fix me if my understanding is wrong
There was a problem hiding this comment.
my bad. trying to mean key is a primitive type, but msg is a struct type, only pushdown dereferences. Will fix it
There was a problem hiding this comment.
my bad, it is push dereference through sort. Will fix and add unnest
099c844 to
4ab0fa9
Compare
mbasmanova
left a comment
There was a problem hiding this comment.
@zhenxiao Some further questions.
There was a problem hiding this comment.
Indeed, but the same can be said about any dereference where base is not a symbol, e.g. f(a).x. Why is it important to filter out cast, but not other functions? Could you add a test that covers these cases?
There was a problem hiding this comment.
I still don't understand this filter. entry.getKey() is a dereference expression coming from a projection above this join. extractAll(entry.getKey()) returns all symbols that are used in that dereference expression. outputSymbols are symbols produced by the join; these are inputs to the project node above. With project over join, the project can only use output symbols of the join. Hence, this check must always be true. Hence, it is not needed. Am I missing something? I commented out this check and the above check for cast and ran the tests. All passed.
There was a problem hiding this comment.
.stream().collect(toImmutableList()) is unnecessary; just change the type of the first argument of getDereferenceSymbolMap to Collection<Expression>.
There was a problem hiding this comment.
Shouldn't this filter use baseExists?
There was a problem hiding this comment.
No. This is to filter out join filters dereferences that not covered in projectExpressions.
baseExists is to filter out dereferences already exists in other dereferences base
There was a problem hiding this comment.
Could you scan through all the node types and see if there are more nodes to add here? I'm thinking about AssignUniqueId, MarkDistinct, Limit and SemiJoin.
There was a problem hiding this comment.
This project node is redundant. Why not remove it?
There was a problem hiding this comment.
there is RemoveRedundantIdentityProjections following PushDownDereferences, so that redundant project node is removed. I will remove it from comments. Or, do you think we should add RemoveRedundantIdentityProjections to PushDownDereferences?
There was a problem hiding this comment.
This project node is redundant.
There was a problem hiding this comment.
This project node is redundant.
There was a problem hiding this comment.
Other examples show an identity projection in the result, but it is missing here. At the same time the code seems to be actually generating an identity projection and there is no logic to remove it. Let's add it here or add logic to remove it to the rule.
There was a problem hiding this comment.
Tests only cover one-level deep dereferences, e.g. msg.x and msg.y. Would you add tests with more levels to test the logic of consolidating a.b.c and a.b into a.b?
zhenxiao
left a comment
There was a problem hiding this comment.
thank you, @mbasmanova
I get comments addressed
will add support for AssignUniqueId, MarkDistinct, Limit, Aggregation and SemiJoin
There was a problem hiding this comment.
No. This is to filter out join filters dereferences that not covered in projectExpressions.
baseExists is to filter out dereferences already exists in other dereferences base
There was a problem hiding this comment.
yep, should only pushdown dereferences with base as DeferenceExpression or SymbolReference. Will fix it
There was a problem hiding this comment.
there is RemoveRedundantIdentityProjections following PushDownDereferences, so that redundant project node is removed. I will remove it from comments. Or, do you think we should add RemoveRedundantIdentityProjections to PushDownDereferences?
Sounds good. Ping me when the changes are ready for review. |
|
@zhenxiao I was trying your patch on some examples and the following query fails with the PushDownDereferences optimizer. Plan for this query without the optimizer: Do you know why that might be the case? |
|
@zhenxiao Can you please add some tests for the unnest queries on multi-level complex data types in arrays ? for example, pushdown in cases where only field y.w.u is accessed after unnesting array(row(x BIGINT, y row(z BIGINT, w row(u BIGINT, v DOUBLE))))) ? The testcases will help us have a better documentation of what this feature does and does not support. |
|
I assume #13271 superceds this one, hence, closing. |
Read necessary fields only for Parquet nested columns
Currently, Presto will read all the fields in a struct for Parquet columns.
e.g.
if it is a parquet file, with struct column s: {a int, b double, c long, d float}
current Presto will read a, b, c, d from s, and output just a and b
For columnar storage as Parquet or ORC, we could do better, by just reading the necessary fields. In the previous example, just read {a int, b double} from s. Not reading other fields to save IO.
This patch introduces an optional NestedFields in ColumnHandle. When optimizing the plan, PruneNestedColumns optimizer will visit expressions, and put candidate nested fields into ColumnHandle. When scanning parquet files, the record reader could use NestedFields to specify necessary fields only for parquet files.
This has an dependency on @jxiang 's #4714, which gives us the flexibility to specify metastore schemas differently from parquet file schemas.
@dain @martint @electrum @cberner @erichwang any comments are appreciated