Alteración rápida de columna NVARCHAR (4000) a NVARCHAR (260)

12

Tengo un problema de rendimiento con concesiones de memoria muy grandes que manejan esta tabla con un par de NVARCHAR(4000)columnas. La cosa es que estas columnas nunca son más grandes que NVARCHAR(260).

Utilizando

ALTER TABLE [table] ALTER COLUMN [col] NVARCHAR(260) NULL

da como resultado que SQL Server reescriba toda la tabla (y use un tamaño de tabla 2x en el espacio de registro), que es miles de millones de filas, solo para no cambiar nada, no es una opción. Aumentar el ancho de la columna no tiene este problema, pero disminuirlo sí.

He intentado crear una restricción CHECK (DATALENGTH([col]) <= 520)o CHECK (LEN([col]) <= 260)y SQL Server todavía decide reescribir toda la tabla.

¿Hay alguna forma de alterar el tipo de datos de la columna como una operación de solo metadatos? ¿Sin el gasto de reescribir toda la tabla? Estoy usando SQL Server 2017 (14.0.2027.2 y 14.0.3192.2).

Aquí hay una tabla DDL de muestra para usar para reproducir:

CREATE TABLE [table](
    id INT IDENTITY(1,1) NOT NULL,
    [col] NVARCHAR(4000) NULL,
    CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED (id ASC)
);

Y luego ejecuta el ALTER.

Nick Whaley
fuente

Respuestas:

16

No sé una manera de lograr directamente lo que estás buscando aquí. Tenga en cuenta que el optimizador de consultas no es lo suficientemente inteligente en este momento como para tener en cuenta las restricciones para los cálculos de concesión de memoria, por lo que la restricción no habría ayudado de todos modos. Algunos métodos que evitan reescribir los datos de la tabla:

  1. CAST la columna como NVARCHAR (260) en todos los códigos que la usan. El optimizador de consultas calculará la concesión de memoria utilizando el tipo de datos fundidos en lugar del bruto.
  2. Cambie el nombre de la tabla y cree una vista que haga el reparto. Esto logra lo mismo que la opción 1, pero puede limitar la cantidad de código que necesita actualizar.
  3. Cree una columna calculada no persistente con el tipo de datos correcto y haga que todas sus consultas se seleccionen de esa columna en lugar de la original.
  4. Cambie el nombre de la columna existente y agregue la columna calculada con el nombre original. Luego, ajuste todas sus consultas haciendo actualizaciones o inserciones en la columna original para usar el nuevo nombre de columna en su lugar.
Joe Obbish
fuente
15

¿Hay alguna forma de alterar el tipo de datos de la columna como una operación de solo metadatos?

No lo creo, así es como funciona el producto en este momento. Hay algunas soluciones realmente buenas para esta limitación propuesta en la respuesta de Joe .

... da como resultado que SQL Server reescriba toda la tabla (y use un tamaño de tabla 2x en el espacio de registro)

Voy a responder a las dos partes de esa declaración por separado.

Reescribiendo la tabla

Como mencioné antes, en realidad no hay forma de evitar esto. Esa parece ser la realidad de la situación, incluso si no tiene sentido desde nuestra perspectiva como clientes.

Mirar DBCC PAGEantes y después de cambiar la columna de 4000 a 260 muestra que todos los datos están duplicados en la página de datos (mi tabla de prueba tenía 'A'260 veces seguidas):

Captura de pantalla de la porción de datos de la página dbcc antes y después

En este punto, hay dos copias de los mismos datos exactos en la página. La columna "antigua" se elimina esencialmente (la identificación cambia de id = 2 a id = 67108865), y la versión "nueva" de la columna se actualiza para señalar el nuevo desplazamiento de los datos en la página:

Captura de pantalla de porciones de metadatos de columna de la página dbcc antes y después

Uso de 2x tamaño de tabla en espacio de registro

Agregar WITH (ONLINE = ON)al final de la ALTERdeclaración reduce la actividad de registro a la mitad , por lo que esta es una mejora que podría hacer para reducir la cantidad de escrituras en el disco / espacio en disco necesario.

Usé este arnés de prueba para probarlo:

USE [master];
GO
DROP DATABASE IF EXISTS [248749];
GO
CREATE DATABASE [248749] 
ON PRIMARY 
(
    NAME = N'248749', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\248749.mdf', 
    SIZE = 2048000KB, 
    FILEGROWTH = 65536KB
)
LOG ON 
(
    NAME = N'248749_log', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\248749_log.ldf', 
    SIZE = 2048000KB, 
    FILEGROWTH = 65536KB
);
GO
USE [248749];
GO

CREATE TABLE dbo.[table]
(
    id int IDENTITY(1,1) NOT NULL,
    [col] nvarchar (4000) NULL,

    CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED (id ASC)
);

INSERT INTO dbo.[table]
SELECT TOP (1000000)
    REPLICATE(N'A', 260)
FROM master.dbo.spt_values v1
    CROSS JOIN master.dbo.spt_values v2
    CROSS JOIN master.dbo.spt_values v3;
GO

Verifiqué sys.dm_io_virtual_file_stats(DB_ID(N'248749'), DEFAULT)antes y después de ejecutar la ALTERdeclaración, y aquí están las diferencias:

Predeterminado (sin conexión) ALTER

  • Escrituras de archivo de datos / bytes escritos: 34,809 / 2,193,801,216
  • Escrituras de archivo de registro / bytes escritos: 40,953 / 1,484,910,080

En línea ALTER

  • Escrituras de archivo de datos / bytes escritos: 36,874 / 1,693,745,152 (22.8% de caída)
  • Escrituras de archivo de registro / bytes escritos: 24,680 / 866,166,272 (41% de caída)

Como puede ver, hubo una ligera caída en las escrituras del archivo de datos y una caída importante en las escrituras del archivo de registro.

Josh Darnell
fuente
2

He estado en una situación similar muchas veces.

Pasos:

Agregue una nueva columna del ancho deseado

Use un cursor, con algunos miles de iteraciones (tal vez diez o veinte mil) por confirmación para copiar datos de la columna anterior a la nueva columna

Soltar columna antigua

Cambiar el nombre de la nueva columna al nombre de la columna anterior

Tada!

Jonesome restablecer monica
fuente
3
¿Qué sucede si algunos registros que ya ha copiado terminan siendo actualizados o eliminados?
George.Palacios
1
Es muy fácil hacer una final update table set new_col = old_col where new_col <> old_col;antes de caer old_col.
Colin 't Hart
1
@ Colin'tHart ese enfoque no funcionará con millones de filas ... la transacción se vuelve enorme y bloquea ...
Jonesome Reinstate Monica el
@samsmith Primero, haga lo que describe arriba. Luego, antes de descartar la columna original, si ha habido alguna actualización de los datos originales mientras tanto, ejecute esa declaración de actualización. Solo debería afectar a las pocas filas que se han modificado. ¿O me estoy perdiendo algo?
Colin 't Hart
Para cubrir las filas actualizadas durante el proceso, tratando de evitar el análisis completo que where new_col <> old_colsin otras cláusulas de filtrado resultará, puede agregar un disparador para llevar estos cambios a medida que suceden y eliminarlos al final del proceso. Sigue siendo un posible éxito en el rendimiento, pero muchas pequeñas cantidades a lo largo del proceso en lugar de un gran éxito al final, probablemente (dependiendo del patrón de actualización de la aplicación para la tabla) que suman mucho menos en total que ese gran éxito .
David Spillett
1

Bueno, hay una alternativa dependiendo del espacio disponible en su base de datos.

  1. Cree una copia exacta de su tabla (p new_table. Ej. ), Excepto la columna desde donde acortará NVARCHAR(4000)a NVARCHAR(260):

    CREATE TABLE [new_table](
        id INT IDENTITY(1,1) NOT NULL,
        [col] NVARCHAR(260) NULL,
        CONSTRAINT [PK_test_new] PRIMARY KEY CLUSTERED (id ASC)
    );
    
  2. En una ventana de mantenimiento, copie los datos de la tabla "rota" ( table) a la tabla "fija" ( new_table) con un simple INSERT ... INTO ... SELECT ....:

    SET IDENTITY_INSERT [new_table] ON
    GO
    INSERT id, col INTO [new_table] SELECT id, col from [table]
    GO
    SET IDENTITY_INSERT [new_table] OFF
    GO
    
  3. Cambie el nombre de la tabla "rota" tablea otra:

    EXEC sp_rename 'table', 'old_table';  
  4. Cambie el nombre de la tabla "fija" new_tablea table:

    EXEC sp_rename 'new_table', 'table';  
  5. Si todo está bien, suelte la tabla renombrada "rota":

     DROP TABLE [old_table]
     GO
    

Ahí tienes.

Contestando tus preguntas

¿Hay alguna forma de alterar el tipo de datos de la columna como una operación de solo metadatos?

No. Actualmente no es posible

¿Sin el gasto de reescribir toda la tabla?

No.
( Vea mi solución y otras ) .

John aka hot2use
fuente
Su "inserción en seleccionar de" resultará, en una tabla grande (millones o miles de millones de filas) en una ENORME transacción, que puede detener el DB durante decenas o cientos de minutos. (Además de hacer que el ldf sea enorme y posiblemente rompa el envío de registros, si está en uso)
Jonesome Reinstate Monica el