¿SQL Server permite (hacer visible) DDL dentro de una transacción a la transacción antes de la confirmación?

9

En PostgreSQL puedo crear una tabla con algunos datos de prueba, y luego, en una transacción, migrarla a una nueva columna de un tipo diferente, lo que da como resultado una reescritura de la tabla 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;

Sin embargo, esa misma cosa en el SQL Server de Microsoft parece generar un error. Compare este db fiddle de trabajo , donde el ADDcomando (columna) está fuera de la transacción,

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

a este violín db que no 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;

Pero en cambio errores

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

¿Hay alguna forma de hacer que esta transacción sea visible, con respecto a DDL, comportarse como PostgreSQL?

Evan Carroll
fuente

Respuestas:

17

En general, no. SQL Server compila todo el lote en el ámbito actual antes de la ejecución, por lo que las entidades a las que se hace referencia deben existir (las recompilaciones a nivel de declaración también pueden ocurrir más adelante). La principal excepción es la resolución de nombres diferidos, pero eso se aplica a las tablas, no a las columnas:

La resolución de nombre diferido solo se puede usar cuando hace referencia a objetos de tabla inexistentes. Todos los demás objetos deben existir en el momento en que se crea el procedimiento almacenado. Por ejemplo, cuando hace referencia a una tabla existente en un procedimiento almacenado, no puede enumerar columnas inexistentes para esa tabla.

Las soluciones comunes incluyen código dinámico (como en la respuesta de Joe ), o separar el DML y el DDL en lotes separados.

Para este caso específico también podría escribir:

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;

Aún no podrá acceder a la columna renombrada ben el mismo lote y alcance, pero hace el trabajo.

Con respecto a SQL Server, hay una escuela de pensamiento que dice que mezclar DDL y DML en una transacción no es una gran idea. Ha habido errores en el pasado donde hacer esto ha resultado en un registro incorrecto y una base de datos irrecuperable. Sin embargo, la gente lo hace, especialmente con tablas temporales. Puede resultar en un código bastante difícil de seguir.

Paul White 9
fuente
12

¿Es esto lo que estás buscando?

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
fuente
2

A la declaración "generalmente no" sobre la respuesta de Paul White, espero que lo siguiente ofrezca una respuesta directa a la pregunta, pero también sirva para mostrar las limitaciones sistémicas de dicho proceso y lo aleje de los métodos que no se prestan a una administración y exposición fáciles riesgos

Se puede mencionar muchas veces que no debe realizar cambios DDL al mismo tiempo que realiza DML. Una buena programación separa estas funciones para mantener la capacidad de soporte y evitar cambios en la cadena de espaguetis.

Y como Paul señaló sucintamente, SQL Server funciona en lotes .

Ahora, para aquellos que dudan de que esto funcione, probablemente no funcione en su instancia, ¡pero algunas versiones como 2017 realmente pueden funcionar! Aquí está la prueba: ingrese la descripción de la imagen aquí

[CÓDIGO DE PRUEBA - PUEDE no funcionar en muchas versiones de 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

[CONCLUSIÓN]

Entonces, sí, puede realizar DDL y DML en el mismo lote para ciertas versiones o parches de SQL Server que @AndriyM, señala dbfiddle en SQL 2017 , pero no todos los DML son compatibles y no hay garantía de que siempre sea así. Si funciona, puede ser una aberración de su versión de SQL Server y esto podría causar problemas dramáticos al parchear o migrar a nuevas versiones.

  • Además, en general, su diseño debe anticipar cambios. Entiendo que las preocupaciones de modificar / agregar columnas pueden tener en una tabla, pero puede diseñar adecuadamente alrededor de esto en lotes.

[CRÉDITO ADICIONAL]

En cuanto a la declaración EXISTS, como dijo Paul, hay muchos otros medios para validar el código antes de pasar al siguiente paso en su código.

  • La declaración EXISTS puede ayudarlo a crear código que funcione en todas las versiones de SQL Server
  • Es una función booleana que permite verificaciones complejas en una declaración
clifton_h
fuente
No, no puede insertar en la nueva columna si está haciendo esto en el mismo lote donde está creando la columna. En términos más generales, no puede hacer referencia estática a las nuevas columnas en el mismo lote después de haberlas creado. El truco IF EXISTS no funciona en este caso. Invoque el DML dinámicamente o hágalo en otro lote.
Andriy M
@AndriyM lo siento, hice una declaración incorrecta sobre dbfiddle. ¿Pero probaste esto en tu instancia? Funciona en 2017 SP1. Subiré un gif, pero ¿probaste esto en tus sistemas?
clifton_h
i.imgur.com/fhAC7lB.png En realidad, puede decir que no se compilará en función de la línea ondulada que se encuentra debajo ben la instrucción de inserción. Estoy usando SQL Server 2014
Andriy M
@AndriyM interesante. Había visto que esto afectaba el trabajo antes, y parece funcionar en algunas versiones de SQL Server, como el SQL Server 2017 que mencioné.
clifton_h
@AndriyM ver nueva edición para publicar
clifton_h