Skip to content
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

Closed
wants to merge 3 commits into from
Closed

Conversation

lxynov
Copy link
Member

@lxynov lxynov commented Apr 29, 2022

Cache table statistics during query planning. So they can be shared by CBOs like ReorderJoins and DetermineJoinDistributionType. 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,

  • Without #11858 and this PR: the planning takes 2 min 24s
  • With #11858 but not this PR: the planning takes 1 min 12s
  • With this PR: the planning takes 49s

Description

Is this change a fix, improvement, new feature, refactoring, or other?

Improvement.

Is this a change to the core query engine, a connector, client library, or the SPI interfaces? (be specific)

Engine.

How would you describe this change to a non-technical end user or system administrator?

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:

# Section
* Improve query planning performance.

@cla-bot cla-bot bot added the cla-signed label Apr 29, 2022
@lxynov lxynov requested a review from findepi April 29, 2022 19:10
@lxynov
Copy link
Member Author

lxynov commented May 5, 2022

@findepi Gentle reminder on this PR.

@findepi
Copy link
Member

findepi commented May 11, 2022

How does this relate to @clemensvonschwerin's #8659?

@lxynov
Copy link
Member Author

lxynov commented May 11, 2022

@findepi The major difference is that #8659 is on the connector side whereas this PR is on the engine side.

@findepi
Copy link
Member

findepi commented May 13, 2022

@przemekak is going to help benchmark this

@findepi
Copy link
Member

findepi commented May 13, 2022

cc @alexjo2144

@alexjo2144
Copy link
Member

Can you give a short explanation of how this is different from the existing CachingStatsProvider? It seems to fit in a similar place as the CachingTableStatsProvider added here, the key is just on the PlanNode instead of the TableHandle

@lxynov
Copy link
Member Author

lxynov commented May 16, 2022

@alexjo2144 Sure. Yeah it's indeed a bit confusing. Essentially the difference is that CachingStatsProvider is created per IterativeOptimizer, whereas CachingTableStatsProvider is created per query. CachingTableStatsProvider helps share table stats cache among CBOs, like ReorderJoins, DetermineJoinDistributionType and DetermineSemiJoinDistributionType.

@@ -34,10 +34,11 @@ PlanNodeStatsEstimate calculateStats(
StatsProvider sourceStats,
Lookup lookup,
Session session,
TypeProvider types);
TypeProvider types,
TableStatsProvider tableStatsProvider);
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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.

implements TableStatsProvider
{
private final Metadata metadata;
private final Map<TableHandle, TableStatistics> cache = new HashMap<>();
Copy link
Member

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)

Copy link
Member Author

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)
Copy link
Member

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

  1. 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
  2. remove the old method overload, which would add , TableStatsProvider tableStatsProvider to many files (mechanical change)

Copy link
Member Author

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.

testing/trino-server-dev/etc/catalog/mariadb.properties Outdated Show resolved Hide resolved
@findepi
Copy link
Member

findepi commented May 17, 2022

please don't rebase when applying comments.

@findepi
Copy link
Member

findepi commented May 23, 2022

please don't rebase when applying comments.

i see a conflict here, so you'll need a rebase.
Please separate rebase and changes using a fixup commit.

@findepi
Copy link
Member

findepi commented May 27, 2022

@lxynov let me know if you need help with the rebase

@lxynov lxynov force-pushed the caching-tablestats branch 2 times, most recently from bfb1298 to be37e0d Compare June 7, 2022 00:30
@lxynov lxynov force-pushed the caching-tablestats branch from be37e0d to 5d549d3 Compare June 7, 2022 05:03
this.statsCalculator = requireNonNull(statsCalculator, "statsCalculator cannot be null");
this.session = requireNonNull(session, "session cannot be null");
this.session = requireNonNull(session, "sesssion cannot be null");
Copy link
Member

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(
Copy link
Member

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");
Copy link
Member

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);

Copy link
Member

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);
Copy link
Member

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);
Copy link
Member

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.");
Copy link
Member

Choose a reason for hiding this comment

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

UnsupportedOE

@findepi findepi mentioned this pull request Jun 30, 2022
@findepi
Copy link
Member

findepi commented Jun 30, 2022

I applied comments and posted a copy of this PR: #13047

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

3 participants