-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
Collect column statistics on write [v2] #11054
Conversation
9fb8caa
to
6912593
Compare
@electrum I'm still working on the integration test |
a88391a
to
c417eb6
Compare
c417eb6
to
996fe84
Compare
Please ignore the |
996fe84
to
bca8699
Compare
return new AutoCloseableCloser(); | ||
} | ||
|
||
public void register(AutoCloseable closeable) |
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.
public <T extends AutoCloseable> T register(T closeable)
throws Exception | ||
{ | ||
Throwable rootCause = null; | ||
for (AutoCloseable closeable : closeables) { |
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.
Guava Closer (which this class mimics) closes in reverse order.
Also, it doesn't close the underlying resource twice, even if close is called twice (eg concurrently)
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 checked that. First i didn't get it, but now i think i got it.
Given the example
stream = closer.register(new OutputStream)
writer = closer.register(new OutputWriter(stream))
it makes total sense
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.
even if close is called twice (eg concurrently)
The Closer
is not thread safe. It is hard to reason what is going on when using it concurrently.
if (rootCause != null) { | ||
throwIfUnchecked(rootCause); | ||
throwIfInstanceOf(rootCause, Exception.class); | ||
throw new Exception(rootCause); |
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.
if we reach here, throw new Error or AssertionError
would be more appropriate.. since this is unreachable
} | ||
if (rootCause != null) { | ||
throwIfUnchecked(rootCause); | ||
throwIfInstanceOf(rootCause, Exception.class); |
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.
replace those 2 with propagateIfPossible(rootCause, Exception.class)
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.
We use throwIfInstanceOf
across the code
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.
Almost all usages of throwIfInstanceOf
are alone. The few places that it is combined with throwIfUnchecked
should be converted to propagateIfPossible
since that is more succinct.
|
||
Optional<NewTableLayout> getInsertLayout(Session session, TableHandle target); | ||
|
||
/** | ||
* Describes statistics that must be collected for a new table |
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.
only new tables? I think also updated ones
@@ -264,6 +266,14 @@ default void dropColumn(ConnectorSession session, ConnectorTableHandle tableHand | |||
return Optional.of(new ConnectorNewTableLayout(partitioningHandle, partitionColumns)); | |||
} | |||
|
|||
/** | |||
* Describes statistics that must be collected for a new table. |
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.
again -- "new"?
return groupingColumns; | ||
} | ||
|
||
public List<Block> getGropingValues() |
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.
typo: groping
(field & ctor & builder too)
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.
Ditto
public ComputedStatistics build() | ||
{ | ||
return new ComputedStatistics( | ||
unmodifiableList(groupingColumns), |
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.
Did you mean new ArrayList<>(groupingColumns)
?
Otherwise this is redundant (ctor does that) and allows creating mutable ComputedStatistics
objects
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.
Removed the unmodifiable*
at all here. Since those are in the constructor.
public Builder(List<String> groupingColumns, List<Block> gropingValues) | ||
{ | ||
this.groupingColumns = requireNonNull(groupingColumns, "groupingColumns is null"); | ||
this.gropingValues = requireNonNull(gropingValues, "gropingValues 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.
convert ctor to builder methods withGroupingColumns
(be sure to make defensive copy there)
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 field are required. Why do you want to make them builder methods? Also defensive copy is made in the constructor. Why make it twice?
Map<TableStatisticType, Block> tableStatistics, | ||
Map<ColumnStatisticMetadata, Block> columnStatistics) | ||
{ | ||
this.groupingColumns = unmodifiableList(requireNonNull(groupingColumns, "groupingColumns 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.
unmodifiableList( new ArrayList<>( requireNonNull( ....
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 messed up during the rebase. That was applied in the commit later. Moving it here.
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.
"Collect column statistics on table write: Planner" -- skimmed only. I refuse to understand SymbolMapper for now
List<Symbol> commitOutputs = ImmutableList.of(symbolAllocator.newSymbol("rows", BIGINT)); | ||
|
||
if (!statisticsMetadata.isEmpty()) { | ||
verify(columnNames.size() == symbols.size(), "columnNames.size() != symbols.size(): %s != %s", columnNames.size(), symbols.size()); |
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.
nit: in case this fails some day, including full collections in the message might be helpful (perhaps even instead of the sizes):
verify(columnNames.size() == symbols.size(), "columnNames.size() != symbols.size(): %s and %s", columnNames, symbols);
} | ||
|
||
FunctionRegistry functionRegistry = metadata.getFunctionRegistry(); | ||
if (!statisticsMetadata.getTableStatistics().isEmpty()) { |
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 should merge this if
into the for
loop above:
for (TableStatisticType type : statisticsMetadata.getTableStatistics()) {
if (type != ROW_COUNT) {
...
}
// plan the row count agg
}
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.
Or:
for (TableStatisticType type : statisticsMetadata.getTableStatistics()) {
if (type == ROW_COUNT) {
// plan the row count agg.
}
else {
// fail
}
}
ColumnStatisticType statisticType = columnStatisticMetadata.getStatisticType(); | ||
Symbol inputSymbol = columnToSymbolMap.get(columnName); | ||
verify(inputSymbol != null, "inputSymbol is null"); | ||
Type inputType = requireNonNull(symbolAllocator.getTypes().get(inputSymbol), "inputType 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.
line above uses verify
, this one uses requireNonNull
for checking nullity, but i see no reason for it
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.
Replaced with a verify. Just in case the type for the symbol is not there.
{ | ||
switch (statisticType) { | ||
case MIN: { | ||
checkArgument(inputType.isOrderable(), "Input type is not orderable: %s", inputType); |
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.
what would happen if you removed this check (and the type wasn't orderable)?
would we get some useful enough exception during createAggregation
in next line?
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 though that the explicit message might be more clear (in case someone ever wanted to compute MIN/MAX statistics for the type that is not orderable).
case MAX: { | ||
checkArgument(inputType.isOrderable(), "Input type is not orderable: %s", inputType); | ||
return createAggregation(QualifiedName.of("max"), input.toSymbolReference(), inputType, inputType); | ||
} |
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.
nit: these {
, }
are redundant in most of the cases and affect readability. IMHO it's better without them, but no strong opinion here
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 usually go without { }
if the switch case blocks are one-liners. I like them for multiliners though.
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.
"Collect column statistics on table write: Execution"
return false; | ||
} | ||
// AggregationOperator doesn't return false unless it is finished. | ||
// HashAggregationOperator doesn't return false unless it is full, that is not the option here |
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.
unless it is full
or there is some unfinishedWork
} | ||
// AggregationOperator doesn't return false unless it is finished. | ||
// HashAggregationOperator doesn't return false unless it is full, that is not the option here | ||
// The assumption is that the spill is always disabled for the statistics aggregation, ans |
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.
typo: ans
|
||
Block[] blocks = new Block[page.getChannelCount()]; | ||
for (int channel = 0; channel < page.getChannelCount(); channel++) { | ||
blocks[channel] = page.getBlock(channel).copyPositions(selectedPositions, 0, statisticsPositionCount); |
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.
com.facebook.presto.spi.Page#getPositions
?
else, leave a comment why not using this
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.
Copy just seems to be safer. Because it is copy. There will be not that much data to copy. And in most of the cases it will return the page unaltered. But it can be get
as well. I don't have a strong opinion here.
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.
Well -- we sometimes return the whole page (few lines earlier), so there cannot be assumption that you do a copy.
(no strong opinion either, just opportunity to simplify the code)
} | ||
|
||
@Override | ||
public Page getOutput() | ||
{ | ||
if (!isBlocked().isDone()) { |
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.
nit: move after if (state != State.FINISHING) { .. }
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.
In most of the implementation isDone
is a simple flag check. But i don't mind changing.
{ | ||
ImmutableList.Builder<ComputedStatistics> statistics = ImmutableList.builder(); | ||
while (!statisticsAggregation.isFinished()) { | ||
Page page = statisticsAggregation.getOutput(); |
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.
verify(statisticsAggregation.isBlocked().isDone());
to fail fast rather than voidly looping in case something goes wrong
} | ||
// Please read the comment in the TableFinishOperator#needsInput method |
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.
They may go out of sync. Why not make a defensive copy?
blocked = NOT_BLOCKED; | ||
} | ||
else { | ||
blocked = allAsList(blockedOnAggregation, blockedOnWrite); |
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.
remove if
, else
's code is generic enough.
if you don't want to go through com.google.common.util.concurrent.Futures#allAsList(com.google.common.util.concurrent.ListenableFuture<? extends V>...)
in all-done case, create a helper method that does the if
@@ -210,18 +240,53 @@ public void addInput(Page page) | |||
@Override | |||
public Page getOutput() | |||
{ | |||
if (state != State.FINISHING || !blocked.isDone()) { | |||
if (!blocked.isDone() || state != State.FINISHING) { |
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.
check state
first (cheaper first)
while (!statisticsAggregation.isFinished()) { | ||
Page page = statisticsAggregation.getOutput(); | ||
if (page == null) { | ||
continue; |
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 should yield here (return). Operator shouldn't do a ton of work within single call, otherwise a query might be "unkillable"
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.
IRL we cannot get here more data than a single page. We group the statistics on per-partition
basis. And we never insert more than 100 partitions at once by default. But even if we inserted 100_000 - there still will be not enough data for more than a several pages. But than you would definitely have more severe problems. So for the sake of code simplicity in this class (which is already complex), i would go with what we have 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.
fine, but please add a comment that we deliberately not yield here
{ | ||
AutoCloseableCloser closer = AutoCloseableCloser.create(); |
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.
wrap in try-with-r
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 is unnecessary. We don't expect the register()
methods to fail.
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.
yea... it's also customary. just a matter a taste
*/ | ||
package com.facebook.presto.spi.statistics; | ||
|
||
public enum ColumnStatisticType |
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.
should these match what's in the ColumnStatistics spi? I think it would make sense for the set of stats we support reading and writing to be the same. If so, there's no max/average_value_size_in_bytes, but there is a total_size_in_bytes.
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.
not necessarily. SPI is a common denominator, something we ingest. Here we have superset of all possible stats external systems can store.
SPI says want we want for CBO.
Here we say what hive need (or other programs using metastore may need, including ourselves)
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.
@rschlussel There is no exact match. NUMBER_OF_TRUE_VALUES
is something boolean specific, NUMBER_OF_NON_NULL_VALUES
is chosen over NUMBER_OF_NULL_VALUES
just because it is easier to compute (no extra projection), and so on.
} | ||
// AggregationOperator doesn't return false unless it is finished. | ||
// HashAggregationOperator doesn't return false unless it is full, that is not the option here | ||
// The assumption is that the spill is always disabled for the statistics aggregation, ans |
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.
s/ans/and
return Optional.of(new Page(statisticsPositionCount, blocks)); | ||
} | ||
|
||
private static boolean isStatisticsPosition(Page page, int position) |
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 add a comment somewhere explaining the multiplexing that you explained to me in person?
public static final List<Type> TYPES = ImmutableList.of(BIGINT, VARBINARY); | ||
public static final int ROW_COUNT_CHANNEL = 0; | ||
public static final int FRAGMENT_CHANNEL = 1; | ||
private static final int WRITER_CHANNELS = 2; |
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.
These are the stats channels, right? Could you call them STATISTICS_CHANNELS
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 is a total count of WRITER_CHANNELS.
{ | ||
this.typeManager = requireNonNull(typeManager, "typeManager is null"); | ||
this.metastore = requireNonNull(metastore, "metastore is null"); | ||
this.timeZone = timeZone; | ||
this.timeZone = requireNonNull(timeZone, "timeZone 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.
you didn't actually touch this file, right? just reformatted/added this null check. You could extract and merge this separately.
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.
No idea why is this change here, and why would i even tough this class here. I extracted it to a separate commit.
HiveColumnStatistics.Builder result = HiveColumnStatistics.builder(); | ||
|
||
// MIN MAX | ||
if (computedStatistics.containsKey(MIN) && computedStatistics.containsKey(MAX)) { |
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.
does this need to be both or neither? Sort of makes sense that they'd both be required fields, but it's not totally useless to have only one. E.g. if min for x was 10 and you had a predicate where x < 8, you'd know that nothing matches.
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.
We always compute MIN/MAX in pairs. Checking this in a single if for simplicity. Will add a assertion though.
@Test | ||
public void testCollectColumnStatisticsOnWriteSwitches() | ||
{ | ||
assertCollectColumnStatisticsOnWrite(false); |
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.
do we normally have tests that our session properties are actually gating the things we think they do? The test is fine, but curious why for this?
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.
Before that there was more complicated logic involving both, table and session property. Since now the logic is trivial i will just remove this test.
@LiteralParameters("x") | ||
@ScalarOperator(XX_HASH_64) | ||
@SqlType(StandardTypes.BIGINT) | ||
public static long xxHash64(@SqlType("char(x)") Slice slice) |
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.
any tests?
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 was needed for the approx_distinct.
Actually the equals semantics for CHAR in Presto are not correct. Because of the binary comparison, the EQUALS
operator would return false for abc
and abc
. That will result into storing wrong statistics for the CHAR type. For now i'm not going to collect the NDV statistic for the CHAR
type, untils the CHAR semantics are fixed.
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.
UPD: It is not possible to save CHAR statistics without setting NDV. Going to go with the return XxHash64.hash(slice);
implementation. But potentially it may return higher number of distinct values.
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.
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.
Actually the equals semantics for CHAR in Presto are not correct. Because of the binary comparison, the
EQUALS
operator would return false forabc⎵⎵
andabc
.
as noted #11101 (comment), internal repr is normalized, with trailing spaces removed (wheever there is a trailing space, this is a bug)
private final Map<TableStatisticType, Block> tableStatistics; | ||
private final Map<ColumnStatisticMetadata, Block> columnStatistics; | ||
|
||
public ComputedStatistics( |
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.
should not be this private if you provide a builder
private final Map<TableStatisticType, Block> tableStatistics = new HashMap<>(); | ||
private final Map<ColumnStatisticMetadata, Block> columnStatistics = new HashMap<>(); | ||
|
||
public Builder(List<String> groupingColumns, List<Block> gropingValues) |
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.
private?
ImmutableList.Builder<Symbol> writerOutputSymbols = ImmutableList.builder(); | ||
writerOutputSymbols.addAll(writerOutputs); | ||
writerOutputSymbols.addAll(partialAggregation.getGroupingSymbols()); | ||
writerOutputSymbols.addAll(partialAggregation.getAggregations().keySet()); |
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.
please chain this:
List<..> outputs = builder().add().add().build()
// by the partial aggregation from all of the writer nodes | ||
StatisticAggregations partialAggregation = aggregations.getPartialAggregation(); | ||
ImmutableList.Builder<Symbol> writerOutputSymbols = ImmutableList.builder(); | ||
writerOutputSymbols.addAll(writerOutputs); |
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.
inline writerOutputs
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 is used in multiple places (:405
)
} | ||
|
||
FunctionRegistry functionRegistry = metadata.getFunctionRegistry(); | ||
if (!statisticsMetadata.getTableStatistics().isEmpty()) { |
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.
Or:
for (TableStatisticType type : statisticsMetadata.getTableStatistics()) {
if (type == ROW_COUNT) {
// plan the row count agg.
}
else {
// fail
}
}
node.getStatisticsAggregationDescriptor().map(descriptor -> descriptor.map(this::map))); | ||
} | ||
|
||
private PartitioningScheme canonicalizePartitionFunctionBinding(PartitioningScheme scheme, PlanNode source) |
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.
canonicalizePartitioningScheme? Or maybe even just canonicalize?
private PartitioningScheme canonicalizePartitionFunctionBinding(PartitioningScheme scheme, PlanNode source) | ||
{ | ||
Set<Symbol> addedOutputs = new HashSet<>(); | ||
ImmutableList.Builder<Symbol> outputs = ImmutableList.builder(); |
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.
should not you use mapAndDistinct
here?
@@ -79,17 +94,24 @@ public OperatorFactory duplicate() | |||
|
|||
private final OperatorContext operatorContext; | |||
private final TableFinisher tableFinisher; | |||
private final Operator statisticsAggregation; |
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.
statisticsAggregationOperator?
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.
Didn't want to make it oververbose, but it looks like it indeed decreases readablity.
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.
Renamed statisticsAggregationOperatorFactory
as well
@Override | ||
public void testUpdateTableColumnStatistics() | ||
{ | ||
// column statistics are not supported by Glue |
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 throw SkipException here and elsewhere?
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 isn't our convention to do so
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 is not, but I think it should be. Telling people that test is passing while it is not possible for test to pass might be a bit misleading. I hope raising SkipException might have a better developer experience.
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 is provided that someone reads the list of passing tests. I don't.
I do read lists of failing tests only. And Jenkins's testng plugin lists all the skipped tests, which isn't entirely interesting when the test is skipped because the functionality simply does not exist
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.
Usually we throw SkipException
if there is no easy way of disabling a test in any other way. (throwing it from somewhere deep inside the mock objects)
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.
"Collect column statistics on table write: Hive Connector"
return ImmutableSet.of(NUMBER_OF_NON_NULL_VALUES, MIN, MAX, NUMBER_OF_DISTINCT_VALUES); | ||
} | ||
if (isVarcharType(type)) { | ||
return ImmutableSet.of(NUMBER_OF_NON_NULL_VALUES, NUMBER_OF_DISTINCT_VALUES, MAX_VALUE_SIZE_IN_BYTES, AVERAGE_VALUE_SIZE_IN_BYTES); |
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.
Hive stores MIN,MAX as well -- we don't use this currently, but we may in the future. Also, other tools may make use of this.
Consider // TODO ...
return ImmutableSet.of(NUMBER_OF_NON_NULL_VALUES, NUMBER_OF_TRUE_VALUES); | ||
} | ||
if (isNumericType(type) || type.equals(DATE) || type.equals(TIMESTAMP)) { | ||
return ImmutableSet.of(NUMBER_OF_NON_NULL_VALUES, MIN, MAX, NUMBER_OF_DISTINCT_VALUES); |
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.
add // TODO #7122 support non-legacy TIMESTAMP
if (type.equals(VARBINARY)) { | ||
return ImmutableSet.of(NUMBER_OF_NON_NULL_VALUES, MAX_VALUE_SIZE_IN_BYTES, AVERAGE_VALUE_SIZE_IN_BYTES); | ||
} | ||
return ImmutableSet.of(); |
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.
There are types that are not here because they are not supported by Hive connector. eg. TIME_WITH_TIME_ZONE, TIMESTAMP_WITH_TIME_ZONE.
I think it would be better to end this method with
// Throwing here to make sure this method is updated when a new type is added in Hive connector
throw new IllegalArgumentException("Unsupported type: " + type);
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.
There are many types that are not here. For example ARRAY/MAP/ROW. If i throw exception here i would need to check if the type is supported above.
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 still would prefer a whitelist approach. Are there types to add other than array/map/row?
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.
No. Yeah, maybe you are right, maybe we should return ImmutableList.empty()
for that types, and throw an exception otherwise.
return ImmutableSet.of(NUMBER_OF_NON_NULL_VALUES, NUMBER_OF_DISTINCT_VALUES, MAX_VALUE_SIZE_IN_BYTES, AVERAGE_VALUE_SIZE_IN_BYTES); | ||
} | ||
if (isCharType(type)) { | ||
return ImmutableSet.of(NUMBER_OF_NON_NULL_VALUES, NUMBER_OF_DISTINCT_VALUES); |
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.
MIN,MAX as well?
Isn't AVERAGE_VALUE_SIZE_IN_BYTES interesting? (with trailing spaces trimmed). Add a TODO
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.
Isn't AVERAGE_VALUE_SIZE_IN_BYTES interesting?
It is not obvious how to compute it for CHAR. Probably some custom function will be needed. I decided to skip it for now. In the optimizer we can use the length from the type itself. Usually the deviation is not major (or otherwise there is less sense of using CHAR)
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 TODO note?
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.
Don't forget to add this TODO about *_VALUE_SIZE_IN_BYTES
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.
Don't forget to add this TODO about *_VALUE_SIZE_IN_BYTES
I'm going to introduce it in a very next PR
reduce(first.getMaxColumnLength(), second.getMaxColumnLength(), SELECT_MAX, true), | ||
mergeAverage(first.getAverageColumnLength(), firstRowCount, second.getAverageColumnLength(), secondRowCount), | ||
reduce(first.getNullsCount(), second.getNullsCount(), ADD, false), | ||
reduce(first.getDistinctValuesCount(), second.getDistinctValuesCount(), SELECT_MAX, false)); |
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 false
? (generally MAX seems suitable and is used with returnFirstNonEmpty=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.
null
can indicate 2 thins.
- statistic is missing (was never computed)
- In case of min/max - that the table is empty
If the table is empty NDV is not gonna be null, but 0.
reduce(first.get().getMin(), second.get().getMin(), SELECT_MIN, true), | ||
reduce(first.get().getMax(), second.get().getMax(), SELECT_MAX, true))); | ||
} | ||
return Optional.empty(); |
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.
if second
is not present, you return empty
if second
is present but has all fields absent, you return first
Why? Leave some explanation in the code.
(here & a few times below)
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.
If only one of these is present - possibly the schema migration took place. If the column as a VARCHAR but become an INTEGER for example. Further we may want to do a proper schema migration here. I haven't decided how would it look like though.
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.
So would something like this be proper?
// normally, either both or none is present
if (first.isPresent() && second.isPresent()) { | ||
return Optional.of(new BooleanStatistics( | ||
reduce(first.get().getTrueCount(), second.get().getTrueCount(), ADD, false), | ||
reduce(first.get().getFalseCount(), second.get().getFalseCount(), ADD, false))); |
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 am lost. we have ColumnStatisticType.NUMBER_OF_TRUE_VALUES
but we don't have ColumnStatisticType.NUMBER_OF_FALSE_VALUES
. Please explain
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.
NUMBER_OF_FALSE_VALUES = NUMBER_OF_NON_NULL - NUMBER_OF_TRUE_VALUES
What we ask engine to compute does not necessary match the statistics we want to store.
{ | ||
if (first.isPresent() && second.isPresent()) { | ||
if (!(firstRowCount.isPresent() && secondRowCount.isPresent())) { | ||
return OptionalDouble.empty(); |
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.
if first is present, but second is not, we return first
if first is present, but second lacks row count, we return empty
Why? Leave some explanation in the code.
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.
Changed to if (!firstRowCount.isPresent() || !secondRowCount.isPresent()) {
. Hopefully it makes more sense.
} | ||
long totalRowCount = firstRowCount.getAsLong() + secondRowCount.getAsLong(); | ||
if (totalRowCount == 0) { | ||
return OptionalDouble.empty(); |
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.
OptionalDouble.of(0)
? In case we have ended up inserting zero rows into existing partition
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.
If there 0 rows the average length is unknow. There is no rows. If we inserted values into empty partition it will go the first.isPresent() ? first : second;
path.
|
||
private static OptionalLong getIntegerValue(ConnectorSession session, Type type, Block block) | ||
{ | ||
return block.isNull(0) ? OptionalLong.empty() : OptionalLong.of(((Number) type.getObjectValue(session, block, 0)).longValue()); |
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 I know why ((Number) type.getObjectValue(session, block, 0)).longValue()
instead of type.getLong(block, 0)
(because the second would work also for eg short decimal, skipping internal-representation-to-external conversion). While this method is called only for integral types, it's better as-is.
Consider leaving a comment hinting at the choice made
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.
"Add properties for column statistics collect"
@@ -1044,4 +1046,18 @@ public boolean isTableStatisticsEnabled() | |||
{ | |||
return tableStatisticsEnabled; | |||
} | |||
|
|||
@NotNull |
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.
remove, boolean
cannot be null
@@ -134,6 +134,8 @@ | |||
|
|||
private boolean tableStatisticsEnabled = true; | |||
|
|||
private boolean collectColumnStatisticsOnWrite; |
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 wouldn't keep the empty line before this one
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.
"Collect column statistics on table write: Documentation"
``VARBINARY`` ``NUMBER_OF_NULLS``, ``MAX_VALUE_SIZE_IN_BYTES``, ``AVERAGE_VALUE_SIZE_IN_BYTES`` | ||
``DATE`` ``NUMBER_OF_NULLS``, ``MIN``, ``MAX``, ``NUMBER_OF_DISTINCT_VALUES`` | ||
``TIMESTAMP`` ``NUMBER_OF_NULLS``, ``MIN``, ``MAX``, ``NUMBER_OF_DISTINCT_VALUES`` | ||
``DECIMAL`` ``NUMBER_OF_NULLS``, ``MIN``, ``MAX``, ``NUMBER_OF_DISTINCT_VALUES`` |
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.
nit: move after REAL
``DOUBLE`` ``NUMBER_OF_NULLS``, ``MIN``, ``MAX``, ``NUMBER_OF_DISTINCT_VALUES`` | ||
``REAL`` ``NUMBER_OF_NULLS``, ``MIN``, ``MAX``, ``NUMBER_OF_DISTINCT_VALUES`` | ||
``BOOLEAN`` ``NUMBER_OF_NULLS``, ``NUMBER_OF_FALSE``, ``NUMBER_OF_TRUE`` | ||
``VARCHAR`` ``NUMBER_OF_NULLS``, ``NUMBER_OF_DISTINCT_VALUES``, ``MAX_VALUE_SIZE_IN_BYTES``, ``AVERAGE_VALUE_SIZE_IN_BYTES`` |
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.
MIN,MAX (if you support them in the code)
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.
No, we do not collect MIN and MAX for Varchar
============= ================================================================================================================ | ||
|
||
Automatic column level statistics collection on write can be enabled using | ||
the ``hive.collect-column-statistics-on-write`` property. |
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 am worried we may miss to update this line when changing the default of this property.
What about different wording, leveraging the property's default is in the table above
Automatic column level statistics collection on write is controlled by ``hive.collect-column-statistics-on-write`` property.
@@ -148,6 +470,8 @@ private static long convertLocalToUtc(DateTimeZone timeZone, long value) | |||
{ | |||
ADD, | |||
SUBTRACT, | |||
SELECT_MIN, | |||
SELECT_MAX, |
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.
nit: why not just "MIN", "MAX"
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.
So i can static-import it. MIN
and MAX
are used by the values from the ColumnStatisticType
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.
Going to rename it back to MIN
and MAX
after i rename values in ColumnStatisticType
02e45bd
to
67fc97a
Compare
@findepi @kokosing @rschlussel @electrum Heads up. I'm going to remove support for the The reasons are next:
I'm going to create a separate PR that:
|
76c4c54
to
2ef0a98
Compare
@arhimondr how does this play with #11107?
|
754ae7f
to
ede8d78
Compare
} | ||
if (rootCause != null) { | ||
throwIfUnchecked(rootCause); | ||
throwIfInstanceOf(rootCause, Exception.class); |
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.
Almost all usages of throwIfInstanceOf
are alone. The few places that it is combined with throwIfUnchecked
should be converted to propagateIfPossible
since that is more succinct.
{ | ||
closed = true; | ||
if (failure != null) { | ||
throwIfUnchecked(failure); |
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.
propagateIfPossible(failure, Exception.class);
@@ -275,6 +277,14 @@ default void dropColumn(ConnectorSession session, ConnectorTableHandle tableHand | |||
return Optional.of(new ConnectorNewTableLayout(partitioningHandle, partitionColumns)); | |||
} | |||
|
|||
/** | |||
* Describes statistics that must be collected |
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.
Nit: end Javadoc sentences in a period
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.
Let's make this say "during a write" for now. We can update to "during a write or analyze" later when that is implemented.
{ | ||
private final List<String> groupingColumns; | ||
private final List<Block> groupingValues; | ||
private final Map<TableStatisticType, Block> tableStatistics; |
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.
Are there any concerns about memory size for Block
? We have to store all of these in memory at once in the coordinator. (I'm not suggesting this is a problem, but rather asking if you've considered it or done any back-of-the-napkin calculations)
} | ||
if (isNumericType(type) || type.equals(DATE) || type.equals(TIMESTAMP)) { | ||
// TODO #7122 support non-legacy TIMESTAMP | ||
return ImmutableSet.of(NUMBER_OF_NON_NULL_VALUES, MIN, MAX, NUMBER_OF_DISTINCT_VALUES); |
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.
Nit: put min/max at the start or end, so that the NUMBER_*
stats are together
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'm wondering if MAX
should be MAX_VALUE
. That might be more consistent with MAX_VALUE_SIZE_IN_BYTES
.
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'm wondering if MAX should be MAX_VALUE. That might be more consistent with MAX_VALUE_SIZE_IN_BYTES.
Yeah. That it will be less chance that it clashes on static import. Let me change that.
|
||
private static boolean isNumericType(Type type) | ||
{ | ||
return type.equals(BIGINT) || type.equals(INTEGER) || type.equals(SMALLINT) || type.equals(TINYINT) || type.equals(DOUBLE) || type.equals(REAL) || type instanceof DecimalType; |
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.
Maybe wrap this to split up the different kinds of types
return type.equals(BIGINT) || type.equals(INTEGER) || type.equals(SMALLINT) || type.equals(TINYINT) ||
type.equals(DOUBLE) || type.equals(REAL) ||
type instanceof DecimalType;
@@ -111,9 +111,9 @@ security options in the Hive connector. | |||
Hive Configuration Properties | |||
----------------------------- | |||
|
|||
================================================== ============================================================ ========== | |||
================================================== ============================================================ ============================= |
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.
We don't need to widen the table now that the default is "false"
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.
Right. I'm staill going to widen it for 2 symbols to cover the RCBINARY
============= ================================================================================================================ | ||
Column Type Collectible Statistics | ||
============= ================================================================================================================ | ||
``TINYINT`` ``NUMBER_OF_NULLS``, ``MIN``, ``MAX``, ``NUMBER_OF_DISTINCT_VALUES`` |
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.
Should we use words here? This is for humans, and AFAIK, these constants like NUMBER_OF_NULLS
are an internal detail of Presto.
Also, let's put min/max last, rather than in the middle of the "number of" stats.
``TINYINT`` number of nulls, number of distinct values, min/max values
``BOOLEAN`` number of nulls, number of true/false values
============= ================================================================================================================ | ||
|
||
Automatic column level statistics collection on write is controlled by | ||
``hive.collect-column-statistics-on-write`` property. |
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.
Automatic column level statistics collection on write is controlled by
the ``collect-column-statistics-on-write`` catalog session property.
The Hive connector can also collect column level statistics: | ||
|
||
============= ================================================================================================================ | ||
Column Type Collectible Statistics |
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.
Let's be consistent with other tables and put the "Collectible Statistics" at the start of the cell.
============= ====================================
Column Type Collectible Statistics
ede8d78
to
f02ae29
Compare
Travis failure looks related:
|
f02ae29
to
81e4b3c
Compare
It seems to be intermittent. I see no reason why this patch should've affected this test in any way. |
hm...did you observe it failing intermittently on master as well? |
At least once. I didn't look for more. https://api.travis-ci.org/v3/job/400752461/log.txt (https://travis-ci.org/prestodb/presto/jobs/400752461, https://travis-ci.org/prestodb/presto/builds/400752451) |
Different metastores may support slightly different column statistics
connector/session/table properties
81e4b3c
to
b6158fb
Compare
Map<TableStatisticType, Block> tableStatistics, | ||
Map<ColumnStatisticMetadata, Block> columnStatistics) | ||
{ | ||
this.groupingColumns = unmodifiableList(new ArrayList<>(requireNonNull(groupingColumns, "groupingColumns 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.
Is there any reason not using ImmutableList.copyOf()
?
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.
ImmutableList
is a Guava
class. We don't have Guava in the classpath of the presto-spi
module.
Important changes since the version 1:
Return HiveColumnStatistics from the HiveMetastore interface
toMove createPartitionValues method to a utility class
were extracted into a separate PR and merged: Preliminary column statistics refactorings #10972Implement AutoCloseableCloser
commit.Collect column statistics on table write: SPI
getInsertStatisticsMetadata
andgetNewTableStatisticsMetadata
method were merged into the singlegetStatisticsCollectionMetadata
methodAggregationOperator
integration with theTableWriterOperator
has changedExtendedHiveMetastore#isColumnStatistitcsSupported()
replaced with theExtendedHiveMetastore#getSupportedColumnStatistics
. That eliminates a need ofCollectibleStatisticsProvider
. Commit:Replace supportsColumnStatistics with getSupportedColumnStatistics
ENABLED_FOR_MARKED_TABLES
option has been removed as well as a related table property.Migrate column statistics on drop and rename column
commit has been droppedCollect column statistics on table write: Smoke Tests
commit