あまりにも頻繁に、データベーステーブルに対して実行されている不十分に記述された複雑なSQLクエリを目にします。このようなクエリの実行には非常に短い時間または非常に長い時間がかかる場合がありますが、CPUやその他のリソースを大量に消費します。それにもかかわらず、多くの場合、複雑なクエリはアプリケーション/人に貴重な情報を提供します。したがって、あらゆる種類のアプリケーションに役立つ資産をもたらします。
クエリの複雑さ
問題のあるクエリを詳しく見てみましょう。それらの多くは複雑です。これにはいくつかの理由が考えられます:
- データ用に選択されたデータ型;
- データベース内のデータの編成と保存;
- 目的の結果セットを取得するためのクエリでのデータの変換と結合。
クエリを最適に実行するには、これら3つの重要な要素を適切に検討し、正しく実装する必要があります。
ただし、データベース開発者とDBAの両方にとって、これはほぼ不可能な作業になる可能性があります。たとえば、既存のレガシーシステムに新しい機能を追加することは非常に難しい場合があります。特に複雑なケースは、レガシーシステムからデータを抽出して変換し、新しいシステムまたは機能によって生成されたデータと比較できるようにする必要がある場合です。レガシーアプリケーションの機能に影響を与えることなくそれを達成する必要があります。
このようなクエリには、次のような複雑な結合が含まれる場合があります。
- サブストリングおよび/または複数のデータ列の連結の組み合わせ;
- 組み込みのスカラー関数;
- カスタマイズされたUDF;
- WHERE句の比較と検索条件の任意の組み合わせ。
前述のように、クエリには通常、複雑なアクセスパスがあります。さらに悪いことに、JOINまたは検索のそのような組み合わせが発生する多くのテーブルスキャンおよび/またはフルインデックススキャンが行われる可能性があります。
クエリでのデータ変換と操作
データベーステーブルに永続的に格納されているすべてのデータは、テーブルからそのデータをクエリするときに、ある時点で変換や操作が必要になることを指摘する必要があります。変換は、単純な変換から非常に複雑な変換までさまざまです。複雑さによっては、変換によって多くのCPUとリソースが消費される可能性があります。
ほとんどの場合、JOINで行われる変換は、データが読み取られて tempdbにオフロードされた後に行われます。 データベース(SQL Server)またはワークファイル データベース/ temp-tablespaces 他のデータベースシステムと同じように。
ワークファイル内のデータはインデックスに登録できないため 、結合された変換とJOINの実行に必要な時間は指数関数的に増加します。取得するデータが大きくなります。したがって、結果として得られるクエリは、データがさらに増えることでパフォーマンスのボトルネックになります。
では、データベース開発者またはDBAは、これらのパフォーマンスのボトルネックを迅速に解決し、最適なパフォーマンスを得るためにクエリを再設計および書き直すための時間をどのように確保できるでしょうか。
このような永続的な問題を効果的に解決するには、2つの方法があります。それらの1つは、仮想列や機能インデックスを使用することです。
機能インデックスとクエリ
通常、行の一意の列/値のセット(一意のインデックスまたは主キー)を示すか、クエリのWHERE句の検索条件で使用される、または使用される可能性のある列/値のセットを表すインデックスを列に作成します。
このようなインデックスが設定されておらず、前述のように複雑なクエリを開発した場合は、次のことに気付くでしょう。
- Explainを使用するとパフォーマンスレベルが低下します テーブルスキャンまたはフルインデックススキャンのクエリと表示
- クエリが原因でCPUとリソースの使用率が非常に高くなります。
- 実行時間が長い。
現在のデータベースは通常、機能を作成できるようにすることでこれらの問題に対処します または関数ベース SQLServer、Oracle、およびMySQL(v 8.x)で指定されているインデックス。または、インデックスオンにすることもできます 式/式ベース 他のデータベース(PostgreSQLおよびDb2)と同様に、インデックス。
Purchase_Date列があるとします。 注文のデータ型TIMESTAMPまたはDATETIMEの テーブル、およびその列にインデックスが付けられています。 注文のクエリを開始します WHERE句のあるテーブル:
SELECT ...
FROM Order
WHERE DATE(Purchase_Date) = '03.12.2020'
このトランザクションにより、インデックス全体がスキャンされます。ただし、列にインデックスが付けられていない場合は、テーブルスキャンが実行されます。
インデックス全体をスキャンした後、そのインデックスは tempdb / workfileに移動します (テーブル全体 テーブルスキャンを取得した場合 )値を一致させる前に 03.12.2020 。
大きなOrderテーブルは多くのCPUとリソースを使用するため、DATE式( Purchase_Date )を持つ機能インデックスを作成する必要があります。 )インデックス列の1つとして、以下に示します:
CREATE ix_DatePurchased on sales.Order(Date(Purchase_Date) desc, ... )
その際、一致する述語を DATE(Purchase_Date)=‘03.12.2020’ にします。 インデックス可能。したがって、値が一致する前にインデックスまたはテーブルをtempdb / workfileに移動する代わりに、インデックスを部分的にのみアクセスおよび/またはスキャンします。その結果、CPUとリソースの使用量が少なくなります。
別の例を見てください。 お客様がいます 列がfirst_name、last_nameのテーブル 。これらの列には、次のようにインデックスが付けられます。
CREATE INDEX ix_custname on Customer(first_name asc, last_name asc),
さらに、これらの列を customer_nameに連結するビューがあります。 列:
CREATE view v_CustomerInfo( customer_name, .... ) as
select first_name ||' '|| last_name as customer_name,.....
from Customer
where ...
完全な顧客名を検索するeコマースシステムからのクエリがあります:
select c.*
from v_CustomerInfo c
where c.customer_name = 'John Smith'
....
この場合も、このクエリは完全なインデックススキャンを生成します。最悪のシナリオでは、 first_name を連結する前に、すべてのデータをインデックスまたはテーブルからワークファイルに移動する全表スキャンになります。 およびlast_name 列と「ジョン・スミス」の値に一致します。
もう1つのケースは、以下に示すように機能インデックスを作成することです。
CREATE ix_fullcustname on sales.Customer( first_name ||' '|| last_name desc, ... )
このようにして、ビュークエリの連結をインデックス可能な述語にすることができます。全表スキャンまたは表スキャンの代わりに、部分インデックススキャンがあります。このようなクエリの実行により、CPUとリソースの使用量が削減され、ワークファイル内の作業が除外されるため、実行時間が短縮されます。
仮想(生成された)列とクエリ
生成された列(仮想列または計算列)は、その場で生成されたデータを保持する列です。データを明示的に特定の値に設定することはできません。これは、DMLクエリでクエリ、挿入、または更新された他の列のデータを参照します。
このような列の値の生成は、式に基づいて自動化されます。これらの式は次を生成する可能性があります:
- 整数値のシーケンス;
- テーブル内の他の列の値に基づく値。
- 組み込み関数またはユーザー定義関数(UDF)を呼び出すことによって値を生成する場合があります。
一部のデータベース(SQLServer、Oracle、PostgreSQL、MySQL、およびMariaDB)では、これらの列は、INSERTおよびUPDATEステートメントの実行でデータを永続的に格納するか、基になる列式をその場で実行するように構成できることに注意することも同様に重要です。テーブルと列をクエリすると、ストレージスペースが節約されます。
ただし、UDF関数の複雑なロジックのように式が複雑な場合、実行時間、リソース、およびCPUクエリコストの節約は期待したほどではない可能性があります。
したがって、式の結果をINSERTまたはUPDATEステートメントに永続的に格納するように列を構成できます。次に、その列に通常のインデックスを作成します。このようにして、CPU、リソースの使用量、およびクエリの実行時間を節約します。繰り返しになりますが、式の複雑さによっては、INSERTとUPDATEのパフォーマンスがわずかに向上する可能性があります。
例を見てみましょう。テーブルを宣言し、次のようにインデックスを作成します。
CREATE TABLE Customer as (
customerID Int GENERATED ALWAYS AS IDENTITY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
customer_name as (first_name ||' '|| last_name) PERSISTED
...
);
CREATE ix_fullcustname on sales.Customer( customer_name desc, ... )
このようにして、連結ロジックを前の例のビューからテーブルに移動し、データを永続的に保存します。通常のインデックスで一致するスキャンを使用してデータを取得します。ここで可能な限り最高の結果です。
生成された列をテーブルに追加し、その列に通常のインデックスを作成することで、変換ロジックをテーブルレベルに移動できます。ここでは、変換されたデータを挿入ステートメントまたは更新ステートメントに永続的に格納します。そうしないと、クエリで変換されます。 JOINスキャンとINDEXスキャンは、はるかに簡単で高速になります。
機能インデックス、生成された列、JSON
グローバルなWebおよびモバイルアプリケーションは、JSONなどの軽いデータ構造を使用してデータをWeb /モバイルデバイスからデータベースに、またはその逆に移動します。 JSONデータ構造のフットプリントが小さいため、ネットワークを介したデータ転送が迅速かつ簡単になります。 JSONを他の構造、つまりXMLと比較して非常に小さいサイズに圧縮するのは簡単です。実行時の解析で構造を上回ることができます。
JSONデータ構造の使用が増加しているため、リレーショナルデータベースにはBLOBデータ型またはCLOBデータ型のいずれかのJSONストレージ形式があります。これらのタイプはどちらも、そのような列のデータをそのままインデックスに登録できないようにします。
このため、データベースベンダーはJSONオブジェクトをクエリおよび変更するためのJSON関数を導入しました。これは、これらの関数をSQLクエリまたは他のDMLコマンドに簡単に統合できるためです。ただし、これらのクエリはJSONオブジェクトの複雑さに依存します。 BLOBおよびCLOBオブジェクトをメモリにオフロードする必要があるため、さらに悪いことに、ワークファイルにオフロードする必要があるため、CPUとリソースを非常に消費します。 クエリや操作の前。
顧客がいると仮定します 顧客の詳細を含む表 CustomerDetailという列にJSONオブジェクトとして保存されたデータ 。以下のようにテーブルのクエリを設定しました:
SELECT CustomerID,
JSON_VALUE(CustomerDetail, '$.customer.Name') AS Name,
JSON_VALUE(CustomerDetail, '$.customer.Surname') AS Surname,
JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') AS PostCode,
JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 1"') + ' '
+ JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 2"') AS Address,
JSON_QUERY(CustomerDetail, '$.customer.address.Country') AS Country
FROM Customer
WHERE ISJSON(CustomerDetail) > 0
AND JSON_VALUE(CustomerDetail, '$.customer.address.Country') = 'Iceland'
AND JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') IN (101,102,110,210,220)
AND Status = 'Active'
ORDER BY JSON_VALUE(CustomerDetail, '$.customer.address.PostCode')
この例では、アイスランドの首都圏の一部に住む顧客のデータをクエリしています。すべてのアクティブ データはワークファイルに取得する必要があります 検索述部を適用する前。それでも、取得するとCPUとリソースの使用量が大きくなりすぎます。
したがって、JSONクエリをより高速に実行するための効果的な手順があります。前述のように、生成された列を介して機能を利用する必要があります。
生成された列を追加することで、パフォーマンスの向上を実現します。生成された列は、JSON関数を使用して列に表されている特定のデータをJSONドキュメントで検索し、その値を列に格納します。
通常のSQLwhere句の検索条件を使用して、これらの生成された列にインデックスを付けてクエリを実行できます。したがって、JSONオブジェクトで特定のデータを検索するのは非常に高速になります。
生成された2つの列を追加します–国 および郵便番号 :
ALTER TABLE Customer
ADD Country as JSON_VALUE(CustomerDetail,'$.customer.address.Country');
ALTER TABLE Customer
ADD PostCode as JSON_VALUE(CustomerDetail,'$.customer.address.PostCode');
CREATE INDEX ix_CountryPostCode on Country(Country asc,PostCode asc);
また、特定の列に複合インデックスを作成します。これで、クエリを以下に示す例に変更できます。
SELECT CustomerID,
JSON_VALUE(CustomerDetail, '$.customer.customer.Name') AS Name,
JSON_VALUE(CustomerDetail, '$.customer.customer.Surname') AS Surname,
JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') AS PostCode,
JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 1"') + ' '
+ JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 2"') AS Address,
JSON_QUERY(CustomerDetail, '$.customer.address.Country') AS Country
FROM Customer
WHERE ISJSON(CustomerDetail) > 0
AND Country = 'Iceland'
AND PostCode IN (101,102,110,210,220)
AND Status = 'Active'
ORDER BY JSON_VALUE(CustomerDetail, '$.customer.address.PostCode')
これにより、データの取得はアイスランド首都圏の一部のアクティブな顧客に限定されます。この方法は、前のクエリよりも高速で効率的です。
結論
全体として、問題の原因となるテーブル(CPU、およびリソースを大量に消費するクエリ)に仮想列または機能インデックスを適用することで、問題をかなり迅速に排除できます。
仮想列と機能インデックスは、通常のリレーショナルテーブルに格納されている複雑なJSONオブジェクトのクエリに役立ちます。ただし、事前に問題を慎重に評価し、それに応じて必要な変更を加える必要があります。
場合によっては、クエリやJSONデータ構造が非常に複雑な場合、CPUとリソースの使用量の一部がクエリからINSERT/UPDATEプロセスに移行することがあります。これにより、全体的なCPUとリソースの節約が予想よりも少なくなります。同様の問題が発生した場合は、テーブルとクエリのより徹底的な再設計が避けられない可能性があります。