web-dev-qa-db-ja.com

このロギングテーブルに対してこのクエリを最適化するにはどうすればよいですか?

イベントをログに記録するテーブルのテーブルレイアウトを最適化しようとしています。

ログテーブルには、関連する3つの列が含まれています。Timestamp, ItemId, LocationId
各行は、特定のtimeで、特定のitemが特定のlocationで見られたことを意味します。

2017-01-01 10:00    Item A has been seen at location 1
2017-01-01 10:01    Item A has been seen at location 1
2017-01-01 11:00    Item B has been seen at location 1
2017-01-01 11:01    Item B has been seen at location 2
2017-01-01 11:02    Item A has been seen at location 2
2017-01-01 11:03    Item B has been seen at location 1

約100の異なる場所、1日あたり20.000の新しいアイテム、1日あたり100万のイベント、および14日間のログがあります。

次に、このデータに対して次のようなクエリを実行する必要があります。

  • 「2017-01-0111:00」の時点で「1」の場所にあったアイテム
    (=どのアイテムが場所1で「2017-01-0111:00」の前に見られ、1で見られた後、「 2017-01-01 11:00 '

このデータを取得するには、実行できます

SELECT DISTINCT  ItemId     
FROM events e1 
WHERE LocationId = 1
  AND e1.TimeStamp < '2017-01-01 11:00'
  AND NOT EXISTS (SELECT 1 FROM events e2
                  WHERE e2.LocationId <> e1.LocationId
                    AND e2.ItemId = e1.ItemId
                    AND e2.TimeStamp >= e1.TimeStamp
                    AND e2.TimeStamp <'2017-01-01 11:00')

enter image description here

現在、データベースの負荷がゼロの場合、このクエリには約15秒かかります。目標は、このクエリを100ミリ秒未満で、高負荷で実行することです。現在の設計ではこれは不可能だと思います。

アイテムと場所に関するインデックスと、タイムスタンプに関するクラスター化されたインデックスがあります

このクエリをより効率的に実行できるテーブルレイアウトはありますか?

または、既存のテーブルで機能するクエリはありますか?

2
HugoRune

別のクエリを試すことができます:

SELECT ItemID
  FROM (SELECT ItemID
              ,ROW_NUMBER() OVER (PARTITION BY ItemID ORDER BY TimeStamp DESC) rn
              ,LocationId
          FROM events
         WHERE TimeStamp < '2017-01-01 11:00'
       ) e1
  WHERE LocationId = 1
    AND rn = 1
;

これがこれ以上良くなるという約束はありません(実際にはもっと悪くなる可能性があります)。それはただ異なるアプローチです。

また、意味がある場合は、可能なTimeStamp値に下限を設定することをお勧めします。探している時間の12時間以上前にすべてを無視できる場合は、多数の行が削除される可能性があります。

5
RDFozz

問題を再現できません。データの準備を間違えたのか、テスト対象のマシンがあなたのマシンと大きく異なるのかはわかりません。そうは言っても、クエリの代わりにシステムを調整する価値があるかもしれません。

私のテーブルには、1450万行、100の異なる場所、および338kの異なるアイテムがあります。各アイテムは、90分ごとに3日間移動し、その後移動を停止します。コードは次のとおりです。

CREATE TABLE [events] (
    [TimeStamp] DATETIME NOT NULL,
    ItemId BIGINT NOT NULL,
    LocationId BIGINT NOT NULL,
    FILLER VARCHAR(60) NOT NULL
);

CREATE CLUSTERED INDEX CI_events ON [events] ([TimeStamp]);

INSERT INTO [events] WITH (TABLOCK)
SELECT *
FROM
(
    SELECT 
      DATEADD(MINUTE, 90 * t.cnt, DATEADD(SECOND, ItemId * (14 * 86400.) / (17. * 20000), '20161220')) [TimeStamp]
    , i.ItemId
    , 1 + ((1 + i.ItemId % 100) + 7 * t.cnt) % 100 LocationId
    , REPLICATE('Z', 30) FILLER
    FROM
    (
        SELECT TOP (17 * 20000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) ItemId
        FROM master..spt_values t1
        CROSS JOIN master..spt_values t2
    ) i
    CROSS JOIN (
        SELECT TOP (48) -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) cnt
        FROM master..spt_values t1
    ) t
) t2
WHERE t2.[TimeStamp] >= '20161223'
OPTION (MAXDOP 1);


CREATE INDEX IX_events_Loc_Item ON [events] (LocationId, ItemId);

CREATE INDEX IX_events_Item_Loc ON [events] (ItemId, LocationId);

クエリプランに基づくと、テーブルに言及していない別の非クラスター化インデックスがあるようです。私はそれが場所とアイテムにあると仮定するつもりです。

あなたのクエリは私のマシンで約250ミリ秒で実行されます:

SELECT DISTINCT ItemId     
FROM events e1 
WHERE LocationId = 1
  AND e1.[TimeStamp] < '2017-01-01 11:00'
  AND NOT EXISTS (SELECT 1 FROM [events] e2
                  WHERE e2.LocationId <> e1.LocationId
                    AND e2.ItemId = e1.ItemId
                    AND e2.[TimeStamp] >= e1.[TimeStamp]
                    AND e2.[TimeStamp] <'2017-01-01 11:00')
OPTION (MAXDOP 1);

クエリは、結合を使用しないように作成することもできます。

SELECT ItemId
FROM events e1
WHERE e1.[TimeStamp] < '2017-01-01 11:00'
GROUP BY ItemId
HAVING MAX(CASE WHEN LocationID = 1 THEN ([TimeStamp]) ELSE NULL END) = MAX([TimeStamp])
OPTION (MAXDOP 1);

クエリプラン:

enter image description here

書き換えはうまく並列化されますが、100ミリ秒の目標時間に到達する可能性は低いです。 1000万行のインデックススキャンを実行しています。 TimeStampのフィルターは単に十分に選択的ではありません。これが役立つかどうかを知るのは本当に難しいですが、アイテムが移動するたびにItemIdごとに増加する列を追加できます。これにより、各アイテムに関連する次のイベントを見つけるために、より少ない行を探すことができます。これが私がテーブルを作成した方法です:

CREATE TABLE [events_with_id] (
    LocationId BIGINT NOT NULL,
    ItemId BIGINT NOT NULL,
    [TimeStamp] DATETIME NOT NULL,
    Item_move_id  BIGINT NOT NULL,
    FILLER VARCHAR(60) NOT NULL
);

CREATE CLUSTERED INDEX CI_events_with_id ON [events_with_id] ([TimeStamp] );

INSERT INTO [events_with_id] WITH (TABLOCK)
SELECT 
  LocationId
, ItemId
, [Timestamp]
, ROW_NUMBER() OVER (PARTITION BY ItemId ORDER BY [Timestamp])
, REPLICATE('Z', 30)
FROM [events];

CREATE INDEX IX_events_new_loc_start ON [events_with_id] (LocationId, [TimeStamp])
    INCLUDE (ItemId, Item_move_id);
CREATE UNIQUE INDEX IX_events_item_item_move ON [events_with_id] (ItemId, Item_move_id);

クエリは次のとおりです。

SELECT DISTINCT ItemId     
FROM [events_with_id] e1 
WHERE LocationId = 1
  AND e1.[TimeStamp] < '2017-01-01 11:00'
  AND NOT EXISTS (SELECT 1 FROM [events_with_id] e2
                  WHERE e2.Item_move_id = e1.Item_move_id + 1
                    AND e2.ItemId = e1.ItemId
                    AND e2.[TimeStamp] <'2017-01-01 11:00')
OPTION (LOOP JOIN, MAXDOP 1);

ヒントがなければ、うまく機能しなかったマージ結合を取得していました。このクエリは私のマシンでは175ミリ秒で実行されるため、最初のクエリよりもわずかに改善されています。

別のオプションは、テーブルの各行の開始時間と終了時間の両方を含めることです。これを更新しておく必要があります。これは、アイテムが移動するたびに行を挿入するよりも難しいでしょう。最初にデータを入力するコードは次のとおりです。

CREATE TABLE [events_with_end] (
    LocationId BIGINT NOT NULL,
    ItemId BIGINT NOT NULL,
    [Start_TimeStamp] DATETIME NOT NULL,
    [End_TimeStamp] DATETIME NULL,
    FILLER VARCHAR(60) NOT NULL
);

CREATE CLUSTERED INDEX CI_events_new ON [events_with_end] ([Start_TimeStamp] );

INSERT INTO [events_with_end] WITH (TABLOCK)
SELECT LocationId, ItemId, [Timestamp], LEAD([Timestamp]) OVER (PARTITION BY ItemId ORDER BY [Timestamp]), REPLICATE('Z', 30)
FROM [events];

CREATE INDEX IX_events_new_loc_start ON [events_with_end] (LocationId, [Start_TimeStamp]) INCLUDE (ItemId, [End_TimeStamp]);

CREATE INDEX IX_events_new_loc_end ON [events_with_end] (LocationId, [End_TimeStamp]) INCLUDE (ItemId);

これで、よりターゲットを絞ったシークを実行できますが、SQL Serverは、開始時間と終了時間の両方でシークのインデックスを作成できません。

SELECT DISTINCT ItemId
FROM [events_with_end] e1
WHERE LocationID = 1 
AND e1.[Start_TimeStamp] < '2017-01-01 11:00'
AND (e1.[End_TimeStamp] > '2017-01-01 11:00' OR e1.[End_TimeStamp] IS NULL)
OPTION (MAXDOP 1);

そのクエリはIX_events_new_loc_endインデックスを使用し、約10ミリ秒で終了します。フィルタ時間をテーブルの先頭(2016-12-25 11:00)に近づけるように変更すると、他のインデックスを使用します。

enter image description here

クエリはまだ約10ミリ秒で終了します。両方のインデックスを作成する必要があるかどうかは明確ではありません。

3
Joe Obbish