SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO /* Focused optimisation for dbo.CharacterTimeline_GetByProject. Diagnosis: - The timeline timing showed this procedure as the clear bottleneck. - The original appearance query joined SceneCharacters to Characters, Scenes, Chapters, Books, and the recursive LocationPaths view three times. - Filtering the project/book scene set once gives SQL Server a small driving set before joining appearances. - Materialising project location paths once avoids repeated recursive view expansion for location, entry location, and exit location labels. */ IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Characters_ProjectID_Importance_Name' AND object_id = OBJECT_ID(N'dbo.Characters')) BEGIN -- Supports CharacterTimeline_GetByProject character list ordering. CREATE INDEX IX_Characters_ProjectID_Importance_Name ON dbo.Characters(ProjectID, IsArchived, CharacterImportance DESC, CharacterName); END; GO IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneCharacters_CharacterID_SceneID' AND object_id = OBJECT_ID(N'dbo.SceneCharacters')) BEGIN -- Supports appearance lookups when the optimizer starts from project characters. CREATE INDEX IX_SceneCharacters_CharacterID_SceneID ON dbo.SceneCharacters(CharacterID, SceneID, RoleInSceneTypeID); 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 LocationPaths materialisation. 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 LocationPaths expansion from parent to child locations. CREATE INDEX IX_Locations_ParentLocationID ON dbo.Locations(ParentLocationID, IsArchived, LocationName); END; GO CREATE OR ALTER PROCEDURE dbo.CharacterTimeline_GetByProject @ProjectID int, @BookID int = NULL AS BEGIN SET NOCOUNT ON; CREATE TABLE #TimelineScenes ( SceneID int NOT NULL PRIMARY KEY, SceneNumber decimal(9,2) NOT NULL, SceneTitle nvarchar(200) NOT NULL, ChapterNumber decimal(9,2) NOT NULL, BookTitle nvarchar(200) NOT NULL, BookSortOrder int NOT NULL, ChapterSortOrder int NOT NULL, SceneSortOrder int NOT NULL ); INSERT #TimelineScenes (SceneID, SceneNumber, SceneTitle, ChapterNumber, BookTitle, BookSortOrder, ChapterSortOrder, SceneSortOrder) SELECT s.SceneID, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle, b.SortOrder, ch.SortOrder, s.SortOrder FROM dbo.Books b INNER JOIN dbo.Chapters ch ON ch.BookID = b.BookID INNER JOIN dbo.Scenes s ON s.ChapterID = ch.ChapterID WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND ch.IsArchived = 0 AND s.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID); CREATE TABLE #TimelineCharacters ( CharacterID int NOT NULL PRIMARY KEY, ProjectID int NOT NULL, CharacterName nvarchar(200) NOT NULL, ShortName nvarchar(100) NULL, Sex nvarchar(50) NULL, BirthDate date NULL, AgeAtSeriesStart int NULL, AgeReferenceSceneID int NULL, Height nvarchar(50) NULL, EyeColour nvarchar(50) NULL, CharacterImportance int NULL, DefaultDescription nvarchar(max) NULL, CreatedDate datetime2 NOT NULL, UpdatedDate datetime2 NOT NULL, IsArchived bit NOT NULL ); INSERT #TimelineCharacters (CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived) SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived FROM dbo.Characters WHERE ProjectID = @ProjectID AND IsArchived = 0; 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 CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived FROM #TimelineCharacters ORDER BY ISNULL(CharacterImportance, 0) DESC, CharacterName; SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart, sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName, sc.LocationID, location.LocationName, location.LocationPath, sc.EntryLocationID, entry.LocationName AS EntryLocationName, sc.ExitLocationID, exitLocation.LocationName AS ExitLocationName, sc.AppearanceNotes, sc.OutfitDescription, sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes, s.SceneNumber, s.SceneTitle, s.ChapterNumber, s.BookTitle, sc.CreatedDate, sc.UpdatedDate FROM #TimelineScenes s INNER JOIN dbo.SceneCharacters sc ON sc.SceneID = s.SceneID INNER JOIN #TimelineCharacters c ON c.CharacterID = sc.CharacterID LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID LEFT JOIN #ProjectLocationPaths location ON location.LocationID = sc.LocationID LEFT JOIN #ProjectLocationPaths entry ON entry.LocationID = sc.EntryLocationID LEFT JOIN #ProjectLocationPaths exitLocation ON exitLocation.LocationID = sc.ExitLocationID ORDER BY s.BookSortOrder, s.ChapterSortOrder, s.SceneSortOrder, c.CharacterName; END; GO