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

Avoid RowConverter for multi column grouping (10% faster clickbench queries) #12269

Merged
merged 31 commits into from
Sep 24, 2024

Conversation

jayzhan211
Copy link
Contributor

@jayzhan211 jayzhan211 commented Aug 31, 2024

Which issue does this PR close?

Closes #9403

Rationale for this change

To avoid Row converter in multi group by clause, we add equality check for group values Array.

We can see a improvement on group by query (much more for string types). The downside is that this is type-specific design unlike Rows that covers all the types

What changes are included in this PR?

Are these changes tested?

Are there any user-facing changes?

Benchmark

Query after 37 is removed since DateTime is not supported

┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query        ┃       main ┃  row-group ┃        Change ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0     │     0.40ms │     0.48ms │  1.19x slower │
│ QQuery 1     │    43.92ms │    46.27ms │  1.05x slower │
│ QQuery 2     │    76.59ms │    76.90ms │     no change │
│ QQuery 3     │    63.93ms │    67.31ms │  1.05x slower │
│ QQuery 4     │   447.49ms │   426.92ms │     no change │
│ QQuery 5     │   704.75ms │   680.61ms │     no change │
│ QQuery 6     │    36.18ms │    35.58ms │     no change │
│ QQuery 7     │    37.50ms │    36.56ms │     no change │
│ QQuery 8     │   687.91ms │   599.47ms │ +1.15x faster │
│ QQuery 9     │   654.23ms │   615.37ms │ +1.06x faster │
│ QQuery 10    │   204.86ms │   175.98ms │ +1.16x faster │
│ QQuery 11    │   237.91ms │   195.13ms │ +1.22x faster │
│ QQuery 12    │   758.26ms │   695.74ms │ +1.09x faster │
│ QQuery 13    │  1011.35ms │   885.85ms │ +1.14x faster │
│ QQuery 14    │   980.44ms │   883.32ms │ +1.11x faster │
│ QQuery 15    │   544.60ms │   468.27ms │ +1.16x faster │
│ QQuery 16    │  1751.52ms │  1318.86ms │ +1.33x faster │
│ QQuery 17    │  1530.76ms │  1311.78ms │ +1.17x faster │
│ QQuery 18    │  3702.09ms │  4322.81ms │  1.17x slower │
│ QQuery 19    │    53.09ms │    52.30ms │     no change │
│ QQuery 20    │  1535.97ms │  1475.38ms │     no change │
│ QQuery 21    │  1830.57ms │  1747.03ms │     no change │
│ QQuery 22    │  4105.92ms │  3851.14ms │ +1.07x faster │
│ QQuery 23    │  8317.37ms │  8089.16ms │     no change │
│ QQuery 24    │   498.14ms │   479.31ms │     no change │
│ QQuery 25    │   506.65ms │   491.19ms │     no change │
│ QQuery 26    │   571.42ms │   544.09ms │     no change │
│ QQuery 27    │  1323.25ms │  1325.59ms │     no change │
│ QQuery 28    │ 10284.17ms │ 10243.26ms │     no change │
│ QQuery 29    │   404.45ms │   441.24ms │  1.09x slower │
│ QQuery 30    │   837.55ms │   847.60ms │     no change │
│ QQuery 31    │   829.55ms │   779.72ms │ +1.06x faster │
│ QQuery 32    │  4698.41ms │  3555.64ms │ +1.32x faster │
│ QQuery 33    │  4180.51ms │  4229.86ms │     no change │
│ QQuery 34    │  4055.17ms │  3981.71ms │     no change │
│ QQuery 35    │  1015.82ms │  1195.95ms │  1.18x slower │
│ QQuery 36    │   143.36ms │   154.85ms │  1.08x slower │
│ QQuery 37    │    99.73ms │   105.03ms │  1.05x slower │
└──────────────┴────────────┴────────────┴───────────────┘

@github-actions github-actions bot added physical-expr Physical Expressions sqllogictest SQL Logic Tests (.slt) labels Aug 31, 2024
@jayzhan211
Copy link
Contributor Author

jayzhan211 commented Sep 2, 2024

@alamb The approach in this PR is to replace RowConverter and check the equality of the group by values by accessing the certain row and iterate all the group by expressions. The downside is that we need to have type-specific implementation but we could see it outperform Rows by eliminating the cost of Rows append and push. Also, this doesn't make sense to upstream to Arrow for me, it is group by specific implementation, so we need to maintain this in Datafusion. Would like an early feedback on this approach!

I'm thinking of support only primitive, string, datetime those non-nested type. For other less common nested types maybe we just fallback to Rows.

}

impl<T: ArrowPrimitiveType> ArrayEq for PrimitiveGroupValueBuilder<T> {
fn equal_to(&self, lhs_row: usize, array: &ArrayRef, rhs_row: usize) -> bool {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

equal_to and append_val are two core functions.

equal_to is to compare the incoming row with the row in group value builder
append_val is to add row into group value builder

@alamb
Copy link
Contributor

alamb commented Sep 2, 2024

Thanks @jayzhan211 -- I will try and review this over the next day or two

(I am catching up from being out last week and I am not back full time until this Thursday)

}

for (i, group_val) in group_values_v2.iter().enumerate() {
if !compare_equal(group_val.as_ref(), *group_idx, &cols[i], row) {
Copy link
Contributor

Choose a reason for hiding this comment

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

As this is called in a loop, this can be optimized/specialized for certain cases like: do the arrays have any nulls or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't get it how could I further optimize the loop based on nulls 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the idea would be change compare_equal to take advantage of cases when, for example, it was known the values couldn't be null (so checking Option isn't needed)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, indeed 👍

Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

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

TLDR is I think this is a really neat idea @jayzhan211 -- in essence it seems to me this PR basically changes from Row comparison to Column by column comparison.

The theory with using RowCoverter at first I believe is that:

  1. It handled all possible types and combinations
  2. The theory was that time spent creating the Row would be paid back by faster comparisons by avoiding dynamic dispatch.

Your benchmark numbers seem to show different results 👌

I thought about how the performance could be so good and I suppose it does make sense because for most aggregate queries, many of the rows will go into an existing group -- so the cost of copying the input, just to find it isn't needed is outweighted

Also, this doesn't make sense to upstream to Arrow for me, it is group by specific implementation, so we need to maintain this in Datafusion. Would like an early feedback on this approach!

I looked at this and I think we could potentially reuse a lot of what is upstream in arrow-rs 's builders. I left comments

I am running the clickbench benchmarks to see if I can confirm the results. If so, I suggest we try and reuse the builders from arrow-rs as much as possible and see how elegant we can make this PR.

But all in all, really nicely done 👏

Comment on lines 305 to 259
let mut group_values_v2 = self
.group_values_v2
.take()
.expect("Can not emit from empty rows");
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a neat optimization as well -- as it saves a copy of the intermediate group values 👍

// }
// }

pub struct ByteGroupValueBuilderNaive<O>
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to my comment above, this looks very similar to the GenericBinaryBuilder in arrow -- it would be great if we could simply reuse that instead of a significant amount of copying 🤔

fn build(self: Box<Self>) -> ArrayRef;
}

pub struct PrimitiveGroupValueBuilder<T: ArrowPrimitiveType>(Vec<Option<T::Native>>);
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks very similar to PrimitiveBuilder in arrow-rs to me https://docs.rs/arrow/latest/arrow/array/struct.PrimitiveBuilder.html (though I think PrimitiveBuilder is likely faster / handles nulls better)

I wonder if you could implement ArrayEq for PrimitiveBuilder using the methods like https://docs.rs/arrow/latest/arrow/array/struct.PrimitiveBuilder.html#method.values_slice

If so I think you would have a very compelling PR here

}

for (i, group_val) in group_values_v2.iter().enumerate() {
if !compare_equal(group_val.as_ref(), *group_idx, &cols[i], row) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the idea would be change compare_equal to take advantage of cases when, for example, it was known the values couldn't be null (so checking Option isn't needed)

@@ -35,9 +35,4 @@ SELECT "URL", COUNT(*) AS c FROM hits GROUP BY "URL" ORDER BY c DESC LIMIT 10;
SELECT 1, "URL", COUNT(*) AS c FROM hits GROUP BY 1, "URL" ORDER BY c DESC LIMIT 10;
SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c FROM hits GROUP BY "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3 ORDER BY c DESC LIMIT 10;
SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10;
SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are these queries removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because I haven't implement DateTime builder, so couldn't pass the test

@alamb
Copy link
Contributor

alamb commented Sep 9, 2024

TLDR is I ran the benchmarks and it does appear to make a measurable performance improvement on several queries 👍

++ BENCH_BRANCH_NAME=row-group
++ ./bench.sh compare main_base row-group
Comparing main_base and row-group
--------------------
Benchmark clickbench_1.json
--------------------
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query        ┃  main_base ┃  row-group ┃        Change ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0     │     0.69ms │     0.70ms │     no change │
│ QQuery 1     │    68.87ms │    69.04ms │     no change │
│ QQuery 2     │   122.40ms │   122.39ms │     no change │
│ QQuery 3     │   129.14ms │   129.84ms │     no change │
│ QQuery 4     │   958.71ms │   950.60ms │     no change │
│ QQuery 5     │  1072.42ms │  1065.00ms │     no change │
│ QQuery 6     │    64.15ms │    65.46ms │     no change │
│ QQuery 7     │    72.57ms │    72.23ms │     no change │
│ QQuery 8     │  1427.25ms │  1352.90ms │ +1.05x faster │
│ QQuery 9     │  1328.70ms │  1333.45ms │     no change │
│ QQuery 10    │   444.86ms │   435.72ms │     no change │
│ QQuery 11    │   493.91ms │   475.07ms │     no change │
│ QQuery 12    │  1158.46ms │  1154.09ms │     no change │
│ QQuery 13    │  2124.34ms │  1776.44ms │ +1.20x faster │
│ QQuery 14    │  1607.17ms │  1306.73ms │ +1.23x faster │
│ QQuery 15    │  1078.30ms │  1062.72ms │     no change │
│ QQuery 16    │  2885.78ms │  2525.19ms │ +1.14x faster │
│ QQuery 17    │  2799.52ms │  2470.93ms │ +1.13x faster │
│ QQuery 18    │  5575.63ms │  5524.24ms │     no change │
│ QQuery 19    │   119.57ms │   118.24ms │     no change │
│ QQuery 20    │  1656.36ms │  1675.11ms │     no change │
│ QQuery 21    │  2014.67ms │  2028.74ms │     no change │
│ QQuery 22    │  4849.99ms │  4824.94ms │     no change │
│ QQuery 23    │ 11274.06ms │ 11309.82ms │     no change │
│ QQuery 24    │   761.95ms │   754.26ms │     no change │
│ QQuery 25    │   667.54ms │   668.69ms │     no change │
│ QQuery 26    │   821.70ms │   839.38ms │     no change │
│ QQuery 27    │  2467.71ms │  2493.84ms │     no change │
│ QQuery 28    │ 15737.24ms │ 15634.42ms │     no change │
│ QQuery 29    │   560.78ms │   551.43ms │     no change │
│ QQuery 30    │  1296.29ms │  1242.39ms │     no change │
│ QQuery 31    │  1318.14ms │  1291.30ms │     no change │
│ QQuery 32    │  4569.20ms │  4241.25ms │ +1.08x faster │
│ QQuery 33    │  5020.91ms │  5031.03ms │     no change │
│ QQuery 34    │  5014.64ms │  4965.98ms │     no change │
│ QQuery 35    │  1818.84ms │  1850.31ms │     no change │
│ QQuery 36    │   316.76ms │   302.74ms │     no change │
│ QQuery 37    │   217.15ms │   219.32ms │     no change │
│ QQuery 38    │   188.03ms │   191.14ms │     no change │
│ QQuery 39    │  1039.10ms │   788.09ms │ +1.32x faster │
│ QQuery 40    │    86.06ms │    83.63ms │     no change │
│ QQuery 41    │    77.68ms │    79.76ms │     no change │
│ QQuery 42    │    97.39ms │    91.99ms │ +1.06x faster │
└──────────────┴────────────┴────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary        ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (main_base)   │ 85404.65ms │
│ Total Time (row-group)   │ 83170.54ms │
│ Average Time (main_base) │  1986.15ms │
│ Average Time (row-group) │  1934.20ms │
│ Queries Faster           │          8 │
│ Queries Slower           │          0 │
│ Queries with No Change   │         35 │
└──────────────────────────┴────────────┘
--------------------
Benchmark clickbench_extended.json
--------------------
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Query        ┃ main_base ┃ row-group ┃    Change ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━┩
│ QQuery 0     │ 2652.87ms │ 2682.38ms │ no change │
│ QQuery 1     │  793.53ms │  794.12ms │ no change │
│ QQuery 2     │ 1576.90ms │ 1609.73ms │ no change │
│ QQuery 3     │  705.29ms │  718.99ms │ no change │
└──────────────┴───────────┴───────────┴───────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Benchmark Summary        ┃           ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Total Time (main_base)   │ 5728.58ms │
│ Total Time (row-group)   │ 5805.22ms │
│ Average Time (main_base) │ 1432.15ms │
│ Average Time (row-group) │ 1451.31ms │
│ Queries Faster           │         0 │
│ Queries Slower           │         0 │
│ Queries with No Change   │         4 │
└──────────────────────────┴───────────┘
--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query        ┃  main_base ┃  row-group ┃        Change ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0     │     2.29ms │     2.27ms │     no change │
│ QQuery 1     │    38.05ms │    37.76ms │     no change │
│ QQuery 2     │    93.47ms │    93.56ms │     no change │
│ QQuery 3     │    98.55ms │    96.67ms │     no change │
│ QQuery 4     │   902.00ms │   910.55ms │     no change │
│ QQuery 5     │   937.33ms │   936.86ms │     no change │
│ QQuery 6     │    34.33ms │    33.78ms │     no change │
│ QQuery 7     │    40.59ms │    39.35ms │     no change │
│ QQuery 8     │  1368.30ms │  1296.26ms │ +1.06x faster │
│ QQuery 9     │  1293.07ms │  1273.53ms │     no change │
│ QQuery 10    │   339.78ms │   331.25ms │     no change │
│ QQuery 11    │   375.98ms │   381.26ms │     no change │
│ QQuery 12    │  1025.16ms │  1034.74ms │     no change │
│ QQuery 13    │  1811.09ms │  1792.68ms │     no change │
│ QQuery 14    │  1453.37ms │  1448.76ms │     no change │
│ QQuery 15    │  1012.38ms │  1012.83ms │     no change │
│ QQuery 16    │  2746.71ms │  2728.55ms │     no change │
│ QQuery 17    │  2719.38ms │  2632.64ms │     no change │
│ QQuery 18    │  5524.24ms │  5525.66ms │     no change │
│ QQuery 19    │    92.83ms │    90.08ms │     no change │
│ QQuery 20    │  1752.39ms │  1751.25ms │     no change │
│ QQuery 21    │  1946.27ms │  1981.08ms │     no change │
│ QQuery 22    │  4974.66ms │  5049.46ms │     no change │
│ QQuery 23    │  9841.76ms │  9781.88ms │     no change │
│ QQuery 24    │   549.84ms │   559.25ms │     no change │
│ QQuery 25    │   467.91ms │   475.16ms │     no change │
│ QQuery 26    │   631.52ms │   617.63ms │     no change │
│ QQuery 27    │  2445.52ms │  2463.11ms │     no change │
│ QQuery 28    │ 14923.62ms │ 15024.79ms │     no change │
│ QQuery 29    │   530.70ms │   523.81ms │     no change │
│ QQuery 30    │  1106.49ms │  1060.93ms │     no change │
│ QQuery 31    │  1140.91ms │  1090.09ms │     no change │
│ QQuery 32    │  4550.65ms │  4133.54ms │ +1.10x faster │
│ QQuery 33    │  4929.55ms │  4922.02ms │     no change │
│ QQuery 34    │  4789.55ms │  4834.44ms │     no change │
│ QQuery 35    │  1753.55ms │  1829.42ms │     no change │
│ QQuery 36    │   283.39ms │   267.52ms │ +1.06x faster │
│ QQuery 37    │   120.66ms │   122.89ms │     no change │
│ QQuery 38    │   141.70ms │   136.39ms │     no change │
│ QQuery 39    │   897.86ms │   918.07ms │     no change │
│ QQuery 40    │    58.14ms │    61.45ms │  1.06x slower │
│ QQuery 41    │    48.73ms │    48.17ms │     no change │
│ QQuery 42    │    66.33ms │    63.97ms │     no change │
└──────────────┴────────────┴────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary        ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (main_base)   │ 79860.62ms │
│ Total Time (row-group)   │ 79415.32ms │
│ Average Time (main_base) │  1857.22ms │
│ Average Time (row-group) │  1846.87ms │
│ Queries Faster           │          3 │
│ Queries Slower           │          1 │
│ Queries with No Change   │         39 │

@jayzhan211
Copy link
Contributor Author

┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query        ┃       main ┃  row-group ┃        Change ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0     │     0.53ms │     0.41ms │ +1.31x faster │
│ QQuery 1     │    49.01ms │    41.52ms │ +1.18x faster │
│ QQuery 2     │    88.41ms │    79.98ms │ +1.11x faster │
│ QQuery 3     │    71.89ms │    73.76ms │     no change │
│ QQuery 4     │   475.98ms │   435.24ms │ +1.09x faster │
│ QQuery 5     │   715.67ms │   689.91ms │     no change │
│ QQuery 6     │    36.87ms │    38.72ms │  1.05x slower │
│ QQuery 7     │    42.85ms │    44.72ms │     no change │
│ QQuery 8     │   767.47ms │   682.42ms │ +1.12x faster │
│ QQuery 9     │   698.12ms │   687.16ms │     no change │
│ QQuery 10    │   214.09ms │   202.99ms │ +1.05x faster │
│ QQuery 11    │   233.88ms │   227.44ms │     no change │
│ QQuery 12    │   765.70ms │   763.87ms │     no change │
│ QQuery 13    │  1105.55ms │   967.40ms │ +1.14x faster │
│ QQuery 14    │  1087.28ms │   904.65ms │ +1.20x faster │
│ QQuery 15    │   538.36ms │   528.94ms │     no change │
│ QQuery 16    │  1707.60ms │  1418.83ms │ +1.20x faster │
│ QQuery 17    │  1580.22ms │  1281.82ms │ +1.23x faster │
│ QQuery 18    │  4503.70ms │  4718.10ms │     no change │
│ QQuery 19    │    58.30ms │    68.69ms │  1.18x slower │
│ QQuery 20    │  1015.23ms │  1086.18ms │  1.07x slower │
│ QQuery 21    │  1306.73ms │  1345.37ms │     no change │
│ QQuery 22    │  3515.27ms │  3727.30ms │  1.06x slower │
│ QQuery 23    │  8564.19ms │  8835.99ms │     no change │
│ QQuery 24    │   509.14ms │   524.45ms │     no change │
│ QQuery 25    │   506.78ms │   517.49ms │     no change │
│ QQuery 26    │   586.24ms │   566.43ms │     no change │
│ QQuery 27    │  1418.28ms │  1498.42ms │  1.06x slower │
│ QQuery 28    │ 10867.59ms │ 11017.18ms │     no change │
│ QQuery 29    │   410.02ms │   419.97ms │     no change │
│ QQuery 30    │   865.76ms │   820.34ms │ +1.06x faster │
│ QQuery 31    │   779.85ms │   776.05ms │     no change │
│ QQuery 32    │  4989.34ms │  3710.45ms │ +1.34x faster │
│ QQuery 33    │  5195.54ms │  5364.02ms │     no change │
│ QQuery 34    │  4955.46ms │  5048.30ms │     no change │
│ QQuery 35    │  1082.95ms │  1171.24ms │  1.08x slower │
│ QQuery 36    │   143.30ms │   144.68ms │     no change │
│ QQuery 37    │    99.65ms │   108.31ms │  1.09x slower │
│ QQuery 38    │   104.12ms │   108.27ms │     no change │
│ QQuery 39    │   383.56ms │   321.16ms │ +1.19x faster │
│ QQuery 40    │    37.12ms │    36.89ms │     no change │
│ QQuery 41    │    34.16ms │    33.11ms │     no change │
│ QQuery 42    │    42.21ms │    41.49ms │     no change │
└──────────────┴────────────┴────────────┴───────────────┘

@jayzhan211
Copy link
Contributor Author

jayzhan211 commented Sep 14, 2024

@alamb I found FirstN mode is non-trivial If I switch to Arrow's Builder instead of Vec what I had done before. Is it reasonable to implement takeN logic in Arrow's Builder upstream?

Or maybe we should revert to the previous vector implementation since the performance doesn't differ a lot and it is easier for FirstN mode to implement. And, there are a few PRs showing that Vec outperform Builder mode.

#12460
#12121

@alamb
Copy link
Contributor

alamb commented Sep 15, 2024

@alamb I found FirstN mode is non-trivial If I switch to Arrow's Builder instead of Vec what I had done before. Is it reasonable to implement takeN logic in Arrow's Builder upstream?

I think using Vec, if possible, is likely to be the best idea as Vec is well understood and highly optimized in Rust (and also has a fast conversion to Arrow arrays)

@alamb
Copy link
Contributor

alamb commented Sep 15, 2024

@jayzhan211 BTW the results in #12269 (comment) are quite impressive

This is a great example of using data driven decisions to come to a better internal architecture / approach (e.g showing with data that using single row comparisons is actually better than using RowCoverter). Very nice 👌

@jayzhan211
Copy link
Contributor Author

Query that compute with Datetime are slower 🤔

@github-actions github-actions bot removed the sqllogictest SQL Logic Tests (.slt) label Sep 16, 2024
array: &ArrayRef,
rhs_row: usize,
) -> bool {
array_row.equal_to(lhs_row, array, rhs_row)
Copy link
Contributor Author

@jayzhan211 jayzhan211 Sep 16, 2024

Choose a reason for hiding this comment

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

Nulls are handled in the equal_to, is there any other place we could further optimize?

Copy link
Contributor

Choose a reason for hiding this comment

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

One idea I had is that you could defer actually copying the new rows into group_values so rather than calling the function once for each new group, you could call it once per batch, and it could insert all the new values in one function call

That would save some function call overhead as well as the downcasting of arrays and maybe would vectorize better

@github-actions github-actions bot added the sqllogictest SQL Logic Tests (.slt) label Sep 16, 2024
Signed-off-by: jayzhan211 <[email protected]>
Signed-off-by: jayzhan211 <[email protected]>
Signed-off-by: jayzhan211 <[email protected]>
Signed-off-by: jayzhan211 <[email protected]>
Signed-off-by: jayzhan211 <[email protected]>
Signed-off-by: jayzhan211 <[email protected]>
Signed-off-by: jayzhan211 <[email protected]>
Signed-off-by: jayzhan211 <[email protected]>
Signed-off-by: jayzhan211 <[email protected]>
Signed-off-by: jayzhan211 <[email protected]>
Signed-off-by: jayzhan211 <[email protected]>
@jayzhan211 jayzhan211 marked this pull request as draft September 21, 2024 07:46
Signed-off-by: jayzhan211 <[email protected]>
@jayzhan211 jayzhan211 marked this pull request as ready for review September 22, 2024 03:57
Copy link
Contributor

@eejbyfeldt eejbyfeldt left a comment

Choose a reason for hiding this comment

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

Have to agree with @alamb really nice PR with impressive performance improvements.

Left some minor comment on things I noticed while looking through the code.


fn append_val(&mut self, array: &ArrayRef, row: usize) {
// non-null fast path
if !self.nullable || !array.is_null(row) {
Copy link
Contributor

Choose a reason for hiding this comment

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

For me it also took sometime when reading to convince my self that || was correct. Maybe it would be easier to read if it we swaped the two cases and removed the !? e.g

        if self.nullable && array.is_null(row) {
            self.group_values.push(T::default_value());
            self.nulls.push(false);
            self.has_null = true;
        } else {
            let elem = array.as_primitive::<T>().value(row);
            self.group_values.push(elem);
            self.nulls.push(true);
        }

}

fn size(&self) -> usize {
let group_values_size = self.group_values.len();
Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding is that size should return allocated size in bytes, so this does not look correct for self.group_values. I would expect us need to do something like

        let group_values_size = self.group_values.iter().map(|column| column.size()).sum()

and add a size method to ArrayRowEq so it can do some size calculation based on the field.

Comment on lines 88 to 89
let len = cols.len();
let mut v = Vec::with_capacity(len);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should give len a better name like n_cols or just "inline" it like

Suggested change
let len = cols.len();
let mut v = Vec::with_capacity(len);
let mut v = Vec::with_capacity(cols.len());

because for me it currently just hurts readability.

let l = self.offsets[lhs_row].as_usize();
let r = self.offsets[lhs_row + 1].as_usize();
let existing_elem = unsafe { self.buffer.as_slice().get_unchecked(l..r) };
existing_elem.len() == rhs_elem.len() && rhs_elem == existing_elem
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really need to check the length manually? Will that not just be a part of the == for slices. (https://doc.rust-lang.org/1.81.0/src/core/slice/cmp.rs.html#59)

Suggested change
existing_elem.len() == rhs_elem.len() && rhs_elem == existing_elem
rhs_elem == existing_elem

fn take_n(&mut self, n: usize) -> ArrayRef {
debug_assert!(self.len() >= n);

let mut nulls_count = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like we never use the result in nulls_count? Could it just be removed?

Signed-off-by: jayzhan211 <[email protected]>
@jayzhan211
Copy link
Contributor Author

One idea I had is that you could defer actually copying the new rows into group_values so rather than calling the function once for each new group, you could call it once per batch, and it could insert all the new values in one function call

That would save some function call overhead as well as the downcasting of arrays and maybe would vectorize better

I think the challenge of processing in batch is that if we got multiple same row, we should push the first one in group values but reject another n-1 ones as duplicated row values. The dependency is not vectorizable, since we need to check them iteratively.

Signed-off-by: jayzhan211 <[email protected]>
@jayzhan211
Copy link
Contributor Author

@alamb @eejbyfeldt Thanks for your review, could take another quick look then merge this PR.

TODOs of follow on

  • Implement a ByteGroupValueBuilder for StringView and the other primitive based types (e.g. DecimalArray, the date/time stuff, etc)
  • Simplify / Improve single group value case GroupValuesPrimitive and GroupValuesByes, maybe with ArrowRowEq
  • Vectorize the row equality check + append row

@alamb
Copy link
Contributor

alamb commented Sep 24, 2024

I am running the benchmarks one more time

@alamb
Copy link
Contributor

alamb commented Sep 24, 2024

--------------------
Benchmark clickbench_1.json
--------------------
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query        ┃  main_base ┃  row-group ┃        Change ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0     │     0.65ms │     0.65ms │     no change │
│ QQuery 1     │    69.31ms │    70.17ms │     no change │
│ QQuery 2     │   126.99ms │   121.74ms │     no change │
│ QQuery 3     │   134.96ms │   135.57ms │     no change │
│ QQuery 4     │   993.97ms │   987.83ms │     no change │
│ QQuery 5     │  1064.89ms │  1041.30ms │     no change │
│ QQuery 6     │    66.24ms │    65.07ms │     no change │
│ QQuery 7     │    82.73ms │    83.22ms │     no change │
│ QQuery 8     │  1516.58ms │  1399.62ms │ +1.08x faster │
│ QQuery 9     │  1380.23ms │  1403.17ms │     no change │
│ QQuery 10    │   473.74ms │   454.26ms │     no change │
│ QQuery 11    │   513.17ms │   503.41ms │     no change │
│ QQuery 12    │  1200.73ms │  1207.25ms │     no change │
│ QQuery 13    │  2175.58ms │  1856.97ms │ +1.17x faster │
│ QQuery 14    │  1626.70ms │  1328.44ms │ +1.22x faster │
│ QQuery 15    │  1152.71ms │  1150.77ms │     no change │
│ QQuery 16    │  3058.97ms │  2612.74ms │ +1.17x faster │
│ QQuery 17    │  2805.61ms │  2415.64ms │ +1.16x faster │
│ QQuery 18    │  5904.12ms │  5091.17ms │ +1.16x faster │
│ QQuery 19    │   122.17ms │   120.53ms │     no change │
│ QQuery 20    │  1619.79ms │  1655.86ms │     no change │
│ QQuery 21    │  2075.67ms │  2042.54ms │     no change │
│ QQuery 22    │  4569.65ms │  4636.64ms │     no change │
│ QQuery 23    │ 11455.17ms │ 11516.52ms │     no change │
│ QQuery 24    │   744.04ms │   774.71ms │     no change │
│ QQuery 25    │   662.80ms │   662.52ms │     no change │
│ QQuery 26    │   824.42ms │   829.10ms │     no change │
│ QQuery 27    │  2610.68ms │  2555.47ms │     no change │
│ QQuery 28    │ 16169.74ms │ 16193.85ms │     no change │
│ QQuery 29    │   571.62ms │   575.85ms │     no change │
│ QQuery 30    │  1264.40ms │  1197.98ms │ +1.06x faster │
│ QQuery 31    │  1331.55ms │  1284.00ms │     no change │
│ QQuery 32    │  4779.21ms │  4314.61ms │ +1.11x faster │
│ QQuery 33    │  5370.97ms │  5326.00ms │     no change │
│ QQuery 34    │  5329.12ms │  5331.64ms │     no change │
│ QQuery 35    │  1905.69ms │  1938.82ms │     no change │
│ QQuery 36    │   318.31ms │   326.24ms │     no change │
│ QQuery 37    │   209.91ms │   208.10ms │     no change │
│ QQuery 38    │   191.92ms │   188.58ms │     no change │
│ QQuery 39    │  1033.41ms │   798.70ms │ +1.29x faster │
│ QQuery 40    │    89.63ms │    87.00ms │     no change │
│ QQuery 41    │    79.13ms │    79.93ms │     no change │
│ QQuery 42    │    89.63ms │    98.59ms │  1.10x slower │

--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query        ┃  main_base ┃  row-group ┃        Change ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0     │     2.29ms │     2.24ms │     no change │
│ QQuery 1     │    37.95ms │    36.63ms │     no change │
│ QQuery 2     │    91.18ms │    94.10ms │     no change │
│ QQuery 3     │    99.70ms │   102.18ms │     no change │
│ QQuery 4     │   924.55ms │   922.81ms │     no change │
│ QQuery 5     │   974.73ms │   967.65ms │     no change │
│ QQuery 6     │    33.49ms │    34.13ms │     no change │
│ QQuery 7     │    42.11ms │    40.81ms │     no change │
│ QQuery 8     │  1462.34ms │  1401.97ms │     no change │
│ QQuery 9     │  1347.10ms │  1352.15ms │     no change │
│ QQuery 10    │   353.28ms │   343.14ms │     no change │
│ QQuery 11    │   394.97ms │   397.55ms │     no change │
│ QQuery 12    │  1080.53ms │  1083.58ms │     no change │
│ QQuery 13    │  1816.98ms │  1745.72ms │     no change │
│ QQuery 14    │  1502.91ms │  1261.70ms │ +1.19x faster │
│ QQuery 15    │  1084.91ms │  1087.88ms │     no change │
│ QQuery 16    │  2993.31ms │  2553.79ms │ +1.17x faster │
│ QQuery 17    │  2693.79ms │  2368.53ms │ +1.14x faster │
│ QQuery 18    │  5883.96ms │  4961.15ms │ +1.19x faster │
│ QQuery 19    │    97.37ms │    93.87ms │     no change │
│ QQuery 20    │  1676.69ms │  1723.33ms │     no change │
│ QQuery 21    │  1958.76ms │  2012.30ms │     no change │
│ QQuery 22    │  4715.60ms │  4695.54ms │     no change │
│ QQuery 23    │ 10499.53ms │ 10452.82ms │     no change │
│ QQuery 24    │   586.87ms │   575.49ms │     no change │
│ QQuery 25    │   504.91ms │   491.04ms │     no change │
│ QQuery 26    │   649.56ms │   657.69ms │     no change │
│ QQuery 27    │  2557.90ms │  2536.86ms │     no change │
│ QQuery 28    │ 15545.61ms │ 15410.83ms │     no change │
│ QQuery 29    │   515.39ms │   522.92ms │     no change │
│ QQuery 30    │  1101.35ms │  1076.15ms │     no change │
│ QQuery 31    │  1171.23ms │  1093.86ms │ +1.07x faster │
│ QQuery 32    │  4797.56ms │  4238.85ms │ +1.13x faster │
│ QQuery 33    │  5217.26ms │  5227.87ms │     no change │
│ QQuery 34    │  5185.00ms │  5195.91ms │     no change │
│ QQuery 35    │  1862.76ms │  2149.75ms │  1.15x slower │
│ QQuery 36    │   277.32ms │   258.05ms │ +1.07x faster │
│ QQuery 37    │   122.28ms │   122.62ms │     no change │
│ QQuery 38    │   140.98ms │   145.85ms │     no change │
│ QQuery 39    │   984.01ms │   775.28ms │ +1.27x faster │
│ QQuery 40    │    59.33ms │    54.72ms │ +1.08x faster │
│ QQuery 41    │    50.05ms │    46.63ms │ +1.07x faster │
│ QQuery 42    │    61.86ms │    63.49ms │     no change │
└──────────────┴────────────┴────────────┴───────────────┘

--------------------
Benchmark tpch_sf1.json
--------------------
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query        ┃ main_base ┃ row-group ┃        Change ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 1     │  252.62ms │  211.14ms │ +1.20x faster │
│ QQuery 2     │  131.31ms │  120.09ms │ +1.09x faster │
│ QQuery 3     │  138.74ms │  114.57ms │ +1.21x faster │
│ QQuery 4     │   92.29ms │   84.86ms │ +1.09x faster │
│ QQuery 5     │  180.88ms │  158.23ms │ +1.14x faster │
│ QQuery 6     │   60.26ms │   44.42ms │ +1.36x faster │
│ QQuery 7     │  223.37ms │  201.96ms │ +1.11x faster │
│ QQuery 8     │  165.38ms │  149.05ms │ +1.11x faster │
│ QQuery 9     │  259.49ms │  239.59ms │ +1.08x faster │
│ QQuery 10    │  244.94ms │  216.64ms │ +1.13x faster │
│ QQuery 11    │  104.23ms │   92.60ms │ +1.13x faster │
│ QQuery 12    │  135.08ms │  144.47ms │  1.07x slower │
│ QQuery 13    │  304.56ms │  213.47ms │ +1.43x faster │
│ QQuery 14    │   97.08ms │   72.11ms │ +1.35x faster │
│ QQuery 15    │  128.14ms │  115.86ms │ +1.11x faster │
│ QQuery 16    │   85.69ms │   80.46ms │ +1.07x faster │
│ QQuery 17    │  246.47ms │  201.71ms │ +1.22x faster │
│ QQuery 18    │  344.79ms │  316.85ms │ +1.09x faster │
│ QQuery 19    │  162.78ms │  138.60ms │ +1.17x faster │
│ QQuery 20    │  145.63ms │  137.61ms │ +1.06x faster │
│ QQuery 21    │  276.08ms │  273.78ms │     no change │
│ QQuery 22    │   65.63ms │   65.42ms │     no change │
└──────────────┴───────────┴───────────┴───────────────┘

@alamb alamb changed the title Avoid RowConverter for multi column grouping Avoid RowConverter for multi column grouping (10% faster clickbench queries) Sep 24, 2024
Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

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

TLDR is this is great. I love it. Thank you so much @jayzhan211

I am sure we can make it better as follow on. Very impressive

🚀

let b = ByteGroupValueBuilder::<i64>::new(OutputType::Binary);
v.push(Box::new(b) as _)
}
dt => todo!("{dt} not impl"),
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice if this was an internal error rather than a panic

Copy link
Contributor

Choose a reason for hiding this comment

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

in #12620

/// are stored as a zero length string.
offsets: Vec<O>,
/// Null indexes in offsets, if `i` is in nulls, `offsets[i]` should be equals to `offsets[i+1]`
nulls: Vec<usize>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to use Vec rather than Vec<bool> as used in PrimitiveGroupValueBuilder?

Maybe as a follow on PR we can explore using BooleanBufferBuilder from arrow-rs directly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is not easy to handle take_n logic with BooleanBufferBuilder

Copy link
Contributor

Choose a reason for hiding this comment

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

I took a shot at doing so in #12623 - I agree the take_n logic was the trickiest

}
}

fn has_row_like_feature(data_type: &DataType) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor nit -- but since this list of types needs to remain in sync with GroupValuesColumn it might make sense to move it into that module too

Like GroupValuesColumn::is_supported or something

Copy link
Contributor

Choose a reason for hiding this comment

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

in #12620

@alamb alamb merged commit 6a2d88d into apache:main Sep 24, 2024
25 checks passed
@alamb
Copy link
Contributor

alamb commented Sep 24, 2024

I plan to spend some time tomorrow morning (my time) perhaps making some tweaks to this code (e,g no panics) and filing a ticket to track follow on work per @jayzhan211 's comment here: #12269 (comment)

@alamb
Copy link
Contributor

alamb commented Sep 25, 2024

I plan to file a few PRs to try an improve the code a bit before I file additional tickets to add more features. I will update this list:

bgjackma pushed a commit to bgjackma/datafusion that referenced this pull request Sep 25, 2024
…ueries) (apache#12269)

* row like group values to avoid rowconverter

Signed-off-by: jayzhan211 <[email protected]>

* comment out unused

Signed-off-by: jayzhan211 <[email protected]>

* implement to Arrow's builder

Signed-off-by: jayzhan211 <[email protected]>

* cleanup

Signed-off-by: jayzhan211 <[email protected]>

* switch back to vector

Signed-off-by: jayzhan211 <[email protected]>

* clippy

Signed-off-by: jayzhan211 <[email protected]>

* optimize for non-null

Signed-off-by: jayzhan211 <[email protected]>

* use truncate

Signed-off-by: jayzhan211 <[email protected]>

* cleanup

Signed-off-by: jayzhan211 <[email protected]>

* cleanup

Signed-off-by: jayzhan211 <[email protected]>

* fix first N bug

Signed-off-by: jayzhan211 <[email protected]>

* fix null check

Signed-off-by: jayzhan211 <[email protected]>

* fast path null

Signed-off-by: jayzhan211 <[email protected]>

* fix bug

Signed-off-by: jayzhan211 <[email protected]>

* fmt

Signed-off-by: jayzhan211 <[email protected]>

* fix error

Signed-off-by: jayzhan211 <[email protected]>

* clippy

Signed-off-by: jayzhan211 <[email protected]>

* adjust spill mode max mem

Signed-off-by: jayzhan211 <[email protected]>

* revert test_create_external_table_with_terminator_with_newlines_in_values

Signed-off-by: jayzhan211 <[email protected]>

* fix null handle bug

Signed-off-by: jayzhan211 <[email protected]>

* cleanup

Signed-off-by: jayzhan211 <[email protected]>

* support binary

Signed-off-by: jayzhan211 <[email protected]>

* add binary test

Signed-off-by: jayzhan211 <[email protected]>

* use Vec<T> instead of Option<Vec<T>>

Signed-off-by: jayzhan211 <[email protected]>

* add test and doc

Signed-off-by: jayzhan211 <[email protected]>

* debug assert

Signed-off-by: jayzhan211 <[email protected]>

* mv & rename

Signed-off-by: jayzhan211 <[email protected]>

* fix take_n logic

Signed-off-by: jayzhan211 <[email protected]>

* address comment

Signed-off-by: jayzhan211 <[email protected]>

* cleanup

Signed-off-by: jayzhan211 <[email protected]>

---------

Signed-off-by: jayzhan211 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
physical-expr Physical Expressions sqllogictest SQL Logic Tests (.slt)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve performance for grouping by variable length columns (strings)
4 participants