-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
Add table stats cache #12196
Add table stats cache #12196
Conversation
@findepi Gentle reminder on this PR. |
How does this relate to @clemensvonschwerin's #8659? |
@przemekak is going to help benchmark this |
cc @alexjo2144 |
Can you give a short explanation of how this is different from the existing |
@alexjo2144 Sure. Yeah it's indeed a bit confusing. Essentially the difference is that |
core/trino-main/src/main/java/io/trino/cost/TableStatsProvider.java
Outdated
Show resolved
Hide resolved
core/trino-main/src/main/java/io/trino/cost/TableStatsProvider.java
Outdated
Show resolved
Hide resolved
@@ -34,10 +34,11 @@ PlanNodeStatsEstimate calculateStats( | |||
StatsProvider sourceStats, | |||
Lookup lookup, | |||
Session session, | |||
TypeProvider types); | |||
TypeProvider types, | |||
TableStatsProvider tableStatsProvider); |
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.
Why do we need a new entity here, the tableStatsProvider
?
Could StatsProvider sourceStats
do the job?
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.
StatsProvider sourceStats
is called to get children plan nodes' stats.
TableStatsProvider tableStatsProvider
is used when calculating the "ultimate" source stats: table scan stats. In fact, all this PR does is to wire in a CachingTableStatsProvider
for TableScanStatsRule
to use.
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.
You can easily imagine CachingStatsProvider
that caches stats on per-PlanNode basis. This would be a generally useful implementation, potentially allowing us to cut down some stats calculation cost.
The currently existing CachingStatsProvider
caches stats in Memo's group. This is useful as well -- the group contains alternative plans (currently always exactly one at a time) that produce same relation. Same relation implies that once the stats are calculated, they are applicable to all group members.
Now, these two concepts are different, but not exclusionary:
- we could have "L2" cache that's on per-PlanNode basis
- then "L1" cache that's based on Memo group
Now the question is, whether per-PlanNode basis and per-TableHandle basis are significantly different.
I think there are not. I don't think we ever have a case where we have two TableScanNode
that have same TableHandle
.
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.
Now the question is, whether per-PlanNode basis and per-TableHandle basis are significantly different.
Actually they would be different.
Plan nodes have no equality, they compare by identity. Upon each exit from IterativeOptimizer (exit from Memo), a fully new plan structure is created. This can be improved a bit (eg produce new plan only if anything changed), but still would break identity-based caching, except for root nodes (table scans). Thus, the solution would look more generic, but would not actually be.
core/trino-main/src/main/java/io/trino/cost/CachingTableStatsProvider.java
Outdated
Show resolved
Hide resolved
implements TableStatsProvider | ||
{ | ||
private final Metadata metadata; | ||
private final Map<TableHandle, TableStatistics> cache = new HashMap<>(); |
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.
A connector creates new TableHandles during various ConnectorMetadata.apply*
calls.
A table handle may become "seen" in the plan and then discarded, make obsolete.
I think we should use weak keys here. Otherwise we need to size this as a regular cache.
(WeakHashMap
provides weak keys with equality-based lookup, so i'd recommend that)
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 don't fully understand this comment.. My understanding is that CachingTableStatsProvider
is created per query and its lifecycle is within query planning. So the cache here can be GCed after the query planning. What issue did you see with it?
@@ -106,7 +107,7 @@ public HashGenerationOptimizer(Metadata metadata) | |||
} | |||
|
|||
@Override | |||
public PlanNode optimize(PlanNode plan, Session session, TypeProvider types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator, WarningCollector warningCollector) | |||
public PlanNode optimize(PlanNode plan, Session session, TypeProvider types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator, WarningCollector warningCollector, TableStatsProvider tableStatsProvider) |
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.
It would greatly help reviewing, if you had two commits
- all the logic, which adds new
PlanOptimizer.optimize
overload, delegating to the old one- this would change only those PlanOptimizers which use this new info, or which delegate to some other PlanOptimizer
- remove the old method overload, which would add
, TableStatsProvider tableStatsProvider
to many files (mechanical change)
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.
Makes sense. Separated them. Please feel free to squash them when merging if you feel appropriate.
...in/src/test/java/io/trino/sql/planner/optimizations/TestRemoveUnsupportedDynamicFilters.java
Outdated
Show resolved
Hide resolved
please don't rebase when applying comments. |
i see a conflict here, so you'll need a rebase. |
@lxynov let me know if you need help with the rebase |
bfb1298
to
be37e0d
Compare
be37e0d
to
5d549d3
Compare
this.statsCalculator = requireNonNull(statsCalculator, "statsCalculator cannot be null"); | ||
this.session = requireNonNull(session, "session cannot be null"); | ||
this.session = requireNonNull(session, "sesssion cannot be null"); |
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.
revert
@@ -825,11 +824,15 @@ private AggregationNode aggregation(String id, PlanNode source) | |||
Optional.empty(), | |||
Optional.empty()); | |||
|
|||
return singleAggregation( |
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.
why this change?
@@ -29,7 +31,7 @@ | |||
|
|||
public CachingTableStatsProvider(Metadata metadata) | |||
{ | |||
this.metadata = metadata; | |||
this.metadata = requireNonNull(metadata, "metadata is null"); |
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.
Squash "Address comments" with respective commits.
(You did rebase anyway, and split the commit into two in this PR, so keeping some changes as a fixup commit doesn't make review easier)
@Override | ||
public final Optional<PlanNodeStatsEstimate> calculate(T node, StatsProvider sourceStats, Lookup lookup, Session session, TypeProvider types, TableStatsProvider tableStatsProvider) | ||
{ | ||
return doCalculate(node, sourceStats, lookup, session, types, tableStatsProvider) | ||
.map(estimate -> normalizer.normalize(estimate, node.getOutputSymbols(), types)); | ||
} | ||
|
||
protected abstract Optional<PlanNodeStatsEstimate> doCalculate(T node, StatsProvider sourceStats, Lookup lookup, Session session, TypeProvider types); | ||
|
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.
Can you give "Add table stats cache 2: remove method overloading" commit a better tittle?
e.g. how would you name the commit, it it was going in a separate PR?
{ | ||
TableStatsProvider EMPTY = (session, tableHandle) -> TableStatistics.empty(); | ||
|
||
public TableStatistics getTableStatistics(Session session, TableHandle tableHandle); |
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.
it's instantiated on per-query basis, so can take session
as constructor argument
@@ -34,10 +34,11 @@ PlanNodeStatsEstimate calculateStats( | |||
StatsProvider sourceStats, | |||
Lookup lookup, | |||
Session session, | |||
TypeProvider types); | |||
TypeProvider types, | |||
TableStatsProvider tableStatsProvider); |
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.
You can easily imagine CachingStatsProvider
that caches stats on per-PlanNode basis. This would be a generally useful implementation, potentially allowing us to cut down some stats calculation cost.
The currently existing CachingStatsProvider
caches stats in Memo's group. This is useful as well -- the group contains alternative plans (currently always exactly one at a time) that produce same relation. Same relation implies that once the stats are calculated, they are applicable to all group members.
Now, these two concepts are different, but not exclusionary:
- we could have "L2" cache that's on per-PlanNode basis
- then "L1" cache that's based on Memo group
Now the question is, whether per-PlanNode basis and per-TableHandle basis are significantly different.
I think there are not. I don't think we ever have a case where we have two TableScanNode
that have same TableHandle
.
@@ -56,12 +52,18 @@ public Pattern<TableScanNode> getPattern() | |||
|
|||
@Override | |||
protected Optional<PlanNodeStatsEstimate> doCalculate(TableScanNode node, StatsProvider sourceStats, Lookup lookup, Session session, TypeProvider types) | |||
{ | |||
throw new IllegalStateException("This is not expected to be called because the other overload is implemented."); |
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.
UnsupportedOE
I applied comments and posted a copy of this PR: #13047 |
Cache table statistics during query planning. So they can be shared by CBOs like
ReorderJoins
andDetermineJoinDistributionType
. It helps reduce Iceberg metadata processing when planning a query joining Iceberg tables.To demo the helpfulness of this PR and #11858, I did some experiment with a locally-running Trino and a production join query,
#11858
and this PR: the planning takes 2 min 24s#11858
but not this PR: the planning takes 1 min 12sDescription
Improvement.
Engine.
Improve query planning performance.
Related issues, pull requests, and links
#11708 #11858
Documentation
(x) No documentation is needed.
( ) Sufficient documentation is included in this PR.
( ) Documentation PR is available with #prnumber.
( ) Documentation issue #issuenumber is filed, and can be handled later.
Release notes
( ) No release notes entries required.
(x) Release notes entries required with the following suggested text: