ルールに基づくクエリのリライト方法には、主にサブクエリ関連のリライト、外部結合の除去、条件の簡略化によるリライト、および非SPJ(Select Project Join)のリライトなどが含まれます。
サブクエリ関連のリライト
オプティマイザーは通常、サブクエリに対してネスト実行方式を採用します。つまり、親クエリが1行のデータを生成するたびに、サブクエリを1回ずつ実行する必要があります。この方式ではサブクエリを複数回実行するため、実行効率は非常に低くなります。サブクエリの最適化手法としては、一般的に結合操作にリライトすることが行われ、これにより実行効率を大幅に向上させることができます。主な利点は以下の通りです:
サブクエリの繰り返し実行を回避できます。
オプティマイザーは統計情報に基づいて、より優れた結合順序や結合方法を選択できます。
サブクエリの結合条件やフィルタ条件を親クエリの条件にリライトすることで、オプティマイザーは条件プッシュダウンなどのさらなる最適化を行うことができます。
サブクエリのリライト手法には、主にビューのマージ、サブクエリの展開、ANY/ALL を MAX/MIN にリライトする方法などがあります。
ビューのマージ
ビューのマージとは、ビューを表すサブクエリを、そのビューを含むクエリにマージすることです。ビューをマージすると、オプティマイザーは結合順序の選択肢、アクセスパスの選択肢を増やし、さらなる他の書き換え操作を行うことができるため、より優れた実行計画を選択できます。
OceanBaseデータベースはSPJビューのマージをサポートしています。以下の例では、クエリQ1をQ2に書き換えています:
obclient>CREATE TABLE t1 (c1 INT, c2 INT);
Query OK, 0 rows affected
obclient>CREATE TABLE t2 (c1 INT PRIMARY KEY, c2 INT);
Query OK, 0 rows affected
obclient>CREATE TABLE t3 (c1 INT PRIMARY KEY, c2 INT);
Query OK, 0 rows affected
Q1:
obclient>SELECT t1.c1, v.c1
FROM t1, (SELECT t2.c1, t3.c2
FROM t2, t3
WHERE t2.c1 = t3.c1) v
WHERE t1.c2 = v.c2;
<==>
Q2:
obclient>SELECT t1.c1, t2.c1
FROM t1, t2, t3
WHERE t2.c1 = t3.c1 AND t1.c2 = t3.c2;
Q1を書き換えない場合、その結合順序は以下のようになります:
t1,v(t2,t3)t1,v(t3,t2)v(t2,t3),t1v(t3,t2),t1
ビューのマージによる書き換え後、選択可能な結合順序は以下のとおりです:
t1,t2,t3t1,t3,t2t2,t1,t3t2,t3,t1t3,t1,t2t3,t2,t1
これにより、ビューのマージは結合順序の選択性を向上させることがわかります。複雑なクエリについては、ビューをマージすることで、パスの選択肢と書き換え可能な領域が広がり、オプティマイザーがより優れた計画を生成できるようになります。
サブクエリの展開
サブクエリの展開とは、WHERE 条件内のサブクエリを親クエリに昇格させ、結合条件として親クエリと並列して展開することです。変換後、サブクエリは存在せず、外部の親クエリは複数テーブルの結合になります。
このように書き換える利点は、オプティマイザーがパス選択、結合方法、結合ソートを行う際にサブクエリ内のテーブルを考慮できるため、より優れた実行計画を得られることです。対象となるサブクエリ式には一般的に NOT IN、IN、NOT EXIST、EXIST、ANY、ALL が含まれます。
サブクエリの展開方法は以下の通りです:
条件を書き換えて、生成される結合ステートメントが元のステートメントと同じ行を返せるようにします。
半結合(Semi Join/Anti Join)に展開する
以下の例のように、
t2.c2には一意性がないため、Semi Joinに書き換えます。このステートメントの書き換え後の実行計画は以下の通りです:obclient> CREATE TABLE t1 (c1 INT, c2 INT); Query OK, 0 rows affected obclient> CREATE TABLE t2 (c1 INT PRIMARY KEY, c2 INT); Query OK, 0 rows affected obclient> EXPLAIN SELECT * FROM t1 WHERE t1.c1 IN (SELECT t2.c2 FROM t2); Query Plan: ======================================= |ID|OPERATOR |NAME|EST. ROWS|COST| --------------------------------------- |0 |HASH SEMI JOIN| |495 |3931| |1 | TABLE SCAN |t1 |1000 |499 | |2 | TABLE SCAN |t2 |1000 |433 | ======================================= Outputs & filters: ------------------------------------- 0 - output([t1.c1], [t1.c2]), filter(nil), equal_conds([t1.c1 = t2.c2]), other_conds(nil) 1 - output([t1.c1], [t1.c2]), filter(nil), access([t1.c1], [t1.c2]), partitions(p0) 2 - output([t2.c2]), filter(nil), access([t2.c2]), partitions(p0)クエリの前の演算子を
NOT INに変更すると、Anti Joinに書き換えることができます。具体的な計画は以下の例のようになります:obclient> EXPLAIN SELECT * FROM t1 WHERE t1.c1 NOT IN (SELECT t2.c2 FROM t2); Query Plan: ================================================ |ID|OPERATOR |NAME|EST. ROWS|COST | ------------------------------------------------ |0 |NESTED-LOOP ANTI JOIN| |0 |520245| |1 | TABLE SCAN |t1 |1000 |499 | |2 | TABLE SCAN |t2 |22 |517 | ================================================ Outputs & filters: ------------------------------------- 0 - output([t1.c1], [t1.c2]), filter(nil), conds(nil), nl_params_([t1.c1], [(T_OP_IS, t1.c1, NULL, 0)]) 1 - output([t1.c1], [t1.c2], [(T_OP_IS, t1.c1, NULL, 0)]), filter(nil), access([t1.c1], [t1.c2]), partitions(p0) 2 - output([t2.c2]), filter([(T_OP_OR, ? = t2.c2, ?, (T_OP_IS, t2.c2, NULL, 0))]), access([t2.c2]), partitions(p0)サブクエリを内部結合に展開する
上記の例のクエリ Q1 で、
t2.c2をt2.c1に変更すると、t2.c1は主キーであるため、サブクエリの出力に一意性があります。この場合、直接内部結合に変換できます。以下のようになります:Q1: obclient> SELECT * FROM t1 WHERE t1.c1 IN (SELECT t2.c1 FROM t2); <==> Q2: obclient> SELECT t1.* FROM t1, t2 WHERE t1.c1 = t2.c1;Q1 の書き換え後の計画は以下の通りです:
obclient> EXPLAIN SELECT * FROM t1 WHERE t1.c1 IN (SELECT t2.c1 FROM t2); Query Plan: ==================================== |ID|OPERATOR |NAME|EST. ROWS|COST| ------------------------------------ |0 |HASH JOIN | |1980 |3725| |1 | TABLE SCAN|t2 |1000 |411 | |2 | TABLE SCAN|t1 |1000 |499 | ==================================== Outputs & filters: ------------------------------------- 0 - output([t1.c1], [t1.c2]), filter(nil), equal_conds([t1.c1 = t2.c1]), other_conds(nil) 1 - output([t2.c1]), filter(nil), access([t2.c1]), partitions(p0) 2 - output([t1.c1], [t1.c2]), filter(nil), access([t1.c1], [t1.c2]), partitions(p0)NOT IN、IN、NOT EXIST、EXIST、ANY、ALLの各サブクエリ式についても、同様の書き換え操作を適用できます。
ANY/ALLをMAX/MINで書き換える
ANY/ALL のサブクエリにおいて、サブクエリに GROUP BY 句、集約関数、および HAVING 条件がない場合、以下の式は集約関数 MIN/MAX を用いて等価に変換できます。ここで、col_item は非 NULL 属性を持つ個別の列です。
val > ALL(SELECT col_item ...) <==> val > (SELECT MAX(col_item) ...);
val >= ALL(SELECT col_item ...) <==> val >= (SELECT MAX(col_item) ...);
val < ALL(SELECT col_item ...) <==> val < (SELECT MIN(col_item) ...);
val <= ALL(SELECT col_item ...) <==> val <= (SELECT MIN(col_item) ...);
val > ANY(SELECT col_item ...) <==> val > (SELECT MIN(col_item) ...);
val >= ANY(SELECT col_item ...) <==> val >= (SELECT MIN(col_item) ...);
val < ANY(SELECT col_item ...) <==> val < (SELECT MAX(col_item) ...);
val <= ANY(SELECT col_item ...) <==> val <= (SELECT MAX(col_item) ...);
サブクエリを MAX/MIN を含むサブクエリに変更し、さらに MAX/MIN を組み合わせて書き換えることで、書き換え前の内部テーブルへの複数回のスキャンを削減できます。以下の例を参照してください:
obclient> SELECT c1 FROM t1 WHERE c1 > ANY(SELECT c1 FROM t2);
<==>
obclient> SELECT c1 FROM t1 WHERE c1 > (SELECT MIN(c1) FROM t2);
MAX/MIN を組み合わせて書き換えた後、t2.c1 の主キーソートを活用して LIMIT 1 を直接 TABLE SCAN に押し下げ、MIN 値を出力することができます。実行計画は以下のとおりです:
obclient> EXPLAIN SELECT c1 FROM t1 WHERE c1 > ANY(SELECT c1 FROM t2);
Query Plan:
===================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
---------------------------------------------------
|0 |SUBPLAN FILTER | |1 |73 |
|1 | TABLE SCAN |t1 |1 |37 |
|2 | SCALAR GROUP BY| |1 |37 |
|3 | SUBPLAN SCAN |subquery_table|1 |37 |
|4 | TABLE SCAN |t2 |1 |36 |
===================================================
Outputs & filters:
-------------------------------------
0 - output([t1.c1]), filter([t1.c1 > ANY(subquery(1))]),
exec_params_(nil), onetime_exprs_(nil), init_plan_idxs_([1])
1 - output([t1.c1]), filter(nil),
access([t1.c1]), partitions(p0)
2 - output([T_FUN_MIN(subquery_table.c1)]), filter(nil),
group(nil), agg_func([T_FUN_MIN(subquery_table.c1)])
3 - output([subquery_table.c1]), filter(nil),
access([subquery_table.c1])
4 - output([t2.c1]), filter(nil),
access([t2.c1]), partitions(p0),
limit(1), offset(nil)
外部結合の除去
外部結合操作には、左外部結合、右外部結合、完全外部結合があります。結合処理において、外部結合では左右の順序を入れ替えることができないため、オプティマイザーが選択可能な結合パスが制限されます。外部結合の除去とは、外部結合を内部結合に変換することで、オプティマイザーが利用できる結合パスの選択肢を増やす手法です。
外部結合の除去を行うには、「NULL値拒否条件」が必要です。これは WHERE 条件に含まれる条件で、内部テーブルが生成した値が NULL の場合に出力を FALSE にするものです。
以下の例を見てください:
obclient>SELECT t1.c1, t2.c2 FROM t1 LEFT JOIN t2 ON t1.c2 = t2.c2;
これは外部結合であり、その出力行において t2.c2 は NULL になる可能性があります。条件 t2.c2 > 5 を追加すると、この条件でフィルタリングした後、t2.c1 の出力が NULL になることはあり得ないため、外部結合を内部結合に変換できます。
obclient> SELECT t1.c1, t2.c2 FROM t1 LEFT JOIN t2 ON t1.c2 = t2.c2 WHERE t2.c2 > 5;
<==>
obclient> SELECT t1.c1, t2.c2 FROM t1 INNER JOIN t2 ON t1.c2 = t2.c2
WHERE t2.c2 > 5;
条件の簡略化と書き換え
HAVING条件の除去
クエリに集約操作や GROUP BY 句がない場合、HAVING 句は WHERE 句に統合して HAVING 句を削除できます。これにより、HAVING 条件を WHERE 条件内で一元的に管理し、さらなる最適化を図ることが可能です。
obclient>SELECT * FROM t1, t2 WHERE t1.c1 = t2.c1 HAVING t1.c2 > 1;
<==>
obclient>SELECT * FROM t1, t2 WHERE t1.c1 = t2.c1 AND t1.c2 > 1;
書き換え後の実行計画は以下の例のようになります。t1.c2 > 1 条件が TABLE SCAN ステートメント層に下げられています。
obclient> EXPLAIN SELECT * FROM t1, t2 WHERE t1.c1 = t2.c1 HAVING t1.c2 > 1;
Query Plan:
=========================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
-----------------------------------------
|0 |NESTED-LOOP JOIN| |1 |59 |
|1 | TABLE SCAN |t1 |1 |37 |
|2 | TABLE GET |t2 |1 |36 |
=========================================
Outputs & filters:
-------------------------------------
0 - output([t1.c1], [t1.c2], [t2.c1], [t2.c2]), filter(nil),
conds(nil), nl_params_([t1.c1])
1 - output([t1.c1], [t1.c2]), filter([t1.c2 > 1]),
access([t1.c1], [t1.c2]), partitions(p0)
2 - output([t2.c1], [t2.c2]), filter(nil),
access([t2.c1], [t2.c2]), partitions(p0)
等価関係の導出
等価関係の導出とは、比較演算子の伝搬性を利用して新しい条件式を導き出し、処理する行数を削減したり、より効果的なインデックスを選択したりすることです。
OceanBaseデータベースでは、等価結合に対する導出が可能です。例えば、あるテーブルに a 列と b 列があり、a = b AND a > 1 から a = b AND a > 1 AND b > 1 を導き出すことができます。もし b 列にインデックスが存在し、かつ b > 1 の選択率がそのインデックスで非常に低い場合、b 列が存在するテーブルへのアクセス性能を大幅に向上させることができます。
以下の例のように、条件 t1.c1 = t2.c2 AND t1.c1 > 2 は等価導出後に t1.c1 = t2.c2 AND t1.c1 > 2 AND t2.c2 > 2 となります。実行計画を見ると、t2.c2 が TABLE SCAN にプッシュダウンされ、t2.c2 に対応するインデックスが使用されていることがわかります。
obclient> CREATE TABLE t1(c1 INT PRIMARY KEY, c2 INT);
Query OK, 0 rows affected
obclient> CREATE TABLE t2(c1 INT PRIMARY KEY, c2 INT, c3 INT, KEY IDX_c2(c2));
Query OK, 0 rows affected
/*このコマンドはMySQLモードで実行する必要があります*/
obclient> EXPLAIN EXTENDED_NOADDR SELECT t1.c1, t2.c2 FROM t1, t2 WHERE t1.c1 = t2.c2 AND t1.c1 > 2;
Query Plan:
==========================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
------------------------------------------
|0 |MERGE JOIN | |5 |78 |
|1 | TABLE SCAN|t2(IDX_c2)|5 |37 |
|2 | TABLE SCAN|t1 |3 |37 |
==========================================
Outputs & filters:
-------------------------------------
0 - output([t1.c1], [t2.c2]), filter(nil),
equal_conds([t1.c1 = t2.c2]), other_conds(nil)
1 - output([t2.c2]), filter(nil),
access([t2.c2]), partitions(p0),
is_index_back=false,
range_key([t2.c2], [t2.c1]), range(2,MAX ; MAX,MAX),
range_cond([t2.c2 > 2])
2 - output([t1.c1]), filter(nil),
access([t1.c1]), partitions(p0),
is_index_back=false,
range_key([t1.c1]), range(2 ; MAX),
range_cond([t1.c1 > 2])
永恒真理/偽の除去
以下のような永遠に真または偽となる条件は、除去できます:
false and expr= 常にFalsetrue or expr= 常にTrue
以下の例のように、WHERE 0 > 1 AND c1 = 3 の場合、0 > 1 により AND 部分が常に偽となるため、このSQLは実行せずに直接戻すことができ、クエリの実行速度を向上させることができます。
obclient> EXPLAIN EXTENDED_NOADDR SELECT * FROM t1 WHERE 0 > 1 AND c1 = 3;
Query Plan:
===================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
-----------------------------------
|0 |TABLE SCAN|t1 |0 |38 |
===================================
Outputs & filters:
-------------------------------------
0 - output([t1.c1], [t1.c2]), filter([0], [t1.c1 = 3]), startup_filter([0]),
access([t1.c1], [t1.c2]), partitions(p0),
is_index_back=false, filter_before_indexback[false,false],
range_key([t1.__pk_increment], [t1.__pk_cluster_id], [t1.__pk_partition_id]),
range(MAX,MAX,MAX ; MIN,MIN,MIN)always false
SPJ以外のリライト
複数ソートの除去
複数ソートの除去とは、Order Itemから不要な項目を削除し、ソート処理のオーバヘッドを削減することです。以下の3つの場合にソートの除去が可能です:
ORDER BY式のリストに重複する列がある場合、重複を除去してからソートできます。obclient> SELECT * FROM t1 WHERE c2 = 5 ORDER BY c1, c1, c2, c3 ; <==> obclient> SELECT * FROM t1 WHERE c2 = 5 ORDER BY c1, c2, c3;ORDER BY列に、WHERE句で単一値条件を持つ列が含まれている場合、その列によるソートは省略できます。obclient> SELECT * FROM t1 WHERE c2 = 5 ORDER BY c1, c2, c3; <==> obclient> SELECT * FROM t1 WHERE c2 = 5 ORDER BY c1, c3;本層クエリに
ORDER BYがあるもののLIMITがなく、かつその本層クエリが親クエリの集合演算の中に位置する場合、ORDER BYは除去できます。これは、2つの順序付けられた集合にUNION演算を行うと、結果が無秩序になるためです。しかし、ORDER BYにLIMITが含まれる場合、意味としては最大/最小のN個を取ることになります。この場合はORDER BYを除去できず、そうしないと意味的なエラーが発生します。obclient> (SELECT c1,c2 FROM t1 ORDER BY c1) UNION (SELECT c3,c4 FROM t2 ORDER BY c3); <==> obclient> (SELECT c1,c2 FROM t1) UNION (SELECT c3,c4 FROM t2);
LIMITのプッシュダウンとサブクエリ
LIMIT のプッシュダウンとは、LIMIT をサブクエリ内に移動することを指します。OceanBaseデータベースは現在、意味を変えることなく、LIMIT をビュー(例1)または UNION に対応するサブクエリ(例2)にプッシュダウンできるようになっています。
例1:LIMIT をビューにプッシュダウンします。
obclient> SELECT * FROM (SELECT * FROM t1 ORDER BY c1) a LIMIT 1;
<==>
obclient> SELECT * FROM (SELECT * FROM t1 ORDER BY c1 LIMIT 1) a LIMIT 1;
例2:LIMIT を UNION に対応するサブクエリにプッシュダウンします。
obclient> (SELECT c1,c2 FROM t1) UNION ALL (SELECT c3,c4 FROM t2) LIMIT 5;
<==>
obclient> (SELECT c1,c2 FROM t1 LIMIT 5) UNION ALL (SELECT c3,c4 FROM t2 limit 5) LIMIT 5;
外部結合または交差結合におけるLIMITの押し下げ
外部結合および交差結合について、SQLステートメントにWINDOW FUNCTION、DISTINCT、GROUP BY、またはHAVINGがなく、かつWHERE条件またはORDER BYが結合された一方のテーブルのみに関連している場合、またはWHERE条件とORDER BYがない場合、LIMITステートメントを結合されたテーブルの一方(外部結合)または複数方(複数テーブルの交差結合)に押し下げることができます。このような書き換え方法を「外部結合または交差結合におけるLIMITの押し下げ」と呼びます。LIMITを押し下げることで、結合する行数を効果的に削減し、クエリ実行のオーバーヘッドを低減できます。
外部結合において、LIMITを押し下げる際は、押し下げられたテーブルにビューのレイヤーをカプセル化します。以下は左外部結合のQ1クエリを例としたものです:
Q1:
SELECT * FROM t1 LEFT JOIN t2 ON t1.c1 = t2.c1 LIMIT 1;
==>
Q2:
SELECT * FROM V LEFT JOIN t2 ON t1.c1 = t2.c1 LIMIT 1;
V: (SELECT * FROM t1 LIMIT 1);
Q1クエリには上記の制限事項がなく、WHERE条件やORDER BYステートメントもないため、左側のテーブルt1にビューVをカプセル化し、LIMIT 1をビューV内にカプセル化することで、Q2クエリに書き換えることができます。注意点として、LIMITを押し下げた後は、元のLIMITも保持する必要があります。書き換え後の実行計画は以下のとおりです:
=============================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
---------------------------------------------
|0 |LIMIT | |1 |4 |
|1 | MERGE OUTER JOIN| |1 |4 |
|2 | SUBPLAN SCAN |VIEW1 |1 |2 |
|3 | TABLE SCAN |t1(idx)|1 |2 |
|4 | SORT | |1 |2 |
|5 | TABLE SCAN |t2 |1 |2 |
=============================================
Outputs & filters:
-------------------------------------
0 - output([1]), filter(nil), rowset=256, limit(1), offset(nil)
1 - output(nil), filter(nil), rowset=256,
equal_conds([VIEW1.t1.c1 = t2.c1]), other_conds(nil)
2 - output([VIEW1.t1.c1]), filter(nil), rowset=256,
access([VIEW1.t1.c1])
3 - output([t1.c1]), filter(nil), rowset=256,
access([t1.c1]), partitions(p0),
limit(1), offset(nil)
4 - output([t2.c1]), filter(nil), rowset=256, sort_keys([t2.c1, ASC])
5 - output([t2.c1]), filter(nil), rowset=256,
access([t2.c1]), partitions(p0)
同様に、右外部結合でも上記の条件を満たす場合、LIMITを右側のテーブルに押し下げることができます。
交差結合については、LIMITを交差結合の両側に押し下げることができます。例:
Q3:
SELECT 1 FROM t1, t2 WHERE t1.c1 > 0 ORDER BY t1.c1 LIMIT 1;
==>
Q4:
SELECT 1 FROM V1, V2 LIMIT 1;
V1: SELECT 1 FROM t1 WHERE t1.c1 > 0 ORDER BY t1.c1 LIMIT 1;
V2: SELECT 1 FROM t2 LIMIT 1;
Q3クエリには上記の外部結合におけるLIMITの押し下げで述べた制限事項がなく、t1テーブル上のWHERE条件とORDER BYのみが含まれている場合、それぞれt1とt2テーブルにビューV1とV2を作成し、LIMITをビュー内に押し下げることで、クエリQ4に書き換えることができます。書き換え後の実行計画は以下のとおりです:
=====================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
-----------------------------------------------------
|0 |LIMIT | |1 |5 |
|1 | NESTED-LOOP JOIN CARTESIAN| |1 |5 |
|2 | SUBPLAN SCAN |VIEW1|1 |3 |
|3 | TOP-N SORT | |1 |3 |
|4 | TABLE SCAN |t1 |3 |3 |
|5 | MATERIAL | |1 |2 |
|6 | SUBPLAN SCAN |VIEW2|1 |2 |
|7 | TABLE SCAN |t2 |1 |2 |
=====================================================
Outputs & filters:
-------------------------------------
0 - output([1]), filter(nil), rowset=256, limit(1), offset(nil)
1 - output(nil), filter(nil), rowset=256,
conds(nil), nl_params_(nil)
2 - output(nil), filter(nil), rowset=256,
access(nil)
3 - output([t1.c1]), filter(nil), rowset=256, sort_keys([t1.c1, ASC]), topn(1)
4 - output([t1.c1]), filter([t1.c1 > 0]), rowset=256,
access([t1.c1]), partitions(p0)
5 - output(nil), filter(nil), rowset=256
6 - output(nil), filter(nil), rowset=256,
access(nil)
7 - output([1]), filter([t2.c1 > 0]), rowset=256,
access([t2.c1]), partitions(p0),
limit(1), offset(nil)
上記の条件を満たす複数テーブル結合のSQLクエリについては、外部結合のLIMITの押し下げと交差結合のLIMITの押し下げを個別に、あるいは複数回適用することで、書き換えの余地を広げ、より優れた書き換え効果を得ることができます。
DISTINCTの削除
Select Itemに定数のみが含まれる場合は、
DISTINCTを削除し、LIMIT 1を追加できます。obclient> SELECT DISTINCT 1,2 FROM t1 ; <==> obclient> SELECT 1,2 FROM t1 LIMIT 1; obclient> CREATE TABLE t1 (c1 INT PRIMARY KEY, c2 INT); Query OK, 0 rows affected obclient> EXPLAIN EXTENDED_NOADDR SELECT DISTINCT 1,2 FROM t1; Query Plan: =================================== |ID|OPERATOR |NAME|EST. ROWS|COST| ----------------------------------- |0 |TABLE SCAN|t1 |1 |36 | =================================== Outputs & filters: ------------------------------------- 0 - output([1], [2]), filter(nil), access([t1.c1]), partitions(p0), limit(1), offset(nil), is_index_back=false, range_key([t1.c1]), range(MIN ; MAX)always trueSelect Itemに一意性を保証する制約のある列が含まれている場合、
DISTINCTを削除できます。以下の例では、(c1, c2) が主キーとなり、c1、c2、c3 の一意性を保証するため、DISTINCTを削除できます。obclient> CREATE TABLE t2(c1 INT, c2 INT, c3 INT, PRIMARY KEY(c1, c2)); Query OK, 0 rows affected obclient> SELECT DISTINCT c1, c2, c3 FROM t2; <==> obclient> SELECT c1, c2 c3 FROM t2; obclient> EXPLAIN SELECT DISTINCT c1, c2, c3 FROM t2; Query Plan: =================================== |ID|OPERATOR |NAME|EST. ROWS|COST| ----------------------------------- |0 |TABLE SCAN|t2 |1000 |455 | =================================== Outputs & filters: ------------------------------------- 0 - output([t2.c1], [t2.c2], [t2.c3]), filter(nil), access([t2.c1], [t2.c2], [t2.c3]), partitions(p0)
MIN/MAXの書き換え
MIN/MAX関数のパラメータがインデックス付きプレフィックス列であり、かつGROUP BY句がない場合、このscalar aggregateをインデックススキャンで1行だけを処理する形に変換できます。例:obclient> CREATE TABLE t1 (c1 INT PRIMARY KEY, c2 INT, c3 INT, KEY IDX_c2_c3(c2,c3)); Query OK, 0 rows affected obclient> SELECT MIN(c2) FROM t1; <==> obclient> SELECT MIN(c2) FROM (SELECT c2 FROM t1 ORDER BY c2 LIMIT 1) AS t; obclient> EXPLAIN SELECT MIN(c2) FROM t1; Query Plan: ================================================== |ID|OPERATOR |NAME |EST. ROWS|COST| -------------------------------------------------- |0 |SCALAR GROUP BY| |1 |37 | |1 | SUBPLAN SCAN |subquery_table|1 |37 | |2 | TABLE SCAN |t1(idx_c2_c3) |1 |36 | ================================================== Outputs & filters: ------------------------------------- 0 - output([T_FUN_MIN(subquery_table.c2)]), filter(nil), group(nil), agg_func([T_FUN_MIN(subquery_table.c2)]) 1 - output([subquery_table.c2]), filter(nil), access([subquery_table.c2]) 2 - output([t1.c2]), filter([(T_OP_IS_NOT, t1.c2, NULL, 0)]), access([t1.c2]), partitions(p0), limit(1), offset(nil)SELECT MIN/SELECT MAXのパラメータが定数であり、かつGROUP BY句が含まれる場合、MIN/MAXを定数に置き換えることで、MIN/MAXの計算負荷を削減できます。obclient> SELECT MAX(1) FROM t1 GROUP BY c1; <==> obclient> SELECT 1 FROM t1 GROUP BY c1; obclient> EXPLAIN EXTENDED_NOADDR SELECT MAX(1) FROM t1 GROUP BY c1; Query Plan: =================================== |ID|OPERATOR |NAME|EST. ROWS|COST| ----------------------------------- |0 |TABLE SCAN|t1 |1000 |411 | =================================== Outputs & filters: ------------------------------------- 0 - output([1]), filter(nil), access([t1.c1]), partitions(p0), is_index_back=false, range_key([t1.c1]), range(MIN ; MAX)always trueSELECT MIN/SELECT MAXのパラメータが定数であり、かつGROUP BY句が含まれない場合、以下の例のように書き換えることで、インデックススキャンで1行だけを処理できます。obclient> SELECT MAX(1) FROM t1; <==> obclient> SELECT MAX(t.a) FROM (SELECT 1 AS a FROM t1 LIMIT 1) t; obclient> EXPLAIN EXTENDED_NOADDR SELECT MAX(1) FROM t1; Query Plan: ================================================== |ID|OPERATOR |NAME |EST. ROWS|COST| -------------------------------------------------- |0 |SCALAR GROUP BY| |1 |37 | |1 | SUBPLAN SCAN |subquery_table|1 |37 | |2 | TABLE SCAN |t1 |1 |36 | ================================================== Outputs & filters: ------------------------------------- 0 - output([T_FUN_MAX(subquery_table.subquery_col_alias)]), filter(nil), group(nil), agg_func([T_FUN_MAX(subquery_table.subquery_col_alias)]) 1 - output([subquery_table.subquery_col_alias]), filter(nil), access([subquery_table.subquery_col_alias]) 2 - output([1]), filter(nil), access([t1.c1]), partitions(p0), limit(1), offset(nil), is_index_back=false, range_key([t1.c1]), range(MIN ; MAX)always true