The OceanBase Optimizer Team, led by Xifeng (OceanBase Senior Technical Expert) and Shanwen (Technical Expert), is dedicated to building the world's leading distributed query optimizer.
In the previous article about Subquery Promotion, we mentioned that there are two types of subqueries. The first type returns a set and the second type returns a specific value. The previous article introduced the rewriting and promotion of the first type. This article introduces the way to rewrite the second type of subquery.
The second type of rewriting-subquery results only returns one value, which is associated with aggregate functions. Q1 is a typical example. The way this subquery is used is common in business SQL. Here, a PLAY table is given, which records the schedule information of all movies. TICKETS record all the actual ticketing information. Q1 finds out all the shows where the ticket price is lower than the average selling price of the movie.
-- Movie table
MOVIE(movie_id, movie_name, release_date)
-- Schedule table
PLAY(play_id, movie_id, time, price, seats)
-- Ticket schedule
TICKETS(play_id, movie_id, real_price, sale_date);
Q1:
SELECT *
FROM PLAY P
(SELECT AVG(T.real_price)
FROM TICKETS T
WHERE T.movie_id = P.movie_id);
When this SQL statement is executed, the execution engine iterates over each row in the PLAY
. After it obtains the PLAY.movie_id
, it fills in the subquery, calculates the aggregate function AVG(TICKETS.real_price)
, and determines whether the filter conditions are met. In essence, this process is similar to a NEST-LOOP Join. The number of times a subquery is calculated depends on the number of rows PLAY
.
It is not difficult to find that Q1 has two main characteristics:
This type of subquery is called an aggregate-related subquery in OceanBase. An aggregate-based subquery is promoted to rewrite this type of SQL.
Let’s take a movie schedule as an example. We know that a movie will schedule many shows, which means the schedule PLAY
movie_id
contains many repeated values. Take a closer look at Q1. It is not difficult to find that for PLAY.movie_id
identical records, subqueries are repeatedly calculated, and the calculation results of these subqueries are the same. In response to this situation, the natural idea is can the subquery only be calculated once for the same PLAY.movie_id
record? Based on this idea, OceanBase rewrites the preceding Q1 query to Q2.
Q2:
SELECT P.*
FROM PLAY P,
(SELECT movie_id,
AVG(real_price) AS avg_price
FROM TICKETS T
GROUP BY movie_id) V
WHERE P.movie_id = V.movie_id
AND P.price < V.avg_price;
In Q2, we used a grouping query to calculate the average ticket price of each movie in advance. When the main query needs to use different statistical values, it can be obtained directly from the results aggregated in advance. View V in Q2 achieves this effect. It only needs to scan the TICKETS
table once to get the average selling price of all movies. After that, Q2 only needs to join the PLAY
and the V
according to movie_id. Then, it can quickly find out which films have lower average selling prices.
View V in Q2:
SELECT movie_id,
AVG(real_price) AS avg_price
FROM TICKETS T
GROUP BY movie_id;
The change from Q1 to Q2 is to rewrite the subqueries related to an aggregate class into GROUP BY + JOIN. The rewritten SQL first performs grouping aggregation and then joins, so we call it aggregation-first aggregation subquery promotion. The rewritten query is expected to scan the PLAY
table and the TICKETS
table once each. This rewriting has the following benefits:
PLAY
and TICKETS
have indexes on the movie_id, we can use merge aggregation to optimize the calculation of view V and use merge join to handle the join between PLAY
and V.(movie_id, price)
indexes on the PLAY
, you can use the V
as the driver table, use the NEST-LOOP JOIN to convert the P.movie_id = V.movie_id AND P.price < V.avg_price
into the filter conditions on the PLAY,
and use the index scan to reduce the number of scans by PLAY
.PLAY
and the V
.As you can see, the rewritten SQL has more room for plan selection. In the original Q1 query, we can only use the PLAY
in the main query to drive the calculation of the subquery, which is essentially a NEST LOOP JOIN process. After rewriting, we can use more JOIN algorithms and use the view generated by subquery promotion to drive tables in the main query to join.
Next, let's look at a special example. Q3 SQL found out all the sessions with lower than 50% seating rate. If we rewrite this SQL with reference to Q2, we will get SQL in the form of Q4. Q4 and Q3 are not equivalent, which is a trap for the promotion of aggregate subqueries.
Q3:
SELECT *
FROM PLAY P
WHERE p.seats * 0.5 >
(SELECT count(*)
FROM TICKETS T
WHERE T.play_id = P.play_id);
Q4:
SELECT P.*
FROM PLAY P,
(SELECT play_id,
count(*) AS cnt
FROM TICKETS T
GROUP BY play_id) V
WHERE P.play_id = V.play_id
AND P.seats * 0.5 > V.cnt;
We consider an extreme situation where a movie has not sold a ticket, so the seating rate of this movie is 0%. Then, it should be exported to the results.
count(*)
. If the result of further p.seats * 0.5 > 0
is true, the record of this field will be output to the result.TICKETS,
the aggregation result of this field will not be included in the view V that is aggregated in advance, and the inner join of Play
and V
cannot be successful. The record of this session will not be output to Q4 results.As you can see, Q3 and Q4 are semantically inequivalent.
Based on the preceding analysis, we found that for count(*)
, if the result of the relevant condition of the subquery is an empty set, the use of the inner join will get the wrong result. At the same time, the result of the count(*)
is 0 when the result set of the relevant condition of the subquery is an empty set. Then, you can get a Q5 equivalent to Q3 by slightly modifying Q4.
Q5:
SELECT P.*
FROM PLAY P
LEFT JOIN
(SELECT play_id,
count(*) AS cnt
FROM TICKETS T
GROUP BY play_id) V ON P.play_id = V.play_id
WHERE P.seats * 0.5 > (CASE WHEN V.play_id IS NULL THEN 0 ELSE V.cnt end);
In Q5, we use left join and case when
to distinguish between rows that can match data in subqueries and rows that fail to be matched. We can generate the correct filter conditions for both types to replace the filter conditions that originally contained subqueries.
(1) Let’s consider a record that can match data in a subquery: it can be joined to a V
row. In the result of the outer join, the V.play_id
will not be NULL, and the final filter condition will be P.seats * 0.5 > V.cnt
.
(2) Let’s consider a record that cannot match data in a subquery: it fails to be joined to a V
row. In the result of the outer join, the V.play_id
must be NULL, and the final filter condition will be P.seats * 0.5 > 0
.
When the relevant conditions in the subquery are all equivalent conditions, we can use Q1->Q2 or Q3 ->Q4 to rewrite. Next, we consider a more complex subquery Q6: Find all the movies that earned more than $50,000 at the box office within a week of release. There is a non-equivalent condition in this query: T.sale_date < M.release_date +7
. In Q6, the range to be aggregated by a subquery is changed to T.Movie_id = ?and T.sale_date < ?+7
. This range cannot be aggregated in advance, and it fails to be transformed using the method given in the previous section.
Q6:
SELECT *
FROM MOVIE M
WHERE
(SELECT sum(real_price)
FROM TICKETS T
WHERE T.movie_id = M.movie_id
AND T.sale_date < M.release_date + 7) > 50000;
In OceanBase, Q6 is rewritten to Q7. For each row in the M
table, Q7 will find the matching set of records in the T table and then perform grouping and summation statistics (movie_id
is assumed to be the primary key of the MOVIE
) to obtain the total box office. Finally, it is determined in Having
whether the total box office exceeds 50000. Semantically, Q6 and Q7 are equivalent.
Q7:
SELECT M.*
FROM MOVIE M, TICKETS T
WHERE T.movie_id = M.movie_id
AND T.sale_date < M.release_date + 7
GROUP BY M.movie_id
HAVING sum(T.real_price) > 50000;
The change from Q6 to Q7 is to rewrite a subquery related to an aggregate class into JOIN + GROUP BY. The rewritten SQL goes to join first and then group aggregation, so we call it join-first aggregation subquery promotion. Intuitively, the query after the rewrite does not improve the execution performance compared with the query before. Q7 still needs to find the records for each row of the MOVIE
table that meet the conditional T.movie_id = M.movie_id AND T.sale_date < M.release_date +7
in TICKETS for aggregation. This rewrite method has the following benefits:
It is similar to the aggregation-first subquery promotion. When the aggregation function in the subquery is count(*)
, the join-first aggregation subquery promotion also has a similar trap. The two have the same problem to consider, but the way to solve the problem is different. We will leave this problem as homework for readers. Please think about how Q8 should improve the join-first aggregation subquery.
Q8:
SELECT *
FROM MOVIE M
WHERE
(SELECT count(*)
FROM TICKETS T
WHERE T.movie_id = M.movie_id
AND T.sale_date > M.release_date + 30) > 0;
This article mainly introduces aggregation subquery promotion, a rewrite strategy for aggregation-related subqueries, and the pitfalls of this rewrite strategy. According to the different join conditions in the subquery, this rewrite strategy can be subdivided into aggregation-first subquery promotion and join-first subquery promotion. Aggregate subquery promotion can rewrite aggregate subqueries into join forms. It allows the optimizer to select richer join algorithms and join orders. On the whole, this is a rule-based rewriting strategy. We have introduced how OceanBase rewrites two types of subqueries. The next article will focus on GroupBy and introduce some rewriting and optimization practices of OceanBase in grouping.
OceanBase Rewriting Series: Group By Equivalent Transformation Method - Grouping Down
OceanBase - August 25, 2022
OceanBase - August 25, 2022
OceanBase - August 25, 2022
OceanBase - September 9, 2022
ApsaraDB - September 30, 2021
ApsaraDB - October 19, 2023
A financial-grade distributed relational database that features high stability, high scalability, and high performance.
Learn MoreHigh Performance Computing (HPC) and AI technology helps scientific research institutions to perform viral gene sequencing, conduct new drug research and development, and shorten the research and development cycle.
Learn MoreA HPCaaS cloud platform providing an all-in-one high-performance public computing service
Learn MoreConnect your on-premises render farm to the cloud with Alibaba Cloud Elastic High Performance Computing (E-HPC) power and continue business success in a post-pandemic world
Learn MoreMore Posts by OceanBase