sql >> Base de Datos >  >> RDS >> Database

Minimizar el impacto de ampliar una columna de IDENTIDAD - parte 3

[ Parte 1 | Parte 2 | Parte 3 | Parte 4 ]

Hasta ahora, en esta serie, he demostrado el impacto físico directo en la página al aumentar el tamaño de int a bigint , y luego iteró a través de varios de los bloqueadores comunes para esta operación. En esta publicación, quería examinar dos posibles soluciones:una simple y otra increíblemente complicada.

La Manera Fácil

Me robaron un poco mi trueno en un comentario en mi publicación anterior:Keith Monroe sugirió que podría volver a sembrar la tabla al negativo inferior límite del tipo de datos enteros, duplicando su capacidad para nuevos valores. Puedes hacer esto con DBCC CHECKIDENT :

DBCC CHECKIDENT(N'dbo.TableName', RESEED, -2147483648);

Esto podría funcionar, suponiendo que los valores sustitutos no tengan significado para los usuarios finales (o, si lo tienen, que los usuarios no se asusten si de repente obtienen números negativos). Supongo que podrías engañarlos con una vista:

CREATE VIEW dbo.ViewName
AS
  SELECT ID = CONVERT(bigint, CASE WHEN ID < 0 
    THEN (2147483648*2) - 1 + CONVERT(bigint, ID)
    ELSE ID END) 
  FROM dbo.TableName;

Esto significa que el usuario que agregó ID = -2147483648 en realidad vería +2147483648 , el usuario que agregó ID = -2147483647 vería +2147483649 , y así. Sin embargo, tendría que ajustar otro código para asegurarse de hacer el cálculo inverso cuando el usuario pase ese ID , por ejemplo

ALTER PROCEDURE dbo.GetRowByID
  @ID bigint
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @RealID bigint;
 
  SET @RealID = CASE WHEN @ID > 2147483647 
    THEN @ID - (2147483648*2) + 1
    ELSE @ID END;
 
  SELECT ID, @ID /*, other columns */ 
   FROM dbo.TableName 
   WHERE ID = @RealID;
END
GO

No estoy loco por esta ofuscación. En absoluto. Es desordenado, engañoso y propenso a errores. Y fomenta la visibilidad de las claves sustitutas, por lo general, IDENTITY. los valores no deben exponerse a los usuarios finales, por lo que realmente no debería importarles si son clientes 24, 642, -376 o números mucho más grandes en cualquier lado de cero.

Esta "solución" también asume que no tiene código en ninguna parte que ordene por IDENTITY columna para presentar primero las filas insertadas más recientemente, o infiere que la IDENTITY más alta el valor debe ser la fila más reciente. Código que hace confiar en el orden de clasificación de la IDENTITY la columna, ya sea explícita o implícitamente (que podría ser más de lo que cree si es el índice agrupado), ya no presentará las filas en el orden esperado; mostrará todas las filas creadas después de RESEED , comenzando con el primero, y luego mostrará todas las filas creadas antes del RESEED , empezando por el primero.

El beneficio principal de este enfoque es que no requiere que cambie el tipo de datos y, como resultado, el RESEED el cambio no requiere ningún cambio en los índices, restricciones o claves foráneas entrantes.

La desventaja, además de los cambios de código mencionados anteriormente, por supuesto, es que esto solo le da tiempo a corto plazo. Eventualmente, también agotarás todos los enteros negativos disponibles. Y no creas que esto duplica la vida útil de la versión actual de la tabla en términos de tiempo – en muchos casos, el crecimiento de los datos se acelera, no se mantiene constante, por lo que usará los próximos 2 mil millones de filas mucho más rápido que los primeros 2 mil millones.

Un Camino Más Difícil

Otro enfoque que podría tomar es dejar de usar una IDENTITY columna por completo; en su lugar, podría convertir a usar una SEQUENCE . Podrías crear un nuevo bigint columna, establezca el valor predeterminado en el siguiente valor de una SEQUENCE , actualice todos esos valores con los valores de la columna original (en lotes si es necesario), suelte la columna original y cambie el nombre de la nueva columna. Creemos esta tabla ficticia e insertemos una sola fila:

CREATE TABLE dbo.SequenceDemo
(
  ID int IDENTITY(1,1),
  x char(1),
  CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID)
);
GO
 
INSERT dbo.SequenceDemo(x) VALUES('x');

A continuación, crearemos una SEQUENCE que comienza justo más allá del límite superior de un int:

CREATE SEQUENCE dbo.BeyondInt
AS bigint
START WITH 2147483648 INCREMENT BY 1;

A continuación, los cambios en la tabla necesarios para cambiar a usar SEQUENCE para la nueva columna:

BEGIN TRANSACTION;
 
-- add a new "identity" column:
ALTER TABLE dbo.SequenceDemo ADD ID2 bigint;
GO
 
-- set the new column equal to the existing identity values
-- for large tables, may need to do this in batches:
UPDATE dbo.SequenceDemo SET ID2 = ID;
 
-- now make it not nullable and add the default from our SEQUENCE:
ALTER TABLE dbo.SequenceDemo ALTER COLUMN ID2 bigint NOT NULL;
ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT DF_SD_Identity DEFAULT NEXT VALUE FOR dbo.BeyondInt FOR ID2;
 
-- need to drop the existing PK (and any indexes):
ALTER TABLE dbo.SequenceDemo DROP CONSTRAINT PK_SD_Identity;
 
-- drop the old column and rename the new one:
ALTER TABLE dbo.SequenceDemo DROP COLUMN ID;
EXEC sys.sp_rename N'dbo.SequenceDemo.ID2', N'ID', 'COLUMN';
 
-- now put the PK back up:
ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID);
 
COMMIT TRANSACTION;

En este caso, la siguiente inserción produciría los siguientes resultados (tenga en cuenta que SCOPE_IDENTITY() ya no devuelve un valor válido):

INSERT dbo.SequenceDemo(x) VALUES('y');
SELECT Si = SCOPE_IDENTITY();
SELECT ID, x FROM dbo.SequenceDemo;
 
/* results
 
Si
----
NULL
 
ID           x
----------   -
1            x
2147483648   y           */

Si la tabla es grande y necesita actualizar la nueva columna en lotes en lugar de la transacción única anterior, como lo describí aquí, permitiendo a los usuarios interactuar con la tabla mientras tanto, necesitará tener un disparador en su lugar para anular la SEQUENCE valor para cualquier fila nueva que se inserte, de modo que continúen coincidiendo con lo que se genera en cualquier código de llamada. (Esto también supone que todavía tiene algo de espacio en el rango de números enteros para continuar aceptando algunas actualizaciones; de lo contrario, si ya agotó el rango, tendrá que tomarse un tiempo de inactividad, o usar la solución fácil anterior en el corto plazo .)

Dejemos todo y empecemos de nuevo, luego agreguemos la nueva columna:

DROP TABLE dbo.SequenceDemo;
DROP SEQUENCE dbo.BeyondInt;
GO
 
CREATE TABLE dbo.SequenceDemo
(
  ID int IDENTITY(1,1),
  x char(1),
  CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID)
);
GO
 
INSERT dbo.SequenceDemo(x) VALUES('x');
GO
 
CREATE SEQUENCE dbo.BeyondInt
AS bigint
START WITH 2147483648 INCREMENT BY 1;
GO
 
ALTER TABLE dbo.SequenceDemo ADD ID2 bigint;
GO

Y aquí está el activador que agregaremos:

CREATE TRIGGER dbo.After_SequenceDemo
ON dbo.SequenceDemo
AFTER INSERT
AS
BEGIN
  UPDATE sd SET sd.ID2 = sd.ID
    FROM dbo.SequenceDemo AS sd
    INNER JOIN inserted AS i
    ON sd.ID = i.ID;
END

Esta vez, la siguiente inserción seguirá generando filas en el rango inferior de enteros para ambas columnas, hasta que se hayan actualizado todos los valores preexistentes y se hayan confirmado el resto de los cambios:

INSERT dbo.SequenceDemo(x) VALUES('y');
SELECT Si = SCOPE_IDENTITY();
SELECT ID, ID2, x FROM dbo.SequenceDemo;
 
/* results
 
Si
----
2
 
ID    ID2   x
----  ----  --
1     NULL  x
2     2     y          */

Ahora, podemos continuar actualizando el ID2 existente valores mientras se siguen insertando nuevas filas dentro del rango inferior:

SET NOCOUNT ON;
 
DECLARE @r INT = 1;
 
WHILE @r > 0
BEGIN
  BEGIN TRANSACTION;
 
  UPDATE TOP (10000)
    dbo.SequenceDemo
    SET ID2 = ID WHERE ID2 IS NULL;
 
  SET @r = @@ROWCOUNT;
 
  COMMIT TRANSACTION;
 
  -- CHECKPOINT;    -- if simple
  -- BACKUP LOG ... -- if full
END

Una vez que hayamos actualizado todas las filas existentes, podemos continuar con el resto de los cambios y luego soltar el disparador:

BEGIN TRANSACTION;
ALTER TABLE dbo.SequenceDemo ALTER COLUMN ID2 BIGINT NOT NULL;
ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT DF_SD_Identity DEFAULT NEXT VALUE FOR dbo.BeyondInt FOR ID2;
ALTER TABLE dbo.SequenceDemo DROP CONSTRAINT PK_SD_Identity;
ALTER TABLE dbo.SequenceDemo DROP COLUMN ID;
EXEC sys.sp_rename N'dbo.SequenceDemo.ID2', N'ID', 'COLUMN';
ALTER TABLE dbo.SequenceDemo ADD CONSTRAINT PK_SD_Identity PRIMARY KEY CLUSTERED (ID);
DROP TRIGGER dbo.InsteadOf_SequenceDemo
COMMIT TRANSACTION;

Ahora, la siguiente inserción generará estos valores:

INSERT dbo.SequenceDemo(x) VALUES('z');
SELECT Si = SCOPE_IDENTITY();
SELECT ID, x FROM dbo.SequenceDemo;
 
/* results
 
Si
----
NULL
 
ID            x
----------    -
1             x
2             y
2147483648    z          */

Si tiene un código que se basa en SCOPE_IDENTITY() , @@IDENTITY , o IDENT_CURRENT() , también tendría que cambiar, ya que esos valores ya no se completan después de una inserción, aunque OUTPUT La cláusula debería continuar funcionando correctamente en la mayoría de los escenarios. Si necesitas tu código para seguir creyendo la tabla genera una IDENTITY valor, entonces podría usar un disparador para falsificar esto; sin embargo, solo podría completar @@IDENTITY al insertar, no SCOPE_IDENTITY() . Esto aún puede requerir cambios, porque en la mayoría de los casos, no desea confiar en @@IDENTITY para cualquier cosa (así que, si va a hacer cambios, elimine todas las suposiciones sobre una IDENTITY columna en absoluto).

CREATE TRIGGER dbo.FakeIdentity
ON dbo.SequenceDemo
INSTEAD OF INSERT
AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @lowestID bigint = (SELECT MIN(id) FROM inserted);
  DECLARE @sql nvarchar(max) = N'DECLARE @foo TABLE(ID bigint IDENTITY(' 
    + CONVERT(varchar(32), @lowestID) + N',1));';
  SELECT @sql += N'INSERT @foo DEFAULT VALUES;' FROM inserted;
  EXEC sys.sp_executesql @sql;
  INSERT dbo.SequenceDemo(ID, x) SELECT ID, x FROM inserted;
END

Ahora, la siguiente inserción generará estos valores:

INSERT dbo.SequenceDemo(x) VALUES('a');
SELECT Si = SCOPE_IDENTITY(), Ident = @@IDENTITY;
SELECT ID, x FROM dbo.SequenceDemo;
 
/* results
 
Si      Ident
----    -----
NULL    2147483649
 
ID            x
----------    -
1             x
2             y
2147483648    z
2147483649    a         */

Con esta solución alternativa, aún tendrá que lidiar con otras restricciones, índices y tablas con claves foráneas entrantes. Las restricciones locales y los índices son bastante sencillos, pero me ocuparé de la situación más compleja con claves foráneas en la siguiente parte de esta serie.

Uno que no funcionará, pero me gustaría que funcionara

ALTER TABLE SWITCH puede ser una forma muy poderosa de realizar algunos cambios en los metadatos que son difíciles de lograr de otra manera. Y, contrariamente a la creencia popular, esto no solo implica la creación de particiones y no se limita a Enterprise Edition. El siguiente código funcionará en Express y es un método que la gente ha usado para agregar o eliminar la IDENTITY propiedad en una tabla (nuevamente, sin tener en cuenta las claves foráneas y todos esos otros molestos bloqueadores).

CREATE TABLE dbo.WithIdentity
(
  ID int IDENTITY(1,1) NOT NULL
);
 
CREATE TABLE dbo.WithoutIdentity
(
  ID int NOT NULL
);
 
ALTER TABLE dbo.WithIdentity SWITCH TO dbo.WithoutIdentity;
GO
 
DROP TABLE dbo.WithIdentity;
EXEC sys.sp_rename N'dbo.WithoutIdentity', N'dbo.WithIdentity', 'OBJECT';

Esto funciona porque los tipos de datos y la nulabilidad coinciden exactamente, y no se presta atención a la IDENTITY atributo. Sin embargo, intente mezclar tipos de datos y las cosas no funcionan tan bien:

CREATE TABLE dbo.SourceTable
(
  ID int IDENTITY(1,1) NOT NULL
);
 
CREATE TABLE dbo.TrySwitch
(
  ID bigint IDENTITY(1,1) NOT NULL
);
 
ALTER TABLE dbo.SourceTable SWITCH TO dbo.TrySwitch;

Esto resulta en:

Mensaje 4944, nivel 16, estado 1
La declaración ALTER TABLE SWITCH falló porque la columna 'ID' tiene un tipo de datos int en la tabla de origen 'dbo.SourceTable' que es diferente de su tipo bigint en la tabla de destino 'dbo.TrySwitch'.

Sería fantástico si un SWITCH La operación podría usarse en un escenario como este, donde la única diferencia en el esquema en realidad no *requirió* ningún cambio físico para adaptarse (nuevamente, como mostré en la parte 1, los datos se vuelven a escribir en páginas nuevas, aunque no hay necesidad de hacerlo).

Conclusión

Esta publicación investigó dos posibles soluciones para ganar tiempo antes de cambiar su IDENTITY existente columna, o abandonar IDENTITY ahora mismo a favor de una SEQUENCE . Si ninguna de estas soluciones es aceptable para usted, consulte la parte 4, donde abordaremos este problema de frente.

[ Parte 1 | Parte 2 | Parte 3 | Parte 4 ]