web-dev-qa-db-ja.com

トップテーブルでグループ化されたサブクエリCOUNTを取得する最も効率的な方法は?

次のスキーマを考える

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
);

そして次のルール

  1. dataレコードはloggerおよびcategoryを参照します
  2. loggerには常にgroupがあります
  3. groupは複数のloggersを持つことができます
  4. 記録された最近のデータのみをカウントしたい

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のコストは大幅に増える可能性があります(現在の階層から少しだけジュースが必要な場合でも)。 。

7
James

@JoeObbishからのすばらしい回答のおかげで、クエリプランをよりよく理解し、それがどこで苦労しているか、どのインデックスを使用してそれを改善できるかを理解することができました。この間、私がこれを各ロガーからのlatest読み取りにのみ適用できるようにする必要があることを言及するのを忘れたため、目標の投稿は少し変更されました。もしlogger_acategory_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行のテーブルの場合。これにはかなり満足しています。おそらくまだ改善の余地があると思いますが、現状ではこれで十分です。

これが最後の 計画 です。他の提案や改善は歓迎します。

0
James

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つの手掛かりは、相対矢印の太さを調べることでわかります。

thick arrows

その演算子から多くの行が返されるので、詳細を確認する価値があります。クラスタ化インデックススキャンの実際の行数を見ると、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'))。行の目標の概念に慣れ、理解している場合にのみ、これを行う必要があります。そのヒントを追加すると別の計画になるはずですが、それがどれほど効率的であるかは言えません。

8
Joe Obbish

まず、各テーブルですべての候補キーが宣言され、外部キーが強制されるようにします。

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_datalogger_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;

実行計画は次のとおりです。

Estimated plan

dbfiddle

また、2017 RTMから最新の累積的な更新にインスタンスをパッチする必要があります。

4
Paul White 9

なぜグループへの参加が必要なのですか?

なぜカテゴリー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  

私は実際にあなたが外部キーを宣言していることを望みます。

これらの各結合列にインデックスが必要です。

0
paparazzo

問題領域は次のとおりです。

  1. Improper data type:データ型がINTの場合、データページが少なく、index fragmentationがないことを意味します。NewSequentialIDの場合、more data pageno index fragmentationを意味し、UNIQUEIDENTIFIERを使用すると、両方の問題が発生します。INTデータ型は理想的な選択です。
  2. 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データ型で定義する必要があります。
  3. このクエリが非常に重要で頻繁に使用される場合は、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_namepartition by d.CatgoryIDである必要があります。

0
KumarHarsh