触发器在 UPDATE/INSERT 上有效,但在 运行 通过 BCP 时失败(日期格式问题)

Trigger works on UPDATE/INSERT but fails when run through BCP (date format issue)

我有一个非常简单的 table - 四列:

CREATE TABLE [dbo].[SampleTable]
(
    [item]   VARCHAR(32)   NOT NULL,
    [symbol] VARCHAR(64)   NOT NULL,
    [date]   DATE          NOT NULL,
    [value]  NVARCHAR(255) NOT NULL,

    CONSTRAINT [PK_SampleTable] 
        PRIMARY KEY CLUSTERED ([symbol] ASC, [item] ASC, [date] DESC)
)

要更新 table,我使用视图和触发器:

CREATE VIEW [dbo].[SampleTable_in]
AS
    SELECT [item], [symbol], [date], [value]
    FROM [dbo].[SampleTable]
GO

CREATE TRIGGER [dbo].[SampleTable_in_trig] 
ON [dbo].[SampleTable_in]
/*
    Trigger on the insert view to facilitate:
    1. Insertions into the table overwrite (update) any records that already exist
    2. Sparse value updates (if the last value in the series is the same, value is not inserted)
    3. Insertions where value is null are considered DELETES
*/
INSTEAD OF INSERT, UPDATE
AS
    SET NOCOUNT, XACT_ABORT ON;

    BEGIN TRY
        BEGIN TRANSACTION
            /* When inserted symbol, item, date exists and values don't match, update */
            UPDATE u
            SET [value] = i.[value]
            FROM [dbo].[SampleTable] u
            INNER JOIN INSERTED i ON u.[symbol] = i.[symbol]
                                  AND u.[item] = i.[item]
                                  AND u.[date] = i.[date]
            WHERE i.[value] IS NOT NULL
              AND i.[value] != u.[value]

            /* When inserted symbol, item, date exists and inserted value is null, delete */
            DELETE [dbo].[SampleTable]
            FROM [dbo].[SampleTable] u
            INNER JOIN INSERTED i ON u.[symbol] = i.[symbol]
                                  AND u.[item] = i.[item]
                                  AND u.[date] = i.[date]
            WHERE i.[value] IS NULL

            /* When inserted symbol, item does not exist and inserted value is not null, insert */
            INSERT INTO [dbo].[SampleTable] ([item], [symbol], [date], [value])
                SELECT
                    i.[item], i.[symbol], i.[date], i.[value]
                FROM
                    INSERTED i
                LEFT OUTER JOIN 
                    [dbo].[SampleTable] u ON u.[symbol] = i.[symbol]
                                          AND u.[item] = i.[item]
                WHERE 
                    i.[value] IS NOT NULL
                    AND u.[symbol] IS NULL

            /* When inserted symbol, item, date does not exist and value from prior date does not match inserted value, insert */
            INSERT INTO [dbo].[SampleTable] ([item], [symbol], [date], [value])
                SELECT
                    i.[item], i.[symbol], i.[date], i.[value]
                FROM
                    INSERTED i
                LEFT OUTER JOIN 
                    [dbo].[SampleTable] u ON u.[symbol] = i.[symbol]
                                          AND u.[item] = i.[item]
                                          AND u.[date] = i.[date]
                WHERE
                    i.[value] IS NOT NULL
                    AND u.[symbol] IS NULL
                    AND i.[value] != (SELECT TOP 1 [value] 
                                      FROM [dbo].[SampleTable] uu 
                                      WHERE uu.[symbol] = i.[symbol] 
                                        AND uu.[item] = i.[item] 
                                        AND uu.[date] < i.[date] 
                                      ORDER BY [date] DESC)

        COMMIT TRANSACTION
    END TRY

    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK
        EXEC proc_error_handler
    END CATCH
GO

我的输入数据以 TSV 格式文件提供,格式为 <item>\t<symbol>\t<date>\t<value>,日期格式为 YYYYMMDD。我无法更改输入格式,并且当我尝试 bcp 直接进入视图时 SQL 正在提高 Invalid character value for cast specification(启用 FIRE_TRIGGERS)。

如果我将基础 table 中的日期格式从 DATE 更改为 SMALLDATETIME,bcp 将完全按预期工作,但我的理解是这种旧格式不应该用于新工作(出于各种原因我想避免进行此更改)。

同样,如果我将输入日期格式更改为 YYYY-MM-DD,这会按预期工作,但是正如所解释的那样,我无法更改源数据格式并且必须在此处构建一个额外的处理步骤以这样做

我试过改变视图,使用 CASTCONVERT 将日期字段格式化为 SMALLDATETIME 和常规 INSERT 工作正常,但是bcp 仍然不高兴,触发器现在抛出 Cannot insert the value NULL into column 'date'

CREATE VIEW [dbo].[SampleTable_in]
AS
    SELECT [item], [symbol], CAST([date] AS SMALLDATETIME) AS [date], [value]
    FROM [dbo].[SampleTable]
GO

进一步深入研究,当我创建该视图并使用触发器将输入重定向到 table,所有列都设置为 NULL 时,日期被替换为 NULL 在输入上(我假设 bcp 进程在它到达 SQL 之前)

除了更改目标 table 或输入数据之外,是否有人对如何解决此问题有任何想法?

编辑: 我使用的 bcp 命令是 bcp TestDB.dbo.SampleTable_in in test_file.tsv -c -b 50000 -T -h FIRE_TRIGGERS -k

数据匹配这个小样本(但行数比这多得多):

SOURCE  M01 20210813    FOO
SOURCE  M02 20210813    FOO
SYMBOL  M01 20210813    M01
SYMBOL  M02 20210813    M02
DESC    M01 20210813    A short description
DESC    M02 20210813    Some other desc

当我更改视图以更改日期字段的格式时(CASTCONVERTFORMAT),日期字段显示为 [=28] =] 仅值

使用输入源和您的 BCP 命令我没有办法对此进行测试,但是我会尝试以下方法:

首先,将您的视图定义更改为:

CREATE VIEW [dbo].[SampleTable_in]
AS
    SELECT  [item], 
            [symbol], 
            CONVERT(NVARCHAR(8), [date], 112) AS [date],
            [value]
    FROM [dbo].[SampleTable]
GO

然后将触发器定义更改为:

CREATE TRIGGER [dbo].[SampleTable_in_trig] 
ON [dbo].[SampleTable_in]
/*
    Trigger on the insert view to facilitate:
    1. Insertions into the table overwrite (update) any records that already exist
    2. Sparse value updates (if the last value in the series is the same, value is not inserted)
    3. Insertions where value is null are considered DELETES
*/
INSTEAD OF INSERT, UPDATE
AS
    SET NOCOUNT, XACT_ABORT ON;

    BEGIN TRY
        BEGIN TRANSACTION
            /* When inserted symbol, item, date exists and values don't match, update */
            UPDATE u
            SET [value] = i.[value]
            FROM [dbo].[SampleTable] u
            INNER JOIN INSERTED i ON u.[symbol] = i.[symbol]
                                  AND u.[item] = i.[item]
                                  AND u.[date] = CONVERT(DATE, i.[date], 112)
            WHERE i.[value] IS NOT NULL
              AND i.[value] != u.[value]

            /* When inserted symbol, item, date exists and inserted value is null, delete */
            DELETE [dbo].[SampleTable]
            FROM [dbo].[SampleTable] u
            INNER JOIN INSERTED i ON u.[symbol] = i.[symbol]
                                  AND u.[item] = i.[item]
                                  AND u.[date] = CONVERT(DATE, i.[date], 112)
            WHERE i.[value] IS NULL

            /* When inserted symbol, item does not exist and inserted value is not null, insert */
            INSERT INTO [dbo].[SampleTable] ([item], [symbol], [date], [value])
                SELECT
                    i.[item], i.[symbol], CONVERT(DATE, i.[date], 112), i.[value]
                FROM
                    INSERTED i
                LEFT OUTER JOIN 
                    [dbo].[SampleTable] u ON u.[symbol] = i.[symbol]
                                          AND u.[item] = i.[item]
                WHERE 
                    i.[value] IS NOT NULL
                    AND u.[symbol] IS NULL

            /* When inserted symbol, item, date does not exist and value from prior date does not match inserted value, insert */
            INSERT INTO [dbo].[SampleTable] ([item], [symbol], [date], [value])
                SELECT
                    i.[item], i.[symbol], CONVERT(DATE, i.[date], 112), i.[value]
                FROM
                    INSERTED i
                LEFT OUTER JOIN 
                    [dbo].[SampleTable] u ON u.[symbol] = i.[symbol]
                                          AND u.[item] = i.[item]
                                          AND u.[date] = CONVERT(DATE, i.[date], 112)
                WHERE
                    i.[value] IS NOT NULL
                    AND u.[symbol] IS NULL
                    AND i.[value] != (SELECT TOP 1 [value] 
                                      FROM [dbo].[SampleTable] uu 
                                      WHERE uu.[symbol] = i.[symbol] 
                                        AND uu.[item] = i.[item] 
                                        AND uu.[date] < CONVERT(DATE, i.[date], 112)
                                      ORDER BY [date] DESC)

        COMMIT TRANSACTION
    END TRY

    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK
        --EXEC proc_error_handler
    END CATCH
GO

总而言之,我认为这不是您的触发器的问题,而是您的 BCP 命令 and/or 视图定义的问题。简而言之,BCP 无法确定如何将 YYYYMMDD 输入字符串转换为 Date 数据类型。简单的答案是将问题从 BCP 中移开,让触发器解决。


根据 OP 的反馈,这也在日期列中输入了 NULL。

因此,我不得不得出结论,问题是当我们修改 [date] 列时,BCP 不会写入它,因为它看起来是派生的,因此在视图中是不可写的列(参见可写视图的规则)没有后备存储。一方面,使用像这样的 T-SQL 触发器使用而不是触发器创建直写视图,它的 cases/conditions 行为并不总是有很好的记录。另一方面,BCP 是 SQL 服务器中最古老的工具之一,它使用(不再记录的)特殊接口连接到 SQL 服务器,该接口并不总是遵循类似 INSERT 的规则。

综上所述,我只想说,我认为没有办法让这种方法按照您想要的方式工作。因此,在这一点上,我认为您唯一可靠的做法是放弃使用触发器方法的直写视图,而是使用暂存 table.

我会像这样定义暂存 table:

CREATE TABLE [dbo].[SampleTable_in]
(
    [item]   VARCHAR(32)   NOT NULL,
    [symbol] VARCHAR(64)   NOT NULL,
    [date]   NVARCHAR(8)   NOT NULL,
    [value]  NVARCHAR(255) NOT NULL
)

这将替换您的视图,您可以将(修改后的)触发器应用于它,或者将它们的逻辑合并到 BCP 完成后运行的存储过程中。