-
Notifications
You must be signed in to change notification settings - Fork 462
PARQUET-2249: Introduce IEEE 754 total order & NaN-counts #514
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
This commit is a combination of the following PRs: * Introduce IEEE 754 total order apache#221 * Add nan_count to handle NaNs in statistics apache#196 Both these PRs try to solve the same problems; read the description of the respective PRs for explanation. This PR is the result of an extended discussion in which it was repeatedly brought up that another possible solution to the problem could be the combination of the two approaches. Please refer to this discussion on the mailing list and in the two PRs for details. the mailing list discussion can be found here: https://lists.apache.org/thread/lzh0dvrvnsy8kvflvl61nfbn6f9js81s The contents of this PR are basically a straightforward combination of the two approaches: * IEEE total order is introduced as a new order for floating point types * nan_count and nan_counts fields are added Legacy writers may not write nan_count(s) fields, so readers have to handle them being absent. Also, legacy writers may have included NaNs into min/max bounds, so readers also have to handle that. As there are no legacy writers writing IEEE total order, nan_count(s) are defined to be mandatory if this order is used, so readers can assume their presense when this order is used. This commit removes `nan_pages` from the ColumnIndex, which the nan_counts PR mandated. We don't need them anymore, as IEEE total order solves this: As this is a new order and there are thus no legacy writers and readers, we have the freedom to define that for only-NaN pages using this order, we can actually write NaNs into min & max bounds in this case, and readers can assume that isNaN(min) signals an only-NaN page.
alamb
left a comment
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 went through the proposal and I left some stylistic comments which I think might help clarity, but are not required.
Thank you @JFinis for pushing this along
| sort orders for logical and primitve types and also special orders for types | ||
| where the common sort order is not unambiguously defined (e.g., NaN ordering | ||
| for floating point types). The details are documented in the |
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 is a spelling mistake (primitve). Also here is a proposal to make this more concise
| sort orders for logical and primitve types and also special orders for types | |
| where the common sort order is not unambiguously defined (e.g., NaN ordering | |
| for floating point types). The details are documented in the | |
| sort orders for logical and primitive types and also special orders for | |
| types with potentially ambiguous semantics (e.g., NaN ordering | |
| for floating point types). The details are documented in the |
| /** | ||
| * count of NaN values in the column; only present if physical type is FLOAT | ||
| * or DOUBLE, or logical type is FLOAT16. | ||
| * Readers MUST distinguish between nan_count not being present and nan_count == 0. | ||
| * If nan_count is not present, readers MUST NOT assume nan_count == 0. | ||
| */ | ||
| 9: optional i64 nan_count; |
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 would find this easier to understand if it were more concise and didn't have a double negative. Here is a suggestion:
| /** | |
| * count of NaN values in the column; only present if physical type is FLOAT | |
| * or DOUBLE, or logical type is FLOAT16. | |
| * Readers MUST distinguish between nan_count not being present and nan_count == 0. | |
| * If nan_count is not present, readers MUST NOT assume nan_count == 0. | |
| */ | |
| 9: optional i64 nan_count; | |
| /** | |
| * count of NaN values in the column; only present if physical type is FLOAT | |
| * or DOUBLE, or logical type is FLOAT16. | |
| * If this field is not present, readers must assume NaNs may be present | |
| * (MUST NOT assume nan_count == 0). | |
| */ | |
| 9: optional i64 nan_count; |
| * | ||
| * When writing statistics the following rules should be followed: | ||
| * - NaNs should not be written to min or max statistics fields. | ||
| * - It is suggested to always set the nan_count field for floating |
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.
The heading says "the following rules should be followed" but the first one says "it is suggested" which seems to imply it is optional. I recommend removing the "suggested"
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 'should' is still optional per RFC2119. If this isn't optional, 'must' or 'shall' is the correct term. But this is a problem in more places in this document, so I take it you're not following that RFC.
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 the requirement should be to set nan_count, so I agree that this should not be a suggestion.
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 whole document uses "should", where "shall" or "must" would actually be more appropriate. Therefore, I was going with the general speak of this document for consistency. I totally agree though; it always bothered me a lot that the spec always uses "should", making it sounds as if all of this is just a mere suggestion and implementors can do what they want.
That being said, if we want to change this, I would rather do it in one PR that updates the whole document to stay consistent.
| * - NaNs should not be written to min or max statistics fields except | ||
| * in the column index, where min_values and max_values are not optional | ||
| * so a NaN value must be written if all non-null values in a page | ||
| * are NaN. |
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.
| * - NaNs should not be written to min or max statistics fields except | |
| * in the column index, where min_values and max_values are not optional | |
| * so a NaN value must be written if all non-null values in a page | |
| * are NaN. | |
| * - NaNs should not be written to min or max statistics fields except | |
| * in the column index when a page contains only NaN values. In this | |
| * case, since min_values and max_values are required, a NaN value | |
| * must be written. |
| * - Writing the nan_count field is mandatory when using this ordering, | ||
| * especialy also if it is zero. |
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 if the field is mandatory, the "especially" part makes no sense
| * - Writing the nan_count field is mandatory when using this ordering, | |
| * especialy also if it is zero. | |
| * - Writing the nan_count field is mandatory when using this ordering. |
| * still included NaN in min_values and max_values even if the page had | ||
| * non-NaN values. To mitigate this, IEEE754_TOTAL_ORDER is recommended. | ||
| * - If the order of this column is IEEE754_TOTAL_ORDER, then min_values[i] | ||
| * * If IEEE754_TOTAL_ORDER is used for the column and all non-null 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.
I think the heading - bullet is different:
| * * If IEEE754_TOTAL_ORDER is used for the column and all non-null values | |
| * - If IEEE754_TOTAL_ORDER is used for the column and all non-null values |
| * might not be NaN values in any page, as NaNs should not be included | ||
| * in min_values or max_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.
| * might not be NaN values in any page, as NaNs should not be included | |
| * in min_values or max_values. | |
| * might not be NaN values in any page, as NaNs are not included | |
| * in min_values or max_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.
This looks imprecise to me. NaNs are included in min_values/max_values when IEEE754TotalOrder is used. So perhaps we should simply say that If this field is not present, readers MUST assume that there might be NaN values in any page?
| * must be followed: | ||
| * - Writing the nan_count field is mandatory when using this ordering, | ||
| * especialy also if it is zero. | ||
| * - NaNs should not be written to min or max statistics fields except |
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 the spec should specify exactly what should be written, not just rule out what shouldn't be. I assume you meant to specify here that the smallest/largest non-NaN value must be written instead?
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 also think this rule (assuming I understood you correctly) is a bit overcomplicated? Not sure why we'd make the rules different depending or not whether it is a column index. May I propose the following instead which applies regardless of whether it is a column index?
When writing statistics for columns with this order, the following rules must be followed:
- Min or max statistics if written must contain the smallest or largest non-NaN value respectively. If there are exclusively NaNs they must contain the smallest and largest NaN values respectively instead, as defined by the IEEE 754 total order.
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.
Sure, we can also keep it the same for the column index and statistics here. My initial idea was to keep the behavior close to what we currently have where possible. But I agree that we can alos instead make it different from the current behavior and therefore more consistent between column index and statistics.
| struct DataPageHeader { | ||
| /** | ||
| * Number of values, including NULLs, in this data page. | ||
| * Number of values, including nulls, in this data page. |
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 please remove these unnecessary changes? It pollutes git blame and is no more clear.
| * | ||
| * (*) Because the sorting order is not specified properly for floating | ||
| * point values (relations vs. total ordering) the following | ||
| * (*) Because the precise sorting order is ambiguous for floating |
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 it should be clear that this is referring to the TypeDefinedOrder for floating points.
| * anything else. It also defines an order between different bit representations | ||
| * of the same value. | ||
| * | ||
| * The formal definition is as follows: |
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 need to copy this here? Having a version here that duplicates the IEEE-754 version makes me worry that they will get out of sync or have errors due to copying. We should make it clear that the IEEE-754 version is correct if the two ever differ.
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 what lines your comment is referring to. What is the part you see duplicated?
| * * If IEEE754_TOTAL_ORDER is used for the column and all non-null values | ||
| * of a page are NaN, then min_values[i] and max_values[i] must be set to | ||
| * the smallest and largest NaN value contained in the page, as defined | ||
| * by the IEEE 754 total order. |
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 does this require actual bound for NaN rather than using a standard NaN value?
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.
My intentaion was here that if we already use an order that orders NaNs in a well defined manner, we can also use it. I agree we could also settle for "any NaN values is okay in this case", but that is somewhat going against the spirit of IEEE total order.
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.
FWIW it is all these strange special cases that are why I still prefer the total order PR - nan_counts sound simple but in practice there be dragons.
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 agree with @tustvold, I don't think the added complexity of this proposal is worth it given the meager added benefit. This proposal still means that query engines that do use IEEE total ordering will be unable to filter due to the lack of knowledge of the sign of the NaNs seen. This is the flip side of total ordering not quite working for engines that lump all NaNs together. So the only real benefit to this proposal is to not "poison" the statistics.
| * null counts are 0. | ||
| */ | ||
| 5: optional list<i64> null_counts | ||
|
|
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: unnecessary whitespace change.
| * | ||
| * (*) Because the sorting order is not specified properly for floating | ||
| * point values (relations vs. total ordering) the following | ||
| * (*) Because the precise sorting order is ambiguous for floating |
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.
| * (*) Because the precise sorting order is ambiguous for floating | |
| * (*) Because the TypeDefinedOrder is ambiguous for floating |
Perhaps explicitly use TypeDefinedOrder or TYPE_ORDER here?
| * - If the nan_count field is set, a reader can compute | ||
| * nan_count + null_count == num_values to deduce whether all non-null | ||
| * values are NaN. | ||
| * - When looking for NaN values, min and max should be ignored. |
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.
Moving these to line 1113 as it has duplicate content.
| * For columns of physical type FLOAT or DOUBLE, or logical type FLOAT16, | ||
| * NaN values are not to be included in these bounds. If all non-null values | ||
| * of a page are NaN, then a writer must do the following: | ||
| * - If the order of this column is TypeDefinedOrder, then no column index |
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 the order of this column is TypeDefinedOrder, then no column index | |
| * - If the order of this column is TYPE_ORDER, then no column index |
Just to be consistent
| * - If the order of this column is IEEE754_TOTAL_ORDER, then min_values[i] | ||
| * * If IEEE754_TOTAL_ORDER is used for the column and all non-null values | ||
| * of a page are NaN, then min_values[i] and max_values[i] must be set to | ||
| * the smallest and largest NaN value contained in the page, as defined | ||
| * by the IEEE 754 total order. |
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 the order of this column is IEEE754_TOTAL_ORDER, then min_values[i] | |
| * * If IEEE754_TOTAL_ORDER is used for the column and all non-null values | |
| * of a page are NaN, then min_values[i] and max_values[i] must be set to | |
| * the smallest and largest NaN value contained in the page, as defined | |
| * by the IEEE 754 total order. | |
| * - If the order of this column is IEEE754_TOTAL_ORDER, then min_values[i] | |
| * and max_values[i] must be set to the smallest and largest NaN value | |
| * contained in the page, as defined by the IEEE 754 total order. |
| * might not be NaN values in any page, as NaNs should not be included | ||
| * in min_values or max_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.
This looks imprecise to me. NaNs are included in min_values/max_values when IEEE754TotalOrder is used. So perhaps we should simply say that If this field is not present, readers MUST assume that there might be NaN values in any page?
| * might not be NaN values in any page, as NaNs should not be included | ||
| * in min_values or max_values. | ||
| */ | ||
| 8: optional list<i64> nan_counts |
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 remember that there was concern with introducing nan_counts to column index which is not applicable to all types. If we don't receive any objection during this round of discussion, can we conclude that we have consensus on 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.
That's the trade-off for nan_counts in general: You add fields to thrift that are only relevant to floating point types. Let's see if there is any objection on this.
|
@tustvold @crepererum @westonpace I would especially like to get your opions on this, as you were proponents of total order for simplicity. Does this PR still cover what you wanted to get out of this? @mapleFU @etseidl @gszadovszky @pitrou FYI, as you were part of the initial discussion on the previous PRs. |
tustvold
left a comment
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.
Am I understanding correctly that the change to add nan_counts means that NaNs are now excluded from the min/max statistics? I believe this creates a new problem for engines that use total ordering as the sign of the NaNs is not known, and therefore there are scenarios where predicates can't be pushed down where they could be in the absence of nan_counts.
My 2 cents is that there probably is no perfect solution here, adding nan_counts is worse for some engines and better for some others, and as such my personal preference would still be to go with the simpler solution, that has existing implementations and can ship today.
The argument for nan_counts appears to be that the presence of a NaN has the effect of poisoning the statistics. I don't really follow why NaNs are special in this regard, any extreme value has the same effect, if people want to effectively push down such predicates they should be sorting so as to cluster data points of similar magnitude. It has been said that people coming from numpy use NaN instead of Null, but as was said on the mailing list, I don't think the onus should be on parquet to handle this, rather people should convert NaNs to Nulls if that is the semantic they want.
That being said if people are motivated to push ahead with this hybrid solution, I don't really feel strongly. Ultimately the issue is fixing the fact predicates can't be pushed down on floats in general, how that applies to NaNs IMO is kind of irrelevant to the vast majority of workloads. This has dragged on for 2 years, we'd almost reached consensus and I can't help feeling we've been startled by one dissenting opinion...
No, the predicates still can be pushed down, regardless of the semantics of your engine. If your engine uses total ordering with predicate
It's not worse for any engine as far as I can see, a strict improvement for everyone in filtering capabilities.
NaNs aren't extreme values for engines without total ordering, they are simply unordered. |
|
I seem to remember from a previous discussion around the need to have signed NaNs to allow for predicate pushdown, but I can't remember the example. Regardless my broader point still stands that we are adding a non-trivial amount of complexity, there is a lot of subtlety both at read and write time, to yield an improvement that to be brutally honest I am not really sure will benefit all that many real workloads. It's all tradeoffs, if people want to and are motivated to proceed with the hybrid approach I don't feel strongly, but I personally prefer the simpler option that is more likely to be implemented correctly across the many many parquet implementations. The proposal here is strictly more complex than the existing specification which implementations still implement incorrectly. |
Signed NaN counts are only useful to avoid treating a But only having a single NaN count doesn't block predicate pushdown whatsoever, and since you're fine with NaN poisoning anyway I don't think you'd care about the above |
Gut feeling wise, I agree with your assessment. I feel like I personally can implement this correctly in our engine, but I have now spent years of my life thinking about this, so I guess I'm not representative for the average Parquet maintainer. I also feel that this has such a high chance of being implemented incorrectly that the more straightforward solution of just IEEE total ordering would be preferrable. My gut feels like the risk of added implementation complexity and thus chance for wrong implementations is higher than the advantage of not having "NaN poisoning" of statistics. |
|
I have filed a ticket to track trying to implement this in the arrow-rs implementation of Parquet, which will hopefully help us quantify how complicated this proposal is to implement: |
I think we have waited enough time for feedback. Do you want to start a vote with two proposals on the dev ML, @JFinis? |
This commit is a combination of the following PRs:
Both these PRs try to solve the same problems; read the description of the respective PRs for explanation.
This PR is the result of an extended discussion in which it was repeatedly brought up that another possible solution to the problem could be the combination of the two approaches. Please refer to this discussion on the mailing list and in the two PRs for details. the mailing list discussion can be found here:
https://lists.apache.org/thread/lzh0dvrvnsy8kvflvl61nfbn6f9js81s
The contents of this PR are basically a straightforward combination of the two approaches:
Legacy writers may not write nan_count(s) fields,
so readers have to handle them being absent. Also, legacy writers may have included NaNs into min/max bounds, so readers also have to handle that.
As there are no legacy writers writing IEEE total order, nan_count(s) are defined to be mandatory if this order is used, so readers can assume their presense when this order is used.
This commit removes
nan_pagesfrom the ColumnIndex, which the nan_counts PR mandated. We don't need them anymore, as IEEE total order solves this: As this is a new order and there are thus no legacy writers and readers, we have the freedom to define that for only-NaN pages using this order, we can actually write NaNs into min & max bounds in this case, and readers can assume that isNaN(min) signals an only-NaN page.Note: If this PR is chosen to be adopted, I will update the commit message to state the problem without just pointing onto the other PRs. But until then, I rather link to them instead of repeating myself, in case this PR isn't chosen anyway.
Rationale for this change
What changes are included in this PR?
Do these changes have PoC implementations?