diff --git a/PlotLine/Sql/025_AssetTimelinePerformance.sql b/PlotLine/Sql/025_AssetTimelinePerformance.sql new file mode 100644 index 0000000..34120d9 --- /dev/null +++ b/PlotLine/Sql/025_AssetTimelinePerformance.sql @@ -0,0 +1,170 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +/* + Focused optimisation for dbo.AssetTimeline_GetByProject. + + Diagnosis: + - Recent timeline logging showed this procedure at ~12 seconds after it had + previously been ~18-40 ms. + - The current procedure joins StoryAssets, AssetEvents, Scenes, Chapters, + Books, AssetKinds, AssetEventTypes, AssetStates, and LocationPaths. + - The first result set expands the recursive LocationPaths view for asset + locations; the second result set can be vulnerable to a poor cached plan + around the optional @BookID predicate. + - This rewrite mirrors the safer CharacterTimeline shape: filter project/book + scenes once, filter active project assets once, and materialise project + location paths once before joining. +*/ + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_StoryAssets_Project_TimelineList' AND object_id = OBJECT_ID(N'dbo.StoryAssets')) +BEGIN + -- Supports project-scoped asset timeline list filtering and ordering. + CREATE INDEX IX_StoryAssets_Project_TimelineList + ON dbo.StoryAssets(ProjectID, IsArchived, IsResolved, Importance DESC, AssetName); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AssetEvents_StoryAssetID_SceneID' AND object_id = OBJECT_ID(N'dbo.AssetEvents')) +BEGIN + -- Supports asset-event reads after project assets have been filtered. + CREATE INDEX IX_AssetEvents_StoryAssetID_SceneID + ON dbo.AssetEvents(StoryAssetID, SceneID, AssetEventTypeID); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Locations_Project_Parent_Name' AND object_id = OBJECT_ID(N'dbo.Locations')) +BEGIN + -- Supports project-scoped location path materialisation for asset current locations. + CREATE INDEX IX_Locations_Project_Parent_Name + ON dbo.Locations(ProjectID, IsArchived, ParentLocationID, LocationName); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Locations_ParentLocationID' AND object_id = OBJECT_ID(N'dbo.Locations')) +BEGIN + -- Supports recursive location path expansion from parent to child locations. + CREATE INDEX IX_Locations_ParentLocationID + ON dbo.Locations(ParentLocationID, IsArchived, LocationName); +END; +GO + +IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_StoryAssets_Project_TimelineList' AND object_id = OBJECT_ID(N'dbo.StoryAssets')) +BEGIN + UPDATE STATISTICS dbo.StoryAssets IX_StoryAssets_Project_TimelineList; +END; +GO + +IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AssetEvents_StoryAssetID_SceneID' AND object_id = OBJECT_ID(N'dbo.AssetEvents')) +BEGIN + UPDATE STATISTICS dbo.AssetEvents IX_AssetEvents_StoryAssetID_SceneID; +END; +GO + +IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AssetEvents_SceneID' AND object_id = OBJECT_ID(N'dbo.AssetEvents')) +BEGIN + UPDATE STATISTICS dbo.AssetEvents IX_AssetEvents_SceneID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.AssetTimeline_GetByProject + @ProjectID int, + @BookID int = NULL +AS +BEGIN + SET NOCOUNT ON; + + CREATE TABLE #TimelineScenes + ( + SceneID int NOT NULL PRIMARY KEY, + BookSortOrder int NOT NULL, + ChapterSortOrder int NOT NULL, + SceneSortOrder int NOT NULL + ); + + INSERT #TimelineScenes (SceneID, BookSortOrder, ChapterSortOrder, SceneSortOrder) + SELECT s.SceneID, b.SortOrder, c.SortOrder, s.SortOrder + FROM dbo.Books b + INNER JOIN dbo.Chapters c ON c.BookID = b.BookID + INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID + WHERE b.ProjectID = @ProjectID + AND b.IsArchived = 0 + AND c.IsArchived = 0 + AND s.IsArchived = 0 + AND (@BookID IS NULL OR b.BookID = @BookID) + OPTION (RECOMPILE); + + CREATE TABLE #ProjectAssets + ( + StoryAssetID int NOT NULL PRIMARY KEY, + ProjectID int NOT NULL, + AssetName nvarchar(200) NOT NULL, + AssetKindID int NOT NULL, + Description nvarchar(max) NULL, + Importance int NOT NULL, + CurrentStateID int NULL, + CurrentLocationID int NULL, + IsResolved bit NOT NULL, + CreatedDate datetime2 NOT NULL, + UpdatedDate datetime2 NOT NULL, + IsArchived bit NOT NULL + ); + + INSERT #ProjectAssets (StoryAssetID, ProjectID, AssetName, AssetKindID, Description, Importance, CurrentStateID, CurrentLocationID, IsResolved, CreatedDate, UpdatedDate, IsArchived) + SELECT StoryAssetID, ProjectID, AssetName, AssetKindID, Description, Importance, CurrentStateID, CurrentLocationID, IsResolved, CreatedDate, UpdatedDate, IsArchived + FROM dbo.StoryAssets + WHERE ProjectID = @ProjectID + AND IsArchived = 0 + OPTION (RECOMPILE); + + CREATE TABLE #ProjectLocationPaths + ( + LocationID int NOT NULL PRIMARY KEY, + LocationName nvarchar(200) NOT NULL, + LocationPath nvarchar(max) NOT NULL + ); + + ;WITH ProjectLocationTree AS + ( + SELECT LocationID, LocationName, CAST(LocationName AS nvarchar(max)) AS LocationPath + FROM dbo.Locations + WHERE ProjectID = @ProjectID AND ParentLocationID IS NULL AND IsArchived = 0 + + UNION ALL + + SELECT child.LocationID, child.LocationName, CAST(parent.LocationPath + N' > ' + child.LocationName AS nvarchar(max)) AS LocationPath + FROM dbo.Locations child + INNER JOIN ProjectLocationTree parent ON parent.LocationID = child.ParentLocationID + WHERE child.ProjectID = @ProjectID AND child.IsArchived = 0 + ) + INSERT #ProjectLocationPaths (LocationID, LocationName, LocationPath) + SELECT LocationID, LocationName, LocationPath + FROM ProjectLocationTree; + + SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, sa.Importance, + sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID, + location.LocationName AS CurrentLocationName, location.LocationPath AS CurrentLocationPath, + sa.IsResolved, sa.CreatedDate, sa.UpdatedDate, sa.IsArchived + FROM #ProjectAssets sa + INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID + LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID + LEFT JOIN #ProjectLocationPaths location ON location.LocationID = sa.CurrentLocationID + WHERE sa.Importance >= 5 OR sa.IsResolved = 0 + ORDER BY sa.Importance DESC, sa.AssetName; + + SELECT ae.AssetEventID, ae.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, + ae.SceneID, ae.AssetEventTypeID, aet.TypeName AS AssetEventTypeName, aet.MarkerText, + ae.FromStateID, fs.StateName AS FromStateName, ae.ToStateID, ts.StateName AS ToStateName, + ae.EventTitle, ae.EventDescription, ae.CreatedDate, ae.UpdatedDate + FROM #TimelineScenes scene + INNER JOIN dbo.AssetEvents ae ON ae.SceneID = scene.SceneID + INNER JOIN #ProjectAssets sa ON sa.StoryAssetID = ae.StoryAssetID + INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID + INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID + LEFT JOIN dbo.AssetStates fs ON fs.AssetStateID = ae.FromStateID + LEFT JOIN dbo.AssetStates ts ON ts.AssetStateID = ae.ToStateID + ORDER BY scene.BookSortOrder, scene.ChapterSortOrder, scene.SceneSortOrder, sa.AssetName; +END; +GO