O SQL Server permite (tornar visível) DDL dentro de uma transação para a transação antes da confirmação?

9

No PostgreSQL, posso criar uma tabela com alguns dados de teste e, em uma transação, migrá-la para uma nova coluna de um tipo diferente, resultando na reescrita de uma tabela COMMIT,

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

Seguido por,

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

No entanto, a mesma coisa no SQL Server da Microsoft parece gerar um erro. Compare este db fiddle em funcionamento , onde o ADDcomando (column) está fora da transação,

-- 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;

para este violino db que não funciona,

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

Mas erros

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

Existe alguma maneira de tornar essa transação visível, em relação ao DDL, se comportar como o PostgreSQL?

Evan Carroll
fonte

Respostas:

17

De um modo geral, não. O SQL Server compila todo o lote no escopo atual antes da execução, para que as entidades referenciadas tenham que existir (recompilações no nível da instrução também podem ocorrer posteriormente). A principal exceção é a Resolução de Nome Diferido, mas isso se aplica a tabelas, não a colunas:

A resolução de nome adiada só pode ser usada quando você faz referência a objetos de tabela inexistentes. Todos os outros objetos devem existir no momento em que o procedimento armazenado é criado. Por exemplo, quando você faz referência a uma tabela existente em um procedimento armazenado, não pode listar colunas inexistentes para essa tabela.

As soluções alternativas comuns envolvem código dinâmico (como na resposta de Joe ) ou separam o DML e o DDL em lotes separados.

Para este caso específico, você também pode escrever:

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;

Você ainda não poderá acessar a coluna renomeada bno mesmo lote e escopo, mas ela realizará o trabalho.

No que diz respeito ao SQL Server, existe uma escola de pensamento que diz que misturar DDL e DML em uma transação não é uma boa ideia. Houve erros no passado em que isso resultou em log incorreto e em um banco de dados irrecuperável. No entanto, as pessoas fazem isso, especialmente com tabelas temporárias. Isso pode resultar em um código bastante difícil de seguir.

Paul White 9
fonte
12

É isso que você está procurando?

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;
Joe Obbish
fonte
2

Para a afirmação "geralmente não" da resposta de Paul White, espero que a seguir ofereça uma resposta direta à pergunta, mas também sirva para mostrar as limitações sistêmicas desse processo e afastá-lo de métodos que não permitem fácil gerenciamento e exposição riscos.

Ele pode ser mencionado muitas vezes não fazer DDL muda ao mesmo tempo que você está fazendo DML. Uma boa programação separa essas funções para manter a capacidade de suporte e evitar alterações nas cadeias de espaguete.

E, como Paul apontou sucintamente, o SQL Server trabalha em lotes .

Agora, para quem duvida que isso funcione, provavelmente não funciona em sua instância, mas algumas versões como 2017 podem realmente funcionar! Aqui está a prova: insira a descrição da imagem aqui

[CÓDIGO DE TESTE - PODE não funcionar em muitas versões do 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

[CONCLUSÃO]

Portanto, sim, você pode executar DDL e DML no mesmo lote para determinadas versões ou patches do SQL Server, como aponta @AndriyM - dbfiddle no SQL 2017 , mas nem todo DML é suportado e não há garantia de que sempre será esse o caso. Se funcionar, isso pode ser uma aberração da sua versão do SQL Server e isso pode causar problemas dramáticos ao corrigir ou migrar para novas versões.

  • Além disso, em geral, seu design deve antecipar alterações. Entendo que as preocupações de modificar / adicionar colunas podem ter em uma tabela, mas você pode projetar adequadamente isso em lotes.

[CRÉDITO EXTRA]

Quanto à instrução EXISTS, como Paul afirmou, existem muitos outros meios para validar o código antes de passar para a próxima etapa do seu código.

  • A instrução EXISTS pode ajudá-lo a criar código que funciona em todas as versões do SQL Server
  • É uma função booleana que permite verificações complexas em uma instrução
clifton_h
fonte
Não, você não pode inserir na nova coluna se estiver fazendo isso no mesmo lote em que está criando a coluna. De maneira mais geral, você não pode fazer referência estaticamente às novas colunas no mesmo lote depois de criá-las. O truque IF EXISTS não funciona neste caso. Invoque o DML dinamicamente ou faça-o em outro lote.
Andriy M
@AndriyM desculpe, fez uma declaração incorreta sobre dbfiddle. Mas você tentou isso em sua instância? Funciona no 2017 SP1. Vou fazer o upload de um gif, mas você testou isso em seus sistemas?
Clifton_h 13/05/19
i.imgur.com/fhAC7lB.png Você pode realmente dizer que não será compilado com base na linha ondulada abaixo bna instrução insert. Estou usando o SQL Server 2014
Andriy M
@AndriyM interesting. Eu já tinha visto esse efeito funcionar antes e parece funcionar em algumas versões do SQL Server, como o SQL Server 2017 que mencionei.
Clifton_h 13/05/19
@AndriyM veja a nova edição da publicação
clifton_h 13/05/19