From 81cf66f5262cf86829c6a9926277d7e34aa621b6 Mon Sep 17 00:00:00 2001
From: kevwhitt-hee <kevin.whittaker@hee.nhs.uk>
Date: Thu, 30 Jul 2026 12:15:29 +0100
Subject: [PATCH 1/2] TD-7606 Refactor super admin queries to optimise
 performance

Making better use of CTEs and avoid multiple table scans
---
 ...2607300908_AddIndexesForGetAllDelegates.cs | 43 ++++++++
 .../UserDataService/AdminUserDataService.cs   | 74 +++++++-------
 .../DelegateUserCardDataService.cs            | 98 ++++++++-----------
 .../UserDataService/UserDataService.cs        | 98 +++++++++++--------
 4 files changed, 179 insertions(+), 134 deletions(-)
 create mode 100644 DigitalLearningSolutions.Data.Migrations/202607300908_AddIndexesForGetAllDelegates.cs

diff --git a/DigitalLearningSolutions.Data.Migrations/202607300908_AddIndexesForGetAllDelegates.cs b/DigitalLearningSolutions.Data.Migrations/202607300908_AddIndexesForGetAllDelegates.cs
new file mode 100644
index 0000000000..d9e056b31a
--- /dev/null
+++ b/DigitalLearningSolutions.Data.Migrations/202607300908_AddIndexesForGetAllDelegates.cs
@@ -0,0 +1,43 @@
+namespace DigitalLearningSolutions.Data.Migrations
+{
+    using FluentMigrator;
+    using FluentMigrator.SqlServer;
+
+    [Migration(202607300908)]
+    public class AddIndexesForGetAllDelegates : Migration
+    {
+        public override void Up()
+        {
+            Create.Index("IX_AdminAccounts_Active_UserID_CentreID_ID").OnTable("AdminAccounts")
+                .OnColumn("Active").Ascending()
+                .OnColumn("UserID").Ascending()
+                .OnColumn("CentreID").Ascending()
+                .WithOptions().NonClustered()
+                .Include("ID");
+
+            Create.Index("IX_DelegateAccounts_CentreID_Active_Approved_RegistrationConfirmationHash_UserID").OnTable("DelegateAccounts")
+                .OnColumn("CentreID").Ascending()
+                .OnColumn("Active").Ascending()
+                .OnColumn("Approved").Ascending()
+                .OnColumn("RegistrationConfirmationHash").Ascending()
+                .OnColumn("UserID").Ascending()
+                .WithOptions().NonClustered()
+                .Include("ID")
+                .Include("CandidateNumber")
+                .Include("DateRegistered")
+                .Include("LastAccessed")
+                .Include("SelfReg");
+
+            Create.Index("IX_Users_LearningHubAuthID").OnTable("Users")
+                .OnColumn("LearningHubAuthID").Ascending()
+                .WithOptions().NonClustered();
+        }
+
+        public override void Down()
+        {
+            Delete.Index("IX_AdminAccounts_Active_UserID_CentreID_ID").OnTable("AdminAccounts");
+            Delete.Index("IX_DelegateAccounts_CentreID_Active_Approved_RegistrationConfirmationHash_UserID").OnTable("DelegateAccounts");
+            Delete.Index("IX_Users_LearningHubAuthID").OnTable("Users");
+        }
+    }
+}
diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs
index 8af9cef6e5..91dcbd2efc 100644
--- a/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/AdminUserDataService.cs
@@ -391,24 +391,30 @@ public IEnumerable<AdminAccount> GetAdminAccountsByUserId(int userId)
                 search = search.Trim();
             }
 
-            string BaseSelectQuery = $@"SELECT aa.ID, aa.UserID, aa.CentreID, aa.Active, aa.IsCentreAdmin, aa.IsReportsViewer, aa.IsSuperAdmin, aa.IsCentreManager, 
-		                                aa.LastAccessed, aa.IsContentManager, aa.IsContentCreator, aa.IsSupervisor, aa.IsTrainer, aa.CategoryID, aa.IsFrameworkDeveloper, aa.IsFrameworkContributor,aa.ImportOnly,
-		                                aa.IsWorkforceManager, aa.IsWorkforceContributor, aa.IsLocalWorkforceManager, aa.IsNominatedSupervisor,
-		                                u.ID, u.PrimaryEmail, u.FirstName, u.LastName, u.Active, u.FailedLoginCount,
-		                                c.CentreID, c.CentreName,
-		                                ucd.ID, ucd.Email, ucd.EmailVerified, ucd.CentreID,
-                         (SELECT count(*)
-                         FROM (
-                                SELECT TOP 1 AdminSessions.AdminID FROM AdminSessions WHERE AdminSessions.AdminID = aa.ID
-	                            UNION ALL
-                                SELECT TOP 1 FrameworkCollaborators.AdminID FROM FrameworkCollaborators WHERE FrameworkCollaborators.AdminID = aa.ID
-	                            UNION ALL
-                                SELECT TOP 1 SupervisorDelegates.SupervisorAdminID FROM SupervisorDelegates WHERE SupervisorDelegates.SupervisorAdminID = aa.ID
-                            ) AS tempTable) AS AdminIdReferenceCount
+            string BaseSelectQuery = $@"WITH AdminReferenceCounts AS (
+                                    SELECT AdminID, SUM(AdminReferenceCount) AS AdminIdReferenceCount
+                                    FROM (
+                                        SELECT DISTINCT AdminID, 1 AS AdminReferenceCount FROM AdminSessions
+                                        UNION ALL
+                                        SELECT DISTINCT AdminID, 1 AS AdminReferenceCount FROM FrameworkCollaborators
+                                        UNION ALL
+                                        SELECT DISTINCT SupervisorAdminID AS AdminID, 1 AS AdminReferenceCount FROM SupervisorDelegates
+                                    ) AS AdminReferenceRows
+                                    GROUP BY AdminID
+                                )
+                                SELECT aa.ID, aa.UserID, aa.CentreID, aa.Active, aa.IsCentreAdmin, aa.IsReportsViewer, aa.IsSuperAdmin, aa.IsCentreManager, 
+							        aa.LastAccessed, aa.IsContentManager, aa.IsContentCreator, aa.IsSupervisor, aa.IsTrainer, aa.CategoryID, aa.IsFrameworkDeveloper, aa.IsFrameworkContributor,aa.ImportOnly,
+							        aa.IsWorkforceManager, aa.IsWorkforceContributor, aa.IsLocalWorkforceManager, aa.IsNominatedSupervisor,
+							        u.ID, u.PrimaryEmail, u.FirstName, u.LastName, u.Active, u.FailedLoginCount,
+							        c.CentreID, c.CentreName,
+							        ucd.ID, ucd.Email, ucd.EmailVerified, ucd.CentreID,
+                                    COALESCE(arc.AdminIdReferenceCount, 0) AS AdminIdReferenceCount,
+                                    COUNT(*) OVER() AS TotalCount
                                     FROM   AdminAccounts AS aa INNER JOIN
                                     Users AS u ON aa.UserID = u.ID INNER JOIN
                                     Centres AS c ON aa.CentreID = c.CentreID LEFT OUTER JOIN
-                                    UserCentreDetails AS ucd ON u.ID = ucd.UserID AND c.CentreID = ucd.CentreID";
+                                    UserCentreDetails AS ucd ON u.ID = ucd.UserID AND c.CentreID = ucd.CentreID
+                                    LEFT JOIN AdminReferenceCounts AS arc ON arc.AdminID = aa.ID";
 
             string condition = $@" WHERE ((@adminId = 0) OR (aa.ID = @adminId)) AND 
                                 (u.FirstName + ' ' + u.LastName + ' ' + u.PrimaryEmail + ' ' + COALESCE(ucd.Email, '') + ' ' + COALESCE(u.ProfessionalRegistrationNumber, '') LIKE N'%' + @search + N'%') AND 
@@ -426,30 +432,30 @@ FROM   AdminAccounts AS aa INNER JOIN
                             OFFSET @offset ROWS
                             FETCH NEXT @rows ROWS ONLY";
 
-            IEnumerable<AdminEntity> adminEntity = connection.Query<AdminAccount, UserAccount, Centre, UserCentreDetails, int, AdminEntity>(
+            var adminEntities = new List<AdminEntity>();
+            int resultCount = 0;
+
+            connection.Query<AdminAccount, UserAccount, Centre, UserCentreDetails, int, int, AdminEntity>(
                 sql,
-                (adminAccount, userAccount, centre, userCentreDetails, adminIdReferenceCount) => new AdminEntity(
-                    adminAccount,
-                    userAccount,
-                    centre,
-                    userCentreDetails,
-                    adminIdReferenceCount
-                ),
+                (adminAccount, userAccount, centre, userCentreDetails, adminIdReferenceCount, totalCount) =>
+                {
+                    resultCount = resultCount == 0 ? totalCount : resultCount;
+                    var adminEntity = new AdminEntity(
+                        adminAccount,
+                        userAccount,
+                        centre,
+                        userCentreDetails,
+                        adminIdReferenceCount
+                    );
+                    adminEntities.Add(adminEntity);
+                    return adminEntity;
+                },
                 new { adminId, search, centreId, userStatus, failedLoginThreshold, role, offset, rows },
-                splitOn: "ID,ID,CentreID,ID,AdminIdReferenceCount",
+                splitOn: "ID,ID,CentreID,ID,AdminIdReferenceCount,TotalCount",
                 commandTimeout: 3000
             );
 
-            int ResultCount = connection.ExecuteScalar<int>(
-                            @$"SELECT  COUNT(*) AS Matches
-                            FROM   AdminAccounts AS aa INNER JOIN
-                            Users AS u ON aa.UserID = u.ID INNER JOIN
-                            Centres AS c ON aa.CentreID = c.CentreID LEFT OUTER JOIN
-                            UserCentreDetails AS ucd ON u.ID = ucd.UserID AND c.CentreID = ucd.CentreID {condition}",
-                new { adminId, search, centreId, userStatus, failedLoginThreshold, role },
-                commandTimeout: 3000
-            );
-            return (adminEntity, ResultCount);
+            return (adminEntities, resultCount);
         }
         public int RessultCount(int adminId, string search, int? centreId, string userStatus, int failedLoginThreshold, string role)
         {
diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs
index f2068623a9..25cde5ae9d 100644
--- a/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/DelegateUserCardDataService.cs
@@ -40,7 +40,12 @@ public partial class UserDataService
                 '' AS ProfessionalRegistrationNumber,
                 NULL AS AdminID";
         private const string DelegateUserCardSelectQuery =
-            @"SELECT
+            @"WITH ActiveAdminAccounts AS (
+                SELECT UserID, CentreID, ID
+                FROM AdminAccounts
+                WHERE Active = 1
+            )
+            SELECT
                 da.ID,
                 da.CandidateNumber,
                 c.CentreName,
@@ -67,20 +72,21 @@ public partial class UserDataService
                 da.Active,
                 u.HasBeenPromptedForPrn,
                 u.ProfessionalRegistrationNumber,
-                (SELECT ID
-                    FROM AdminAccounts aa
-                        WHERE aa.UserID = da.UserID
-                            AND aa.CentreID = da.CentreID
-                            AND aa.Active = 1
-                ) AS AdminID
+                aaa.ID AS AdminID
             FROM DelegateAccounts AS da
             INNER JOIN Centres AS c ON c.CentreID = da.CentreID
             INNER JOIN Users AS u ON u.ID = da.UserID
             LEFT JOIN UserCentreDetails AS ucd ON ucd.UserID = da.UserID AND ucd.CentreID = da.CentreID
-            INNER JOIN JobGroups AS jg ON jg.JobGroupID = u.JobGroupID";
+            INNER JOIN JobGroups AS jg ON jg.JobGroupID = u.JobGroupID
+            LEFT JOIN ActiveAdminAccounts AS aaa ON aaa.UserID = da.UserID AND aaa.CentreID = da.CentreID";
 
         private const string DelegateUserSelectQuery =
-            @"SELECT
+            @"WITH ActiveAdminAccounts AS (
+                SELECT UserID, CentreID, ID
+                FROM AdminAccounts
+                WHERE Active = 1
+            )
+            SELECT
 				da.ID,
 				da.Active AS DelegateActive,
 				da.CandidateNumber,
@@ -112,14 +118,14 @@ FROM DelegateAccounts AS da
 				u.ProfessionalRegistrationNumber,
                 u.PrimaryEmail,
                 ucd.Email,
-				(SELECT ID
-					FROM AdminAccounts aa
-						WHERE aa.UserID = da.UserID
-							AND aa.CentreID = da.CentreID
-							AND aa.Active = 1
-				) AS AdminID ";
+				aaa.ID AS AdminID ";
         private const string DelegateUserExportSelectQuery =
-            @"SELECT
+            @"WITH ActiveAdminAccounts AS (
+                SELECT UserID, CentreID, ID
+                FROM AdminAccounts
+                WHERE Active = 1
+            )
+            SELECT
                 da.ID,
                 da.CandidateNumber,
                 c.CentreName,
@@ -147,12 +153,7 @@ FROM AdminAccounts aa
                 da.Active,
                 u.HasBeenPromptedForPrn,
                 u.ProfessionalRegistrationNumber,
-                (SELECT ID
-                    FROM AdminAccounts aa
-                        WHERE aa.UserID = da.UserID
-                            AND aa.CentreID = da.CentreID
-                            AND aa.Active = 1
-                ) AS AdminID
+                aaa.ID AS AdminID
                 ,u.PrimaryEmail
 				,ucd.Email
 				,da.Active as DelegateActive
@@ -160,12 +161,14 @@ FROM DelegateAccounts AS da
             INNER JOIN Centres AS c ON c.CentreID = da.CentreID
             INNER JOIN Users AS u ON u.ID = da.UserID
             LEFT JOIN UserCentreDetails AS ucd ON ucd.UserID = da.UserID AND ucd.CentreID = da.CentreID
-            INNER JOIN JobGroups AS jg ON jg.JobGroupID = u.JobGroupID";
+            INNER JOIN JobGroups AS jg ON jg.JobGroupID = u.JobGroupID
+            LEFT JOIN ActiveAdminAccounts AS aaa ON aaa.UserID = da.UserID AND aaa.CentreID = da.CentreID";
         private const string DelegateUserFromTable = @" FROM DelegateAccounts AS da WITH (NOLOCK)
 			INNER JOIN Centres AS c WITH (NOLOCK) ON c.CentreID = da.CentreID
 			INNER JOIN Users AS u WITH (NOLOCK) ON u.ID = da.UserID
 			LEFT JOIN UserCentreDetails AS ucd WITH (NOLOCK) ON ucd.UserID = da.UserID AND ucd.CentreID = da.CentreID
-			INNER JOIN JobGroups AS jg WITH (NOLOCK) ON jg.JobGroupID = u.JobGroupID ";
+			INNER JOIN JobGroups AS jg WITH (NOLOCK) ON jg.JobGroupID = u.JobGroupID
+			LEFT JOIN ActiveAdminAccounts AS aaa WITH (NOLOCK) ON aaa.UserID = da.UserID AND aaa.CentreID = da.CentreID ";
         private string DelegatewhereConditon = $@" Where ((CentreID = @centreId) OR (@centreId= 0))
                             AND ( FirstName + ' ' + LastName + ' ' + PrimaryEmail + ' ' + COALESCE(Email, '') + ' ' + COALESCE(CandidateNumber, '') LIKE N'%' + @searchString + N'%')
 					        AND ((@isActive = 'Any') OR (@isActive = 'true' AND DelegateActive = 1) OR (@isActive = 'false' AND DelegateActive = 0))
@@ -352,47 +355,26 @@ public List<DelegateUserCard> GetDelegateUserCardsForExportByCentreId(String sea
 
             if (sortBy == "SearchableName")
                 orderBy = " ORDER BY LTRIM(LastName) " + sortDirection + ", LTRIM(FirstName) ";
-            else if(sortBy == "LastAccessed")
+            else if (sortBy == "LastAccessed")
                 orderBy = " ORDER BY LastAccessed " + sortDirection;
             else
                 orderBy = " ORDER BY DateRegistered " + sortDirection;
 
             orderBy += " OFFSET " + offSet + " ROWS FETCH NEXT " + itemsPerPage + " ROWS ONLY ";
 
-            var mainSql = "SELECT * FROM ( " + DelegateUserSelectQuery + DelegateUserFromTable + " ) D " + DelegatewhereConditon + orderBy;
+            var mainSql = "SELECT D.*, COUNT(*) OVER() AS TotalCount FROM ( " + DelegateUserSelectQuery + DelegateUserFromTable + " ) D " + DelegatewhereConditon + orderBy;
 
-            IEnumerable<DelegateUserCard> delegateUserCard = connection.Query<DelegateUserCard>(
+            var delegateUserCards = new List<DelegateUserCard>();
+            int resultCount = 0;
+
+            connection.Query<DelegateUserCard, int, DelegateUserCard>(
                 mainSql,
-                new
+                (delegateUserCard, totalCount) =>
                 {
-                    searchString,
-                    offSet,
-                    itemsPerPage,
-                    sortBy,
-                    sortDirection,
-                    centreId,
-                    isActive,
-                    isPasswordSet,
-                    isAdmin,
-                    isUnclaimed,
-                    isEmailVerified,
-                    registrationType,
-                    jobGroupId,
-                    groupId,
-                    answer1,
-                    answer2,
-                    answer3,
-                    answer4,
-                    answer5,
-                    answer6
+                    resultCount = resultCount == 0 ? totalCount : resultCount;
+                    delegateUserCards.Add(delegateUserCard);
+                    return delegateUserCard;
                 },
-                commandTimeout: 3000
-            );
-
-            var delegateCountQuery = @$"SELECT  COUNT(*) AS Matches FROM ( " + DelegateUserSelectQuery + DelegateUserFromTable + " ) D " + DelegatewhereConditon;
-
-            int ResultCount = connection.ExecuteScalar<int>(
-                delegateCountQuery,
                 new
                 {
                     searchString,
@@ -414,11 +396,13 @@ public List<DelegateUserCard> GetDelegateUserCardsForExportByCentreId(String sea
                     answer3,
                     answer4,
                     answer5,
-                    answer6,
+                    answer6
                 },
+                splitOn: "TotalCount",
                 commandTimeout: 3000
             );
-            return (delegateUserCard, ResultCount);
+
+            return (delegateUserCards, resultCount);
         }
 
         public List<DelegateUserCard> GetDelegatesNotRegisteredForGroupByGroupId(int groupId, int centreId)
diff --git a/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs b/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs
index 178e2f62dc..6c6af7ce4d 100644
--- a/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/UserDataService/UserDataService.cs
@@ -291,7 +291,7 @@ int centreId
         public bool PrimaryEmailInUseAtCentres(string email);
 
         public int? GetUserIdFromLearningHubAuthId(int learningHubAuthId);
-       void DeactivateAdminAccount(int userId, int centreId);
+        void DeactivateAdminAccount(int userId, int centreId);
         int? CheckDelegateIsActive(int delegateId);
     }
 
@@ -606,50 +606,59 @@ public void UpdateUserDetailsAccount(string firstName, string lastName, string p
             );
         }
         public (IEnumerable<SuperAdminDelegateAccount>, int) GetAllDelegates(
-      string search, int offset, int rows, int? delegateId, string accountStatus, string lhlinkStatus, int? centreId, int failedLoginThreshold
-      )
+        string search, int offset, int rows, int? delegateId, string accountStatus, string lhlinkStatus, int? centreId, int failedLoginThreshold)
         {
             if (!string.IsNullOrEmpty(search))
             {
                 search = search.Trim();
             }
-            string BaseSelectQuery = @$"SELECT
-                da.ID,
-                da.Active,
-                da.CentreID,
-                ce.CentreName,
-                ce.Active AS CentreActive,
-                da.DateRegistered,
-                da.LastAccessed,
-                da.CandidateNumber,
-                da.Approved,
-                da.SelfReg,
-                da.UserID,
-                da.RegistrationConfirmationHash,
-                u.ID as UserId,
-                 u.PrimaryEmail AS EmailAddress,
-                u.FirstName,
-                u.LastName,
-                u.Active as UserActive,
-                u.LearningHubAuthID,
-                u.EmailVerified,
-                ucd.ID as UserCentreDetailID,
-                ucd.UserID,
-                ucd.CentreID,
-                ucd.Email as CentreEmail,
-                ucd.EmailVerified as CentreEmailVerified,
-                (SELECT ID
-                    FROM AdminAccounts aa
-                        WHERE aa.UserID = da.UserID
-                            AND aa.CentreID = da.CentreID
-                            AND aa.Active = 1
-                ) AS AdminID
-            FROM DelegateAccounts AS da WITH (NOLOCK)
-            INNER JOIN Centres AS ce WITH (NOLOCK) ON ce.CentreId = da.CentreID
-            INNER JOIN Users AS u WITH (NOLOCK) ON u.ID = da.UserID
-            LEFT JOIN UserCentreDetails AS ucd WITH (NOLOCK) ON ucd.UserID = u.ID
-            AND ucd.CentreId = da.CentreID
-            INNER JOIN JobGroups AS jg WITH (NOLOCK) ON jg.JobGroupID = u.JobGroupID";
+
+            string activeAdminAccountsCte = @"
+                WITH ActiveAdminAccounts AS (
+                    SELECT
+                        aa.UserID,
+                        aa.CentreID,
+                        MIN(aa.ID) AS AdminID
+                    FROM AdminAccounts AS aa WITH (NOLOCK)
+                    WHERE aa.Active = 1
+                    GROUP BY aa.UserID, aa.CentreID
+                )";
+
+            string baseSelectQuery = @$"{activeAdminAccountsCte}
+                SELECT
+                    da.ID,
+                    da.Active,
+                    da.CentreID,
+                    ce.CentreName,
+                    ce.Active AS CentreActive,
+                    da.DateRegistered,
+                    da.LastAccessed,
+                    da.CandidateNumber,
+                    da.Approved,
+                    da.SelfReg,
+                    da.UserID,
+                    da.RegistrationConfirmationHash,
+                    u.ID as UserId,
+                    u.PrimaryEmail AS EmailAddress,
+                    u.FirstName,
+                    u.LastName,
+                    u.Active as UserActive,
+                    u.LearningHubAuthID,
+                    u.EmailVerified,
+                    ucd.ID as UserCentreDetailID,
+                    ucd.UserID,
+                    ucd.CentreID,
+                    ucd.Email as CentreEmail,
+                    ucd.EmailVerified as CentreEmailVerified,
+                    aaa.AdminID
+                FROM DelegateAccounts AS da WITH (NOLOCK)
+                INNER JOIN Centres AS ce WITH (NOLOCK) ON ce.CentreId = da.CentreID
+                INNER JOIN Users AS u WITH (NOLOCK) ON u.ID = da.UserID
+                LEFT JOIN UserCentreDetails AS ucd WITH (NOLOCK) ON ucd.UserID = u.ID
+                    AND ucd.CentreId = da.CentreID
+                LEFT JOIN ActiveAdminAccounts AS aaa WITH (NOLOCK)
+                    ON aaa.UserID = da.UserID
+                    AND aaa.CentreID = da.CentreID";
             string condition = $@" WHERE ((@delegateId = 0) OR (da.ID = @delegateId)) 	AND (u.FirstName + ' ' + u.LastName + ' ' + u.PrimaryEmail + ' ' + COALESCE(ucd.Email, '') + ' ' + COALESCE(da.CandidateNumber, '') LIKE N'%' + @search + N'%')
                                     AND ((ce.CentreID = @centreId) OR (@centreId= 0)) 
                                     AND ((@accountStatus = 'Any') OR (@accountStatus = 'Active' AND da.Active = 1 AND u.Active =1) OR (@accountStatus = 'Inactive' AND (u.Active = 0 OR da.Active =0)) 
@@ -657,7 +666,7 @@ LEFT JOIN UserCentreDetails AS ucd WITH (NOLOCK) ON ucd.UserID = u.ID
 	                                    OR (@accountStatus = 'Claimed' AND  da.RegistrationConfirmationHash is  null) OR (@accountStatus = 'Unclaimed' AND da.RegistrationConfirmationHash is not null))
                                     AND ((@lhlinkStatus = 'Any') OR (@lhlinkStatus = 'Linked' AND u.LearningHubAuthID IS NOT NULL) OR (@lhlinkStatus = 'Not linked' AND u.LearningHubAuthID IS NULL))";
 
-            string sql = @$"{BaseSelectQuery}{condition} ORDER BY LTRIM(u.LastName), LTRIM(u.FirstName)
+            string sql = @$"{baseSelectQuery}{condition} ORDER BY LTRIM(u.LastName), LTRIM(u.FirstName)
                             OFFSET @offset ROWS
                             FETCH NEXT @rows ROWS ONLY";
             IEnumerable<SuperAdminDelegateAccount> delegateEntity = connection.Query<SuperAdminDelegateAccount>(
@@ -667,13 +676,16 @@ OFFSET @offset ROWS
             );
 
             int ResultCount = connection.ExecuteScalar<int>(
-                            @$"SELECT  COUNT(*) AS Matches
+                            @$"{activeAdminAccountsCte}
+                            SELECT  COUNT(*) AS Matches
                             FROM DelegateAccounts AS da WITH (NOLOCK)
                             INNER JOIN Centres AS ce WITH (NOLOCK) ON ce.CentreId = da.CentreID
                             INNER JOIN Users AS u WITH (NOLOCK) ON u.ID = da.UserID
                             LEFT JOIN UserCentreDetails AS ucd WITH (NOLOCK) ON ucd.UserID = u.ID
                             AND ucd.CentreId = da.CentreID
-                            INNER JOIN JobGroups AS jg WITH (NOLOCK) ON jg.JobGroupID = u.JobGroupID {condition}",
+                            LEFT JOIN ActiveAdminAccounts AS aaa WITH (NOLOCK)
+                                ON aaa.UserID = da.UserID
+                                AND aaa.CentreID = da.CentreID {condition}",
                 new { delegateId, search, centreId, accountStatus, failedLoginThreshold, lhlinkStatus },
                 commandTimeout: 3000
             );

From 9baf9d8eabfc97cff1ea6ca9f62c640ed15b152a Mon Sep 17 00:00:00 2001
From: kevwhitt-hee <kevin.whittaker@hee.nhs.uk>
Date: Thu, 30 Jul 2026 15:50:01 +0100
Subject: [PATCH 2/2] Similar CTE and join optimisations for the centres data
 queries

---
 .../DataServices/CentresDataService.cs        | 152 ++++++++++--------
 1 file changed, 87 insertions(+), 65 deletions(-)

diff --git a/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs b/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs
index e539adb012..8d8d499d94 100644
--- a/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs
+++ b/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs
@@ -280,10 +280,7 @@ FROM Centres AS c
           int contractType,
           string centreStatus)
         {
-            if (!string.IsNullOrEmpty(search))
-            {
-                search = search.Trim();
-            }
+            search = string.IsNullOrWhiteSpace(search) ? string.Empty : search.Trim();
             string sql = @"SELECT c.CentreID,
                             c.CentreName,
                             c.ContactForename,
@@ -298,10 +295,10 @@ FROM Centres AS c
                         FROM Centres AS c
                         INNER JOIN Regions AS r ON r.RegionID = c.RegionID
                         INNER JOIN CentreTypes AS ct ON ct.CentreTypeId = c.CentreTypeId
-                        WHERE c.CentreName LIKE N'%' + @search + N'%'
-                        AND ((c.RegionID = @region) OR (@region = 0)) AND ((c.CentreTypeId = @centreType) OR (@centreType = 0))
-                        AND ((c.ContractTypeID = @contractType) OR (@contractType = 0)) AND ((@centreStatus = 'Any') OR (@centreStatus = 'Active' AND c.Active = 1) OR (@centreStatus = 'Inactive' AND c.Active = 0))
-                        ORDER BY LTRIM(c.CentreName)
+                        WHERE (@search = '' OR c.CentreName LIKE N'%' + @search + N'%')
+                        AND (@region = 0 OR c.RegionID = @region) AND (@centreType = 0 OR c.CentreTypeId = @centreType)
+                        AND (@contractType = 0 OR c.ContractTypeID = @contractType) AND ((@centreStatus = 'Any') OR (@centreStatus = 'Active' AND c.Active = 1) OR (@centreStatus = 'Inactive' AND c.Active = 0))
+                        ORDER BY c.CentreName
                             OFFSET @offset ROWS
                             FETCH NEXT @rows ROWS ONLY";
             IEnumerable<CentreEntity> centreEntity = connection.Query<Centre, CentreTypes, Regions, CentreEntity>(
@@ -314,11 +311,11 @@ OFFSET @offset ROWS
                 commandTimeout: 3000
             );
             int resultCount = connection.ExecuteScalar<int>(
-                                @$"SELECT  COUNT(*) AS Matches
+                                @"SELECT COUNT(*) AS Matches
                                 FROM Centres AS c
-                                INNER JOIN Regions AS r ON r.RegionID = c.RegionID
-                                INNER JOIN CentreTypes AS ct ON ct.CentreTypeId = c.CentreTypeId
-                                WHERE c.CentreName LIKE N'%' + @search + N'%' AND ((c.RegionID = @region) OR (@region = 0))  AND ((c.CentreTypeId = @centreType) OR (@centreType = 0)) AND ((c.ContractTypeID = @contractType) OR (@contractType = 0)) AND ((@centreStatus = 'Any') OR (@centreStatus = 'Active' AND c.Active = 1) OR (@centreStatus = 'Inactive' AND c.Active = 0))",
+                                WHERE (@search = '' OR c.CentreName LIKE N'%' + @search + N'%')
+                                AND (@region = 0 OR c.RegionID = @region) AND (@centreType = 0 OR c.CentreTypeId = @centreType)
+                                AND (@contractType = 0 OR c.ContractTypeID = @contractType) AND ((@centreStatus = 'Any') OR (@centreStatus = 'Active' AND c.Active = 1) OR (@centreStatus = 'Inactive' AND c.Active = 0))",
                     new { search, region, centreType, contractType, centreStatus },
                     commandTimeout: 3000
             );
@@ -742,8 +739,7 @@ public void ReactivateCentre(int centreId)
                         CustomCourses AS RoleLimitCustomCourses,
                         Trainers AS RoleLimitTrainers
                         FROM Centres
-                        WHERE (CentreId = @centreId) AND (Active = 1)
-                        ORDER BY CentreName",
+                        WHERE (CentreId = @centreId) AND (Active = 1)",
                 new { centreId }
             );
         }
@@ -844,12 +840,13 @@ ROLLBACK TRANSACTION
 
         public int ResultCount(string search, int region, int centreType, int contractType, string centreStatus)
         {
+            search = string.IsNullOrWhiteSpace(search) ? string.Empty : search.Trim();
             int resultCount = connection.ExecuteScalar<int>(
-                               @$"SELECT  COUNT(*) AS Matches
+                               @"SELECT COUNT(*) AS Matches
                                 FROM Centres AS c
-                                INNER JOIN Regions AS r ON r.RegionID = c.RegionID
-                                INNER JOIN CentreTypes AS ct ON ct.CentreTypeId = c.CentreTypeId
-                                WHERE c.CentreName LIKE N'%' + @search + N'%' AND ((c.RegionID = @region) OR (@region = 0))  AND ((c.CentreTypeId = @centreType) OR (@centreType = 0)) AND ((c.ContractTypeID = @contractType) OR (@contractType = 0)) AND ((@centreStatus = 'Any') OR (@centreStatus = 'Active' AND c.Active = 1) OR (@centreStatus = 'Inactive' AND c.Active = 0))",
+                                WHERE (@search = '' OR c.CentreName LIKE N'%' + @search + N'%')
+                                AND (@region = 0 OR c.RegionID = @region) AND (@centreType = 0 OR c.CentreTypeId = @centreType)
+                                AND (@contractType = 0 OR c.ContractTypeID = @contractType) AND ((@centreStatus = 'Any') OR (@centreStatus = 'Active' AND c.Active = 1) OR (@centreStatus = 'Inactive' AND c.Active = 0))",
                    new { search, region, centreType, contractType, centreStatus },
                    commandTimeout: 3000
            );
@@ -859,55 +856,80 @@ FROM Centres AS c
         public IEnumerable<CentresExport> GetAllCentresForSuperAdminExport(string search, int region,
           int centreType, int contractType, string centreStatus, int exportQueryRowLimit, int currentRun)
         {
-            if (!string.IsNullOrEmpty(search))
-            {
-                search = search.Trim();
-            }
+            search = string.IsNullOrWhiteSpace(search) ? string.Empty : search.Trim();
             string sql = @"SELECT
-                            CentreID,
-                            Active,
-                            CentreName,
-                            ContactSurname + ', ' + ContactForename AS Contact,          ContactEmail,
-                            ContactTelephone,
-                            (SELECT RegionName FROM Regions WHERE            (RegionID = c.RegionID)) AS RegionName,
-                             (SELECT CentreType FROM CentreTypes
-                               WHERE (CentreTypeID = c.CentreTypeID)) AS CentreType,
-                            IPPrefix,
-                            CentreCreated,
-                            (SELECT COUNT(*) AS Expr1 FROM Candidates
-                               WHERE (CentreID = c.CentreID)) AS Delegates,
-                            (SELECT COUNT(Progress.ProgressID) AS Registrations
-                               FROM Progress
-                               INNER JOIN                                    Candidates AS Candidates_1 ON Progress.CandidateID = Candidates_1.CandidateID
-                               WHERE (Candidates_1.CentreID = c.CentreID)) AS CourseEnrolments,
-                             (SELECT COUNT(Progress_1.ProgressID) AS Completions
-                               FROM Progress AS Progress_1
-                               INNER JOIN
-                               Candidates AS Candidates_1 ON Progress_1.CandidateID = Candidates_1.CandidateID
-                               WHERE (Progress_1.Completed IS NOT NULL) AND (Candidates_1.CentreID = c.CentreID)) AS CourseCompletions,
-                             (SELECT SUM(e.Duration) AS Expr1
-                               FROM Sessions AS e INNER JOIN
-                               Candidates AS Candidates_2 ON e.CandidateID = Candidates_2.CandidateID
-                               WHERE        (Candidates_2.CentreID = c.CentreID)) / 60 AS LearningHours,
-                             (SELECT COUNT(*) AS Expr1
-                               FROM  AdminUsers
-                               WHERE (CentreID = c.CentreID)) AS AdminUsers,
-                             (SELECT MAX(ADS.LoginTime) AS Expr1
-                               FROM AdminSessions AS ADS INNER JOIN
-                               AdminUsers AS ADU ON ADS.AdminID = ADU.AdminID
-                               WHERE (ADU.CentreID = c.CentreID)) AS LastAdminLogin,
-                             (SELECT MAX(Sessions.LoginTime) AS Expr1
-                               FROM Sessions INNER JOIN
-                               Customisations ON Sessions.CustomisationID = Customisations.CustomisationID
-                               WHERE (Customisations.CentreID = c.CentreID)) AS LastLearnerLogin, 
-							   (SELECT ContractType FROM ContractTypes WHERE ContractTypeID = c.ContractTypeID) AS ContractType, 
-							   CCLicences, ServerSpaceBytes, 
-                         ServerSpaceUsed
+                            c.CentreID,
+                            c.Active,
+                            c.CentreName,
+                            c.ContactSurname + ', ' + c.ContactForename AS Contact,
+                            c.ContactEmail,
+                            c.ContactTelephone,
+                            r.RegionName,
+                            ct.CentreType,
+                            c.IPPrefix,
+                            c.CentreCreated,
+                            ISNULL(d.Delegates, 0) AS Delegates,
+                            ISNULL(e.CourseEnrolments, 0) AS CourseEnrolments,
+                            ISNULL(co.CourseCompletions, 0) AS CourseCompletions,
+                            ISNULL(l.LearningHours, 0) AS LearningHours,
+                            ISNULL(a.AdminUsers, 0) AS AdminUsers,
+                            al.LastAdminLogin,
+                            ll.LastLearnerLogin,
+                            con.ContractType,
+                            c.CCLicences,
+                            c.ServerSpaceBytes,
+                            c.ServerSpaceUsed
                         FROM Centres AS c
-                        WHERE c.CentreName LIKE N'%' + @search + N'%'
-                        AND ((c.RegionID = @region) OR (@region = 0)) AND ((c.CentreTypeId = @centreType) OR (@centreType = 0))
-                        AND ((c.ContractTypeID = @contractType) OR (@contractType = 0)) AND ((@centreStatus = 'Any') OR (@centreStatus = 'Active' AND c.Active = 1) OR (@centreStatus = 'Inactive' AND c.Active = 0))
-                        ORDER BY LTRIM(c.CentreName)
+                        INNER JOIN Regions AS r ON r.RegionID = c.RegionID
+                        INNER JOIN CentreTypes AS ct ON ct.CentreTypeID = c.CentreTypeID
+                        INNER JOIN ContractTypes AS con ON con.ContractTypeID = c.ContractTypeID
+                        LEFT JOIN (
+                            SELECT CentreID, COUNT(*) AS Delegates
+                            FROM Candidates
+                            GROUP BY CentreID
+                        ) AS d ON d.CentreID = c.CentreID
+                        LEFT JOIN (
+                            SELECT ca.CentreID, COUNT(p.ProgressID) AS CourseEnrolments
+                            FROM Progress AS p
+                            INNER JOIN Candidates AS ca ON p.CandidateID = ca.CandidateID
+                            GROUP BY ca.CentreID
+                        ) AS e ON e.CentreID = c.CentreID
+                        LEFT JOIN (
+                            SELECT ca.CentreID, COUNT(p.ProgressID) AS CourseCompletions
+                            FROM Progress AS p
+                            INNER JOIN Candidates AS ca ON p.CandidateID = ca.CandidateID
+                            WHERE p.Completed IS NOT NULL
+                            GROUP BY ca.CentreID
+                        ) AS co ON co.CentreID = c.CentreID
+                        LEFT JOIN (
+                            SELECT ca.CentreID, SUM(s.Duration) / 60 AS LearningHours
+                            FROM Sessions AS s
+                            INNER JOIN Candidates AS ca ON s.CandidateID = ca.CandidateID
+                            GROUP BY ca.CentreID
+                        ) AS l ON l.CentreID = c.CentreID
+                        LEFT JOIN (
+                            SELECT CentreID, COUNT(*) AS AdminUsers
+                            FROM AdminUsers
+                            GROUP BY CentreID
+                        ) AS a ON a.CentreID = c.CentreID
+                        LEFT JOIN (
+                            SELECT au.CentreID, MAX(ads.LoginTime) AS LastAdminLogin
+                            FROM AdminSessions AS ads
+                            INNER JOIN AdminUsers AS au ON ads.AdminID = au.AdminID
+                            GROUP BY au.CentreID
+                        ) AS al ON al.CentreID = c.CentreID
+                        LEFT JOIN (
+                            SELECT ca.CentreID, MAX(s.LoginTime) AS LastLearnerLogin
+                            FROM Sessions AS s
+                            INNER JOIN Customisations AS cu ON s.CustomisationID = cu.CustomisationID
+                            INNER JOIN Candidates AS ca ON s.CandidateID = ca.CandidateID
+                            WHERE cu.CentreID = ca.CentreID
+                            GROUP BY ca.CentreID
+                        ) AS ll ON ll.CentreID = c.CentreID
+                        WHERE (@search = '' OR c.CentreName LIKE N'%' + @search + N'%')
+                        AND (@region = 0 OR c.RegionID = @region) AND (@centreType = 0 OR c.CentreTypeId = @centreType)
+                        AND (@contractType = 0 OR c.ContractTypeID = @contractType) AND ((@centreStatus = 'Any') OR (@centreStatus = 'Active' AND c.Active = 1) OR (@centreStatus = 'Inactive' AND c.Active = 0))
+                        ORDER BY c.CentreName
                             OFFSET @exportQueryRowLimit * (@currentRun - 1) ROWS
                             FETCH NEXT @exportQueryRowLimit ROWS ONLY";
             IEnumerable<CentresExport> centres = connection.Query<CentresExport>(
