記述が不十分な複雑なSQLクエリが、データベース内の1つまたは複数のテーブルに対して実行されることがよくあります。これらのクエリは実行時間を非常に長くし、CPUやその他のリソースを大量に消費します。それでも、複雑なクエリは、多くの場合、それらを実行しているアプリケーション/人に貴重な情報を提供します。したがって、これらはあらゆる種類のアプリケーションで役立つ資産です。
複雑なクエリはデバッグが困難です
問題のあるクエリを詳しく見ると、それらの多く、特にレポートで使用される特定のクエリは複雑です。
複雑なクエリは、多くの場合5つ以上の大きなテーブルで構成され、多くのサブクエリによって結合されます。各サブクエリには、関連するテーブルの列を結合しながら、単純な計算から複雑な計算やデータ変換を実行するWHERE句があります。
このようなクエリは、多くのリソースを消費せずにデバッグするのが難しくなる可能性があります。その理由は、各サブクエリや結合されたサブクエリが正しい結果を生成するかどうかを判断するのが難しいためです。
典型的なシナリオは次のとおりです。複雑なクエリが関係するビジーなデータベースサーバーの問題を解決するために深夜に電話をかけてきて、すぐに修正する必要があります。開発者またはDBAとして、遅い時間に利用できる時間とシステムリソースが非常に限られている場合があります。したがって、最初に必要なのは、問題のあるクエリをデバッグする方法の計画です。
場合によっては、デバッグ手順がうまくいくことがあります。場合によっては、目標を達成して問題を解決するまでに多くの時間と労力がかかることがあります。
CTE構造でのクエリの記述
しかし、複雑なクエリを記述して、それらを1つずつすばやくデバッグできるようにする方法があったとしたらどうでしょうか。
そのような方法があります。これは、共通テーブル式またはCTEと呼ばれます。
Common Table Expressionは、SQLServer、MySQL(バージョン8.0以降)、MariaDB(バージョン10.2.1)、Db2、Oracleなどの最新のデータベースの標準機能です。これは、1つまたは複数のサブクエリを一時的な名前付き結果セットにカプセル化する単純な構造を持っています。この結果セットは、他の名前付きCTEまたはサブクエリでさらに使用できます。
共通テーブル式は、ある程度、実行時にクエリによってのみ存在し、参照されるVIEWです。
複雑なクエリをCTEスタイルのクエリに変換するには、構造化された考え方が必要です。複雑なクエリをCTE構造に書き換える場合、カプセル化を使用したOOPについても同じことが言えます。
あなたは考える必要があります:
- 各テーブルから取得するデータの各セット。
- これらを結合して、最も近いサブクエリを1つの一時的な名前付き結果セットにカプセル化する方法。
クエリの最終結果に到達するまで、残りのサブクエリとデータのセットごとにこれを繰り返します。各一時的な名前付き結果セットもサブクエリであることに注意してください。
クエリの最後の部分は、非常に「単純な」選択であり、最終結果をアプリケーションに返します。この最後の部分に到達したら、個別に名前が付けられた一時的な結果セットからデータを選択するクエリと交換できます。
このようにして、各一時的な結果セットのデバッグは簡単な作業になります。
単純なものから複雑なものまでクエリを構築する方法を理解するために、CTE構造を見てみましょう。最も単純な形式は次のとおりです。
WITH CTE_1 as (
select .... from some_table where ...
)
select ... from CTE_1
where ...
ここでCTE_1 は、一時的な名前付き結果セットに付ける一意の名前です。必要な数の結果セットが存在する可能性があります。これにより、フォームは次のように拡張されます。
WITH CTE_1 as (
select .... from some_table where ...
), CTE_2 as (
select .... from some_other_table where ...
)
select ... from CTE_1 c1,CTE_2 c2
where c1.col1 = c2.col1
....
最初に、各CTEパーツは個別に作成されます。次に、CTEがリンクされてクエリの最終結果セットが構築されるため、進行します。
次に、架空の販売データベースにクエリを実行して、別のケースを調べてみましょう。前月に各カテゴリで販売された商品の数量と総売上高、および前月よりも総売上高が多かった製品を知りたい。
クエリをいくつかのCTEパーツに構成し、各パーツは前のパーツを参照します。まず、クエリの残りの部分を形成するためにテーブルから必要な詳細データを一覧表示する結果セットを作成します。
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
)
select dt.*
from detailed_data dt.
order by dt.order_date desc, dt.category_name, dt.product_name
次のステップは、各カテゴリと製品名ごとに数量と総売上高のデータを要約することです。
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
)
select ps.*
from product_sales ps
order by ps.year desc, ps.month desc, ps.category_name,ps.product_name
最後のステップは、先月と前月のデータを表す2つの一時的な結果セットを作成することです。その後、最終結果セットとして返されるデータを除外します。
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
), last_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -1
and ps.month = month(CURRENT_DATE) -1
), prev_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -2
and ps.month = month(CURRENT_DATE) -2
)
select lmd.*
from last_month_data lmd, prev_month_data pmd
where lmd.category_name = pmd.category_name
and lmd.product_name = pmd.product_name
and ( lmd.total_quantity > pmd.total_quantity
or lmd.total_product_sales > pmd.total_product_sales )
order by lmd.year desc, lmd.month desc, lmd.category_name,lmd.product_name, lmd.total_product_sales desc, lmd.total_quantity desc
SQLServerでは、CURRENT_DATEの代わりにgetdate()を設定していることに注意してください。
このようにして、最後のパーツを、選択したパーツの結果を確認するために個々のCTEパーツを照会するselectと交換できます。その結果、問題をすばやくデバッグできます。
また、各CTE部分(およびクエリ全体)でExplainを実行することにより、各部分および/またはクエリ全体がテーブルとデータでどの程度うまく機能するかを推定します。
同様に、関連するテーブルに適切なインデックスを書き換えたり追加したりすることで、各部分を最適化できます。次に、クエリ全体を説明して最終的なクエリプランを確認し、必要に応じて最適化を進めます。
CTE構造を使用した再帰クエリ
CTEのもう1つの便利な機能は、再帰クエリを作成することです。
再帰SQLクエリを使用すると、このタイプのSQLとそのレートでは不可能と思われることを実現できます。多くのビジネス上の問題を解決し、複雑なSQL/アプリケーションロジックを単純な再帰SQLに書き直すこともできます。データベースへの呼び出しです。
データベースシステム間で再帰クエリを作成する場合、わずかな違いがあります。ただし、目標は同じです。
再帰CTEの有用性のいくつかの例:
- これを使用して、データのギャップを見つけることができます。
- 組織図を作成できます。
- 事前に計算されたデータを作成して、別のCTEパーツでさらに使用することができます
- 最後に、テストデータを作成できます。
再帰的という単語 それをすべて言います。ある開始点で自分自身を繰り返し呼び出すクエリがあり、非常に重要です 終点 (フェイルセーフ出口 私が呼んでいるように)。
フェイルセーフ出口がない場合、または再帰式がそれを超える場合は、深刻な問題に直面しています。クエリは無限のループに入ります その結果、CPUが非常に高くなり、LOGの使用率が非常に高くなります。メモリやストレージの枯渇につながります。
クエリがうまくいかない場合は、それを無効にするために非常に速く考える必要があります。それができない場合は、すぐにDBAに警告して、データベースシステムが窒息し、暴走したスレッドが停止するのを防ぎます。
例を参照してください:
with RECURSIVE mydates (level,nextdate) as (
select 1 level, FROM_UNIXTIME(RAND()*2147483647) nextdate from DUAL
union all
select level+1, FROM_UNIXTIME(RAND()*2147483647) nextdate
from mydates
where level < 1000
)
SELECT nextdate from mydates
);
この例は、MySQL/MariaDB再帰CTE構文です。これを使用して、1000のランダムな日付を生成します。レベルは、再帰クエリを安全に終了するためのカウンターおよびフェイルセーフ出口です。
示されているように、2行目が開始点であり、4〜5行目がWHERE句の終了点を持つ再帰呼び出しです(6行目)。 8行目と9行目は、再帰クエリを実行してデータを取得する際の呼び出しです。
別の例:
DECLARE @today as date;
DECLARE @1stjanprevyear as date;
select @today = DATEADD(DAY, 0, DATEDIFF(DAY, 0, getdate())),
@1stjanprevyear = DATEFROMPARTS(YEAR(GETDATE())-1, 1, 1) ;
WITH DatesCTE as (
SELECT @1stjanprevyear as CalendarDate
UNION ALL
SELECT dateadd(day , 1, CalendarDate) AS CalendarDate FROM DatesCTE
WHERE dateadd (day, 1, CalendarDate) < @today
), MaxMinDates as (
SELECT Max(CalendarDate) MaxDate,Min(CalendarDate) MinDate
FROM DatesCTE
)
SELECT i.*
FROM InvoiceTable i, MaxMinDates t
where i.INVOICE_DATE between t.MinDate and t.MaxDate
OPTION (MAXRECURSION 1000);
この例はSQLServerの構文です。ここでは、DatesCTE部分で、今日から前年の1月1日までのすべての日付を生成します。これらの日付に属するすべての請求書を返すために使用します。
出発点は@ 1stjanprevyear 変数とフェイルセーフ出口@today 。最大730日が可能です。したがって、最大再帰オプションは、確実に停止するように1000に設定されています。
MaxMinDatesをスキップすることもできます 以下に示すように、パートを作成し、最後のパートを記述します。一致するWHERE句があるため、より高速なアプローチになる可能性があります。
....
SELECT i.*
FROM InvoiceTable i, DatesCTE t
where i.INVOICE_DATE = t.CalendarDate
OPTION (MAXRECURSION 1000);
結論
全体として、複雑なクエリをCTE構造化クエリに変換する方法について簡単に説明して示しました。クエリが異なるCTE部分に分割されている場合、それらを他の部分で使用し、デバッグ目的で最終的なSQLクエリで個別に呼び出すことができます。
もう1つの重要なポイントは、CTEを使用すると、複雑なクエリが管理可能な部分に分割されたときにデバッグが簡単になり、正しい期待される結果セットが返されることです。クエリとDBMSを可能な限り最適に実行するには、クエリの各部分とクエリ全体でExplainを実行することが重要であることを理解することが重要です。
また、クエリでさらに使用するためにデータをオンザフライで生成する強力な再帰CTEクエリ/パーツの記述についても説明しました。
特に、再帰クエリを作成するときは、フェイルセーフ出口を忘れないように十分注意してください 。フェイルセーフ出口で使用される計算を再確認して、停止信号を生成するか、 maxrecursionを使用してください。 SQLServerが提供するオプション。
同様に、他のDBMSは cte_max_recursion_depthを使用する場合があります (MySQL 8.0)または max_recursive_iterations (MariaDB 10.3)追加のフェイルセーフ出口として。
また読む
1つのスポットでSQLCTEについて知っておくべきことすべて