イベントをログに記録するテーブルのテーブルレイアウトを最適化しようとしています。
ログテーブルには、関連する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日間のログがあります。
次に、このデータに対して次のようなクエリを実行する必要があります。
このデータを取得するには、実行できます
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')
現在、データベースの負荷がゼロの場合、このクエリには約15秒かかります。目標は、このクエリを100ミリ秒未満で、高負荷で実行することです。現在の設計ではこれは不可能だと思います。
アイテムと場所に関するインデックスと、タイムスタンプに関するクラスター化されたインデックスがあります
このクエリをより効率的に実行できるテーブルレイアウトはありますか?
または、既存のテーブルで機能するクエリはありますか?
別のクエリを試すことができます:
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時間以上前にすべてを無視できる場合は、多数の行が削除される可能性があります。
問題を再現できません。データの準備を間違えたのか、テスト対象のマシンがあなたのマシンと大きく異なるのかはわかりません。そうは言っても、クエリの代わりにシステムを調整する価値があるかもしれません。
私のテーブルには、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);
クエリプラン:
書き換えはうまく並列化されますが、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
)に近づけるように変更すると、他のインデックスを使用します。
クエリはまだ約10ミリ秒で終了します。両方のインデックスを作成する必要があるかどうかは明確ではありません。