Skip to content

Binary operator monotonicity#14513

Merged
KochetovNicolai merged 6 commits intoClickHouse:masterfrom
amosbird:mf1
Sep 15, 2020
Merged

Binary operator monotonicity#14513
KochetovNicolai merged 6 commits intoClickHouse:masterfrom
amosbird:mf1

Conversation

@amosbird
Copy link
Copy Markdown
Collaborator

@amosbird amosbird commented Sep 5, 2020

I hereby agree to the terms of the CLA available at: https://yandex.ru/legal/cla/?lang=en

Changelog category (leave one):

  • Improvement

Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):

ClickHouse treats partition expr and key expr differently. Partition expr is used to construct an minmax index containing related columns, while primary key expr is stored as an expr. Sometimes user might partition a table at coarser levels, such as partition by i / 1000. However, binary operators are not monotonic and this PR tries to fix that. It might also benifit other use cases.

Currently only simple binary ops are added.

Detailed description / Documentation draft:

None.

@robot-clickhouse robot-clickhouse added the pr-improvement Pull request with some product improvements label Sep 5, 2020
@KochetovNicolai KochetovNicolai self-assigned this Sep 5, 2020
@filimonov
Copy link
Copy Markdown
Contributor

filimonov commented Sep 6, 2020

Hm, afair i've used intDiv in partition by keys and it was working properly...

And i've was also looking for the info about binary ops monotonicity, and didn't find it in code...

Loosely coupled with #11796

@amosbird
Copy link
Copy Markdown
Collaborator Author

amosbird commented Sep 7, 2020

Hm, afair i've used intDiv in partition by keys and it was working properly...

What does the query look like?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It is not correct because of overflow

SELECT 
    18446744073709551614 + number AS arg,
    arg + 1
FROM numbers(2)

┌──────────────────arg─┬─plus(plus(18446744073709551614, number), 1)─┐
│ 18446744073709551614 │                        18446744073709551615 │
│ 18446744073709551615 │                                           0 │
└──────────────────────┴─────────────────────────────────────────────┘

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It should be checked in all other cases too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ouch. Overflow makes things complicated. I wonder if we should provide saturation arithmetic.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks like we want to compare left_point.get<Int64>() >= 0 and right_point.get<Int64>() >= 0

Comment on lines 1343 to 1310
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This code is difficult to read.
It's would be nice to add a comment (or even ascii schema) to each case.

Copy link
Copy Markdown
Member

@KochetovNicolai KochetovNicolai left a comment

Choose a reason for hiding this comment

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

Generally, I would rather update IFunctionBase::getMonotonicityForRange signature to add version constant arguments list. E.g:

Monotonicity getMonotonicityForRange(const IDataType & /*type*/, const Field & /*left*/, const Field & /*right*/, const ColumnsWithTypeAndName & /*arguments*/)

where arguments contain arguments.size() - 1 const columns.

This implementation is also ok (it is implemented and works).

It would be nice to add more tests with binary arithmetics in pk, and also test main cases for each function (unit test).

Cases with overflow must be fixed.

@alexey-milovidov
Copy link
Copy Markdown
Member

@KochetovNicolai If we assume that overflow (both signed and unsigned) is implementation specific behaviour, we can also assume that addition of constant is monotonic.

@KochetovNicolai
Copy link
Copy Markdown
Member

If we assume that overflow (both signed and unsigned) is implementation specific behaviour, we can also assume that addition of constant is monotonic.

I do not like it pretty much cause query result ay be wrong in some corner cases, but maybe it is ok for now.
Still, it is more complicated for multiplication.

Also maybe we need more general definition of interval. E.g. we could work in Z_(2^64) where interval like [4, 2] would be also acceptable.

@alexey-milovidov
Copy link
Copy Markdown
Member

Yes, it's better to provide proper monotonicity info for intervals.

Yes, for multiplication it's more complicated as users can use it with intended overflow as a simple hash function:
PARTITION BY UserID * 0xc4ceb9fe1a85ec53 % 10

@filimonov
Copy link
Copy Markdown
Contributor

Hm, afair i've used intDiv in partition by keys and it was working properly...

And i've was also looking for the info about binary ops monotonicity, and didn't find it in code...

Loosely coupled with #11796

Rechecked. It seems it somehow works for PARTITION key, but doesn't work for primary key - follow that example:

DROP TABLE IF EXISTS intDiv_test;
create table intDiv_test (
    a UInt32, 
    b UInt32
)
Engine = MergeTree
PARTITION BY intDiv(a,536870912) /* splits uint32 range into 8 partitions */
ORDER BY intDiv(b,65536)
SETTINGS index_granularity=1024;

TRUNCATE TABLE intDiv_test; 
INSERT INTO intDiv_test SELECT rand(1), rand(2) from numbers(10000);

INSERT INTO intDiv_test SELECT 10000, 10000 from numbers(10);

INSERT INTO intDiv_test SELECT rand(1), rand(2) from numbers(10000);

OPTIMIZE TABLE intDiv_test FINAL;

set send_logs_level='trace';

-- PARTITION key check:

select * from intDiv_test where a = 10000;

-- WORKS!

-- MinMax index condition: (column 0 in [10000, 10000])
-- Selected 1 parts by date, 1 parts by key, 3 marks by primary key, 3 marks to read from 1 ranges

-- for the reference -with identity it reads much more parts:

select * from intDiv_test where identity(a) = 10000;

-- MinMax index condition: unknown
-- Selected 8 parts by date, 8 parts by key, 24 marks by primary key, 24 marks to read from 8 ranges


-- PRIMARY key check:

-- DON'T work.

select * from intDiv_test where b = 10000;

-- Key condition: unknown
-- Selected 8 parts by date, 8 parts by key, 24 marks by primary key, 24 marks to read from 8 ranges

-- for the reference - with identity it's exactly the same 

select * from intDiv_test where identity(b) = 10000;

-- Key condition: unknown
-- MinMax index condition: unknown
-- Selected 8 parts by date, 8 parts by key, 24 marks by primary key, 24 marks to read from 8 ranges

-- BUT

select * from intDiv_test where intDiv(b,65536) = intDiv(10000,65536);

-- Key condition: (column 0 in [0, 0])
-- Selected 8 parts by date, 1 parts by key, 1 marks by primary key, 1 marks to read from 1 ranges

@alexey-milovidov
Copy link
Copy Markdown
Member

@filimonov Partition elimination does not depend on monotonicity at all - it simply evaluates the expressions for every part.
Treat is as a minmax index with granularity 1 across data parts.

@amosbird amosbird closed this Sep 10, 2020
@amosbird amosbird reopened this Sep 10, 2020
@amosbird amosbird force-pushed the mf1 branch 2 times, most recently from 28a757d to d2f2b18 Compare September 10, 2020 14:01
@amosbird
Copy link
Copy Markdown
Collaborator Author

amosbird commented Sep 10, 2020

Partition elimination does not depend on monotonicity at all - it simply evaluates the expressions for every part.
Treat is as a minmax index with granularity 1 across data parts.

I'm afraid it's not correct.

@filimonov I'll do a case by case explanation.

  • case one: select * from intDiv_test where a = 10000;

    It works because clickhouse stores partition column instead of partition expr. It doesn't matter that you've partitioned it with intDiv(a,536870912). The minmax index still only contains column a.

  • case two: select * from intDiv_test where identity(a) = 10000;

    It doesn't work because identity is not monotonic. (well documented)

  • case three: select * from intDiv_test where b = 10000;

    It doesn't work because clickhouse stores primary expr directly. In order to use the index, we must ensure that the inverse of the expr is monotonic (well commented in

    /** The key functional expression constraint may be inferred from a plain column in the expression.
    ). This is why functions have a property called is_always_monotonic, which is used for such cases, and intDiv doesn't have this property.

  • case four: select * from intDiv_test where identity(b) = 10000;

    It doesn't work because identity is not monotonic. (well documented)

  • case five: select * from intDiv_test where intDiv(b,65536) = intDiv(10000,65536);

    It works because now you have the exact match of primary expr on the left hand side of the predicate. The index is used just like case one.

So what does this PR fix? Suppose you have a table partitioned by i / 1000. Then you query it with where i / 1000 = 3. It doesn't work because the partition column is i and the LHS of the predicate is a function (intDiv) of i, which is not monotonic.

@KochetovNicolai
Copy link
Copy Markdown
Member

Code looks ok, but we need to investigate crash in tests.

@KochetovNicolai
Copy link
Copy Markdown
Member

#0  0x0000000015f524e1 in DB::KeyCondition::isKeyPossiblyWrappedByMonotonicFunctionsImpl(std::__1::shared_ptr<DB::IAST> const&, unsigned long&, std::__1::shared_ptr<DB::IDataType const>&, std::__1::vector<DB::ASTFunction const*, std::__1::allocator<DB::ASTFunction const*> >&) ()
#1  0x0000000015f525b9 in DB::KeyCondition::isKeyPossiblyWrappedByMonotonicFunctionsImpl(std::__1::shared_ptr<DB::IAST> const&, unsigned long&, std::__1::shared_ptr<DB::IDataType const>&, std::__1::vector<DB::ASTFunction const*, std::__1::allocator<DB::ASTFunction const*> >&) ()
#2  0x0000000015f525b9 in DB::KeyCondition::isKeyPossiblyWrappedByMonotonicFunctionsImpl(std::__1::shared_ptr<DB::IAST> const&, unsigned long&, std::__1::shared_ptr<DB::IDataType const>&, std::__1::vector<DB::ASTFunction const*, std::__1::allocator<DB::ASTFunction const*> >&) ()
#3  0x0000000015f54bd8 in DB::KeyCondition::isKeyPossiblyWrappedByMonotonicFunctions(std::__1::shared_ptr<DB::IAST> const&, DB::Context const&, unsigned long&, std::__1::shared_ptr<DB::IDataType const>&, std::__1::vector<std::__1::shared_ptr<DB::IFunctionBase>, std::__1::allocator<std::__1::shared_ptr<DB::IFunctionBase> > >&) ()
#4  0x0000000015f5a89a in DB::KeyCondition::tryParseAtomFromAST(std::__1::shared_ptr<DB::IAST> const&, DB::Context const&, DB::Block&, DB::KeyCondition::RPNElement&) ()
#5  0x0000000015f5b4d0 in DB::KeyCondition::traverseAST(std::__1::shared_ptr<DB::IAST> const&, DB::Context const&, DB::Block&) ()
#6  0x0000000015f5b232 in DB::KeyCondition::traverseAST(std::__1::shared_ptr<DB::IAST> const&, DB::Context const&, DB::Block&) ()
#7  0x0000000015f5b232 in DB::KeyCondition::traverseAST(std::__1::shared_ptr<DB::IAST> const&, DB::Context const&, DB::Block&) ()
#8  0x0000000015f5bd8c in DB::KeyCondition::KeyCondition(DB::SelectQueryInfo const&, DB::Context const&, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, std::__1::shared_ptr<DB::ExpressionActions> const&) ()
#9  0x0000000016028cc4 in DB::MergeTreeDataSelectExecutor::readFromParts(std::__1::vector<std::__1::shared_ptr<DB::IMergeTreeDataPart const>, std::__1::allocator<std::__1::shared_ptr<DB::IMergeTreeDataPart const> > >, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, std::__1::shared_ptr<DB::StorageInMemoryMetadata const> const&, DB::SelectQueryInfo const&, DB::Context const&, unsigned long, unsigned int, std::__1::unordered_map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, long, std::__1::hash<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::equal_to<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::allocator<std::__1::pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const, long> > > const*) const ()
#10 0x000000001602e332 in DB::MergeTreeDataSelectExecutor::read(std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, std::__1::shared_ptr<DB::StorageInMemoryMetadata const> const&, DB::SelectQueryInfo const&, DB::Context const&, unsigned long, unsigned int, std::__1::unordered_map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, long, std::__1::hash<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::equal_to<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::allocator<std::__1::pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const, long> > > const*) const ()
#11 0x0000000015d813d4 in DB::StorageMergeTree::read(std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, std::__1::shared_ptr<DB::StorageInMemoryMetadata const> const&, DB::SelectQueryInfo const&, DB::Context const&, DB::QueryProcessingStage::Enum, unsigned long, unsigned int) ()
#12 0x00000000165726ba in DB::ReadFromStorageStep::ReadFromStorageStep(std::__1::shared_ptr<DB::RWLockImpl::LockHolderImpl>, std::__1::shared_ptr<DB::StorageInMemoryMetadata const>&, DB::SelectQueryOptions, std::__1::shared_ptr<DB::IStorage>, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, DB::SelectQueryInfo const&, std::__1::shared_ptr<DB::Context>, DB::QueryProcessingStage::Enum, unsigned long, unsigned long) ()
#13 0x000000001588ec21 in DB::InterpreterSelectQuery::executeFetchColumns(DB::QueryProcessingStage::Enum, DB::QueryPlan&, std::__1::shared_ptr<DB::PrewhereInfo> const&, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&) ()
#14 0x0000000015892ac9 in DB::InterpreterSelectQuery::executeImpl(DB::QueryPlan&, std::__1::shared_ptr<DB::IBlockInputStream> const&, std::__1::optional<DB::Pipe>) ()
#15 0x0000000015894354 in DB::InterpreterSelectQuery::buildQueryPlan(DB::QueryPlan&) ()
#16 0x0000000015a0b7c8 in DB::InterpreterSelectWithUnionQuery::buildQueryPlan(DB::QueryPlan&) ()
--Type <RET> for more, q to quit, c to continue without paging--
#17 0x0000000015a0b99a in DB::InterpreterSelectWithUnionQuery::execute() ()
#18 0x0000000015b892e2 in ?? ()
#19 0x0000000015b8ac52 in DB::executeQuery(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, DB::Context&, bool, DB::QueryProcessingStage::Enum, bool) ()
#20 0x0000000016233585 in DB::TCPHandler::runImpl() ()
#21 0x00000000162342f0 in DB::TCPHandler::run() ()
#22 0x0000000018aa309b in Poco::Net::TCPServerConnection::start() ()
#23 0x0000000018aa352b in Poco::Net::TCPServerDispatcher::run() ()
#24 0x0000000018c22006 in Poco::PooledThread::run() ()
#25 0x0000000018c1d400 in Poco::ThreadImpl::runnableEntry(void*) ()
#26 0x00007ffff7bbd6db in start_thread (arg=0x7fff5473a700) at pthread_create.c:463
#27 0x00007ffff74daa3f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

01079_new_range_reader_segfault

// variable / constant
else if (right.column && isColumnConst(*right.column))
{
auto constant = (*left.column)[0];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

right.column?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nice catch!

return false;
}

Monotonicity getMonotonicityForRange(const IDataType &, const Field & left_point, const Field & right_point) const override
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually since all of this does not uses generic abstractions, looks like it is better to cover all functions that listed here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Which one is not covered?

Copy link
Copy Markdown
Member

@azat azat Sep 14, 2020

Choose a reason for hiding this comment

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

Actually only the following is covered

  • divide(var, const) (via toDate)

All of the above does not:

  • divide(const, var)
  • intDiv(var, const)
  • intDiv(const, var)
  • plus(var, const)
  • plus(const, var)
  • minus(var, const)
  • minus(const, var)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don't follow. What do you mean by cover here? plus and minus take effect and catch a bug already.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What do you mean by cover here?

Add primary key with these functions into the 01480_binary_operator_monotonicity.sql

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ok. So it's "test coverage" you were talking about.

@KochetovNicolai KochetovNicolai merged commit c1f6198 into ClickHouse:master Sep 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-improvement Pull request with some product improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants