Sql 连接大量CTE表(13000000行+)性能问题

Sql 连接大量CTE表(13000000行+)性能问题,sql,sql-server,tsql,sql-server-2008-r2,common-table-expression,Sql,Sql Server,Tsql,Sql Server 2008 R2,Common Table Expression,我们有一个生产数据库,可以提前几年管理100家分支机构的人员预订,精确到分钟 该系统的一部分是突出差距的报告,即比较分行营业时间和员工预订情况,查看是否有任何分行在无人预订的情况下营业 它还同时检查重叠、重复预订等,基本上需要分钟级别的准确性 我们这样做的方式是使用整数理货表将开放时间和预订的开始和结束时间扩展为分钟: --===== Create and populate the Tally table on the fly SELECT TOP 16777216 IDENT

我们有一个生产数据库,可以提前几年管理100家分支机构的人员预订,精确到分钟

该系统的一部分是突出差距的报告,即比较分行营业时间和员工预订情况,查看是否有任何分行在无人预订的情况下营业

它还同时检查重叠、重复预订等,基本上需要分钟级别的准确性

我们这样做的方式是使用整数理货表将开放时间和预订的开始和结束时间扩展为分钟:

--===== Create and populate the Tally table on the fly
 SELECT TOP 16777216
        IDENTITY(INT,1,1) AS N
   INTO dbo.Tally
   FROM Master.dbo.SysColumns sc1,
        Master.dbo.SysColumns sc2,
        Master.dbo.SysColumns sc3

--===== Add a Primary Key to maximize performance
  ALTER TABLE dbo.Tally
    ADD CONSTRAINT PK_Tally_N 
        PRIMARY KEY CLUSTERED (N) WITH FILLFACTOR = 100
我们利用此静态索引理货表扩展开放时间和预订量,如下所示:

SELECT   [BranchID] ,
        [DayOfWeek] ,
        DATEADD(MINUTE, N - 1, StartTime)
FROM     OpeningHours
        LEFT OUTER JOIN tally ON tally.N BETWEEN 0
                                         AND     DATEDIFF(MINUTE, OpeningHours.StartTime, OpeningHours.EndTime) + 1
问题是,一旦我们有了13000000开放分钟和预定分钟,我们就需要加入结果,看看覆盖了什么:

SELECT   OpenDatesAndMinutes.[Date] ,
                                OpenDatesAndMinutes.[Time] ,
                                OpenDatesAndMinutes.[BranchID] ,
                                ISNULL(BookedMinutes.BookingCount, 0) AS BookingCount
                       FROM     OpenDatesAndMinutes
                                LEFT OUTER JOIN BookedMinutes ON OpenDatesAndMinutes.BranchID = BookedMinutes.BranchID
                                                                 AND OpenDatesAndMinutes.[Date] = BookedMinutes.[Date]
                                                                 AND OpenDatesAndMinutes.[Time] = BookedMinutes.[Time]
正如您可以想象的那样,将13000000行存储在CTE表中的分支、日期和时间连接起来需要很长时间——运行一周并不太糟糕,大约10秒,但如果我们运行6个月,13000000分钟会膨胀到25分钟+

一旦我们将开放会议记录加入到预定会议记录中,我们就会将数据分组到孤岛上,并向用户展示:

CrossTabPrep ( [Date], [Time], [BranchID], [BookingCount], [Grp] )
  AS ( SELECT   [Date] ,
                [Time] ,
                [BranchID] ,
                [BookingCount] ,
                DATEPART(HOUR, Time) * 60 + DATEPART(MINUTE, Time) - ROW_NUMBER() OVER ( PARTITION BY [BranchID], Date, [BookingCount] ORDER BY Time ) AS [Grp]
       FROM     PreRender
     ),
FinalRender ( [BranchID], [Date], [Start Time], [End Time], [Duration], [EntryCount], [EntryColour] )
  AS ( SELECT   [BranchID] ,
                [Date] ,
                MIN([Time]) AS [Start Time] ,
                MAX([Time]) AS [End Time] ,
                ISNULL(DATEDIFF(MINUTE, MIN([Time]), MAX([Time])), 0) AS Duration ,
                [BookingCount] AS EntryCount ,
                CASE WHEN [BookingCount] = 0 THEN 'Red'
                     WHEN [BookingCount] = 1 THEN 'Green'
                     ELSE 'Yellow'
                END AS EntryColour
       FROM     CrossTabPrep
       GROUP BY [BranchID] ,
                [Date] ,
                [BookingCount] ,
                [Grp]
     )
很简单,我的方法有效吗?在保持分钟级精度的同时,是否有任何方法可以改进此方法?当处理像这样的大量CTE表时,将这些数据转储到索引的临时表中并连接它们会有什么好处

我考虑的另一件事是替换大连接使用的日期和时间0数据类型,如果我将这些数据类型转换为整数,效率会更高吗

以下是案例中的完整CTE,有助于:

WITH    OpeningHours ( [BranchID], [DayOfWeek], [StartTime], [EndTime] )
          AS ( SELECT   BranchID ,
                        DayOfWeek ,
                        CONVERT(TIME(0), AM_open) ,
                        CONVERT(TIME(0), AM_close)
               FROM     db_BranchDetails.dbo.tbl_ShopOpeningTimes (NOLOCK)
                        INNER JOIN @tbl_Days Filter_Days ON db_BranchDetails.dbo.tbl_ShopOpeningTimes.DayOfWeek = Filter_Days.DayNumber
               WHERE    CONVERT(TIME(0), AM_open) <> CONVERT(TIME(0), '00:00:00')
               UNION ALL
               SELECT   BranchID ,
                        DayOfWeek ,
                        CONVERT(TIME(0), PM_open) ,
                        CONVERT(TIME(0), PM_close)
               FROM     db_BranchDetails.dbo.tbl_ShopOpeningTimes (NOLOCK)
                        INNER JOIN @tbl_Days Filter_Days ON db_BranchDetails.dbo.tbl_ShopOpeningTimes.DayOfWeek = Filter_Days.DayNumber
               WHERE    CONVERT(TIME(0), PM_open) <> CONVERT(TIME(0), '00:00:00')
               UNION ALL
               SELECT   BranchID ,
                        DayOfWeek ,
                        CONVERT(TIME(0), EVE_open) ,
                        CONVERT(TIME(0), EVE_close)
               FROM     db_BranchDetails.dbo.tbl_ShopOpeningTimes (NOLOCK)
                        INNER JOIN @tbl_Days Filter_Days ON db_BranchDetails.dbo.tbl_ShopOpeningTimes.DayOfWeek = Filter_Days.DayNumber
               WHERE    CONVERT(TIME(0), EVE_open) <> CONVERT(TIME(0), '00:00:00')
             ),
        DateRange ( [Date], [DayOfWeek] )
          AS ( SELECT   CONVERT(DATE, DATEADD(DAY, N - 1, @StartDate)) ,
                        DATEPART(WEEKDAY, DATEADD(DAY, N - 1, @StartDate))
               FROM     tally (NOLOCK)
               WHERE    N <= DATEDIFF(DAY, @StartDate, @EndDate) + 1
             ),
        OpenMinutes ( [BranchID], [DayOfWeek], [Time] )
          AS ( SELECT   [BranchID] ,
                        [DayOfWeek] ,
                        DATEADD(MINUTE, N - 1, StartTime)
               FROM     OpeningHours
                        LEFT OUTER JOIN tally ON tally.N BETWEEN 0
                                                         AND     DATEDIFF(MINUTE, OpeningHours.StartTime, OpeningHours.EndTime) + 1
             ),
        OpenDatesAndMinutes ( [Date], [Time], [BranchID] )
          AS ( SELECT   DateRange.[Date] ,
                        OpenMinutes.[Time] ,
                        OpenMinutes.BranchID
               FROM     DateRange
                        LEFT OUTER JOIN OpenMinutes ON DateRange.DayOfWeek = OpenMinutes.DayOfWeek
               WHERE    OpenMinutes.BranchID IS NOT NULL
             ),
        WhiteListEmployees ( [DET_NUMBERA] )
          AS ( SELECT   DET_NUMBERA
               FROM     [dbo].[tbl_ChrisCache_WhiteList]
               WHERE    [TimeSheetV2_SecurityContext] = @TimeSheetV2_SecurityContext
             ),
        BookedMinutesByRole ( [Date], [Time], [BranchID], BookingCount )
          AS ( SELECT   [BookingDate] ,
                        DATEADD(MINUTE, N - 1, StartTime) ,
                        BranchID ,
                        COUNT(BookingID) AS Bookings
               FROM     tbl_Booking (NOLOCK)
                        INNER JOIN tbl_BookingReason  (NOLOCK) ON dbo.tbl_BookingReason.ReasonID = dbo.tbl_Booking.ReasonID
                        INNER JOIN tbl_ChrisCache  (NOLOCK) ON dbo.tbl_Booking.DET_NUMBERA = dbo.tbl_ChrisCache.DET_NUMBERA
                        INNER JOIN @ValidPosCodes AS Filter_PostCodes ON dbo.tbl_ChrisCache.POS_NUMBERA = Filter_PostCodes.POSCODE
                        LEFT OUTER JOIN tally (NOLOCK) ON tally.N BETWEEN 0
                                                                  AND     DATEDIFF(MINUTE, tbl_Booking.StartTime, tbl_Booking.EndTime) + 1
               WHERE    ( Void = 0 )
                        AND tbl_BookingReason.CoverRequired = 0 --#### Only use bookings that dont require cover
                        AND tbl_booking.BranchID <> '023'   --#### Branch 23 will always have messy data
                        AND ( dbo.tbl_Booking.BookingDate BETWEEN @StartDate
                                                          AND     @EndDate )
               GROUP BY [BookingDate] ,
                        BranchID ,
                        DATEADD(MINUTE, N - 1, StartTime)
             ),
        BookedMinutesByWhiteList ( [Date], [Time], [BranchID], BookingCount )
          AS ( SELECT   [BookingDate] ,
                        DATEADD(MINUTE, N - 1, StartTime) ,
                        BranchID ,
                        COUNT(BookingID) AS Bookings
               FROM     tbl_Booking(NOLOCK)
                        INNER JOIN tbl_BookingReason (NOLOCK) ON dbo.tbl_BookingReason.ReasonID = dbo.tbl_Booking.ReasonID
                        INNER JOIN tbl_ChrisCache (NOLOCK) ON dbo.tbl_Booking.DET_NUMBERA = dbo.tbl_ChrisCache.DET_NUMBERA
                        INNER JOIN WhiteListEmployees Filter_WhiteList ON dbo.tbl_Booking.DET_NUMBERA = Filter_WhiteList.DET_NUMBERA
                        LEFT OUTER JOIN tally (NOLOCK) ON tally.N BETWEEN 0
                                                                  AND     DATEDIFF(MINUTE, tbl_Booking.StartTime, tbl_Booking.EndTime) + 1
               WHERE    ( Void = 0 )
                        AND tbl_BookingReason.CoverRequired = 0 --#### Only use bookings that dont require cover
                        AND tbl_booking.BranchID <> '023'   --#### Branch 23 will always have messy data
                        AND ( dbo.tbl_Booking.BookingDate BETWEEN @StartDate
                                                          AND     @EndDate )
               GROUP BY [BookingDate] ,
                        BranchID ,
                        DATEADD(MINUTE, N - 1, StartTime)
             ),
        BookedMinutes ( [Date], [Time], [BranchID], BookingCount )
          AS ( SELECT   [Date] ,
                        [Time] ,
                        [BranchID] ,
                        BookingCount
               FROM     BookedMinutesByRole
               UNION
               SELECT   [Date] ,
                        [Time] ,
                        [BranchID] ,
                        BookingCount
               FROM     BookedMinutesByWhiteList
             ),
        PreRender ( [Date], [Time], [BranchID], [BookingCount] )
          AS ( SELECT   OpenDatesAndMinutes.[Date] ,
                        OpenDatesAndMinutes.[Time] ,
                        OpenDatesAndMinutes.[BranchID] ,
                        ISNULL(BookedMinutes.BookingCount, 0) AS BookingCount
               FROM     OpenDatesAndMinutes
                        LEFT OUTER JOIN BookedMinutes ON OpenDatesAndMinutes.BranchID = BookedMinutes.BranchID
                                                         AND OpenDatesAndMinutes.[Date] = BookedMinutes.[Date]
                                                         AND OpenDatesAndMinutes.[Time] = BookedMinutes.[Time]
             ),
        CrossTabPrep ( [Date], [Time], [BranchID], [BookingCount], [Grp] )
          AS ( SELECT   [Date] ,
                        [Time] ,
                        [BranchID] ,
                        [BookingCount] ,
                        DATEPART(HOUR, Time) * 60 + DATEPART(MINUTE, Time) - ROW_NUMBER() OVER ( PARTITION BY [BranchID], Date, [BookingCount] ORDER BY Time ) AS [Grp]
               FROM     PreRender
             ),
        DeletedBranches ( [BranchID] )
          AS ( SELECT   [ShopNo]
               FROM     [dbo].[vw_BranchList]
               WHERE    [Branch_Deleted] = 1
             ),
        FinalRender ( [BranchID], [Date], [Start Time], [End Time], [Duration], [EntryCount], [EntryColour] )
          AS ( SELECT   [BranchID] ,
                        [Date] ,
                        MIN([Time]) AS [Start Time] ,
                        MAX([Time]) AS [End Time] ,
                        ISNULL(DATEDIFF(MINUTE, MIN([Time]), MAX([Time])), 0) AS Duration ,
                        --dbo.format_timeV2(ISNULL(DATEDIFF(SECOND, MIN([Time]), MAX([Time])), 0)) AS DurationF ,
                        [BookingCount] AS EntryCount ,
                        CASE WHEN [BookingCount] = 0 THEN 'Red'
                             WHEN [BookingCount] = 1 THEN 'Green'
                             ELSE 'Yellow'
                        END AS EntryColour
               FROM     CrossTabPrep
               GROUP BY [BranchID] ,
                        [Date] ,
                        [BookingCount] ,
                        [Grp]
             )
            SELECT  [BranchID] ,
                    CONVERT(VARCHAR(10), DATEADD(DAY, 7, CONVERT(DATETIME, CONVERT(VARCHAR(10), DATEADD(day, -1 - ( DATEPART(dw, [Date]) + @@DATEFIRST - 2 ) % 7, [Date]), 103) + ' 23:59:59', 103)), 103) AS WeekEnding ,
                    [Date] ,
                    [Start Time] ,
                    [End Time] ,
                    [Duration] ,
                    CONVERT(VARCHAR, ( [Duration] * 60 ) / 3600) + 'h ' + CONVERT(VARCHAR, ROUND(( ( CONVERT(FLOAT, ( ( [Duration] * 60 ) % 3600 )) ) / 3600 ) * 60, 0)) + 'm' AS [DurationF] ,
                    [EntryCount] ,
                    [EntryColour] ,
                    CASE WHEN [EntryCount] = 0 THEN 'Red'
                         WHEN [EntryCount] >= 1 THEN 'Green'
                    END AS DurationColour ,
                    CASE WHEN [EntryCount] = 0 THEN 'This period of open-time isnt covered'
                         WHEN [EntryCount] >= 1 THEN 'This period of open-time is covered by ' + CONVERT(VARCHAR, [EntryCount]) + ' booking(s)'
                    END AS [DurationComment]
            FROM    FinalRender
            WHERE   FinalRender.BranchID NOT IN ( SELECT    [BranchID]
                                                  FROM      DeletedBranches )

这很有趣,因为你在最后用你的问题回答了你自己的问题。您只需尝试一下,但要总结一下:

实现CTE以获得更好的性能。您永远不知道SQL Server何时会多次评估CTE 您可以针对临时表构建indexex。 我不知道你是如何从[DayOfWeek]、DATEADDMINUTE、N-1、StartTime跳到另一个[Date]、[Time]的连接的,但是这里有两列是没有意义的。使用单个日期时间或表示历元秒数的bigint。UnixTimestamp在这里工作得很好。
我的建议不是基于您的数据,而是基于生成的测试数据,因此它可能不完全适用

建议:为了从性能的二次退化至少转变为线性退化,如果数据在批周期中平均分布,则可以使用批处理

在下面的示例中,以3天的批处理间隔处理2年的预订,每个分支机构每天需要2分30秒才能恢复免费时段

测试运行结果:

2 years - 2 minutes and 30 seconds 
4 years - 4 minutes and 55 seconds.
6 years - 6 minutes and 41 seconds
它采用了与讨论中使用的相同的逻辑,即使用数字查找不匹配的分钟数

架构和测试数据创建:

    IF OBJECT_ID('vwRandomNumber') IS NOT NULL
        DROP VIEW vwRandomNumber
    GO
    IF OBJECT_ID('dbo.fnRandNumber') IS NOT NULL
    DROP FUNCTION  dbo.fnRandNumber
    GO
    IF OBJECT_ID('dbo.fnRandomInt') IS NOT NULL
    DROP FUNCTION dbo.fnRandomInt
    GO
    IF OBJECT_ID('tblNumbers') IS NOT NULL
    DROP TABLE dbo.tblNumbers
    GO
    IF OBJECT_ID('Branches') IS NOT NULL
    DROP TABLE Branches
    GO
    IF OBJECT_ID('OpeningHours') IS NOT NULL
    DROP TABLE OpeningHours
    GO
    IF OBJECT_ID('Bookings') IS NOT NULL
    DROP TABLE Bookings
    GO

    CREATE VIEW vwRandomNumber
    AS
    SELECT Rand() RandomNumber;
    GO

    CREATE FUNCTION dbo.fnRandNumber()
    RETURNS FLOAT
    AS
    BEGIN
      RETURN (SELECT TOP 1 RandomNumber FROM vwRandomNumber)
    END;
    GO

    CREATE FUNCTION dbo.fnRandomInt(@FromNumber INT, @ToNumber INT)
    RETURNS INT
    AS
    BEGIN
      RETURN (@FromNumber + ROUND(dbo.fnRandNumber()*(@ToNumber - @FromNumber),0))
    END;
    GO

    CREATE TABLE tblNumbers 
    (
       NumberID INT PRIMARY KEY 
    )

    CREATE TABLE Branches
    (
       BranchID INT
      ,BranchName NVARCHAR(100)
    );
    GO

    ;WITH cteNumbers AS (
      SELECT 1 N 
      UNION ALL
      SELECT N+1 FROM cteNumbers WHERE N<100
    )
    INSERT INTO
        Branches
    SELECT N, CAST(NEWID() AS NVARCHAR(100)) FROM cteNumbers
    OPTION(MAXRECURSION 0)

    CREATE TABLE OpeningHours
    (
        BranchID INT 
      , Date DATETIME
      , OpenFrom DATETIME
      , OpenTo DATETIME 
    );
    GO

    CREATE CLUSTERED INDEX CIX_OpeningHours
    ON OpeningHours ([Date], [BranchID])

    GO

    CREATE TABLE Bookings
    (
         BranchID INT
       , BookingDate DATETIME
       , BookingFrom DATETIME
       , BookingTo DATETIME  
    )

    CREATE CLUSTERED INDEX CIX_Bookings
    ON Bookings ([BookingDate],[BranchID])

    DECLARE @StartDate DATETIME = DATEADD(month,0,DATEADD(D,0,DATEDIFF(d,0,GETDATE())))

    ;WITH cteNumbers AS (
      SELECT 1 N 
      UNION ALL
      SELECT N+1 FROM cteNumbers WHERE N<2000
    )
    INSERT INTO
      OpeningHours
      (
          BranchID
        , Date
        , OpenFrom
        , OpenTo
      )
    SELECT
      Branches.BranchID
    , Dates.Day
    , DATEADD(hour,7,Dates.Day)
    , DATEADD(hour,19,Dates.Day)
    FROM
      (
        SELECT 
          DATEADD(d,N,@StartDate) Day
        FROM
          cteNumbers
      ) Dates
    CROSS JOIN
      Branches
    OPTION(MAXRECURSION 0);

    INSERT INTO Bookings
    SELECT 
       OpeningHours.BranchID
      ,OpeningHours.Date
      ,BookingHours.StartDate
      ,BookingHours.ToDate
    FROM
      OpeningHours
    CROSS APPLY
      (
         SELECT DATEADD(hour, dbo.fnRandomInt(0,3), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(4,9), OpeningHours.OpenFrom) ToDate UNION ALL
         SELECT DATEADD(hour, dbo.fnRandomInt(1,5), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(6,9), OpeningHours.OpenFrom) UNION ALL
         SELECT DATEADD(hour, dbo.fnRandomInt(2,5), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(5,8), OpeningHours.OpenFrom) TODate UNION ALL
          SELECT DATEADD(hour, dbo.fnRandomInt(0,3), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(4,9), OpeningHours.OpenFrom) ToDate UNION ALL
         SELECT DATEADD(hour, dbo.fnRandomInt(1,5), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(6,9), OpeningHours.OpenFrom) UNION ALL
         SELECT DATEADD(hour, dbo.fnRandomInt(2,5), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(5,8), OpeningHours.OpenFrom) TODate UNION ALL
         SELECT DATEADD(hour, dbo.fnRandomInt(0,3), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(4,9), OpeningHours.OpenFrom) ToDate UNION ALL
         SELECT DATEADD(hour, dbo.fnRandomInt(1,5), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(6,9), OpeningHours.OpenFrom) UNION ALL
         SELECT DATEADD(hour, dbo.fnRandomInt(2,5), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(5,8), OpeningHours.OpenFrom) TODate UNION ALL
          SELECT DATEADD(hour, dbo.fnRandomInt(0,3), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(4,9), OpeningHours.OpenFrom) ToDate UNION ALL
         SELECT DATEADD(hour, dbo.fnRandomInt(1,5), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(6,9), OpeningHours.OpenFrom) UNION ALL
         SELECT DATEADD(hour, dbo.fnRandomInt(2,5), OpeningHours.OpenFrom) StartDate
                ,DATEADD(hour, dbo.fnRandomInt(5,8), OpeningHours.OpenFrom) TODate 
      ) BookingHours;

    ;WITH cteNumbers AS (
      SELECT 1 N 
      UNION ALL
      SELECT N+1 FROM cteNumbers WHERE N<5000
    )
    INSERT INTO
        tblNumbers
    SELECT N FROM cteNumbers
    OPTION(MAXRECURSION 0)

    --SELECT COUNT(*) FROM Bookings WHERE 
获取无预订时段的脚本:

    SET NOCOUNT ON

    IF OBJECT_ID('tblBranchFreePeriods') IS NOT NULL
        DROP TABLE tblBranchFreePeriods

    IF OBJECT_ID('tblFreeMinutes') IS NOT NULL
        DROP TABLE tblFreeMinutes

    CREATE TABLE tblBranchFreePeriods
    (
          BranchID INT
        , Date DATETIME
        , PeriodStartDate DATETIME
        , PeriodEndDate DATETIME
    )

    CREATE TABLE tblFreeMinutes 
    (
         BranchID INT 
        ,Date DATETIME
        ,FreeMinute INT
    )

    IF OBJECT_ID('dbo.tblStartDates') IS NOT NULL
                DROP TABLE tblStartDates

    CREATE TABLE tblStartDates
    (
          BranchID INT
        , Date DATETIME 
        , PeriodStartDate DATETIME
    )

    CREATE CLUSTERED INDEX CIX_tblStartDates
        ON tblStartDates([BranchID],[Date])

    IF OBJECT_ID('dbo.tblEndDates') IS NOT NULL
        DROP TABLE tblEndDates

    CREATE TABLE tblEndDates
    (
          BranchID INT
        , Date DATETIME 
        , PeriodEndDate DATETIME
    )

    CREATE CLUSTERED INDEX CIX_tblEndDate
        ON tblEndDates ([BranchID],[Date])


    CREATE CLUSTERED INDEX CIX_tblFreeMinutes
        ON tblFreeMinutes ([BranchID],[Date],FreeMinute)

    DECLARE @ProcessFromDate DATETIME, @ProcessTo DATETIME
    SELECT @ProcessFromDate = MIN(OpenFrom), @ProcessTo = DATEADD(year,2,@ProcessFromDate) FROM OpeningHours 

    DECLARE @BatchSize INT = 3

    DECLARE @StartTime DATETIME = GETDATE()

    WHILE (@ProcessFromDate <= @ProcessTo) BEGIN

            TRUNCATE TABLE tblFreeMinutes
            TRUNCATE TABLE tblStartDates
            TRUNCATE TABLE tblEndDates

            SET @StartTime = GETDATE()              

            DECLARE @DateFrom DATETIME = @ProcessFromDate, @DateTo DATETIME = DATEADD(d,@BatchSize,@ProcessFromDate)

            PRINT 'Date From ' + CAST(@DateFrom AS NVARCHAR(50))
            PRINT 'Date To ' + CAST(@DateTO AS NVARCHAR(50))

            INSERT INTO
                tblFreeMinutes
            SELECT
                OpeningHours.BranchID
               ,OpeningHours.Date
               ,tblOpeningHourMinutes.NumberID Minute   
            FROM
                OpeningHours
            INNER JOIN
              tblNumbers tblOpeningHourMinutes
            ON
                NumberID 
                    BETWEEN DATEDIFF(minute,OpeningHours.Date,OpeningHours.OpenFrom)
                AND
                    DATEDIFF(minute,OpeningHours.Date,OpeningHours.OpenTo)
            LEFT OUTER JOIN
               Bookings
            ON
                    Bookings.BookingDate = OpeningHours.Date   
                AND
                    Bookings.BranchID = OpeningHours.BranchID 
                AND
                    tblOpeningHourMinutes.NumberID
               BETWEEN
                   DATEDIFF(minute,Bookings.BookingDate,Bookings.BookingFrom)
               AND
                   DATEDIFF(minute,Bookings.BookingDAte,Bookings.BookingTo)              
            WHERE
               OpeningHours.Date BETWEEN @DateFrom AND @DateTo
            AND
               Bookings.BookingDate IS NULL
            OPTION ( FORCE ORDER )

            PRINT 'Populate free minutes ' + CAST(DATEDIFF(millisecond,@StartTime,GETDATE()) AS NVARCHAR(50))
            SET @StartTime = GETDATE()


            INSERT INTO
                tblStartDates
            SELECT 
                  tblFreeMinutes.BranchID
                , tblFreeMinutes.Date
                , DATEADD(minute,tblFreeMInutes.FreeMinute,tblFreeMinutes.Date)
            FROM
                tblFreeMinutes
            LEFT OUTER JOIN
                tblFreeMinutes tblFreeMinutesIn  
            ON
                tblFreeMinutesIn.Date = tblFreeMinutes.Date
            AND
                tblFreeMinutesIn.BranchID = tblFreeMinutes.BranchID
            AND
                tblFreeMinutesIn.FreeMinute = tblFreeMinutes.FreeMinute-1
            WHERE
                tblFreeMinutesIn.BranchID IS NULL

            PRINT 'Populate start dates ' + CAST(DATEDIFF(millisecond,@StartTime,GETDATE()) AS NVARCHAR(50))
            SET @StartTime = GETDATE()

            INSERT INTO
                tblEndDates
            SELECT 
                  tblFreeMinutes.BranchID
                , tblFreeMinutes.Date
                , DATEADD(minute,tblFreeMInutes.FreeMinute,tblFreeMinutes.Date)
            FROM
                tblFreeMinutes
            LEFT OUTER JOIN
                tblFreeMinutes tblFreeMinutesIn  
            ON
                tblFreeMinutesIn.Date = tblFreeMinutes.Date
            AND
                tblFreeMinutesIn.BranchID = tblFreeMinutes.BranchID
            AND
                tblFreeMinutesIn.FreeMinute = tblFreeMinutes.FreeMinute+1
            WHERE
                tblFreeMinutesIn.BranchID IS NULL

            PRINT 'Populate end dates ' + CAST(DATEDIFF(millisecond,@StartTime,GETDATE()) AS NVARCHAR(50))
            SET @StartTime = GETDATE()

            INSERT INTO
                tblBranchFreePeriods
            SELECT 
                  tblStartDates.BranchID
                , tblStartDates.Date
                , tblStartDates.PeriodStartDate
                , tblEndDate.PeriodEndDate 
            FROM 
                tblStartDates 
            CROSS APPLY
                (
                    SELECT TOP 1 
                        *
                    FROM
                        tblEndDates
                    WHERE
                        tblEndDates.BranchID = tblStartDates.BranchID
                    AND
                        tblEndDates.Date = tblStartDates.Date
                    AND
                        tblEndDates.PeriodEndDate > tblStartDates.PeriodStartDate
                    ORDER BY
                        PeriodEndDate ASC
                ) tblEndDate

            PRINT 'Return intervals ' + CAST(DATEDIFF(millisecond,@StartTime,GETDATE()) AS NVARCHAR(50))
            SET @StartTime = GETDATE()

            SET @ProcessFromDate = DATEADD(d,@BatchSize+1,@ProcessFromDate)

            PRINT ''
            PRINT ''

            RAISERROR ('',0,0) WITH NOWAIT

            --SELECT * FROM tblBranchFreePeriods

           --BREAK      
    END

    SELECT 
        *
    FROM
        tblBranchFreePeriods
    ORDER BY
        1,2,3

代码中的内联注释会很有帮助。太不清楚了。您有两个CTE,但不显示使用它们的查询。请参阅连接分支,但没有名为Branch的表。@Blam我不知道扩展的CTE的其余部分如何帮助解决连接13000000行的问题?但不管怎样,我已经编辑了我的问题,以包括完整的CTE。Materialize正在向表中写入。永久性或临时性。CTE只是语法,您无法真正控制它的求值次数。好的,有一些技巧可以使用CTE。使用临时表,您可以强制对其进行一次计算,更重要的是创建一个索引。不要等到最后才排除DeleteBranchs。尽快这样做。你能确认一下你所说的物化是什么意思吗?很可能他的意思是将CTE的结果存储在永久表中并对它们进行索引,而不是依赖内存。物化=创建一个临时表。你可以索引一个临时表,但不能索引一个CTE。看起来像是将CTE拆分成两个插入来填充两个索引的临时表,然后将它们合并起来,这绝对是一种方法-看起来我明天会忙得不可开交,为答案干杯