You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 24 Next »

IDIEP-24
AuthorVladimir Ozerov Ozerov
SponsorVladimir Ozerov Ozerov
Created19 Jun 2018
Status

ACTIVE


Motivation

The goal of this IEP is to avoid execution of SQL queries on partitions which do no contain relevant data.

SQL query may scan arbitrary set of cache values. In general case interested values may reside on every cluster node, so broadcast is needed. Widely adopted optimization technique is so-called "partition pruning":

  1. Try extracting information about target partitions from SQL query
  2. If succeeded - execute query only over these partitions

When implemented it will provide the following benefits:

  1. Improved query latency, as we will be able to skip much more partitions than now (only backup partitions are skipped for now)
  2. Improved thin client latency - it will be possible to send requests to target node, thus saving one network hop.
  3. Decreased page cache pressure - less data to read, less data to evict, less number of page locks
  4. Improved system throughput, as less total CPU and IO operations will be required to execute optimized query
  5. Improved thin client latency - it will be possible to send requests to target node, thus saving one network hop.

Partition pruning is already implemented in Apache Ignite in very simplified form [1]. Only WHERE condition with equality is considered and only for SQL queries without joins. We should expand it further.

[1] https://issues.apache.org/jira/browse/IGNITE-4509

Design

In the following sections we first explain how partitions could be extracted from SQL parts, and how certain query rewrite techniques could help us with it. Then we will describe how extracted partition info is assembled in a form of tree. Then we discuss that partition extraction should be performed two times - before split for the whole query, and after split for query parts. Finally, we explain how partition info will be passed to thin clients, and how users will be able to control and monitor partition pruning.

Extracting Partitions

Suppose that for every table we know it's affinity column. It is either PK or explicitly defined affinity column. Then we can analyze WHERE expressions related to the given tables to extract partition info. For JOINs we can compare affinity functions of two tables. If they are compatible, then we can "pass" partition information from one table to another. 

Apache Ignite supports only hash-based sharding, so partition could be extracted only from equality conditions.

In further examples affinity function is denoted as '{...}'. Extracted partition is either concrete number, or query parameter index which will be converted to concrete number later. We will denote first type as "Pn" (e.g. P1, P2), and second as ":INDEX" (e.g. :1, :2). If partition cannot be extracted from condition, we will denote it as "ALL". Empty partition set is denoted as "EMPTY".

Equality

For equality we simply apply affinity function to the value.

Equality with constant on affinity column
SELECT * FROM emp WHERE emp.id = 100
=> {id=100} => P1


Equality with parameter on affinity column
SELECT * FROM emp WHERE emp.id = :1
=> {id=:1} => :1
Non-equality on affinity column
SELECT * FROM emp WHERE emp.id != 100
=> {name!=100} => (ALL)
Equality on non-affinity column
SELECT * FROM emp WHERE emp.name = :1
=> {name=:1} => (ALL)

IN

IN condition with list of values results in a merged list of affected partitions. IN condition with nested SELECT statement will not be supported for now.

Extracting partition from IN
SELECT * FROM emp WHERE emp.id IN (100, :1)
=> ({name=100}, {name=:1}) => (P1, :1)

Range conditions could be converted to IN statements if column is of integer type. 

BETWEEN on affinity column
SELECT * FROM emp WHERE emp.id BETWEEN 100 AND 102
=> {id BETWEEN 100 AND 102} => {100, 101, 102} => (P1, P2, P3)
Range on integer affinity column
SELECT * FROM emp WHERE emp.id > 100 AND emp.id <= 102
=> {id > 100 AND id <= 102} => {101, 102} => (P1, P2)

Composite expressions (AND, OR)

Every WHERE expression can be represented as sequence conjunctive expressions separated by disjunctions. For two OR expressions we return disjunctive set. For AND expressions we return conjunctive set. Concrete partitions can be merged together. Partition placeholders can only be merged with ALL or EMPTY on the other side.

For AND condition there is a special rule. If two sides contain parameters, we cannot simplify them, because final result depend on resolved partitions. If left side contain parameters and right side contain only concrete values, then we can remove concrete values from the left side which are not present on the right side. And vice versa.

AND algebra
(P1) AND (P2) => ()
(P1, P2) AND (P3, P4) => ()
(P1, P2) AND (P2, P3) => (P2)
(P1) AND (ALL) => (P1)
(P1, P2) AND (ALL) => (P1, P2)
(P1, P2) AND () => ()

(:1) AND (:2) => (:1) AND (:2)
(:1) AND (ALL) => (:1)
(:1) AND () => ()

(P1) AND (:2) => (P1) AND (:2)
(P1, :1) AND (P2) => (:1) AND (P2)
(P1, :1) AND (P2, :2) => (P1, :1) AND (P2, :2)
OR algebra
(P1) OR (P2) => (P1, P2)
(P1) OR (ALL) => (ALL)
(P1) OR () => (P1)
(P1, P2) OR (P2, P3) => (P1, P2, P3)


(:1) OR (:2) => (:1, :2)
(P1, :1) OR (P2, :2) => (P1, P2, :1, :2)

JOINs


Query Rewrite

Subqueries

TODO

OR

TODO

Theory

This technique is used excessively by both distributed databases and classical RDBMS vendors (as a part of their partitioning/sharding solutions). First, query is analyzed and optionally re-written to allow for better partition pruning. Second, information of interesting partitions are extracted from query for every table (cache) and subquery (for complex multi-stage distributed queries). 

Query Rewrite

WHERE conditions and JOIN-s are relatively simple targets for partition extraction. The main difficulty is subqueries, as they potentially introduce unlimited nesting that complicates analysis a lot. So the main goal of query rewrite is to minimize number of subqueries. Main techniques are JOIN conversion, correlation and materialization. Interestingly, all these techniques not only simplify partition extraction, but also allow optimizer to choose better execution plan for local execution, as JOINs are much easier to be analyzed and reordered than subqueries. As we shall see below, efforts are usually focused on non-correlated subqueries as they are the most costly for straightforward execution.

JOIN conversion

The goal is to convert a subquery to join if possible. Important prerequisite is that number of resulting rows is not changed. Outer(FK) → inner(PK) joins are possible. E.g. subqueries in SELECT (aka "derived table") and FROM clauses.

Example 1: JOIN conversion for derived table

Before
SELECT emp.name, (SELECT dept.name FROM dept WHERE emp.dept_id=dept.id)
FROM emp
WHERE emp.salary>1000
After
SELECT emp.name, dept.name
FROM emp, dept
WHERE emp.salary>1000 AND emp.dept_id=dept.id

Example 2: JOIN conversion for FROM clause

Before
SELECT emp.name, dept_subquery.name
FROM emp, (SELECT * FROM dept WHERE state='CA') dept_subquery
WHERE emp.salary>1000 AND emp.dept_id=dept_subquery.id
After
SELECT emp.name, dept.name
FROM emp, dept
WHERE emp.salary>1000 AND emp.dept_id=dept.id AND dept.state='CA'

Also, it is possible for some IN-clauses. MariaDB calls it "table pullout optimization" [1]

Example 3: Table pullout optimization

Before
SELECT emp.name
FROM emp
WHERE emp.salary>1000 AND emp.dept_id IN (SELECT id FROM dept WHERE state='CA')
After
SELECT emp.name
FROM emp, dept
WHERE emp.salary>1000 AND emp.dept_id=dept.id AND dept.state='CA'

Another example where inner subquery return at most one row is outer(PK) → inner(FK GROUP BY PK) case. Subquery is not eliminated in this case, but IN expression is replaced with more comfortable JOIN.

Example 4: Replacing IN with JOIN

Before
SELECT dept.name
FROM dept
WHERE dept.id IN (SELECT emp.dept_id FROM emp GROUP BY emp.dept_id HAVING COUNT(*)>10)
After
SELECT dept.name
FROM dept JOIN (SELECT emp.dept_id FROM emp GROUP BY emp.dept_id HAVING COUNT(*)>10) emp_subquery ON dept.id=emp_subquery.dept_id

Correlation

Uncorrelated subquery may need to scan huge result set. If underlying cache is PARTITIONED, then multiple nodes need to be scanned. If underlying cache is REPLICATED, then scan is local. If previous optimizations are not applicable, we can try to push-down condition from outer table to subquery. If condition is equality and both outer and inner columns are co-located, then potentially distributed query is reduced to co-located query. The downside is that subquery must be evaluated multiple times, once for every tuple from outer relation. This might be less than optimal comparing to subquery materialization explained below.  MySQL refer to this as IN-to-EXISTS optimization [2].

Example 5: Converting non-correlated subquery to correlated via condition push-down

Before
SELECT emp.name
FROM emp
WHERE emp.dept_id IN (SELECT id FROM dept WHERE state='CA')
After
SELECT emp.name
FROM emp
WHERE EXISTS (SELECT id FROM dept WHERE dept.id=emp.dept_id AND state='CA')

Materialization

If subquery is not correlated, then it can be executed only once. Result is cached in temporary table and then re-used. If expected number of returned records is small, then it makes sense to execute the request before other query parts, and send obtained result to other map nodes. If result is scalar, then entire query is replaced with a single result.

This way some queries which were too complex to extract target partitions from may be reduced to simpler form, where extraction is possible.

[1] https://mariadb.com/kb/en/library/table-pullout-optimization/
[2] https://dev.mysql.com/doc/refman/8.0/en/subquery-optimization-with-exists.html

Partition Extraction

TODO

Proposed Changes

TODO

Tickets

key summary type created updated due assignee reporter priority status resolution

JQL and issue key arguments for this macro require at least one Jira application link to be configured

  • No labels