web-dev-qa-db-ja.com

SQLでの有限状態マシン

私が抱えている問題について、何らかの意見をお願いします。ストアドプロシージャ全体で繰り返すコードのセクションがあり、処理にかなりの時間がかかるたびに、読み取りの数を組み合わせると、数十万のアイテムのセットで数億に達します。基本的にはアイテムがあり、アイテムには最大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インデックスを使用する前: Before

以降: - After

OPTION (MERGE JOIN, HASH JOIN)を使用すると、結果として this の実行計画と結果が得られました。

After Option

間もなく他の情報で更新されます

7
Maj0r

一度にすべての行を更新するのではなく、代わりにマシンのバッチを実行して、更新ごとにレコードの量を減らすことを検討します。同じコードを保持して、バッチ処理するだけです。

1
Jesse

私のテストリグでは、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秒です。

Test rig results

更新2:OPと連携して、非クラスター化インデックスを削除し、ループ結合ヒントを削除し、一時テーブルに一意のインデックスを追加して、75%以上の改善を実現しました。素晴らしい入力と@PaulWhiteへのクレジット。

1
wBob