非常に巨大な(100,000,000+)テーブルのTOP(1)BY GROUP


8

セットアップ

〜115,382,254行の巨大なテーブルがあります。テーブルは比較的単純で、アプリケーションプロセスの操作を記録します。

CREATE TABLE [data].[OperationData](
    [SourceDeciveID] [bigint] NOT NULL,
    [FileSource] [nvarchar](256) NOT NULL,
    [Size] [bigint] NULL,
    [Begin] [datetime2](7) NULL,
    [End] [datetime2](7) NOT NULL,
    [Date]  AS (isnull(CONVERT([date],[End]),CONVERT([date],'19000101',(112)))) PERSISTED NOT NULL,
    [DataSetCount] [bigint] NULL,
    [Result] [int] NULL,
    [Error] [nvarchar](max) NULL,
    [Status] [int] NULL,
 CONSTRAINT [PK_OperationData] PRIMARY KEY CLUSTERED 
(
    [SourceDeviceID] ASC,
    [FileSource] ASC,
    [End] ASC
))

CREATE TABLE [model].[SourceDevice](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](50) NULL,
 CONSTRAINT [PK_DataLogger] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
))

ALTER TABLE [data].[OperationData]  WITH CHECK ADD  CONSTRAINT [FK_OperationData_SourceDevice] FOREIGN KEY([SourceDeviceID])
REFERENCES [model].[SourceDevice] ([ID])

テーブルは、約500クラスターで、毎日ベースでクラスター化されます。

パーティション

ここに画像の説明を入力してください

また、テーブルはPKによって適切にインデックスが作成され、統計は最新であり、INDEXerは毎晩デフラグされます。

インデックスベースのSELECTは非常に高速で、問題はありませんでした。

問題

最後の(TOP)行を知り、[End]で分割する必要があり[SourceDeciveID]ます。[OperationData]すべてのソースデバイスの最後を取得します。

質問

私はこれを良い方法で、DBを限界まで引き上げずに解決する方法を見つける必要があります。


努力1

最初の試みは明白であるGROUP BYSELECT OVER PARTITION BYクエリでした。ここでの問題も明白です。すべてのクエリは、非常にパーティションの順序をスキャンする必要があります。そのため、クエリは非常に遅く、IOへの影響が非常に大きくなります。

クエリ例1

;WITH cte AS
(
   SELECT *,
         ROW_NUMBER() OVER (PARTITION BY [SourceDeciveID] ORDER BY [End] DESC) AS rn
   FROM [data].[OperationData]
)
SELECT *
FROM cte
WHERE rn = 1

クエリ2の例

SELECT *
FROM [data].[OperationData] AS d 
CROSS APPLY 
(
   SELECT TOP 1 *
   FROM [data].[OperationData] 
   WHERE [SourceDeciveID] = d.[SourceDeciveID]
   ORDER BY [End] DESC
) AS ds

失敗しました!

努力2

TOP行への参照を常に保持するヘルプテーブルを作成しました。

CREATE TABLE [data].[LastOperationData](
    [SourceDeciveID] [bigint] NOT NULL,
    [FileSource] [nvarchar](256) NOT NULL,
    [End] [datetime2](7) NOT NULL,
 CONSTRAINT [PK_LastOperationData] PRIMARY KEY CLUSTERED 
(
    [SourceDeciveID] ASC
)

ALTER TABLE [data].[LastOperationData]  WITH CHECK ADD  CONSTRAINT [FK_LastOperationData_OperationData] FOREIGN KEY([SourceDeciveID], [FileSource], [End])
REFERENCES [data].[OperationData] ([SourceDeciveID], [FileSource], [End])

テーブルを埋めるために、より高い[End]列が挿入された場合に常にソース行を追加/更新するトリガーを作成しました。

CREATE TRIGGER [data].[OperationData_Last]
   ON  [data].[OperationData]
   AFTER INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    MERGE [data].[LastOperationData] AS [target]
    USING (SELECT [SourceDeciveID], [FileSource], [End] FROM inserted) AS [source] ([SourceDeciveID], [FileSource], [End])  
    ON ([target].[SourceDeciveID] = [FileSource].[SourceDeciveID])

    WHEN MATCHED AND [target].[End] < [source].[End] THEN
        UPDATE SET [target].[FileSource] = source.[FileSource], [target].[End] = source.[End]

    WHEN NOT MATCHED THEN  
        INSERT ([SourceDeciveID], [FileSource], [End])  
        VALUES (source.[SourceDeciveID], source.[FileSource], source.[End]);

END

ここでの問題は、IOへの影響も非常に大きく、その理由がわかりません。

クエリプランで確認できるように、[OperationData]テーブル全体に対してスキャンも実行します。

それは私のDBに大きな影響を与えます。 統計

失敗しました!


2
最初のコードブロックでは、クラスター化インデックスの最初の列がどこから来ているのかわかりません-正しいですか?
George.Palacios

はい、申し訳ありませんが、SSMSはCREATE TABLEスクリプトにそれを含めませんが、クエリプラン内にパーティションが表示されます。質問を編集します。
Steffen Mangold

PRIMARY KEY CLUSTEREDあなたの中に含まれているのでそれが役立つかもしれないと思うので、余分なインデックスではありませんか?
Steffen Mangold

エラーだったSoryyは、質問の名前をより明確に変更し、修正しました。
Steffen Mangold

@ypercubeᵀᴹはい、SELECT [SourceID], [Source], [End] FROM insertedいくつかの方法でテーブルスキャンを実行します[OperationData]
Steffen Mangold 2018年

回答:


9

SourceID値のテーブルがあり、メインテーブルのインデックスがにある場合は、を(SourceID, End) include (othercolumns)使用しますOUTER APPLY

SELECT d.*
FROM dbo.Sources s
OUTER APPLY (SELECT TOP (1) *
    FROM data.OperationData d
    WHERE d.SourceID = s.SourceID
    ORDER BY d.[End] DESC) d;

最新のパーティションの後であることがわかっている場合は、Endに次のようなフィルターを含めることができます。 AND d.[End] > DATEADD(day, -1, GETDATE())

編集:クラスター化インデックスがオンになっているので、SourceID, Source, End)SourceもSourcesテーブルに入れて、それに結合します。その後、新しいインデックスは必要ありません。

SELECT d.*
FROM dbo.Sources s -- Small table
OUTER APPLY (SELECT TOP (1) *
    FROM data.OperationData d -- Big table quick seeks
    WHERE d.SourceID = s.SourceID
    AND d.Source = s.Source
    AND d.[End] > DATEADD(day, -1, GETDATE()) -- If you’re partitioning on [End], do this for partition elimination
    ORDER BY d.[End] DESC) d;

インデックスはクエリを本当に高速化しました。それに伴う2番目の問題は、そのような巨大なテーブルのパーティション化されていないインデックスがほぼメンテナンス不能になることです。すべての「ビッグデータ」テーブルで、パーティションインデクサーを使用しています。パーティションごとにオンラインパーティションを維持できます。インデクサーがパーティション分割されるとすぐに、すべてのパーティションを実行する必要があるため、問題は古いものです。
Steffen Mangold

1
@SteffenMangold:インデックス内のデータが少ないほど(必要なものがすべて揃っている限り)優れており、マテリアライズドビューを除外すると、クラスター化インデックスは可能な限り最大量のデータを持ちます。キーによってすべてのデータを取得することが標準であるため、クラスター化インデックスが存在します。この場合、すべてのデータを取得していますが、実際にはキーで取得しているのではなく、キーの一部で取得しています。キーの一部でクエリできるインデックスが必要です。
jmoreno 2018年

本当に申し訳ありませんがSourcesourceID列を参照するテーブルがあります。列のソースはファイル名のみです。少しわかりにくいネーミングです。Sourceデバイス(sourceID)ごとにsource、1つのタイムスタンプで1 つのファイル(列)に対して1つのエントリしか存在できません。また、最新のものEndはひどく断片化されているため、パーティションの削除を行うことはできません。トリガーソリューションを思いついたのはそのためです。ライブクエリはここでは機能しません。
Steffen Mangold

@Rob Farley質問をより明確にするために編集しました
Steffen Mangold

パーティショニングを使用すると、各パーティションへのシークをすべて実行できます。追加の述語を使用すると、それらすべてに煩わされることなく、一部のみを行うようにすることができます。必要に応じて1か月にします。
Rob Farley、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.