ルールに基づくクエリの書き換え方法には、主にサブクエリ関連の書き換え、外部結合の排除、条件の簡略化による書き換え、および非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は一意性を持たないため、セミジョインに書き換えます。このステートメントの書き換え後の実行計画は以下のとおりです: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に変更すると、アンチジョインに書き換えることができます。具体的な計画は以下の例のとおりです: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