私が抱えている問題について、何らかの意見をお願いします。ストアドプロシージャ全体で繰り返すコードのセクションがあり、処理にかなりの時間がかかるたびに、読み取りの数を組み合わせると、数十万のアイテムのセットで数億に達します。基本的にはアイテムがあり、アイテムには最大12台のマシンがあり、それぞれに独自の状態があります。
これらは(簡略化された)テーブル構造です。
_CREATE TABLE dbo.ItemMachineState
(
[itemID] [int],
[machineID] [int],
[stateID] [int]
)
CREATE TABLE dbo.Transition
(
[machineID] [int] NOT NULL,
[eventID] [int] NOT NULL,
[stateID] [int] NOT NULL,
[nextStateID] [int] NOT NULL
)
_
何が起こるかというと、処理中に、対象となる#tempテーブルを作成し、最終的にはアイテムごとにeventIDを設定します。その後、その一時テーブルは次のようにItemStateおよびTransitionに結合されます。
_UPDATE dbo.ItemState
SET stateID = tr.nextStateID
FROM #temp t
JOIN dbo.ItemMachineState ist ON ist.itemID = t.itemID
JOIN Transition tr ON tr.stateID = ist.stateID AND
tr.machineID = ist.machineID AND
tr.eventID = t.eventID
_
したがって、計算されるeventIDは、それぞれの状態に応じて、特定のアイテムのマシンに何が起こるかを決定します。問題は、イベントが関連する場合、イベントが1つの動きで0以上のマシン状態を操作できることです。状態と機械の特定の組み合わせに。
これらの状態遷移の例を次に示します。
ItemID 3468489は、最初はItemMachineStateで次のようになります...
_itemID machineID stateID
----------- ----------- -----------
3468489 12 4
3468489 14 113
3468489 15 157
3468489 16 165
3468489 18 169
3468489 19 165
3468489 20 157
3468489 21 165
3468489 23 173
3468489 24 173
3468489 26 9
3468489 36 9
_
いくつかの作業を行い、最終的にはItemIDとEventIDを持つ#tempテーブルがあります...
_itemID eventID
----------- -----------
3468489 64
_
次に、これらの両方のテーブルをTransitionに結合します。これは、この特定のeventIDでは次のようになります。
_machineID eventID stateID nextStateID
----------- ----------- ----------- -----------
13 64 73 79
13 64 74 79
13 64 75 79
13 64 76 79
13 64 77 79
13 64 78 79
13 64 187 79
13 64 188 79
13 64 189 79
13 64 190 79
13 64 191 79
36 64 9 79
36 64 194 79
36 64 196 79
36 64 208 79
36 64 210 79
36 64 213 79
36 64 218 79
46 64 73 79
47 64 73 79
70 64 73 79
70 64 75 79
70 64 76 79
70 64 77 79
70 64 78 79
_
すべてを一緒に入れて:
_SELECT t.itemID, t.eventID, ist.machineID, ist.stateID, tr.nextStateID
FROM #temp t
JOIN dbo.ItemMachineState ist ON ist.itemID = t.itemID
JOIN Transition tr ON tr.stateID = ist.stateID AND
tr.machineID = ist.machineID AND
tr.eventID = t.eventID
itemID eventID machineID stateID nextStateID
----------- ----------- ----------- ----------- -----------
3468489 64 36 9 79
_
したがって、この特定の例では、このイベントはこのアイテムの1つのマシンにのみ関連していました。これは、stateIDがmachineID 36で9から79に更新され、他のすべてはこのアイテムに対して同じままです。
これに異なるアプローチをする方法についての提案をお願いします。テーブル構造から離れることはできませんが、遷移/イベント中にstateIDをnextStateIDに設定する方法を変更できます。上記のように、これは除去によって機能します。そのマシンのそのイベントの次の状態が何であるかを理解するには、現在の状態とマシンが必要です。これによって何も更新されない場合もあれば、一度に複数のマシンを更新する場合もあります。その機能が気に入っています。この問題に対する最も無駄のない解決策は、単にインデックスを変更したり、クエリヒントを追加したりすることでは見つからないと思います。また、読み取りの数と処理時間を制限する新しいアプローチが必要ですが、同じ機能を提供します。
インデックスなどをこのディスカッションに持ち込まないようにしたかったのは、実際の例を使用する必要があるためです。これは、ここで尋ねようとしていることの本質を汚し、列とテーブルの名前を変更して質問を簡略化しました。いずれにせよ、ここに行きます:
クエリプラン http://Pastebin.com/xhPa4t8d 、スクリプトの作成とインデックス作成 http://Pastebin.com/sp70QuEJ
クエリプランでは、INNER LOOP JOINを強制することに注意してください。単純なJOINのままにすると、クエリの処理に指数関数的に時間がかかります。
@wBob UNIQUE CLUSTEREDインデックスを使用する前:
OPTION (MERGE JOIN, HASH JOIN)
を使用すると、結果として this の実行計画と結果が得られました。
間もなく他の情報で更新されます
一度にすべての行を更新するのではなく、代わりにマシンのバッチを実行して、更新ごとにレコードの量を減らすことを検討します。同じコードを保持して、バッチ処理するだけです。
私のテストリグでは、assetID
およびeventID
に一意のクラスター化インデックスを持つ2番目の一時テーブルを作成し、LOOP
ヒントを削除することで、パフォーマンスが約50%向上しました。これによってクエリ結果が意味的に変更されることはありません。これを試して:
_SELECT DISTINCT assetID, eventID
INTO #Event2
FROM #Event
CREATE UNIQUE CLUSTERED INDEX PK_temp_Event2 ON #Event2 ( assetID, eventID )
UPDATE ast
SET ast.stateID = st.nextStateID
FROM #Event2 AS e
INNER JOIN EFT.AssetState AS ast
ON ast.assetID = e.assetID
INNER JOIN dbo.Transition AS st
ON st.stateID = ast.stateID
AND st.eventID = e.eventID
AND st.machineID = ast.machineID
_
どうやって乗るのか教えてください。機能する場合は、元の#Eventテーブルを適合させることを検討してください。2つの一時テーブルは実際には必要ありません。これはパフォーマンスのためだけのものでした。運動を調整します。
それが機能しない場合は、セットアップをより正確に反映するようにテストリグを改善することを検討できます。非クラスター化インデックスを少なくしたり、まったく使用しないで実験を行ったところ、いくつかの良い結果が得られましたが、明らかに他のクエリがそれらを使用している可能性があります。
_-- Secondary DDL provided;
USE tempdb
GO
IF NOT EXISTS ( SELECT * FROM sys.schemas WHERE name = 'EFT' )
EXEC ('CREATE SCHEMA EFT')
GO
IF OBJECT_ID('[dbo].[Transition]') IS NOT NULL DROP TABLE [dbo].[Transition]
IF OBJECT_ID('[EFT].[AssetState]') IS NOT NULL DROP TABLE [EFT].[AssetState]
IF OBJECT_ID('[dbo].[Event]') IS NOT NULL DROP TABLE [dbo].[Event]
IF OBJECT_ID('[dbo].[State]') IS NOT NULL DROP TABLE [dbo].[State]
IF OBJECT_ID('[dbo].[Machine]') IS NOT NULL DROP TABLE [dbo].[Machine]
IF OBJECT_ID('#Event') IS NOT NULL DROP TABLE #Event
GO
-- #EFT.AssetState
CREATE TABLE [EFT].[AssetState](
[assetID] [int] NOT NULL,
[busDate] [datetime] NOT NULL,
[machineID] [int] NOT NULL,
[stateID] [int] NOT NULL,
CONSTRAINT [PK_AssetState] PRIMARY KEY CLUSTERED
(
[assetID] ASC,
[busDate] ASC,
[machineID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_AssetState_assetID] ON [EFT].[AssetState]
(
[assetID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_AssetState_assetID_stateID] ON [EFT].[AssetState]
(
[assetID] ASC,
[stateID] ASC,
[machineID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_AssetState_machineID_stateID_assetID] ON [EFT].[AssetState]
(
[machineID] ASC,
[stateID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
-- dbo.Transition
CREATE TABLE [dbo].[Transition](
[transitionID] [int] IDENTITY(1,1) NOT NULL,
[machineID] [int] NOT NULL,
[category] [varchar](50) NOT NULL,
[eventID] [int] NOT NULL,
[stateID] [int] NOT NULL,
[nextStateID] [int] NOT NULL,
CONSTRAINT [PK_Transition] PRIMARY KEY CLUSTERED
(
[transitionID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
CONSTRAINT [UK_Transition_machineID_stateID_eventID] UNIQUE NONCLUSTERED
(
[machineID] ASC,
[stateID] ASC,
[eventID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [UK_Transition_machineID_nextStateID_eventID] ON [dbo].[Transition]
(
[machineID] ASC,
[eventID] ASC,
[stateID] ASC,
[nextStateID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
CREATE TABLE [dbo].[State](
[stateID] INT PRIMARY KEY
)
GO
CREATE TABLE [dbo].[Event](
[eventID] INT PRIMARY KEY
)
GO
CREATE TABLE [dbo].[Machine](
[machineID] INT PRIMARY KEY
)
GO
ALTER TABLE [dbo].[Transition] WITH CHECK ADD CONSTRAINT [FK_Transition_NextState] FOREIGN KEY([nextStateID])
REFERENCES [dbo].[State] ([stateID])
GO
ALTER TABLE [dbo].[Transition] CHECK CONSTRAINT [FK_Transition_NextState]
GO
ALTER TABLE [dbo].[Transition] WITH CHECK ADD CONSTRAINT [FK_Transition_State] FOREIGN KEY([stateID])
REFERENCES [dbo].[State] ([stateID])
GO
ALTER TABLE [dbo].[Transition] CHECK CONSTRAINT [FK_Transition_State]
GO
ALTER TABLE [dbo].[Transition] WITH CHECK ADD CONSTRAINT [FK_Transition_StateEvent] FOREIGN KEY([eventID])
REFERENCES [dbo].[Event] ([eventID])
GO
ALTER TABLE [dbo].[Transition] CHECK CONSTRAINT [FK_Transition_StateEvent]
GO
ALTER TABLE [dbo].[Transition] WITH CHECK ADD CONSTRAINT [FK_Transition_StateMachine] FOREIGN KEY([machineID])
REFERENCES [dbo].[Machine] ([machineID])
GO
ALTER TABLE [dbo].[Transition] CHECK CONSTRAINT [FK_Transition_StateMachine]
GO
-- #Event
CREATE TABLE #Event
(
assetID INT ,
busDate DATETIME,
eventID INT
)
CREATE CLUSTERED INDEX IX_Ev_assetID ON #Event ( assetID )
GO
-- Create dummy data
-- populate AssetState with 2,658,200 records
-- 2,658,200
;WITH cte AS (
SELECT TOP 1000000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
CROSS JOIN master.sys.columns c2
CROSS JOIN master.sys.columns c3
)
INSERT INTO [EFT].[AssetState]( assetID, busDate, machineID, stateID )
SELECT
items.rn AS assetID,
'1 Jan 2015' AS busDate,
machines.rn AS machineID,
items.rn % 7 AS stateID
FROM
( SELECT TOP 221520 * FROM cte ) items
CROSS JOIN
( SELECT TOP (12) * FROM cte ) machines
GO
-- Get a random selection for temp table
INSERT INTO #Event ( assetID, busDate, eventID )
SELECT TOP (2128660) assets.assetID, assets.busDate, assets.assetID % 99 AS eventID
FROM ( SELECT DISTINCT assetID, busDate FROM [EFT].[AssetState] ) assets
CROSS JOIN
( SELECT TOP (12) * FROM [EFT].[AssetState] ) machines
ORDER BY NEWID()
GO
-- Get selection for Transition table
INSERT INTO [dbo].[State] ( stateID )
SELECT assetID
FROM ( SELECT DISTINCT TOP 99 assetID FROM [EFT].[AssetState] ) m
GO
INSERT INTO [dbo].[Event] ( eventID )
SELECT assetID
FROM ( SELECT DISTINCT TOP 99 assetID FROM [EFT].[AssetState] ) m
GO
INSERT INTO [dbo].[Machine] ( machineID )
SELECT machineID
FROM ( SELECT DISTINCT machineID FROM [EFT].[AssetState] ) m
GO
INSERT INTO dbo.Transition ( machineID, category, eventID, stateID, nextStateID )
SELECT TOP (1214)
m.machineID,
CASE x.rn % 3 WHEN 0 THEN 'X' WHEN 1 THEN 'Y' WHEN 2 THEN 'Z' END category,
( x.rn % 99 ) + 1 eventID,
( x.rn % 7 ) + 1 stateID,
( x.rn % 7 ) + 2 nextStateID
FROM ( SELECT DISTINCT machineID FROM [EFT].[AssetState] ) m
CROSS JOIN
( SELECT TOP (102) ROW_NUMBER() OVER( ORDER BY ( SELECT NULL ) ) rn, * FROM [EFT].[AssetState] ) x
ORDER BY NEWID()
GO
--:exit
-- Original
DECLARE @startDate DATETIME = GETDATE()
BEGIN TRAN
UPDATE EFT.AssetState
SET stateID = st.nextStateID
FROM #Event AS e
INNER LOOP JOIN
EFT.AssetState AS ast
ON ast.assetID = e.assetID
INNER JOIN
Transition AS st
ON st.stateID = ast.stateID
AND st.eventID = e.eventID
AND st.machineID = ast.machineID;
SELECT @@rowcount r, DATEDIFF( s, @startDate, GETDATE() ) diff1
ROLLBACK TRAN
GO
-- Revised
DECLARE @startDate DATETIME = GETDATE()
IF OBJECT_ID('tempdb..#Event2') IS NOT NULL DROP TABLE #Event2
SELECT DISTINCT assetID, eventID
INTO #Event2
FROM #Event
CREATE UNIQUE CLUSTERED INDEX PK_temp_Event2 ON #Event2 ( assetID, eventID )
BEGIN TRAN
UPDATE ast
SET ast.stateID = st.nextStateID
FROM #Event2 AS e
INNER JOIN EFT.AssetState AS ast
ON ast.assetID = e.assetID
INNER JOIN dbo.Transition AS st
ON st.stateID = ast.stateID
AND st.eventID = e.eventID
AND st.machineID = ast.machineID
SELECT @@rowcount r, DATEDIFF( s, @startDate, GETDATE() ) diff2
ROLLBACK TRAN
GO
_
更新1:2,200万件のレコードが読み取れないということですか? WHERE
句がないため、スキャンを取得します。ネストされたループ結合の外部テーブルのシークを取得する可能性がありますが、小さい方のテーブルが一番上になります。 OPTION ( MERGE JOIN, HASH JOIN )
を試して、基本的にネストされたループを除外して、ここでどのように進むかを見てみたくなります。この方法には、結合順序を強制しないという追加の利点もあります。これは、必ずしもプロダクションの修正とは限らない情報を収集するためにこれを提案しています。テストリグを改善してセットアップをより正確に反映するための提案はありますか?
2つのクエリにどのくらい時間がかかりますか? Plan Explorer (無料版)のようなものを通してそれらを実行してみることもできます。 2番目のリグのタイミングにインデックスの作成時間を含めるテストリグに誤りがあることに気づきました。除外してください。私の結果では、元のクエリは15秒、修正されたクエリは7秒です。
更新2:OPと連携して、非クラスター化インデックスを削除し、ループ結合ヒントを削除し、一時テーブルに一意のインデックスを追加して、75%以上の改善を実現しました。素晴らしい入力と@PaulWhiteへのクレジット。