次のスキーマを考える
CREATE TABLE categories
(
id UNIQUEIDENTIFIER PRIMARY KEY,
name NVARCHAR(50)
);
CREATE TABLE [group]
(
id UNIQUEIDENTIFIER PRIMARY KEY
);
CREATE TABLE logger
(
id UNIQUEIDENTIFIER PRIMARY KEY,
group_id UNIQUEIDENTIFIER,
uuid CHAR(17)
);
CREATE TABLE data
(
id UNIQUEIDENTIFIER PRIMARY KEY,
logger_uuid CHAR(17),
category_name NVARCHAR(50),
recorded_on DATETIME
);
そして次のルール
data
レコードはlogger
およびcategory
を参照しますlogger
には常にgroup
がありますgroup
は複数のlogger
sを持つことができますcategory_name
は行ごとに一意ではありません。これは、特定のデータレコードをカテゴリに関連付ける方法にすぎません。id
は、実際には代理キーです。
のような結果セットを達成するための最適な方法は何でしょうか
category_id | logger_group_count
--------------------------------
12345 4
67890 2
..... ...
つまり、いいえを数えます。ロガーがデータを記録した各カテゴリのグループの数
最初のスタブとして私は思いつきました:
SELECT g.id, COUNT(DISTINCT(a.id)) AS logger_group_count
FROM categories g
LEFT OUTER JOIN data d ON d.category_name = g.name
INNER JOIN logger s ON s.uuid = d.logger_uuid
INNER JOIN group a ON a.id = s.group_id
GROUP BY g.id
しかし非常に遅い遅い(〜45秒)、data
には40万以上のレコードがあります-ここに クエリプラン とここa fiddle で遊ぶ。
ハードウェアの使用状況など、他のことを検討する前に、クエリを最大限に活用していることを確認したいと思います。AzureSQLのコストは大幅に増える可能性があります(現在の階層から少しだけジュースが必要な場合でも)。 。
@JoeObbishからのすばらしい回答のおかげで、クエリプランをよりよく理解し、それがどこで苦労しているか、どのインデックスを使用してそれを改善できるかを理解することができました。この間、私がこれを各ロガーからのlatest読み取りにのみ適用できるようにする必要があることを言及するのを忘れたため、目標の投稿は少し変更されました。もしlogger_a
がcategory_x @ 11:50
およびcategory_y @ 11:51
の下でデータを記録した場合、これをcategory_y
として数えたいだけです。
これが結果のSQLです
;WITH logger_data AS (
SELECT
category_name,
logger_uuid,
recorded_on,
RN = ROW_NUMBER() OVER (PARTITION BY logger_uuid ORDER BY recorded_on DESC)
FROM data
)
SELECT c.id, count(DISTINCT l.group_id) FROM categories c
INNER JOIN logger_data d on d.category_name = c.name
INNER JOIN logger l ON l.uuid = d.logger_uuid
WHERE RN = 1
GROUP BY c.id
これは依然として高価なクエリですが、次のインデックスが適用されています
CREATE CLUSTERED INDEX ix_latest ON "dbo"."data"
(
logger_uuid,
recorded_on DESC
)
GO
CREATE CLUSTERED INDEX ix_groups ON "dbo"."logger"
(
group_id
)
〜25秒から〜3秒、および〜500,000行のテーブルの場合。これにはかなり満足しています。おそらくまだ改善の余地があると思いますが、現状ではこれで十分です。
これが最後の 計画 です。他の提案や改善は歓迎します。
SQL Serverの新しいバージョンを使用しているため、実際の計画では多くの情報が得られます。 SELECT
演算子の警告サインを参照してください。つまり、SQL Serverはクエリのパフォーマンスに影響を与える可能性のある警告を生成しました。あなたは常にそれらを見るべきです:
_<Warnings>
<PlanAffectingConvert ConvertIssue="Seek Plan" Expression="[s].[logger_uuid]=CONVERT_IMPLICIT(nchar(17),[d].[uuid],0)" />
<PlanAffectingConvert ConvertIssue="Seek Plan" Expression="CONVERT_IMPLICIT(nvarchar(100),[d].[name],0)=[g].[name]" />
</Warnings>
_
スキーマによって引き起こされる2つのデータ型変換があります。警告に基づいて、名前は実際にはNVARCHAR(100)
であり、_logger_uuid
_はNCHAR(17)
であると思います。質問に投稿されたテーブルスキーマが正しくない可能性があります。これらの変換が発生する根本的な原因を理解し、修正する必要があります。一部のタイプのデータ型変換は、インデックスシークを妨げ、カーディナリティ推定の問題を引き起こし、他の問題を引き起こします。
チェックするもう一つの重要なことは、待機統計です。これらは、SELECT
演算子の詳細でも確認できます。待機統計のXMLとクエリで費やされた時間は次のとおりです。
_<WaitStats>
<Wait WaitType="RESOURCE_GOVERNOR_IDLE" WaitTimeMs="49515" WaitCount="3773" />
<Wait WaitType="SOS_SCHEDULER_YIELD" WaitTimeMs="57164" WaitCount="2466" />
</WaitStats>
<QueryTimeStats ElapsedTime="67135" CpuTime="10007" />
_
私はクラウドの専門家ではありませんが、クエリでCPUを完全に 使用できないようです 。これはおそらく、現在のAzure層に関連しています。クエリの実行に必要なCPUは約10秒でしたが、67秒かかりました。その時間の50秒はスロットルに費やされ、その時間の7秒はあなたに与えられたが、同時に実行されている他のクエリで使用されたと思います。悪いニュースは、ティアが原因でクエリが遅くなる可能性があることです。良いニュースは、CPUの削減は実行時間の5倍の削減につながる可能性があることです。つまり、クエリで1秒のCPUを使用できる場合は、実行時間が約5秒になる可能性があります。
次に、オペレーターの詳細の「実際の時間統計」プロパティを見て、CPU時間の消費場所を確認できます。計画では行モードを使用しているため、オペレーターのCPU時間は、そのオペレーターとその子が費やした時間の合計です。これは比較的単純な計画なので、_logger_data
_のクラスター化インデックススキャンが6527ミリ秒のCPU時間を使用することを発見するのに時間がかかりません。それを呼び出すループ結合は10006ミリ秒のCPU時間を使用するため、クエリのすべてのCPUがそのステップで費やされます。そのステップで問題が発生していることを示すもう1つの手掛かりは、相対矢印の太さを調べることでわかります。
その演算子から多くの行が返されるので、詳細を確認する価値があります。クラスタ化インデックススキャンの実際の行数を見ると、14088885行が返され、14100798行が読み取られていることがわかります。ただし、テーブルのカーディナリティは484803行です。直感的には、これはかなり非効率に見えますよね?クラスタ化インデックススキャンは、テーブルの行数よりもはるかに多くを返します。テーブルに異なる結合タイプまたはアクセス方法を使用する他のいくつかの計画は、より効率的である可能性があります。
SQL Serverが多くの行を読み取って返したのはなぜですか?クラスタ化インデックスは、ネストされたループの内側にあります。ループの外側(logger
テーブルのスキャン)によって38行が返されるため、_logger_data
_のスキャンは38回実行されます。 484803 * 38 = 18422514は、読み取られた行数にかなり近いです。では、なぜSQL Serverは非効率的であると感じるような計画を選択したのでしょうか。それはテーブルの57スキャンを行うと推定しているので、おそらくあなたが得た計画はそれが疑ったよりも効率的でした。
プランにTOP
演算子が含まれている理由を疑問に思っているかもしれません。 SQL Serverは、クエリのクエリプランを作成するときに 行の目標 を導入しました。これは必要以上に詳細かもしれませんが、短いバージョンでは、SQL Serverは必ずしもクラスター化インデックススキャンからすべての行を返す必要はありません。場合によっては、固定数の行のみが必要で、スキャンの最後に到達する前にそれらの行を検出する場合、早期に停止することがあります。スキャンは、早期に停止できる場合はそれほど高価ではないため、行の目標が存在する場合、オペレーターのコストは数式によって 割り引かれます 。つまり、SQL Serverはクラスター化インデックスを57回スキャンすることを期待していますが、必要な単一の行を非常に迅速に見つけることができると考えています。 TOP
演算子が存在するため、各スキャンからの単一行のみが必要です。
クエリオプティマイザーが_logger_data
_テーブルを38回スキャンしないプランを選択するように促すことで、クエリをより高速にすることができます。これは、データ型変換を排除するのと同じくらい簡単かもしれません。これにより、SQL Serverはスキャンではなくインデックスシークを実行できます。そうでない場合は、変換を修正し、_logger_data
_のカバリングインデックスを作成します。
_CREATE INDEX IX ON logger_data (category_name, logger_uuid);
_
クエリオプティマイザーは、コストに基づいてプランを選択します。このインデックスを追加すると、クラスター化インデックススキャンの代わりにインデックスシークを介してテーブルにアクセスする方が安くなるため、logger_dataで多くのスキャンを実行する低速プランを取得する可能性が低くなります。
インデックスを追加できない場合は、クエリヒントを追加して、行の目標の導入を無効にすることを検討できます:USE HINT('DISABLE_OPTIMIZER_ROWGOAL'))
。行の目標の概念に慣れ、理解している場合にのみ、これを行う必要があります。そのヒントを追加すると別の計画になるはずですが、それがどれほど効率的であるかは言えません。
まず、各テーブルですべての候補キーが宣言され、外部キーが強制されるようにします。
CREATE TABLE dbo.categories
(
id uniqueidentifier NOT NULL
CONSTRAINT [UQ dbo.categories id]
UNIQUE NONCLUSTERED,
[name] nvarchar(50) NOT NULL
CONSTRAINT [PK dbo.categories name]
PRIMARY KEY CLUSTERED
);
-- Choose a better name for this table
CREATE TABLE dbo.[group]
(
id uniqueidentifier NOT NULL
CONSTRAINT [PK dbo.group id]
PRIMARY KEY CLUSTERED
);
CREATE TABLE dbo.logger
(
id uniqueidentifier
CONSTRAINT [UQ dbo.logger id]
UNIQUE NONCLUSTERED,
group_id uniqueidentifier NOT NULL
CONSTRAINT [FK dbo.group id]
FOREIGN KEY (group_id)
REFERENCES [dbo].[group] (id),
uuid char(17) NOT NULL
CONSTRAINT [PK dbo.logger uuid]
PRIMARY KEY CLUSTERED
);
CREATE TABLE dbo.logger_data
(
id uniqueidentifier
CONSTRAINT [PK dbo.logger_data id]
PRIMARY KEY NONCLUSTERED,
logger_uuid char(17) NOT NULL
CONSTRAINT [FK dbo.logger_data uuid]
FOREIGN KEY (logger_uuid)
REFERENCES dbo.logger (uuid),
category_name nvarchar(50) NOT NULL
CONSTRAINT [dbo.logger_data name]
FOREIGN KEY (category_name)
REFERENCES dbo.categories ([name]),
recorded_on datetime NOT NULL,
INDEX [dbo.logger_data logger_uuid recorded_on]
CLUSTERED (logger_uuid, recorded_on)
);
また、logger_data
のlogger_uuid, recorded_on
に一意でないクラスター化インデックスを追加しました。
次に、実行プランの最大のタスクは、データテーブルの484,836行のスキャンです。特定のロガーの最新の読み取りのみに関心があり、現在48のロガーしかないため、そのフルスキャンを48のシングルトンシークに置き換える方が効率的です。
SELECT
category_id = C.id,
logger_group_count = COUNT_BIG(DISTINCT L.group_id)
FROM dbo.logger AS L
CROSS APPLY
(
-- Latest reading per logger
SELECT TOP (1)
LD.recorded_on,
LD.category_name
FROM dbo.logger_data AS LD
WHERE LD.logger_uuid = L.uuid
ORDER BY
LD.recorded_on DESC
) AS LDT1
JOIN dbo.categories AS C
ON C.[name] = LDT1.category_name
GROUP BY
C.id
ORDER BY
C.id;
実行計画は次のとおりです。
また、2017 RTMから最新の累積的な更新にインスタンスをパッチする必要があります。
なぜグループへの参加が必要なのですか?
なぜカテゴリーgなのですか?
SELECT c.id, COUNT(DISTINCT(s.group_id)) AS logger_group_count
FROM categories c
JOIN data d
ON d.category_name = c.name
JOIN logger s
ON s.uuid = d.logger_uuid
GROUP BY c.id
私は実際にあなたが外部キーを宣言していることを望みます。
これらの各結合列にインデックスが必要です。
問題領域は次のとおりです。
Improper data type
:データ型がINT
の場合、データページが少なく、index fragmentation
がないことを意味します。NewSequentialID
の場合、more data page
とno index fragmentation
を意味し、UNIQUEIDENTIFIER
を使用すると、両方の問題が発生します。INTデータ型は理想的な選択です。Data type and length of both column should be same in relationship column
:たとえば、計画内のa.category_name = g.NAME
Logger_dataクラスター化インデックススキャンは、両方の列の長さが50または100であることを示唆しているため、オプティマイザーはConvert_Implicit
の実行に時間を費やす必要がありません。さらに、関係は、CategoryID int`のようなintデータ型で定義する必要があります。Denormalization
を考えることができます。以下のクエリを試してください、
SELECT g.id
,sum(CASE
WHEN rn = 1
THEN 1
ELSE 0
END)
FROM categories g
INNER JOIN (
SELECT d.category_name
,ROW_NUMBER() OVER (
PARTITION BY d.category_name
,s.group_id ORDER BY s.group_id
) rn
FROM data d
INNER JOIN logger s ON s.uuid = d.logger_uuid
--INNER JOIN [group] a ON a.id = s.group_id
) a ON a.category_name = g.NAME
GROUP BY g.id
私は@Paparazzi
のアイデアが好きなので、それを組み込みました。
計画はあなたより良いと思います。上記の修正とインデックスの調整により、パフォーマンスがさらに向上します。
ここで修正する必要があります
ROW_NUMBER()over(partition by d.category_name,a.id order by s.group_id )rn
order by s.group_id
、最新のレコードを提供するorder by DateOrIDcolumn desc
である必要があります。サンプルでは、最新のレコードを見つける方法を理解できません。
また、partition by d.category_name
はpartition by d.CatgoryID
である必要があります。