【问题标题】:Check constraint does not work on bulk insert for more than 250 records检查约束不适用于超过 250 条记录的批量插入
【发布时间】:2016-01-08 09:00:23
【问题描述】:

我的查询:

INSERT into PriceListRows (PriceListChapterId,[No])
    SELECT TOP 250 100943 ,N'2'
    FROM #AnyTable

此查询工作正常,并根据需要引发以下异常:

INSERT 语句与 CHECK 约束冲突 “CK_PriceListRows_RowNo_Is_Not_Unqiue_In_PriceList”。冲突 发生在数据库“TadkarWeb”、表“dbo.PriceListRows”中。

但是将SELECT TOP 250 更改为SELECT TOP 251(是的!只是将250 更改为251!)查询成功运行,没有任何检查约束异常!

为什么会有这种奇怪的行为?

注意事项:

  1. 我的检查约束是一个检查某种唯一性的函数。它查询大约 4 个表。

  2. 我检查了 SQL Server 2012 SP2 和 SQL Server 2014 SP1

** 编辑 1 **

检查约束函数:

ALTER FUNCTION [dbo].[CheckPriceListRows_UniqueNo] (
    @rowNo nvarchar(50),
    @rowId int,
    @priceListChapterId int,
    @projectId int)
RETURNS bit
AS
BEGIN
    IF EXISTS (SELECT 1 
               FROM RowInfsView 
               WHERE PriceListId = (SELECT PriceListId 
                                    FROM ChapterInfoView 
                                    WHERE Id = @priceListChapterId) 
                 AND (@rowID IS NULL OR Id <> @rowId) 
                 AND No = @rowNo 
                 AND (@projectId IS NULL OR 
                      (ProjectId IS NULL OR ProjectId = @projectId)))
        RETURN 0 -- Error

     --It is ok!
    RETURN 1
END

** 编辑 2 ** 检查约束代码(SQL Server 2012 产生的):

ALTER TABLE [dbo].[PriceListRows]  WITH NOCHECK ADD  CONSTRAINT [CK_PriceListRows_RowNo_Is_Not_Unqiue_In_PriceList] CHECK  (([dbo].[tfn_CheckPriceListRows_UniqueNo]([No],[Id],[PriceListChapterId],[ProjectId])=(1)))
GO

ALTER TABLE [dbo].[PriceListRows] CHECK CONSTRAINT [CK_PriceListRows_RowNo_Is_Not_Unqiue_In_PriceList]
GO

** 编辑 3 **

执行计划在这里:https://www.dropbox.com/s/as2r92xr14cfq5i/execution%20plans.zip?dl=0

** 编辑 4 ** RowInfsView 定义为:

SELECT        dbo.PriceListRows.Id, dbo.PriceListRows.No, dbo.PriceListRows.Title, dbo.PriceListRows.UnitCode, dbo.PriceListRows.UnitPrice, dbo.PriceListRows.RowStateCode, dbo.PriceListRows.PriceListChapterId, 
                         dbo.PriceListChapters.Title AS PriceListChapterTitle, dbo.PriceListChapters.No AS PriceListChapterNo, dbo.PriceListChapters.PriceListCategoryId, dbo.PriceListCategories.No AS PriceListCategoryNo, 
                         dbo.PriceListCategories.Title AS PriceListCategoryTitle, dbo.PriceListCategories.PriceListClassId, dbo.PriceListClasses.No AS PriceListClassNo, dbo.PriceListClasses.Title AS PriceListClassTitle, 
                         dbo.PriceListClasses.PriceListId, dbo.PriceLists.Title AS PriceListTitle, dbo.PriceLists.Year, dbo.PriceListRows.ProjectId, dbo.PriceListRows.IsTemplate
FROM            dbo.PriceListRows INNER JOIN
                         dbo.PriceListChapters ON dbo.PriceListRows.PriceListChapterId = dbo.PriceListChapters.Id INNER JOIN
                         dbo.PriceListCategories ON dbo.PriceListChapters.PriceListCategoryId = dbo.PriceListCategories.Id INNER JOIN
                         dbo.PriceListClasses ON dbo.PriceListCategories.PriceListClassId = dbo.PriceListClasses.Id INNER JOIN
                         dbo.PriceLists ON dbo.PriceListClasses.PriceListId = dbo.PriceLists.Id

【问题讨论】:

  • 您要插入相同的 2 个值(100943 和 2)250 次?听起来你应该已经在第二行得到异常
  • @jamez 完全正确!这是为了测试。我应该得到错误,但我不知道为什么我不明白。 (实际上我得到了 250 条记录插入,但不是 251 条!)
  • 是的,很奇怪。我编辑了帖子。
  • @MahmoudMoravej 。 . . Martin Smith 在回答类似问题 (sqlblog.com/blogs/alexander_kuznetsov/archive/2009/06/25/…) 时引用了此博客。我怀疑某种外键约束或唯一约束可能更适合您想要做的事情。
  • 您选择了前 1 名而没有排序依据,因此它可以从 ChapterInfoView 中选择任意随机行与 RowInfsView 进行比较,所以在不了解您的数据的情况下,我会说这是您问题的根源。跨度>

标签: sql-server tsql user-defined-functions check-constraints


【解决方案1】:

解释是你的执行计划正在使用"wide"(index by index)更新计划。

在计划的第 1 步将行插入到聚集索引中。并在第 2 步验证每一行的检查约束。

在所有行都插入到聚集索引中之前,不会将任何行插入到非聚集索引中。

这是因为在聚集索引插入/约束检查和非聚集索引插入之间有两个blocking operators。急切的假脱机(第 3 步)和排序(第 4 步)。在消耗完所有输入行之前,这两种方法都不会产生输出行。

标量 UDF 的计划使用非聚集索引来尝试查找匹配的行。

在检查约束运行时,尚未将任何行插入到非聚集索引中,因此此检查为空。

当您插入较少的行时,您会得到一个“窄”(逐行)更新计划并避免该问题。

我的建议是避免在检查约束中进行这种验证。很难确定代码在所有情况下都能正常工作(例如不同的执行计划和隔离级别),另外它们在针对表的查询中block parellelism。尝试以声明方式进行(需要连接到其他表的唯一约束通常可以通过索引视图来实现)。


一个简化的复制是

CREATE FUNCTION dbo.F(@Z INT)
RETURNS BIT
AS
  BEGIN
      RETURN CASE WHEN EXISTS (SELECT * FROM dbo.T1 WHERE  Z = @Z) THEN 0 ELSE 1 END
  END

GO

CREATE TABLE dbo.T1
  (
     ID INT IDENTITY PRIMARY KEY,
     X  INT,
     Y  CHAR(8000) DEFAULT '',
     Z  INT,
     CHECK (dbo.F(Z) = 1),
     CONSTRAINT IX_X UNIQUE (X, ID),
     CONSTRAINT IX_Z UNIQUE (Z, ID)
  )

--Fails with check constraint error
INSERT INTO dbo.T1 (Z)
SELECT TOP (10) 1 FROM master..spt_values;

/*I get a wide update plan for TOP (2000) but this may not be reliable 
  across instances so using trace flag 8790 to get a wide plan. */
INSERT INTO dbo.T1 (Z)
SELECT TOP (10) 2 FROM master..spt_values
OPTION (QUERYTRACEON 8790);

GO

/*Confirm only the second insert succceed (Z=2)*/
SELECT * FROM dbo.T1;

DROP TABLE dbo.T1;    
DROP FUNCTION dbo.F; 

【讨论】:

    【解决方案2】:

    您可能会遇到不正确的查询优化,但如果没有所有相关表中的数据,我们无法重现该错误。

    但是,对于这种检查,我建议使用触发器而不是基于函数的检查约束。在触发器中,您可以使用 SELECT 语句来调试它没有按预期工作的原因。例如:

    CREATE TRIGGER trg_PriceListRows_CheckUnicity ON PriceListRows
    FOR INSERT, UPDATE
    AS
    IF @@ROWCOUNT>0 BEGIN
        /*
        SELECT * FROM inserted i
        INNER JOIN RowInfsView r
        ON r.PriceListId = (
            SELECT c.PriceListId 
            FROM ChapterInfoView c
            WHERE c.Id = i.priceListChapterId
        ) 
        AND r.Id <> i.Id
        AND r.No = i.No 
        AND (r.ProjectId=i.ProjectId OR r.ProjectId IS NULL AND i.ProjectId IS NULL)
        */
        IF EXISTS (
            SELECT * FROM inserted i
            WHERE EXISTS (
                SELECT * FROM RowInfsView r
                WHERE r.PriceListId = (
                    SELECT c.PriceListId 
                    FROM ChapterInfoView c
                    WHERE c.Id = i.priceListChapterId
                ) 
                AND r.Id <> i.Id
                AND r.No = i.No 
                AND (r.ProjectId=i.ProjectId OR r.ProjectId IS NULL AND i.ProjectId IS NULL)
            )
        ) BEGIN
            RAISERROR ('Duplicate rows!',16,1)
            ROLLBACK
            RETURN
        END
    END
    

    这样,您可以查看正在检查的内容并更正您的视图和/或现有数据。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-10
      • 1970-01-01
      • 2013-10-20
      • 2020-03-18
      • 2011-02-20
      相关资源
      最近更新 更多