升级到 SQL Server 2019 后,函数抛出“内存不足”错误

dsu*_*aro 11 sql-server functions sql-server-2019

我已将 SQL Server 2012 数据库(大小为 8GB)移至具有相同内存和 CPU 配置的新设置的 SQL Server 2019 虚拟机,并将兼容级别更改为 SQL Server 2019。

我的应用程序中的所有内容都运行良好,除了一个存储过程,它包含一个带有两个参数的大 SQL 查询(并且没有花哨的选项)。当这个 SP 执行时,它让 SQL Server 进程的内存上升到指定的最大级别,然后返回错误:

“内存不足,无法运行此查询”

当我在 SSMS 的单独查询窗口中执行 SQL 查询(在存储过程中)时,它会立即执行并返回预期的 300 行。此外,当我将数据库的兼容性级别更改为“SQL Server 2017”并执行存储过程时,一切正常。

我首先认为这可能是参数嗅探问题,但没有一种解决方法有帮助(例如OPTION (RECOMPILE))。

我已经将问题深入到标量值函数的调用中。每次调用这个函数,都会出现内存错误。

这是该函数的 DDL(抱歉,部分是德语):

CREATE FUNCTION [dbo].[GetWtmTime] (
    @WorkTimeModelID uniqueidentifier,
    @Date DATETIME,
    @SequenceNo TINYINT)
  RETURNS VARCHAR(5)
AS
BEGIN
    -- SET DATEFIRST 7; has to be executed before calling this function
    DECLARE @WtmTime VARCHAR(5)
    DECLARE @WtmWeeks INT
    DECLARE @WtmTakeHolidays BIT
    DECLARE @WtmMaxMemberCount TINYINT
    SELECT @WtmWeeks = AnzahlWochen
         , @WtmTakeHolidays = ÜbernimmtFeiertage 
         , @WtmMaxMemberCount = MaxAnzahlMitglieder
    FROM Arbeitszeitmodelle 
    WHERE ArbeitszeitmodellID = @WorkTimeModelID;
    IF @WtmWeeks = 1
    BEGIN
        IF (dbo.IstFeiertag(@Date, 0) = 1     -- Holiday
            AND @WtmMaxMemberCount = 1)
        BEGIN
            IF @WtmTakeHolidays = 0
            BEGIN 
                IF @Date >= '20130901'
                    SET @WtmTime = 'KD'
                ELSE
                    SET @WtmTime = 'ZA';
            END ELSE
            BEGIN
                IF EXISTS ( SELECT *
                            FROM AzmWochen
                            WHERE ArbeitszeitmodellID = @WorkTimeModelID
                                AND Folgenummer = @SequenceNo
                                AND AzmZeitMo IN ('KD','T')
                                AND AzmZeitDi IN ('KD','T')
                                AND AzmZeitMi IN ('KD','T')
                                AND AzmZeitDo IN ('KD','T')
                                AND AzmZeitFr IN ('KD','T')
                                AND AzmZeitSa IN ('KD','T')
                                AND AzmZeitSo IN ('KD','T') )
                    SET @WtmTime = 'T';
                ELSE
                    SET @WtmTime = 'G';
            END
        END ELSE IF DATEPART(dw, @Date) = 1             -- Sunday
            SELECT @WtmTime = AzmZeitSo FROM AzmWochen 
                WHERE ArbeitszeitmodellID = @WorkTimeModelID
                    AND Folgenummer = @SequenceNo;
        ELSE IF DATEPART(dw, @Date) = 2             -- Monday
            SELECT @WtmTime = AzmZeitMo FROM AzmWochen 
                WHERE ArbeitszeitmodellID = @WorkTimeModelID
                    AND Folgenummer = @SequenceNo;
        ELSE IF DATEPART(dw, @Date) = 3             -- Tuesday
            SELECT @WtmTime = AzmZeitDi FROM AzmWochen 
                WHERE ArbeitszeitmodellID = @WorkTimeModelID
                    AND Folgenummer = @SequenceNo;
        ELSE IF DATEPART(dw, @Date) = 4             -- Wednesday
            SELECT @WtmTime = AzmZeitMi FROM AzmWochen 
                WHERE ArbeitszeitmodellID = @WorkTimeModelID
                    AND Folgenummer = @SequenceNo;
        ELSE IF DATEPART(dw, @Date) = 5             -- Thursday
            SELECT @WtmTime = AzmZeitDo FROM AzmWochen 
                WHERE ArbeitszeitmodellID = @WorkTimeModelID
                    AND Folgenummer = @SequenceNo;
        ELSE IF DATEPART(dw, @Date) = 6             -- Friday
            SELECT @WtmTime = AzmZeitFr FROM AzmWochen 
                WHERE ArbeitszeitmodellID = @WorkTimeModelID
                    AND Folgenummer = @SequenceNo;
        ELSE                                            -- Saturday
            SELECT @WtmTime = AzmZeitSa FROM AzmWochen 
                WHERE ArbeitszeitmodellID = @WorkTimeModelID
                    AND Folgenummer = @SequenceNo;
    END ELSE
    BEGIN
        DECLARE @NUMWEEKS INT
        SELECT @NUMWEEKS = DATEDIFF(week, CONVERT(CHAR(10), '01.01.2000', 104), @Date)
        IF DATEPART(dw, @Date) = 1 
            SET @NUMWEEKS = @NUMWEEKS - 1;
        DECLARE @WEEKNUMBER INT 
        IF @NUMWEEKS % 2 = 0
            SET @WEEKNUMBER = 1
        ELSE
            SET @WEEKNUMBER = 2;
        IF DATEPART(dw, @Date) = 1              -- Sunday
            SELECT @WtmTime = AzmZeitSo FROM AzmWochen 
                WHERE Folgenummer = @SequenceNo AND Wochennummer = @WEEKNUMBER 
                  AND ArbeitszeitmodellID = @WorkTimeModelID
        ELSE IF DATEPART(dw, @Date) = 2     -- Monday
            SELECT @WtmTime = AzmZeitMo FROM AzmWochen 
                WHERE Folgenummer = @SequenceNo AND Wochennummer = @WEEKNUMBER 
                  AND ArbeitszeitmodellID = @WorkTimeModelID
        ELSE IF DATEPART(dw, @Date) = 3     -- Tuedsay
            SELECT @WtmTime = AzmZeitDi FROM AzmWochen 
                WHERE Folgenummer = @SequenceNo AND Wochennummer = @WEEKNUMBER 
                  AND ArbeitszeitmodellID = @WorkTimeModelID
        ELSE IF DATEPART(dw, @Date) = 4     -- Wednesday
            SELECT @WtmTime = AzmZeitMi FROM AzmWochen 
                WHERE Folgenummer = @SequenceNo AND Wochennummer = @WEEKNUMBER 
                  AND ArbeitszeitmodellID = @WorkTimeModelID
        ELSE IF DATEPART(dw, @Date) = 5     -- Thursday
            SELECT @WtmTime = AzmZeitDo FROM AzmWochen 
                WHERE Folgenummer = @SequenceNo AND Wochennummer = @WEEKNUMBER 
                  AND ArbeitszeitmodellID = @WorkTimeModelID
        ELSE IF DATEPART(dw, @Date) = 6     -- Friday
            SELECT @WtmTime = AzmZeitFr FROM AzmWochen 
                WHERE Folgenummer = @SequenceNo AND Wochennummer = @WEEKNUMBER 
                  AND ArbeitszeitmodellID = @WorkTimeModelID
        ELSE                                    -- Saturday
            SELECT @WtmTime = AzmZeitSa FROM AzmWochen 
                WHERE Folgenummer = @SequenceNo AND Wochennummer = @WEEKNUMBER 
                  AND ArbeitszeitmodellID = @WorkTimeModelID
    END
    IF @Date >= '20130901' AND @WtmTime = 'ZA'
        SET @WtmTime = 'KD';

    RETURN @WtmTime;
END


CREATE FUNCTION [dbo].[IstFeiertag] (
    @Datum DATETIME,
    @IstEvangelisch BIT)
  RETURNS INT
AS
BEGIN
    DECLARE @I INT
    DECLARE @Y INT
    DECLARE @A INT
    DECLARE @B INT
    SET @I = DATEPART(year, @Datum) / 100 - DATEPART(year, @Datum) / 400 + 4;
    SET @Y = @I - DATEPART(year, @Datum) / 300 + 11;
    SET @A = (((DATEPART(year, @Datum) % 19) * 19) + @Y) % 30;
    SET @B = (((DATEPART(year, @Datum) % 4) * 2 + 4 * DATEPART(year, @Datum) + 6 * @A + @I) % 7) + @A - 9;
    DECLARE @OstTag INT
    DECLARE @OstMon INT
    IF @B < 1
    BEGIN
        SET @OstTag = 31 + @B
        SET @OstMon = 3
    END ELSE
    BEGIN
        IF ((@B = 26) OR ((@A = 28) AND (@B = 25) AND ((11 * (@Y + 1) % 30) < 19)))
        BEGIN
            SET @B = @B - 7;
        END
        SET @OstTag = @B
        SET @OstMon = 4
    END

    DECLARE @Ostersonntag DATETIME
    SET @Ostersonntag = dbo.CreateDate(DATEPART(year, @Datum), @OstMon, @OstTag)

    IF @Datum >= @Ostersonntag
    BEGIN
        DECLARE @TAGE INT
        SET @TAGE = DATEDIFF(day, @Ostersonntag, @Datum)
        IF @TAGE = 0 OR @TAGE = 1 OR @TAGE = 39 OR @TAGE = 50 OR @TAGE = 60
        BEGIN
            RETURN 1
        END
    END
    DECLARE @TEMP INT 
    SET @TEMP = DATEPART(month, @Datum) * 100 + DATEPART(day, @Datum)
    IF @TEMP = 101 OR @TEMP = 106 OR @TEMP = 501 OR @TEMP = 815 OR @TEMP = 1026
         OR @TEMP = 1101 OR @TEMP = 1208 OR @TEMP = 1225 OR @TEMP = 1226
    BEGIN
        RETURN 1
    END 
    RETURN 0
END
GO


CREATE FUNCTION [dbo].[CreateDate] (
    @Year int, 
    @Month int, 
    @Day int)
  RETURNS DATETIME
AS
BEGIN
    declare @d datetime;
    set @d = dateadd(year,(@Year - 1753),'1/1/1753');
    set @d = dateadd(month,@Month - 1,@d);
    return dateadd(day,@Day - 1,@d)
END
GO
Run Code Online (Sandbox Code Playgroud)

这些是表定义(德语):

CREATE TABLE [dbo].[Arbeitszeitmodelle]
(
    [ArbeitszeitmodellID] uniqueidentifier ROWGUIDCOL NOT NULL 
        CONSTRAINT [DF_Arbeitszeitmodelle_ArbeitszeitmodellID] DEFAULT (newid()) 
        CONSTRAINT [PK_Arbeitszeitmodelle_ArbeitszeitmodellID] PRIMARY KEY CLUSTERED,
    [Name] nvarchar(25) NOT NULL,   
    [MaxAnzahlMitglieder]  tinyint NOT NULL 
    CONSTRAINT [CK_Arbeitszeitmodelle_MaxAnzahlMitglieder] CHECK (([MaxAnzahlMitglieder] > 0) AND ([MaxAnzahlMitglieder] < 10)), 
    [AnzahlWochen] tinyint NOT NULL
    CONSTRAINT [CK_Arbeitszeitmodelle_AnzahlWochen] CHECK (([AnzahlWochen] > 0) AND ([AnzahlWochen] < 5)),
    [ÜbernimmtFeiertage] bit 
);

CREATE TABLE [dbo].[AzmWochen]
(
    [AzmWochenID] uniqueidentifier ROWGUIDCOL NOT NULL 
        CONSTRAINT [DF_AzmWochen_AzmWochenID] DEFAULT (newid()) 
        CONSTRAINT [PK_AzmWochen_AzmWochenID] PRIMARY KEY CLUSTERED,
    [Folgenummer] tinyint NOT NULL
        CONSTRAINT [CK_AzmWochen_Folgenummer] CHECK (([Folgenummer] > 0) AND ([Folgenummer] < 10)),     
        [Wochennummer] tinyint NOT NULL
        CONSTRAINT [CK_AzmWochen_Wochennummer] CHECK (([Wochennummer] > 0) AND ([Wochennummer] < 3)),
        [ArbeitszeitmodellID] uniqueidentifier NOT NULL 
        CONSTRAINT [FK_AzmWochen_ArbeitszeitmodellID] FOREIGN KEY ([ArbeitszeitmodellID]) REFERENCES [dbo].[Arbeitszeitmodelle] ([ArbeitszeitmodellID]) ON UPDATE CASCADE ON DELETE CASCADE,    
    [AzmZeitMo] varchar(5) NOT NULL,
    [AzmZeitDi] varchar(5) NOT NULL,
    [AzmZeitMi] varchar(5) NOT NULL,
    [AzmZeitDo] varchar(5) NOT NULL,
    [AzmZeitFr] varchar(5) NOT NULL,
    [AzmZeitSa] varchar(5) NOT NULL,
    [AzmZeitSo] varchar(5) NOT NULL
);
ALTER TABLE AzmWochen ADD CONSTRAINT [UQ_AzmWochen_FolgeWochen] UNIQUE ([ArbeitszeitmodellID] ASC, [Folgenummer] ASC, [Wochennummer] ASC);
Run Code Online (Sandbox Code Playgroud)

我尝试了以下提示:

  • OPTION(USE HINT('FORCE_LEGACY_CARDINALITY_ESTIMATION'))
  • OPTION(USE HINT('QUERY_OPTIMIZER_COMPATIBILITY_LEVEL_140'))

...但他们并没有阻止错误。

我将两个表、测试数据和函数(GetWtmTime取决于其他两个标量函数)插入到一个空的测试数据库中,并且能够执行该函数两次。然后我又得到了内存错误。

Pau*_*ite 11

原因

SQL Server 正在尝试内联该函数,但由于复杂性而失败。

这样做时使用如此多的内存是出乎意料的,几乎可以肯定是一个错误。

dbo.IstFeiertag完整再现需要嵌套函数的定义。

解决方法

添加WITH INLINE = OFF到函数定义。解决此问题后,您应该能够删除该选项以获得函数内联的性能优势。

报告和状态

您应该将此问题报告给 Microsoft。如果您有支持协议,请走那条路。或者,在User Voice上发布错误报告,并通过智能查询处理团队发送电子邮件至 smartqp@microsoft.com。

Joe Sack(Microsoft SQL Server 产品团队首席项目经理)评论道:

感谢您的举报。保罗怀特提醒了我,我已向我们的团队报告以进行调查。


解析度

此问题修复程序已作为SQL Server 2019 累积更新 2 的一部分发布。