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