EF LINQ to SQL,除以零错误,生成的查询以错误的顺序放置参数

EF LINQ to SQL, dividing by zero error, generated query puts parameters in wrong order

编辑 在 post

底部重现此错误的步骤

这个问题我的数据结构:

    public class StockRequest
    {
        public int StartYear { get; set; }
        public StockInterval StockInterval { get; set; }
    }

    public class StockInterval
    {
        /// <summary>
        ///  Can be 0 = non-recurring, 1 = annual, 2 = once every 2 years, 3 = once every 3 years
        /// </summary>
        public int IntervalInYears { get; set; }
    }

如果我想获取 2021 年的所有库存请求。以下数据将满足该条件:

var nonRecurringRequest = new StockRequest() { StartYear = 2021, StockInterval = new StockInterval() { IntervalInYears = 0 } };
var annualRequest = new StockRequest() { StartYear = 2020, StockInterval = new StockInterval() { IntervalInYears = 1 } };
var everyTwoYearsRequest = new StockRequest() { StartYear = 2019, StockInterval = new StockInterval() { IntervalInYears = 2 } };
var everyThreeYearsRequest = new StockRequest() { StartYear = 2018, StockInterval = new StockInterval() { IntervalInYears = 3 } };

EF 查询中的关键 where 子句是:

query.Where(x => 
   x.StartYear <= selectedYear && 
  (
    x.StartYear == selectedYear || 
    (x.StockInterval.IntervalInYears != 0 && selectedYear - x.StartYear % x.StockInterval.IntervalInYears == 0) 
  )
);

导致问题的部分是非经常性库存请求(间隔 0)。你不能 mod 那是因为你除以零。但是,我知道这一点并且在过去通过在尝试 mod 之前首先检查 属性 (IntervalInYears) 是否不为零来解决这个问题。由于 WHERE 的第一部分未通过检查,因此不会继续到 mod 部分。

出于某种原因,这一次不起作用。当我检查生成的查询时,它将 0 放在第一位:

WHERE 
StockRequests.[StartYear] <= @stockYear
AND 
(
    StockRequests.[StartYear] = @stockYear OR 
    (
        0 <> StockIntervals.[IntervalInYears] AND 
        0 = (@stockYear - StockRequests.[StartYear]) % StockIntervals.[IntervalInYears] 
    )
)

在 SQL 服务器中执行该操作会生成被零除错误。但是,翻转 0 和 StockIntervals.IntervalInYears:

的边
WHERE 
StockRequests.[StartYear] <= @stockYear
AND 
(
    StockRequests.[StartYear] = @stockYear OR 
    (
        StockIntervals.[IntervalInYears] <> 0  AND 
        0 = (@stockYear - StockRequests.[StartYear]) % StockIntervals.[IntervalInYears] 
    )
)

现在可以正常使用了。为什么 EF 对此进行切换,我如何在 EF 中修复它?我没有在 EF 查询中将 0 放在第一位,而且我不记得以前发生过这种情况,这是我一直用来确保我没有尝试除以零的解决方案并且它曾经有效。我知道我可以手动编写 SQL 查询并执行它,但投影超过 200 行。

编辑 重现: Table 创建脚本:

CREATE TABLE [dbo].[StockIntervals](
[Id] [uniqueidentifier] NOT NULL,
[Name] [nvarchar](255) NOT NULL,
[IntervalInYears] [int] NOT NULL,
 CONSTRAINT [PK_dbo.StockIntervals] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[StockIntervals] ADD  DEFAULT ((0)) FOR [IntervalInYears]
GO

CREATE TABLE [dbo].[StockRequests](
[Id] [uniqueidentifier] NOT NULL,
[Count] [int] NOT NULL,
[DateRequested] [datetime] NOT NULL,
[StartYear] [int] NOT NULL,
[StockIntervalId] [uniqueidentifier] NOT NULL,
[EndYear] [int] NULL,


CONSTRAINT [PK_dbo.StockRequests] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[StockRequests]  WITH CHECK ADD  CONSTRAINT [FK_dbo.StockRequests_dbo.StockIntervals_StockIntervalId] FOREIGN KEY([StockIntervalId])
REFERENCES [dbo].[StockIntervals] ([Id])
GO

ALTER TABLE [dbo].[StockRequests] CHECK CONSTRAINT [FK_dbo.StockRequests_dbo.StockIntervals_StockIntervalId]
GO

填充 Tables:

INSERT INTO [dbo].[StockIntervals]
       ([Id]
       ,[Name]
       ,[IntervalInYears])
 VALUES
       ('738A431E-D517-4C17-9ECA-A1A0942E236B', 'Non-recurring one time', 0),
       ('CCB746A7-F644-4C7E-ADBE-AE14DE01B19E', 'Annual', 1),
       ('80C6CAE6-5287-41E6-A5FE-AAA53035EC19', 'Every 2 years', 2),
       ('B34EE256-C40B-4F03-8232-14B681186C7A', 'Every 3 years', 3)

GO

INSERT INTO [dbo].[StockRequests]
       ([Id]
       ,[Count]
       ,[DateRequested]
       ,[StartYear]
       ,[StockIntervalId]
       ,[EndYear])
 VALUES
       ('4a5ae94e-0a85-4195-8e7e-8cc556307b30'
       ,15
       ,'2022-01-11 15:16:41.567'
       ,2021
       ,'738A431E-D517-4C17-9ECA-A1A0942E236B'
       ,null),
       ('f0d83b68-0da1-4824-9eeb-2e52ff369db5'
       ,60
       ,'2022-01-11 15:16:41.567'
       ,2020
       ,'CCB746A7-F644-4C7E-ADBE-AE14DE01B19E'
       ,null),
       ('a49b4b9e-80d6-4fca-ad78-6c8996616c97'
       ,1000
       ,'2022-01-11 15:16:41.567'
       ,2019
       ,'80C6CAE6-5287-41E6-A5FE-AAA53035EC19'
       ,null),
       ('cc21a265-f8df-4d2d-9eae-5f6f97ef9909'
       ,50
       ,'2022-01-11 15:16:41.567'
       ,2018
       ,'B34EE256-C40B-4F03-8232-14B681186C7A'
       ,null)
GO

运行 这个查询:

DECLARE @stockYear int = 2021

SELECT * FROM 
dbo.StockRequests
INNER JOIN dbo.StockIntervals on StockIntervalId = StockIntervals.Id
WHERE 
    StockRequests.[StartYear] <= @stockYear
    AND 
    (
        StockRequests.[StartYear] = @stockYear OR 
        (
            0 <> StockIntervals.[IntervalInYears]  AND 
            0 = (@stockYear - StockRequests.[StartYear]) % StockIntervals.[IntervalInYears] 
        )
    )

没有错误。好的,现在尝试插入一条新记录:

INSERT INTO [dbo].[StockRequests]
VALUES ('FFA820F1-E361-4AC5-AB00-E621BFFEF9B5', 20, '2022-01-11 16:22:11.567', 2020, '738A431E-D517-4C17-9ECA-A1A0942E236B', null)

运行再次查询。除以零错误发生。玩过数据后,这种行为是有道理的。如果 @stockYear 大于或小于 StartYear 并且该记录的间隔为零,它将出错,因为如果到达查询的最内部部分,并且间隔为零并且它不会'没有布尔表达式快捷方式。好的

但是将查询的一行切换为:

StockIntervals.[IntervalInYears] <> 0

现在可以了!虽然不确定这是怎么回事,我已经 运行 我的脚本通过许多场景来触发错误,但它总是由上面的方法解决。如果没有短路,切换操作数应该还是会报错。然而,事实并非如此。所以人们说操作数顺序无关紧要,但我能够证明它看起来很重要。

您似乎在假设 T-SQL 中的 ANDOR 将始终按照查询中指定的顺序短路.绝对不是这样的。

的确,它通常会短路一个逻辑表达式。毕竟,为什么要做不必要的工作?但它可能不符合查询中指定的顺序。未指定逻辑运算符以 任何 特定顺序执行,优化器通常会根据短路可能性的估计或评估中涉及的工作量等因素选择切换它们,只要遵循运算符优先级规则(ANDOR 之前等)。

因为评估所有可能的执行计划的 space 过于庞大,优化器使用积极的修剪来根据启发式删除选项。这两个谓词:

(
    StockRequests.[StartYear] = @stockYear OR 
    (
        0 <> StockIntervals.[IntervalInYears]  AND 
        0 = (@stockYear - StockRequests.[StartYear]) % StockIntervals.[IntervalInYears] 
    )
)

(
    StockRequests.[StartYear] = @stockYear OR 
    (
        StockIntervals.[IntervalInYears] <> 0  AND 
        0 = (@stockYear - StockRequests.[StartYear]) % StockIntervals.[IntervalInYears] 
    )
)

就查询意图而言完全相同。问题是优化器将选择如何处理它们。在您的情况下,碰巧以一种方式放置比较器会导致某些优化落入(或不落入)位置,因此 AND 可能会翻转。

this fiddle, 可以看出,运行 在 SQL Server 2019 上,两者 选项正确短路,翻转 AND 也是如此。我不得不翻转 OR 使其失败,然后 AND 的顺序无关紧要。请注意,任何查询中的逻辑都没有更改,并且 AND = 比较器本身的顺序不会 force 优化器的手, 它只是有时会引导它沿着特定的路径前进。

所以这在很大程度上取决于优化器决定做什么,您不能预先保证它将始终正确执行。是的,您已经看到它这样做了一百次,但是第一百次可能会发生变化,可能是因为统计数据发生变化,或者更新 SQL 服务器,或者更改基数估计器版本,或者数据库兼容级别,或任何可能导致重新编译的事情。

唯一保证方式确保在特定顺序短路是使用CASE(或NULLIF 编译成 CASE)。 This is documented by Microsoft,只要你不使用任何聚合函数,它就可以工作。

In other words, do not expect something like CASE WHEN x > 0 THEN SUM(1 / x) END to work, because the SUM is often evaluated at an earlier stage. It only works with scalar values. As far as I am aware I would expect the same issue would apply to subqueries and window functions.

因此,您可以使用 NULLIF

解决您的问题
(
    StockRequests.[StartYear] = @stockYear OR 
    (
        StockIntervals.[IntervalInYears] <> 0  AND 
        0 = (@stockYear - StockRequests.[StartYear]) % NULLIF(StockIntervals.[IntervalInYears], 0)
    )
)

在 Entity Framework 中你可以使用 (value == 0 ? null : value)

query.Where(x => 
   x.StartYear <= selectedYear && 
  (
    x.StartYear == selectedYear || 
    (x.StockInterval.IntervalInYears != 0
     && selectedYear - x.StartYear %
        (x.StockInterval.IntervalInYears == 0 ? null : x.StockInterval.IntervalInYears)
        == 0) 
  )
);