Error al Importar BACPAC a una base de datos Azure SQL Database

Hace un par de días tuve un inconveniente al intentar restaurar un archivo BACPAC a una base de datos Azure SQL Database, y el error que se mostraba no indicaba nada obvio y tuve que hacer diversas pruebas para por fin resolver el problema, sin embargo no encontré alguna documentación que sustentara el problema, así que asumo que es un bug. A continuación el error exacto que se mostraba:

Error encountered during the service operation.
	Value cannot be null.
Parameter name: dataString

chrome_2018-03-09_08-26-32

Sigue leyendo “Error al Importar BACPAC a una base de datos Azure SQL Database”

Anuncios

Implementando Dynamic Data Masking en Azure SQL Database

Dentro de Azure SQL Database se ofrecen una serie de características que ayudan a proteger la información que dentro de la base de datos se almacena. Una de ellas es la llamada “Dynamic Data Masking“, la cual fue introducida en la versión SQL Server 2016. Esta caracteristica permite enmascarar o cambiar la información cuando es consultada con el fin de no exponer información que puede ser confidencial o sensible. Un ejemplo clásico de este tipo de información son los números de tarjeta de crédito, los correos electrónicos, los números de documentos de identidad, etc. Sigue leyendo “Implementando Dynamic Data Masking en Azure SQL Database”

Estructura Interna del Log de Transacciones

Como habíamos revisado hace algún tiempo en este post, las bases de datos en SQL Server cuentan con dos tipos de archivos, los cuales son: los datafiles y el transaction log. Ya habíamos también revisado de manera general, en este post, para que servía el transaction log y cual era su importancia dentro de la base de datos. Ahora en este post vamos a dar algunos detalles adicionales sobre su estructura interna.

El log de transacciones de una base de datos esta dividido internamente en secciones llamadas VLF (Virtual Log Files) . Los VLFs se crean automáticamente cuando el log de transacciones se crea o cambia de tamaño, y dependiendo del espacio que se le este asignando a la creación o incremento de tamaño, se crearan más o menos VLFs. A continuación se detallan las reglas  de creación de los VLFs:

  • Porciones de crecimiento menor o igual a 64MB = 4 VLFs
  • Porciones de crecimiento mayor a 64MB y menor o igual a 1GB = 8 VLFs
  • Porciones de crecimiento mayor a 1GB = 16 VLFs

Estas reglas de creación de VLFs están vigentes desde hace mucho tiempo, sin embargo a partir de SQL Server 2014, se agrego una variación a ellas con el propósito de reducir la cantidad de VLFs que se crean en las bases de datos. Esta variación tiene como lógica lo siguiente:

¿La porción de crecimiento es menor a 1/8 del tamaño total del archivo log de transacciones?

  • Si: Se crea solo 1 VLF del tamaño de la porción de crecimiento
  • No: Se crean VLFs siguiendo las reglas de arriba.

Para poder verificar el numero de VLFs creados dentro de una base de datos se debe utilizar el comando: DBCC LOGINFO. Si se ejecuta ese comando desde el contexto de la base de datos que se desea consultar no se necesita enviar ningún parámetro, pero si se desea consultar los VLFs de una base de datos distinta a la que estamos conectados debemos de pasar el nombre de la base de datos como parámetro:

DBCC LOGINFO('master')

Ssms_2018-01-29_12-24-37

En el resultado producido por el comando anterior, la columna “Status” es la que nos indicara si ese VLF esta “Activo”, es decir, contiene registros de transacciones que no pueden ser sobre escritas, ya sea porque pertenecen a una transacción activa o aun no se ha respaldado el log de transacciones con un backup de log. Esta ultima razón es valida cuando la base de datos se encuentra en modelo de recuperación Full o Bulk-logged. Es importante mantener un adecuado control de VLFs “Activos” (Status=2), ya que esto determina si el autocrecimiento del log de transacciones se disparará cuando no hayan más VLFs Inactivos (Status=0) dentro de nuestra base de datos.

Ahora, la pregunta que algunos pueden estarse haciendo es, ¿y eso … que tiene de malo? Bueno, para comenzar, esa es la razón por la que muchas bases de datos tienen archivos de log de transacciones bastante grandes, en ocasiones, incluso mas grandes que la misma data de la base de datos. Y la otra razón muy importante es que si se disparan muchos eventos de auto crecimiento se crearan también muchos VLFs, y eso causara un archivo de log altamente fragmentado que experimentara problemas de performance cuando se generen los backups de log y cuando se da el proceso de recuperación de la base de datos.

A continuación vamos a ver un ejemplo de creación de una base de datos con un log de transacciones de 512 MB. Según las reglas definidas, este debería nacer con 8 VLFs:

USE [master]
GO
IF DB_ID('VLFDB') > 0
  DROP DATABASE [VLFDB]
go
CREATE DATABASE [VLFDB]
  ON  PRIMARY
    (
      NAME = VLFDB,
      FILENAME = N'F:\SQL2K16\DATA\VLFDB.mdf' ,
      SIZE = 512 MB ,
      MAXSIZE = UNLIMITED,
      FILEGROWTH = 1024KB
    )
  LOG ON
    (
      NAME = VLFDB_log,
      FILENAME = N'G:\SQL2K16\LOG\VLFDB_log.ldf' ,
      SIZE = 512 MB ,
      MAXSIZE = UNLIMITED ,
      FILEGROWTH = 64 MB
    )
GO
use [VLFDB]
GO
DBCC LOGINFO

Ssms_2018-01-30_12-14-08

Ahora veremos que sucede si agrego más espacio a mi log de transacciones, específicamente lo agrandaremos a 576 MB, es decir 512 MB (tamaño original) + 64 MB (porción de crecimiento), por lo tanto la porción de crecimiento es 64MB, lo cual es exactamente 1/8 del tamaño original de mi log de transacciones. Según las reglas explicadas en este post se deberían crear 4 VLFs ya que la porción de espacio que se agrego no es menor a 1/8 sino igual a 1/8. Veamos:

ALTER DATABASE [VLFDB]
  MODIFY FILE
    (name = VLFDB_log
    ,size = 576 MB)
GO
use [VLFDB]
GO
DBCC LOGINFO

2018-01-30_12-29-41

Ahora agregaremos 64MB adicionales al log de transacciones, esto hará que mi archivo tenga un tamaño total de 640 MB. Los 64MB adicionales, ahora si son menor al 1/8 del tamaño total del log (576 MB), por ende se debería crear un solo VLF:

ALTER DATABASE [VLFDB]
  MODIFY FILE
    (name = VLFDB_log
    ,size = 640 MB)
GO
use [VLFDB]
GO
DBCC LOGINFO

2018-01-30_13-05-48

Como podemos observar nuestro log de transacciones se comporta como lo esperábamos con respecto a las reglas expuestas en este post. Espero haber sido claro con respecto a los VLFs, en caso tengan alguna duda por favor hacérmela saber en los comentarios.

Para que sirve el ANSI_PADDING

22effbMuchas veces me he encontrado con scripts que tenían entre sus lineas iniciales la configuración explicita del ANSI_PADDING, y siempre la ignoraba al no saber que implicaba tenerla e ignorar los subsecuentes efectos que podría causar el configurar dicha opción en ON u OFF. Sin embargo hace poco tuve un caso algo extraño para mi en un cliente, el cual reportaba que su data parecía cortada al final. Luego de revisar los procedimientos almacenados que guardaban la información en esta tabla y confirmar que estos no estaban haciendo algo raro al momento de grabar la información, me percate que la tabla al que el cliente hacia referencia fue creada con la opción ANSI_PADDING en OFF.

Esta es una opción de configuración de la sesión que básicamente controla la forma en la que SQL Server almacena los espacios en blanco o ceros situados al final del valor de la columna. Los tipos de datos que son afectados por esta opción de configuración son: char, varchar, binary, varbinary. Es importante notar que NO están considerados los tipos de datos UNICODE, es decir los siguientes: nchar y nvarchar. Finalmente hay que indicar que esta opción solo afecta al momento de crear una tabla, agregar o modificar una columna, y si ya se tienen registros en la tabla, estos NO son afectados con el cambio.

Cuando se crea o se altera una columna y se tiene la opción de ANSI_PADDING ON, el cual es el valor por defecto, las columnas mantendrán los espacios en blanco o ceros al que se coloquen al final del valor de la columna, en el caso particular de los char se rellanaran con espacios en blanco hasta alcanzar el valor máximo de caracteres definidos para la columna. Caso contrario, cuando se crea o altera una columna y la sesión tiene la opción de ANSI_PADDING OFF, SQL Server eliminara los espacios en blanco o ceros situados al final del valor de la columna. Sin embargo si hubieran espacios en blanco o ceros al inicio del valor de la columna, estos se mantienen y no son afectados.

Microsoft recomienda siempre mantener el valor por defecto ANSI_PADDING ON en la documentación oficial

Para poder comprobar esto vamos a crear dos tablas, ambas con columnas char y varchar, pero la diferencia entre ellas sera el valor de la opción ANSI_PADDING que tendrá al momento de crear la tabla.

SET ANSI_PADDING ON
GO
CREATE TABLE TablaConPadding
  (col1 char(50)
  ,col2 varchar(50))
GO

SET ANSI_PADDING OFF
GO
CREATE TABLE TablaSinPadding
  (col1 char(50)
  ,col2 varchar(50))
GO

Una vez creadas vamos a verificar si efectivamente la opción de ANSI_PADDING esta colocada a nivel de las columnas creadas:

select
  t.object_id
  ,t.name as table_name
  ,c.column_id
  ,c.name as column_name
  , c.is_ansi_padded
from sys.columns c
inner join sys.tables t on c.object_id = t.object_id
where t.name IN ('TablaConPadding','TablaSinPadding')

Ssms_2018-01-09_14-29-46

Ahora veremos su comportamiento insertando algunos valores sin espacios para ver cuanto es en realidad lo que se ha guardado del valor completo, para esto utilizaremos la función DATALENGHT la cual retorna el numero de bytes que ocupa un dato.

-- insertamos los valores sin espacios
insert into TablaConPadding values ('Mundo','Mundo');
insert into TablaSinPadding values ('Mundo','Mundo');

-- revisamos la cantidad de bytes guardados
select 'TablaConPadding' as TableName
  ,col1, col2
  ,DATALENGTH(col1) BytesCharCol
  ,DATALENGTH(col2) BytesVarcharCol
from TablaConPadding
union all
select 'TablaSinPadding' as TableName
  ,col1, col2
  ,DATALENGTH(col1) BytesCharCol
  ,DATALENGTH(col2) BytesVarcharCol
from TablaSinPadding

Ssms_2018-01-09_15-08-41

Como se puede observar, en la tabla que se creo con la opción ANSI_PADDING ON, la columna “col1”, la cual es CHAR(50), se relleno con espacios en blanco a la derecha hasta llegar a los 50 bytes de limite que tiene. Mientras que en la tabla que se creo con la opción de ANSI_PADDING OFF, la columna “col1” se comporta como si fuera un VARCHAR ya que no considera todos los espacios en blanco que se colocarían en la derecha como se hizo con la primera tabla.

Ahora ingresaremos valores con tres espacios al inicio y al final, luego verificaremos cuanto espacio ocupan estos datos nuevos dentro de nuestras dos tablas:

-- insertamos los valores
insert into TablaConPadding values ('   Hola   ','   Hola   ');
insert into TablaSinPadding values ('   Hola   ','   Hola   ');

-- revisamos la cantidad de bytes guardados
select 'TablaConPadding' as TableName
  ,col1, col2
  ,DATALENGTH(col1) BytesCharCol
  ,DATALENGTH(col2) BytesVarcharCol
from TablaConPadding
union all
select 'TablaSinPadding' as TableName
  ,col1, col2
  ,DATALENGTH(col1) BytesCharCol
  ,DATALENGTH(col2) BytesVarcharCol
from TablaSinPadding

select col1, col2, LEN(col1),LEN(col2) from TablaConPadding
select col1, col2, LEN(col1),LEN(col2) from TablaSinPadding

2018-01-09_16-03-18

Ahora, si notamos la tabla que se creo con ANSI_PADDING ON, si considero los caracteres en blanco tanto a la derecha como a la izquierda, tanto en el caso de los CHAR como el de los VARCHAR, repitiendose el comportamiento de los CHAR de llenar todo el espacio disponible. Finalmente la tabla que se creo con ANSI_PADDING OFF se puede verificar que mantuvo los caracteres en blanco de la izquierda, pero elimino los de la derecha, eso era el comporamiento esperado.

Finalmente actualizaremos la tabla que se creo con ANSI_PADDING OFF para que su columna “col2” funcione con ANSI_PADDING ON y comprobaremos que los registros existentes no son afectados y la nueva configuración entra en funcionamiento desde ese punto en adelante.

-- cambiamos la col2
SET ANSI_PADDING ON
GO
ALTER TABLE TablaSinPadding
  ALTER COLUMN col2 varchar(50)
GO

-- insertamos los valores
insert into TablaSinPadding values ('   Dummy   ','   Dummy   ');

-- revisamos la cantidad de bytes guardados
select 'TablaSinPadding' as TableName
  ,col1, col2
  ,DATALENGTH(col1) BytesCharCol
  ,DATALENGTH(col2) BytesVarcharCol
from TablaSinPadding

2018-01-09_16-44-29

Como vemos en los resultados, luego de agregar el valor “Dummy” con tres espacios al inicio y tres al final, la columna “col2” contabilizo 11 bytes correspondientes a los 3 espacios en blanco iniciales y 3 finales, más los 5 caracteres de la palabra “Dummy”. Los registros existentes se mantienen con sus valores anteriores.

T-SQL Tuesday #98 – Your Technical Challenges Conquered

sqltuesdayThis is the first time I post for this fantastic initiative called T-SQL Tuesday, created by Adam Mechanic (b|t), and in this occasion hosted by Arun Sirpal (b|t) on this post. I postponed my participation on this blog party, mostly because my blog is mostly targeted to spanish speaking readers, however I did not want to let it pass this time, because I have a really good story to share.

Not long ago I entered into a new company where they had several database related problems, however the most urgent was the infamous blocking sessions. These blocking problems were created by a poorly programmed application, but since it was so old and so critical, nobody had the curage or knowledge to change a bit of the code. The duration of each blocked session could reach up to 2 minutes, yes you read that right, 2 minutes.

Digging into the issue I found that the most common blocking pattern was as follow:

  • Session 1: updated an auditing column on the parent table. (Blocker Session)
  • Session 2: inserted a record on the child table for the same record updated on session 1. (Blocked Session)

Off the top of my head I could no find a reason why session 2 got blocked by session 1, as it was not touching any column from parent table. Reviewing the blocking resources I found that since both tables were joined by a foreign key constraint, when the insert in the child table occurred, SQL Server had to check on the parent table if the value for the foreign key was valid, so it had to do a cluster index seek on the parent table. I thougth I found the cause and then I decided that since there were so much blocking in this database I would enable the “Read Commited Snapshot” isolation level, allowing readers not to be blocked by writers and viceversa, that would allow the insert statement to complete without getting blocked, right? However when I reviewed the database configuration it was already enabled.

Turns out that Foreign Key validation is always done under the “Read Commited” isolation level regardless of the isolation level you configured on your user database. So I spent several hours banging my head on the wall trying to come up with a workaround to solve the issue or at least reduce the impact, and then it hit me like a lightning, I have to avoid validating against the Cluster index, so I came up with two possible work arounds:

  1. Convert the Cluster Primary Key into a Non-cluster primary key, which would require to lock the table during the whole process, rebuild indexes, and re-create foreign key constraints, plus the possibility of ending up with a “Heap” table or choosing a new Cluster index (major change).
  2. Create a Unique Index on the same primary key, meaning having a duplicate index, but much faster to implement.

I chose to do the later, so after creating the unique key, I rebuild only foreign key constraint on the child table and the problem was gone, SQL Server was smart enough to choose the unique index instead of the Cluster index to build the foreign key. After that change all blocking problems between both tables were gone, we had to struggle with other patters of blocking processes but those were much simpler and easier to solve.

I hope you enjoied this experience that challenge my technical skills to the limit. Here is a script to reproduce the issue,

use master
GO
-- Create the test database and configure it as read commited snapshot
IF (DB_ID('WeirdLockIssue') IS NOT NULL)
BEGIN
  ALTER DATABASE WeirdLockIssue SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
  DROP DATABASE WeirdLockIssue;
END
CREATE DATABASE WeirdLockIssue
GO
CREATE DATABASE WeirdLockIssue
GO
ALTER DATABASE WeirdLockIssue SET READ_COMMITTED_SNAPSHOT ON;
GO
USE WeirdLockIssue
GO
-- create test tables
CREATE TABLE dbo.ParentTable
  (ParentTableId int not null identity(1,1)
  ,DateCreated datetime not null
  ,DateUpdated datetime null
  ,CONSTRAINT pk_ParentTable PRIMARY KEY (ParentTableId))
GO
CREATE TABLE dbo.ChildTable
  (ChildTableId int not null identity(1,1)
  ,ParentTableId int not null
  ,DateCreated datetime not null
  ,DateUpdated datetime null
  ,CONSTRAINT pk_ChildTable PRIMARY KEY (ChildTableId)
  ,CONSTRAINT fk_ChildTable_ParentTableId
    FOREIGN KEY (ParentTableId)
      REFERENCES dbo.ParentTable (ParentTableId))
GO
-- insert test record on parent
INSERT INTO ParentTable (DateCreated) VALUES (GETDATE())

/*
-- Open new session and execute the following (session 1)
USE WeirdLockIssue
GO
BEGIN TRANSACTION
UPDATE ParentTable
SET DateUpdated = GETDATE()
WHERE ParentTableId=1
*/

/*
-- Open new session and execute the following (session 2)
use WeirdLockIssue
go
INSERT INTO dbo.ChildTable  (ParentTableId  ,DateCreated)
VALUES  (1,GETDATE())
*/

-- to solve the issue, create a unique index on parent table
CREATE UNIQUE INDEX uix_ParentTable_ParentTableId
  ON dbo.ParentTable (ParentTableId)
-- rebuild foreign key constraint
ALTER TABLE dbo.ChildTable
  DROP CONSTRAINT fk_ChildTable_ParentTableId
GO
ALTER TABLE dbo.ChildTable
  ADD CONSTRAINT fk_ChildTable_ParentTableId
    FOREIGN KEY (ParentTableId)
      REFERENCES dbo.ParentTable (ParentTableId)
GO
-- Query to confirm that the child table is aiming the unique key index
select
  fk.name as ForeignKeyName,
  obj.name as ReferencedTable,
  ix.name as ReferencedIndex
from sys.foreign_keys fk
inner join sys.objects obj
  on fk.referenced_object_id = obj.object_id
inner join sys.indexes ix
  on obj.object_id = ix.object_id
  and fk.key_index_id = ix.index_id
where fk.parent_object_id = OBJECT_ID('dbo.ChildTable')

¿Problemas con “Collations”?

No es raro encontrar situaciones en las que un procedimiento almacenado o una consulta funcionaba correctamente en casi todos los ambientes excepto en uno, y normalmente nos encontramos con el siguiente error:

Cannot resolve the collation conflict between “Modern_Spanish_100_CI_AI” and “SQL_Latin1_General_CP1_CI_AS” in the equal to operation.

ira maquina

Este error es producido porque las columnas de condición que se esta usando para un JOIN entre dos tablas son de tipo cadena (CHAR, NCHAR, VARCHAR, NVARCHAR), y el collation de ambas es distinto.

Primero debemos definir que es el “collation”. El “collation” en resumen es una configuracion de servidor, bases de datos y columnas que determinan el comportamiento que tendra la data de tipo caracter con relacion al idioma seleccionado. Para mayor informacion sobre el tema recomiento el siguiente link. Esta configuración no se puede obviar o desaparecer. Para el servidor se determina al momento de la instalacion, asi que si para instalar SQL Server siempre hacer “Next” hasta llegar al final, seguramente instalaste el collation por defecto de SQL Server. Para las bases de datos se determina al momento de crearlas, si no se coloco valor, heredaran la del servidor. Y finalmente la de las columnas se determinan al momento de crear la tabla o agregar nuevas columnas a una tabla existente, si no se determino valor, heredará la de la base de datos. Como se puede observar todo es una catarata de configuraciones que si no se planean correctamente pueden causar problemas mas adelante.

Regresando al tema, en este caso yo tengo instalado una instancia de base de datos con el collation configurado en “Modern_Spanish_100_CI_AI”, mientras que tengo una base de datos configurada en collation “SQL_Latin1_General_CP1_CI_AS”. Ahora dentro de mi base de datos de usuario tengo una tabla a la cual quiero unir con un JOIN a otra tabla temporal que contiene algunos valores, y la quiero unir usado la columna comun entre ambas, la cual es “Codigo”, sin embargo como el collation entre el servidor y la base de datos es diferente me sale el error indicado mas arriba. Para poder reproducir el escenario se puede utilizar el siguiente codigo:

-- verificamos collation del servidor
SELECT SERVERPROPERTY('Collation') AS Collation
/*
Collation
------------------------
Modern_Spanish_100_CI_AI
*/

-- creamos la base de datos con un collation distinto al de la instancia
CREATE DATABASE CollationTest  COLLATE SQL_Latin1_General_CP1_CI_AS

USE CollationTest
GO
-- creamos la tabla
CREATE TABLE TablaA
	(Codigo varchar(30) not null
	,Nombre varchar(255) not null
	,CONSTRAINT pk_TablaA PRIMARY KEY (Codigo))

-- insertamos alguna data de prueba
INSERT INTO TablaA
select 'COD' + CAST(object_id AS VARCHAR(27)), name
from sys.objects

-- creamos una tabla temporal
CREATE TABLE #tmp_tablaA
(Codigo varchar(30) not null
,Nombre varchar(255) not null)

-- agregamos algunos registros a la temporal
insert into #tmp_tablaA
select top 5*
from TablaA
order by 1 desc

-- hacemos una consulta uniendo ambas tablas, y se
-- generará el error
select *
from TablaA a
inner join #tmp_tablaA t on a.Codigo = t.Codigo

Ahora que pudimos generar el error, hay que entender porque se produjo. En este caso el error se debe a que al momento de crear la tabla temporal, esta se crea por defecto con el “collation” definido en la instancia de base de datos al momento de instalarse, es por eso que los “collation” de ambas tablas son distintos.

Hay 2 soluciones a este problema, la primera es la mas rápida, y es que al momento de crear la tabla temporal, explicitamente se le indique el “collation” que usará para sus columnas con el objetivo de que sea el mismo de la base de datos usuario. Mas que una solución permanente es un “workaround” al problema ya que no lo estamos resolviendo sino mas bien tapando, ya que ese mismo patrón de código puede aparecer en otras partes de la aplicación o de los procedimientos almacenados, y tendriamos el mismo problema. Para aplicarla usaremos el siguiente código:

USE CollationTest
GO
-- creamos la tabla temporal con collation explicito
CREATE TABLE #tmp_tablaA
(Codigo varchar(30) COLLATE SQL_Latin1_General_CP1_CI_AS not null
,Nombre varchar(255) COLLATE SQL_Latin1_General_CP1_CI_AS not null )

-- insertamos data de prueba a la temporal
insert into #tmp_tablaA
select top 5*
from TablaA
order by 1 desc

-- hacemos el join, y esta vez sin error
select *
from TablaA a
inner join #tmp_tablaA t on a.Codigo = t.Codigo

La segunda solución es más permanente y considero que es la que se debería seguir, sin embargo requiere de mas tiempo y cuidado para implementarla. Lo que haremos en esta segunda solución es cambiar el “collation” de la instancia para que sea igual al de la base de datos. Normalmente todos las instancias donde esta  una misma base de datos deben tener el mismo “collation” configurado, sin embargo esto no siempre es así. Esta segunda solución propuesta la desarrollare en detalle en otro post.

Espero les sea util la información incluida en este post. Nos vemos. Hasta la próxima.

Cambiar Columna a Identity sin Reconstruir Tabla

Una regla de oro que yo tengo en cuanto al diseño de tablas es que estas siempre deben tener como primary key una columna de tipo Identity. Esto nos ayuda a tener una llave primaria que sea corta, optimizada para la búsqueda y sin posibilidad de actualización. Sin embargo hace poco tiempo me encontré en una situación en la que una de las tablas más grandes una de las bases de datos que yo administro no tenía una llave Identity, sino que era una columna de tipo integer normal, y había un requerimiento de transformarla a Identity.

Si bien es cierto uno podría ir al SSMS y abrir la tabla en modo diseño para luego colocar la columna Id como Identity, pero… esto no es tan simple ya que si se hace de esta manera lo que hará tras bambalinas es que SSMS creara un script para crear una nueva tabla con la estructura que se desea, ósea con la columna Id como Identity, y luego pasara toda la data desde la tabla original a la nueva tabla, para finalmente eliminar la tabla original y renombrar la nueva tabla como la original. Como se ha descrito, son varios pasos e involucra un algo uso del Transaction Log dependiendo del tamaño de la tabla. Esto podría ser aceptable en el caso que la tabla no sea muy grande, sin embargo ese no era mi caso, entonces luego de revisar varios foros y paginas salió la solución que voy a describir a continuación, la cual no requiere la reconstrucción de la tabla, sino que hace uso de un comando propio de particionamiento de tablas para poder lograr el objetivo.

Para poder mostrar el ejemplo vamos a crear una tabla a la que agregaremos 1 millon de registros, esta tabla tendrá tres columnas, la columna Id será de tipo entero pero no Identity.

CREATE TABLE dbo.BigTable
(id int not null primary key
,nombre varchar(50) not null
,apellido varchar(50) not null)
GO

WITH ID(number) AS
(
SELECT 1 AS number
UNION ALL
SELECT number + 1
FROM ID
WHERE number < 1000000
)
INSERT INTO dbo.BigTable
SELECT number, 'nombre_' + CAST(number AS VARCHAR), 'apellido_' + CAST(number AS VARCHAR)
FROM ID
OPTION(maxrecursion 0)

Ahora para poder convertir la columna Id a Identity debemos crear otra tabla que sea un espejo en estructura a mi tabla “BigTable”, pero esta nueva tabla si tendrá la columna Id como Identity.

CREATE TABLE dbo.BigTable_Tmp
(id int IDENTITY(1,1) not null primary key
,nombre varchar(50) not null
,apellido varchar(50) not null)
GO

Seguidamente aplicaremos el comando que hace que toda la magia sea posible, literalmente este comando es el que te salvara de incontables minutos viendo como tu tabla se reconstruye, tu Transaction Log crece junto a tus preocupaciones de si es que algo sale mal tendrás que tener tu tabla bloqueada por más tiempo.

ALTER TABLE dbo.BigTable
  SWITCH TO dbo.BigTable_Tmp
GO

Finalmente debemos eliminar la tabla original y renombrar la nueva para que tenga el nombre de la tabla original y las aplicaciones y otros objetos la sigan referenciando sin ningún problema.

DROP TABLE dbo.BigTable
GO
EXEC sp_rename 'dbo.BigTable_Tmp','BigTable'
GO

Como se puede observar el proceso es bastante sencillo y te permitirá ahorrar muchísimo tiempo y dolores de cabeza. En caso la tabla a modificar tenga relaciones (FKs) con otras tablas estas deberán ser eliminadas previas a la eliminación de la tabla original, y reconstruidas luego del renombrado de la tabla nueva.