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

Replacing SummingMergeTree implementation to use standard aggregation functions #1330

Merged

Conversation

vavrusa
Copy link
Contributor

@vavrusa vavrusa commented Oct 10, 2017

The issue with the current SummingMergeTree implementation is that merging is quite slow when the table contains nested structures ending with Map. The goal is to:

  1. Reimplement SummingSortedBlockInputStream to use built-in aggregation functions (sum and sumMap)
  2. Add specialized version for sumMap() for case with composite keys
  3. Get rid of the custom SummingSortedBlockInputStream::mergeMap() implementation

I decided to split these into separate PRs, this first replaces numeric field summation and simple nested structure summation (single key, single value). When the nested structure contains a composite key, it falls back to existing implementation.

This is basically useful for testing SummingSortedBlockInputStream
against the AggregatingBlockInputStream, which are used in the
{Summing,Aggregating}MergeTree table engines respectively.
…ctions

This replaces custom summation function implementations with an implementation
using built-in aggregation functions (sum and sumMap). The goal is to be able to
use specialized variants of aggregation functions, and to have a single
efficient implementation.
@vavrusa
Copy link
Contributor Author

vavrusa commented Oct 10, 2017

Current implementation when just summing numeric column:

$ ./dbms/src/DataStreams/tests/aggregating_stream 500000 sum
Elapsed 0.02 sec., 26839873.32 rows/sec.

SummingSorted
One × 2

499996 abc abc
499997 def abcd
499998 abcd def
499999 defg abc

Current implementation when adding a nested column (1 key 1 value):

$ ./dbms/src/DataStreams/tests/aggregating_stream 500000 sum nested
Elapsed 1.93 sec., 259157.86 rows/sec.

SummingSorted
One × 2

499996 abc abc [0,1,2,3,4] [75001,74999,75003,74997,75000]
499997 def abcd [0,1,2,3,4] [75001,74999,75000,75003,74997]
499998 abcd def [0,1,2,3,4] [74998,75002,74997,75000,75003]
499999 defg abc [0,1,2,3,4] [75001,74999,75003,74997,75000]

The PR implementation when just summing numeric column:

Elapsed 0.01 sec., 35880875.49 rows/sec.

SummingSorted
One × 2

499996 abc abc
499997 def abcd
499998 abcd def
499999 defg abc

The PR implementation (1 key 1 value, 1 key 2 values):

$ ./dbms/src/DataStreams/tests/aggregating_stream 500000 sum nested
Elapsed 0.26 sec., 1935681.19 rows/sec.

SummingSorted
One × 2

499996 abc abc [0,1,2,3,4] [75001,74999,75003,74997,75000]
499997 def abcd [0,1,2,3,4] [75001,74999,75000,75003,74997]
499998 abcd def [0,1,2,3,4] [74998,75002,74997,75000,75003]
499999 defg abc [0,1,2,3,4] [75001,74999,75003,74997,75000]

$ ./dbms/src/DataStreams/tests/aggregating_stream 500000 sum nested multivalue
Elapsed 2.14 sec., 233978.77 rows/sec.

SummingSorted
 One × 2

499996 abc abc [0,1,2,3,4] [75001,74999,75003,74997,75000] [750010,749990,750030,749970,750000]
499997 def abcd [0,1,2,3,4] [75001,74999,75000,75003,74997] [750010,749990,750000,750030,749970]
499998 abcd def [0,1,2,3,4] [74998,75002,74997,75000,75003] [749980,750020,749970,750000,750030]
499999 defg abc [0,1,2,3,4] [75001,74999,75003,74997,75000] [750010,749990,750030,749970,750000]

The function is rewritten to avoid allocations on every insert with
Field deserialising each array. The key type is now specialized,
so it can be accessed directly. The value type is variant type,
but only individual values are deserialised (which is cheap, since they're PODs).
The function also support summing of multiple columns by the same key.

The SummingSortedBlockInputStream uses the function in case of
Nested structure with one numeric key and many values to sum.
@vavrusa
Copy link
Contributor Author

vavrusa commented Oct 12, 2017

Implemented more sumMap a bit more efficiently, and added support for single key and multiple arrays to sum. New implementation:

$ ./dbms/src/DataStreams/tests/aggregating_stream 500000 sum nested
Elapsed 0.08 sec., 6263231.08 rows/sec.

SummingSorted
 One × 2

499996 abc abc [0,1,2,3,4] [75001,74999,75003,74997,75000]
499997 def abcd [0,1,2,3,4] [75001,74999,75000,75003,74997]
499998 abcd def [0,1,2,3,4] [74998,75002,74997,75000,75003]
499999 defg abc [0,1,2,3,4] [75001,74999,75003,74997,75000]

$ ./dbms/src/DataStreams/tests/aggregating_stream 500000 sum nested multivalue
Elapsed 0.10 sec., 5039763.74 rows/sec.

SummingSorted
 One × 2

499996 abc abc [0,1,2,3,4] [75001,74999,75003,74997,75000] [750010,749990,750030,749970,750000]
499997 def abcd [0,1,2,3,4] [75001,74999,75000,75003,74997] [750010,749990,750000,750030,749970]
499998 abcd def [0,1,2,3,4] [74998,75002,74997,75000,75003] [749980,750020,749970,750000,750030]
499999 defg abc [0,1,2,3,4] [75001,74999,75003,74997,75000] [750010,749990,750030,749970,750000]

The implementation is a bit faster with HashMap instead of std::map, but it's not ordered so it doesn't maintain the property of keys being sorted in the result array, maybe that's not needed though @bocharov ?

Binary function variant could be further specialized as it doesn't need to store vector of values for each key, but I'm not sure if the performance gain is worth code duplication at this point.

@vavrusa
Copy link
Contributor Author

vavrusa commented Oct 12, 2017

I should proofread my comments, sorry 🙈

desc.function->add(desc.state.data(), &col, cursor->pos, nullptr);
// This stream discards rows that are zero across all summed columns
if (!res)
res = col->get64(cursor->pos) != 0;
Copy link
Member

Choose a reason for hiding this comment

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

It should discard rows, that was summed to zero (that becomes zero after summation).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, added a better comment. For unsigned types, it's true that the row is summed to zero if all input numbers are also zero. The slight difference is with signed numbers sequence, like -1 +1, such row will result in zero, but it won't be deleted until the next merge. I don't know how to get state cheaply out of an aggregation function (there's only interface for inserting to column), which is why I used input.

Copy link
Member

Choose a reason for hiding this comment

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

Ok.

(The only way to look at aggregation state is "insertResultInto" and it can be expensive.)

@@ -69,15 +69,20 @@ class SummingSortedBlockInputStream : public MergingSortedBlockInputStream
* and can be deleted at any time.
*/

/// Stores numbers of key-columns and value-columns.
struct MapDescription
struct AggregateDescription
Copy link
Member

Choose a reason for hiding this comment

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

Missing comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

if (desc.created)
{
desc.function->insertResultInto(desc.state.data(), *desc.merged_column);
desc.function->destroy(desc.state.data());
Copy link
Member

@alexey-milovidov alexey-milovidov Oct 12, 2017

Choose a reason for hiding this comment

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

The code is not exception safe: destroy is not called in exception, memory leak when the function with non-trivial state (sumMap) was used. Probably you should also add a destructor for AggregateDescription.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point! Added try/catch and an explicit state destructor.

ColumnPtr merged_column;
std::vector<char> state;
bool created = false;
/* Compatibility with the mergeMap */
Copy link
Member

Choose a reason for hiding this comment

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

Why not separate struct?

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 don't know whether the legacy mergeMap, or sumMap will be used when identifying maps in block, so instead of building two structures, I'm using just one. I'm happy to rework it to separate structs, it doesn't matter much.

Copy link
Member

@alexey-milovidov alexey-milovidov left a comment

Choose a reason for hiding this comment

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

Almost Ok, only few modifications required.

@alexey-milovidov alexey-milovidov merged commit 41b0bea into ClickHouse:master Oct 13, 2017
@ludv1x
Copy link
Contributor

ludv1x commented Oct 17, 2017

@vavrusa It looks like that your changes broke the following tests:

00148_summing_merge_tree_nested_map_multiple_values:                   [ FAIL ] - return code 9
Received exception from server:
Code: 9. DB::Exception: Received from localhost:9000, ::1. DB::Exception: Sizes of columns doesn't match: k: 4, payload: 8. 

00146_summing_merge_tree_nested_map:                                   [ FAIL ] - return code 9
Received exception from server:
Code: 9. DB::Exception: Received from localhost:9000, ::1. DB::Exception: Sizes of columns doesn't match: d: 4, payload: 8. 

00084_summing_merge_tree:                                              [ FAIL ] - return code 9
Received exception from server:
Code: 9. DB::Exception: Received from localhost:9000, ::1. DB::Exception: Sizes of columns doesn't match: d: 2, x: 4. 

00043_summing_empty_part:                                              [ FAIL ] - return code 9
Received exception from server:
Code: 9. DB::Exception: Received from localhost:9000, ::1. DB::Exception: Sizes of columns doesn't match: d: 1, v: 8. 

Could you check it, please?

@vavrusa
Copy link
Contributor Author

vavrusa commented Oct 17, 2017

I'll take a look!

@vavrusa
Copy link
Contributor Author

vavrusa commented Oct 17, 2017

@ludv1x I ran the mentioned test and fixes several issues here #1367 it'd be great to be able to run these via CI or something automatically, are you working on that?

@ludv1x
Copy link
Contributor

ludv1x commented Oct 18, 2017

@vavrusa Yes, we have CI and autotests for each PR, but they are run only for people from the whitelist.
So, you are not in the whitelist and your PRs are not tested.
I think we have to add you into it-)

I made special comment here to force tests running #1367 (comment)

@vavrusa
Copy link
Contributor Author

vavrusa commented Oct 18, 2017

@ludv1x Ah thanks. Can I see the test results when it finishes or is it internal?

@ludv1x
Copy link
Contributor

ludv1x commented Oct 18, 2017

@vavrusa You can see only final statistics, logs are not available publicly.

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

Successfully merging this pull request may close these issues.

None yet

3 participants