Finite-State-Maschinen in SQL

7

Ich hätte gerne einen Beitrag zu einem Problem, das ich habe. Wir haben einen Codeabschnitt, den wir in unseren gespeicherten Prozeduren wiederholen, und jedes Mal, wenn die Verarbeitung einige Zeit in Anspruch nimmt, beträgt die Anzahl der Lesevorgänge in Kombination mit Hunderttausenden von Elementen Hunderte von Millionen. Grundsätzlich haben wir Artikel, und Artikel können bis zu 12 Maschinen mit jeweils eigenem Status haben.

Dies sind die (vereinfachten) Tabellenstrukturen:

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
)

Während der Verarbeitung erstellen wir eine # temp-Tabelle, gegen die wir arbeiten, und sie hat schließlich eine eventID pro Element. Diese temporäre Tabelle wird dann wie folgt wieder mit ItemState und Transition verknüpft:

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

Die von uns berechnete Ereignis-ID bestimmt also, was mit den Maschinen eines bestimmten Gegenstands passiert, je nachdem, in welchem ​​Zustand sie sich befinden. Das Problem ist, dass ein Ereignis null oder mehr Maschinenzustände in einer Bewegung manipulieren kann, wenn dieses Ereignis relevant ist zu dieser besonderen Kombination von Zustand und Maschine.

Hier ist ein Beispiel für eine dieser Zustandsverschiebungen:

ItemID 3468489 sieht zuerst in ItemMachineState so aus ...

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

Wir arbeiten und haben schließlich eine # temp-Tabelle mit einer ItemID und einer EventID ...

itemID      eventID
----------- -----------
3468489     64

Dann verbinden wir diese beiden Tabellen mit Transition, was für diese bestimmte Ereignis-ID folgendermaßen aussieht:

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

Alles zusammen:

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

In diesem Beispiel war dieses Ereignis nur für eine Maschine für diesen Artikel relevant. Die stateID wird auf machineID 36 von 9 auf 79 aktualisiert, und alles andere bleibt für dieses Element gleich.

Ich hätte gerne Vorschläge, wie ich das anders angehen kann. Wir können uns nicht von der Tabellenstruktur entfernen, aber wir können ändern, wie wir bei Übergängen / Ereignissen stateID auf nextStateID setzen. Wie Sie oben sehen können, funktioniert dies durch Eliminierung; Wir brauchen den aktuellen Zustand und die Maschine, um herauszufinden, wie der nächste Zustand für diese Maschine und dieses Ereignis aussieht. In einigen Fällen wird dadurch nichts aktualisiert, in anderen Fällen werden mehrere Computer gleichzeitig aktualisiert, und diese Funktion gefällt uns. Ich denke nicht, dass die schlankste Lösung für dieses Problem durch einfaches Ändern von Indizes oder Hinzufügen von Abfragehinweisen gefunden werden kann und dass wir einen neuen Ansatz benötigen, der die Anzahl der Lesevorgänge und die Verarbeitungszeit begrenzt, uns aber die gleiche Funktionalität bietet.


Ich wollte vermeiden, Indizes und dergleichen in diese Diskussion einzubeziehen, da ich dann echte Beispiele verwenden müsste, die die Essenz dessen, was ich hier zu fragen versuche, verschmutzen. Ich habe den Namen von Spalten und Tabellen geändert, um meine Frage zu vereinfachen. Auf jeden Fall geht es los:

Abfrageplan http://pastebin.com/xhPa4t8d , Erstellen und Indizieren von Skripten http://pastebin.com/sp70QuEJ

Beachten Sie, dass wir im Abfrageplan einen INNER LOOP JOIN erzwingen. Wenn die Abfrage einem einfachen JOIN überlassen wird, dauert die Verarbeitung exponentiell länger.


Verwenden des @ wBob UNIQUE CLUSTERED-Index vor: Vor

und danach: Nach

Die Verwendung OPTION (MERGE JOIN, HASH JOIN)führte zu diesem Ausführungsplan und den Ergebnissen:

Nach Option

Wird in Kürze mit anderen Informationen aktualisiert

Maj0r
quelle
3
Können Sie Ihre Frage mit dbo.ItemStateddl und einigen Daten aktualisieren ? Sie können sqlfiddle.com verwenden, um einen Repro einzurichten. Gibt es in diesen Tabellen auch Indizes, FKs und Trigger? Sie können einen tatsächlichen Ausführungsplan-XML-Code veröffentlichen (Pastebin verwenden und hier verlinken). Das wird dir helfen, bessere und schnellere Antworten zu bekommen :-)
Kin Shah
Im Abfrageplan sind Ihre geschätzten Zeilen (4292220) weit entfernt von den tatsächlichen Zeilen (747348). Stellen Sie sicher, dass Ihre Statistiken aktualisiert werden. Anstatt den Join zu erzwingen FROM #Event AS e INNER LOOP JOIN EFT.AssetState AS ast, können Sie auch einen Abfragehinweis verwenden OPTION (LOOP JOIN, QUERYTRACEON 8649, QUERYTRACEON 4199);. Was ist der Gesamtspeicher auf dem Server und der maximale Speicher sowie die maximale Dop-Einstellung? Sie können auch bestimmte Zustände if exists (select .. some criteria = true) then update else do nothingzusammen mit Stapelaktualisierungen überprüfen .
Kin Shah
Warum benutzt du OPTION (MERGE JOIN, HASH JOIN)gleichzeitig? Hast du es versucht OPTION (LOOP JOIN, QUERYTRACEON 8649, QUERYTRACEON 4199);?
Kin Shah
@Kin Dies ermöglicht es dem Optimierer grundsätzlich, einen der beiden Verknüpfungstypen auszuwählen, die Tabellen in der für sie geeigneten Reihenfolge neu anzuordnen, jedoch keine verschachtelten Schleifen zu verwenden. Dies hilft auszuschließen, ob verschachtelte Schleifen hier ein Problem darstellen.
wBob

Antworten:

1

Ich würde in Betracht ziehen, nicht alle Zeilen gleichzeitig zu aktualisieren und stattdessen mehrere Computer zu durchlaufen, damit das Datensatzvolumen pro Aktualisierung abnimmt. Sie können den gleichen Code behalten, indem Sie ihn einfach stapeln.

Jesse
quelle
1

Ich habe eine 50% ige Leistungssteigerung in meinem Prüfstand um durch eine zweite temporäre Tabelle mit einem eindeutigen Clustered - Index erstellt auf assetIDund eventIDund den Abwurf LOOPHinweis. Dies sollte Ihre Abfrageergebnisse nicht semantisch ändern. Versuche dies:

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

Lass mich wissen, wie es dir geht. Wenn es funktioniert, sollten Sie Ihre ursprüngliche # Event-Tabelle anpassen - es sind keine zwei temporären Tabellen erforderlich, dies war nur für die Perfektion. Übung abstimmen.

Wenn es nicht funktioniert, können wir versuchen, den Prüfstand zu verbessern, um Ihr Setup genauer wiederzugeben. Ich habe einige Experimente mit weniger oder keinen nicht gruppierten Indizes durchgeführt und auch einige gute Ergebnisse erzielt, obwohl sie offensichtlich von anderen Abfragen verwendet werden könnten.

Prüfstand

-- 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

Update 1: Meinen Sie 22 Millionen Datensätze, die nicht gelesen wurden? Sie haben keine WHEREKlauseln, also werden Sie Scans bekommen. Möglicherweise werden Sie nach der äußeren Tabelle eines Joins mit verschachtelten Schleifen suchen, aber die kleinere Tabelle befindet sich zu Recht oben. Ich wäre versucht zu versuchen OPTION ( MERGE JOIN, HASH JOIN ), verschachtelte Schleifen hier grundsätzlich auszuschließen. Sehen Sie, wie Sie vorankommen. Diese Methode hat auch den zusätzlichen Vorteil, dass die Verknüpfungsreihenfolge nicht erzwungen wird. Ich schlage dies vor, um Informationen zu sammeln, die nicht unbedingt eine Lösung für die Produktion darstellen. Irgendwelche Vorschläge zur Verbesserung des Prüfstands, um Ihr Setup genauer widerzuspiegeln?

Wie lange dauern die beiden Abfragen? Sie könnten versuchen, sie durch etwas wie Plan Explorer (kostenlose Version) zu führen, da ich denke, dass dies aufschlussreich sein wird. Ich habe festgestellt, dass in meinem Prüfstand ein Fehler aufgetreten ist, bei dem ich die Indexerstellungszeit in das Timing für den zweiten Prüfstand einbeziehe. Bitte schließen Sie dies aus. Für meine Ergebnisse erhalte ich eine ursprüngliche Abfrage von 15 Sekunden und eine überarbeitete Abfrage von 7 Sekunden:

Testergebnisse

Update 2: Arbeitete mit OP zusammen, um nicht geclusterte Indizes zu entfernen, Schleifenverbindungshinweise zu entfernen und der temporären Tabelle einen eindeutigen Index hinzuzufügen, um eine Verbesserung von 75 +% zu erzielen. Fantastischer Input und Dank an @PaulWhite.

wBob
quelle
Auf Wunsch wurde ein eigener Chatraum für die Diskussion um diese Antwort eingerichtet.
Paul White 9