Timeline performance improvements.

This commit is contained in:
Nick Beckley 2026-06-02 21:57:36 +01:00
parent 1dad31efab
commit f1279f55ee

View File

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