在提交之前,SQL Server是否允许事务中的DDL(使其可见)到事务中?


9

在PostgreSQL中,我可以创建一个包含一些测试数据的表,然后在事务中将其迁移到其他类型的新列,从而在上重写一个表COMMIT

CREATE TABLE foo ( a int );
INSERT INTO foo VALUES (1),(2),(3);

其次是,

BEGIN;
  ALTER TABLE foo ADD COLUMN b varchar;
  UPDATE foo SET b = CAST(a AS varchar);
  ALTER TABLE foo DROP COLUMN a;
COMMIT;

但是,Microsoft SQL Server中的同一件事似乎会产生错误。比较此工作的db fiddle,其中ADD(column)命令在事务之外,

-- txn1
BEGIN TRANSACTION;
  ALTER TABLE foo ADD b varchar;
COMMIT;

-- txn2
BEGIN TRANSACTION;
  UPDATE foo SET b = CAST( a AS varchar );
  ALTER TABLE foo DROP COLUMN a;
COMMIT;

到这个无效的数据库小提琴

-- txn1
BEGIN TRANSACTION;
  ALTER TABLE foo ADD b varchar;
  UPDATE foo SET b = CAST( a AS varchar );
  ALTER TABLE foo DROP COLUMN a;
COMMIT;

但是错误

Msg 207 Level 16 State 1 Line 2
Invalid column name 'b'.

无论如何,对于DDL,该事务的行为类似于PostgreSQL吗?

Answers:


17

一般来说,没有。SQL Server 在执行之前会在当前范围内编译整个批处理,因此必须存在引用的实体(语句级别的重新编译也可能在以后发生)。主要例外是“ 延迟名称解析”,但适用于表,而不适用于列:

仅当您引用不存在的表对象时,才可以使用延迟名称解析。创建存储过程时,所有其他对象必须存在。例如,当您在存储过程中引用现有表时,无法列出该表不存在的列。

常见的解决方法包括动态代码(如Joe的答案所示),或将DML和DDL分为单独的批处理。

对于这种特定情况,您还可以编写:

BEGIN TRANSACTION;

    ALTER TABLE dbo.foo
        ALTER COLUMN a varchar(11) NOT NULL
        WITH (ONLINE = ON);

    EXECUTE sys.sp_rename
        @objname = N'dbo.foo.a',
        @newname = N'b',
        @objtype = 'COLUMN';

COMMIT TRANSACTION;

您仍然将无法b在相同的批次和范围中访问重命名的列,但确实可以完成工作。

关于SQL Server,有一种流派认为在事务中混合使用DDL和DML不是一个好主意。过去有一些错误会导致错误的日志记录和不可恢复的数据库。尽管如此,人们还是这样做的,尤其是使用临时表。这可能会导致一些很难理解的代码。


12

这是您要找的东西吗?

BEGIN TRANSACTION;
  ALTER TABLE foo ADD b varchar;
  EXEC sp_executesql N'UPDATE foo SET b = CAST( a AS varchar )';
  ALTER TABLE foo DROP COLUMN a;
COMMIT;

2

对于保罗·怀特(Paul White)的回答“通常不”,我希望以下内容可以直接回答该问题,但也可以显示这种过程的系统性局限性,并引导您摆脱那些不便于管理和暴露的方法。风险。

可以被多次提及不是让DDL改变你正在DML同一时间。良好的编程会将这些功能分开,以保持可支持性,并避免意大利面条拉丝的变化。

正如Paul简洁指出的那样,SQL Server是批量工作的

现在,对于那些怀疑此方法可行的人,它可能不适用于您的实例,但某些版本(如2017)实际上可以工作!这是证明: 在此处输入图片说明

[测试代码-可能不适用于许多版本的SQL Server]

USE master
GO
CREATE TABLE foo (a VARCHAR(11) )
GO
BEGIN TRANSACTION;
    INSERT INTO dbo.foo (a)
    VALUES ('entry')
/*****
[2] Check Values
*****/
    SELECT a FROM dbo.foo
/*****
[3] Add Column
*****/
    ALTER TABLE dbo.foo
        ADD b VARCHAR(11)
/*****
[3] Insert value into this new column in the same batch
-- Again, this is just an example. Please do not do this in production
*****/
    IF EXISTS (SELECT * FROM sys.columns WHERE object_ID('foo') = object_id
            AND name = 'b')
        INSERT INTO dbo.foo (b)
        VALUES ('d')
COMMIT TRANSACTION;
/*****
[4] SELECT outside transaction
-- this will fail
*****/
    --IF EXISTS (SELECT * FROM sys.columns WHERE object_ID('foo') = object_id
    --      AND name = 'b')
    --  SELECT b FROM dbo.foo
-- this will work...but a SELECT * ???
IF EXISTS (SELECT * FROM sys.columns WHERE object_ID('foo') = object_id
            AND name = 'b')
        SELECT * FROM dbo.foo

DROP TABLE dbo.foo

[结论]

所以是的,您可以像@AndriyM一样对SQL Server的某些版本或补丁在同一批中执行DDL和DML -SQL 2017上的dbfiddle指出,但并非所有DML都受支持,并且不能保证总是如此。如果有效,则可能是您的SQL Server版本的异常,这可能会在您修补或迁移到新版本时引起严重的问题。

  • 另外,一般而言,您的设计应该预期更改。我了解在表上可能存在修改/添加列的问题,但是您可以对此进行批量设计。

[额外积分]

至于Paul所说的EXISTS语句,在进入下一步之前,还有许多其他方法可以验证代码。

  • EXISTS语句可以帮助您创建适用于所有版本的SQL Server的代码
  • 它是一个布尔函数,允许在一条语句中进行复杂的检查

不,如果您在创建列的同一批处理中这样做,则不能将其插入新列。更一般而言,创建新列后,您将不能在同一批中静态引用新列。IF EXISTS技巧在这种情况下不起作用。动态调用DML或以另一批方式执行。
Andriy M,

@AndriyM对不起,错误地发表了有关dbfiddle的声明。但是您是否在实例上尝试过此操作?它适用于2017 SP1。我将上传gif,但是您是否在系统上对此进行了测试?
clifton_h

i.imgur.com/fhAC7lB.png实际上,您可以说它不会基于b插入语句下的波浪线进行编译。我正在使用SQL Server 2014
Andriy M,

@AndriyM有趣。我之前曾看到过这种影响,并且似乎可以在某些版本的SQL Server上运行,例如我提到的SQL Server 2017。
clifton_h

@AndriyM看到要发布的新编辑
clifton_h
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.