GuilleSQL :: Microsoft SQL Server, SSIS, y más !!

Cómo cargar Tablas Particionadas con SELECT INTO en SQL Server: ALTER TABLE SWITCH PARTITION

Volver a: [SELECT INTO, INSERT INTO y el LOG de SQL Server: alternativas para cargar tablas en un Data Warehouse]


Este capítulo muestra como técnica de Carga Rápida en un Data Warehouse (u otros entornos, aunque para cargas incrementales de grandes tablas de hecho en un Data Warehouse resulta de especial interés por sus volúmenes de datos y la probabilidad de utilización del Modo de Recuperación Simple) la utilización de SELECT INTO para cargar Tablas Particionadas, mediante la creación de una tabla intermedia con SELECT INTO en una base de datos en Modo de Recuperación Simple, que asignaremos seguidamente como una Partición de la Tabla Particionada de destino a través de la sentencia ALTER TABLE SWITCH. Se explica el escenario, y se incluye código de ejemplo (CREATE PARTITION FUNCTION, CREATE PARTITION SCHEME, etc.).

Una interesante técnica de Carga Rápida en entornos de Data Warehouse, consiste en la utilización de Tablas Particionadas como medio para la realización de Cargas Incrementales Rápidas con SELECT INTO en bases de datos configuradas con Modo de Recuperación Simple, especialmente útil para realizar cargas incrementales en grandes Tablas de Hechos (Fact Table) de un Data Warehouse en SQL Server (recordemos que con SELECT INTO la tabla de destino NO debe existir, por lo que a priori, dificil hacer una carga incremental sobre una tabla existente, verdad?). Además, el Particionamiento de Tablas e Índices en SQL Server tiene muchos detalles que deberemos tener en cuenta. Este escenario particular, se basa en las siguientes premisas:

  • Utilizar el Modo de Recuperación Simple en la base de datos de destino, para aprovecharse de las mejoras de rendimiento de las Operaciones de Registro Mínimo, como es el caso de SELECT INTO.
  • Utilizar una Tabla Particionada como tabla destino. En este caso, el diseño de las Particiones se debe basar en los conjuntos de filas que se desean cargar, es decir, si deseamos realizar una carga incremental cada mes podemos definir una partición para cada mes, como se mostrará más adelante en el ejemplo incluido en este capítulo. Así, utilizaremos la sentencia ALTER TABLE SWITCH para conseguir el efecto de carga de la partición deseada (como veremos más adelante en el ejemplo incluido), motivo por el cual, deben de existir las Particiones deseadas antes de la Carga, y además, dichas particiones deben estar vacías para poder ser destino de una sentencia ALTER TABLE SWITCH.
  • Generar una tabla temporal con SELECT INTO, con exactamente la misma estructura que la tabla destino. Aquí el truco está en dos sitios: por un lado, generar dicha tabla temporal con los datos que se desean cargar en la partición de la tabla destino, mediante una operación SELECT INTO en una base de datos en Modo de Recuperación Simple, ofrece un rendimiento extremo al minimizar las escrituras en Log de forma más que sensible (esto ya lo vimos antes en este artículo, pero volveremos a recordarlo ahora después ;-). Por otro lado, es requisito que dicha tabla temporal tenga exactamente la misma estructura que la tabla de destino (es decir, campos, tipos de datos, admisión de nulos, intercalación, precisión, etc.) para poder ejecutar la sentencia ALTER TABLE SWITCH. También es requisito, que la tabla temporal de origen tenga un CHECK CONSTRAINT que garantice que su contenido corresponda a la partición de la tabla particionada de destino sobre la que se desea cargar / intercambiar a través de ALTER TABLE SWITCH.

Para entenderlo bien, no hay nada como utilizar un ejemplo. En el siguiente trozo de código, se crea una Función de Particionamiento (CREATE PARTITION FUNCTION), se crea un Esquema de Particionamiento (CREATE PARTITION SCHEMA), y se crea una Tabla Particionada. Aquí, el truco que hemos utilizado es crear de antemano (en la Función de Particionamiento) todas las particiones deseadas, tanto las que inicialmente contendrán datos, como las que contendrán datos en un futuro (es decir, inicialmente estarán vacías para poder ejecutar ALTER TABLE SWITCH con éxito). Este razonamiento podría generalizarse, de tal modo que en la Función de Particionamiento podríamos generar de antemano todas las particiones deseadas de forma exagerada (ej: hasta Diciembre de 2025). En el presente ejemplo, se utilizarán 7 Particiones, de las cuales, sólo insertaremos datos en las 3 primeras Particiones. Así mismo, el Esquema de Particionamiento utilizado, asociará todas las Particiones al mismo Grupo de Ficheros (Filegroup), lo cual resulta más que suficiente para el objetivo que deseamos cumplir.

-- Me posiciono en la base de datos de ejemplo (GuilleSQL)
-- Esta base de datos, tiene 500MB de Datos y 100MB de Log
USE GuilleSQL
GO

-- Crear la Función de Particionamiento.
-- Particion 1: < 200801
-- Particion 2: >= 200801 AND < 200802
-- Particion 3: >= 200802 AND < 200803
-- Particion 4: >= 200803 AND < 200804
-- Particion 5: >= 200804 AND < 200805
-- Particion 6: >= 200805 AND < 200806
-- Particion 7: >= 200806
CREATE PARTITION FUNCTION fn_ParticionadoPorMes(int)
AS RANGE RIGHT FOR VALUES(200801, 200802, 200803, 200804, 200805, 200806)
GO

-- Crear el Esquema de Particionamiento.
-- Me resulta suficiente con un único FileGroup, para mi objetivo
CREATE PARTITION SCHEME sch_ParticionadoPorMes
AS PARTITION fn_ParticionadoPorMes
ALL TO ( [PRIMARY] );
GO

-- Crear Tabla Particionada
CREATE TABLE GuilleSQL.dbo.tbl_ParticionadaPorMes
(
   Nombre varchar(200) NOT NULL
   ,MesParticion int NOT NULL
) ON sch_ParticionadoPorMes(MesParticion)
GO

-- Rellenar las primeras cuatro particiones de la tabla
-- Las tres últimas particiones se dejan vacías
INSERT INTO GuilleSQL.dbo.tbl_ParticionadaPorMes (Nombre, MesParticion)
SELECT TOP 100 FirstName, 200712 FROM AdventureWorks.Person.Contact

INSERT INTO GuilleSQL.dbo.tbl_ParticionadaPorMes (Nombre, MesParticion)
SELECT TOP 100 FirstName, 200801 FROM AdventureWorks.Person.Contact

INSERT INTO GuilleSQL.dbo.tbl_ParticionadaPorMes (Nombre, MesParticion)
SELECT TOP 100 FirstName, 200802 FROM AdventureWorks.Person.Contact

INSERT INTO tbl_ParticionadaPorMes (Nombre, MesParticion)
SELECT TOP 100 FirstName, 200803 FROM AdventureWorks.Person.Contact
GO

Con esto, hemos construido nuestro punto de partida, es decir, la tabla particionada de destino sobre la cual deseamos realizar una carga, incluyendo como particiones vacías aquellas sobre las cuales se desea cargar datos. Para quién desee poder repertir este ejemplo, he utilizado la base de datos de Adventure Works (la que viene con SQL Server 2005) para generar los datos de esta prueba. También quería comentar, que para esta prueba, he utilizado una base de datos con 500MB de Datos y 100MB de Log, configurada en Modo de Recuperación Simple.

Explicado todo esto, vamos a realizar la prueba que realmente nos interesa. En el siguiente trozo de código Transact-SQL, se crea una tabla temporal con exactamente la misma estructura que la tabla destino y diez millones de filas. Para conseguirlo, he necesitado realizar un casting (con la función CAST) que garantice el tipo de dato y precisión de destino, así como utilizar la función ISNULL para garantizar que el campo se crea como NOT NULL, y utilizar la palabra reservada COLLATE para garantizar la misma intercalación que en la tabla destino. Por supuesto, como en la base de datos de Adventure Works no existe ninguna tabla con 10.000.000 de filas, he realizado un CROSS JOIN de una tabla con sí misma, para así conseguir el número de filas deseado (utilizando la expresión TOP). Seguidamente, he creado un CHECK CONSTRAINT que garantiza que el contenido de la tabla temporal creada con SELECT INTO, es el correspondiente al de la partición de destino que deseamos cargar. Finalmente, cargamos la tabla de destino ejecutando una sentencia ALTER TABLE SWITCH sobre la Partición deseada de la tabla (dicha partición debe estar vacía). A continuación se puede observar el código del ejemplo:

-- Crear Tabla Temporal de 10.000.000 de filas con SELECT INTO
-- Tiempo de Ejecución: 8 seg., y no crece el fichero de Log
SELECT TOP 10000000
   ISNULL(CAST(C.FirstName AS VARCHAR(200)), 'Desconocido') COLLATE Modern_Spanish_CI_AS AS Nombre,
   200804 AS MesParticion
INTO GuilleSQL.dbo.tbl_CargaRapida
FROM AdventureWorks.Person.Contact C, AdventureWorks.Person.Contact

-- Crear un Check Constraint en la Tabla Temporal para garantizar el dominio de los datos
-- es decir, garantizar que los datos de la tabla corresponden a la Partición en que se desea asignar.
-- Tiempo de Ejecución: 3 seg.
ALTER TABLE GuilleSQL.dbo.tbl_CargaRapida ADD CONSTRAINT CHK_CargaRapida CHECK ( MesParticion = 200804)

-- Asignar la Tabla Temporal como una de las Particiones vacías de la tabla destion: 0 seg.
ALTER TABLE GuilleSQL.dbo.tbl_CargaRapida
SWITCH TO GuilleSQL.dbo.tbl_ParticionadaPorMes PARTITION 5

Llegados a este punto, lo primero que quiero es decir que esto funciona, y seguidamente, quiero advertir que el tiempo empleado ha sido un total de 11 segundos (8 segundos del SELECT INTO de 10.000.000 de filas, 3 segundos de ALTER TABLE ADD CONSTRAINT, y 0 segundos en el intercambio de particiones con ALTER TABLE SWITCH). También, quiero comentar que el fichero de Log no ha crecido, manteniendo su tamaño inicial de 100MB.

Claro, que nunca se sabe lo que se tiene hasta que se pierde... jeje ;-) Por supuesto, he probado a realizar un INSERT INTO en lugar de SELECT INTO, como se muestra en el siguiente ejemplo, y se puede encontrar una diferencia abismal: tiempo de ejecución de 2 minutos con 26 segundos, que ha sido motivado a la enorme cantidad de escritura en Log, pues el fichero de Log ha aumentado hasta 4,5GB. Pues tenemos dos problemas: uno de rendimiento, y otro de almacenamiento !!

TRUNCATE TABLE GuilleSQL.dbo.tbl_CargaRapida
GO

INSERT INTO GuilleSQL.dbo.tbl_CargaRapida (Nombre, MesParticion)
SELECT TOP 10000000
   ISNULL(CAST(C.FirstName AS VARCHAR(200)), 'Desconocido') COLLATE Modern_Spanish_CI_AS AS Nombre,
   200804 AS MesParticion
FROM AdventureWorks.Person.Contact C, AdventureWorks.Person.Contact
GO

Creo que con todo esto, queda bastante evidenciada las mejoras de rendimiento y almacenamiento propias de la utilización de SELECT INTO frente a INSERT INTO, y en este caso, se evidencia también la ventaja de utilizar Tablas Particionadas y la sentencia ALTER TABLE SWITCH para realizar Cargas de Alto Rendimiento, como es el caso típico de las Tablas de Hechos (Fact Tables) de un Data Warehouse (DW) en SQL Server.

Aprovecho para recordar, que aunque este ejemplo ha sido con 10 millones de filas, bien es cierto que se trataba de filas con tan sólo dos campos. En entornos reales de producción, encontraríamos el mismo problema de rendimiento y almacenamiento con muchas menos filas. En consecuencia, en entornos reales de Producción con tablas que almacenan grandes volúmenes de datos, y estructuras de tabla más complejas (ej: con índices, etc.), las mejoras de rendimiento y almacenamiento pueden ser más que notables. Además, recordar que en entornos de producción existe concurrencia, es decir, que no estamos solos... y la máquina se tiene que repartir entre todas las cosas que la pidamos. Al final, aunque los entornos SQL Server empresariales, suelen utilizar Redes de Almacenamiento SAN por Fiber Channel, de Alto Rendimiento (y Alta Disponibilidad), de poco nos va a servir sin un diseño y desarrollo de base de datos optimizado.

Por supuesto, no siempre podremos aprovecharnos de SELECT INTO y ALTER TABLE SWITCH. Por ejemplo, si estamos realizando una carga de un fichero de Log (ej: el Log de un ISA Server), podemos realizar una carga incremental con SELECT INTO y ALTER TABLE SWITCH. Sin embargo, si estamos cargando siniestros de pólizas en una empresa de seguros, a lo mejor no podemos aprovecharnos de SELECT INTO y ALTER TABLE SWITCH, debido a que en muchos casos se insertan datos con caracter retroactivo, lo que implica que no podremos cargar sólo el mes vencido, ya que habrá modificaciones sobre multitud de las particiones anteriores, por lo que la cosa de complica (en un Log, dificil que nos inserten datos con efecto retroactivo, verdad? ;-).

Y hasta aquí, hemos llegado con este capítulo de SELECT INTO, Tablas Particionadas, y sentencias ALTER TABLE SWITCH en SQL Server.

Volver a: [SELECT INTO, INSERT INTO y el LOG de SQL Server: alternativas para cargar tablas en un Data Warehouse]




Miembros de
Miembros de GITCA (Global IT Community Association)

Menu de Usuario
  Iniciar Sesión
  Registrarse
  Restablecer Contraseña
  Ventajas de Registrarse

Acerca de
  Contigo desde Oct 2007
  771 usuarios registrados
  86146 pageloads/mes
  Ranking Alexa 498160

Social Networks
Sigue a Portal GuilleSQL en Linkedin !!
Sigue a Portal GuilleSQL en Twitter !!



Archivo

Marzo de 2019 (1)
Octubre de 2018 (1)
Julio de 2018 (1)
Junio de 2018 (4)
Mayo de 2018 (5)
Abril de 2018 (3)
Marzo de 2018 (2)
Febrero de 2018 (7)
Enero de 2018 (1)
Diciembre de 2017 (15)
Noviembre de 2017 (7)
Junio de 2017 (3)
Mayo de 2017 (1)
Marzo de 2017 (3)
Enero de 2017 (4)
Junio de 2016 (1)
Mayo de 2016 (2)
Abril de 2016 (2)
Septiembre de 2015 (2)
Agosto de 2015 (2)
Junio de 2015 (10)
Mayo de 2015 (4)
Abril de 2015 (8)
Marzo de 2015 (11)
Octubre de 2014 (3)
Septiembre de 2014 (7)
Agosto de 2014 (5)
Julio de 2014 (2)
Mayo de 2014 (4)
Abril de 2014 (4)
Marzo de 2014 (4)
Febrero de 2014 (1)
Enero de 2014 (5)
Diciembre de 2013 (8)
Noviembre de 2013 (2)
Octubre de 2013 (7)
Septiembre de 2013 (6)
Agosto de 2013 (1)
Julio de 2013 (6)
Junio de 2013 (11)
Mayo de 2013 (7)
Abril de 2013 (6)
Febrero de 2013 (5)
Enero de 2013 (7)
Diciembre de 2012 (12)
Noviembre de 2012 (13)
Octubre de 2012 (5)
Septiembre de 2012 (3)
Agosto de 2012 (6)
Julio de 2012 (4)
Junio de 2012 (1)
Mayo de 2012 (2)
Abril de 2012 (7)
Marzo de 2012 (16)
Febrero de 2012 (9)
Enero de 2012 (5)
Diciembre de 2011 (10)
Noviembre de 2011 (10)
Octubre de 2011 (4)
Septiembre de 2011 (5)
Agosto de 2011 (2)
Julio de 2011 (2)
Junio de 2011 (4)
Mayo de 2011 (2)
Abril de 2011 (6)
Marzo de 2011 (4)
Febrero de 2011 (10)
Enero de 2011 (5)
Diciembre de 2010 (6)
Noviembre de 2010 (4)
Octubre de 2010 (8)
Septiembre de 2010 (4)
Agosto de 2010 (1)
Julio de 2010 (3)
Mayo de 2010 (5)
Abril de 2010 (6)
Marzo de 2010 (8)
Febrero de 2010 (3)
Enero de 2010 (1)
Diciembre de 2009 (9)
Noviembre de 2009 (14)
Octubre de 2009 (2)
Septiembre de 2009 (8)
Agosto de 2009 (2)
Julio de 2009 (10)
Junio de 2009 (9)
Mayo de 2009 (10)
Abril de 2009 (9)
Marzo de 2009 (3)
Febrero de 2009 (2)
Enero de 2009 (3)
Noviembre de 2008 (2)
Octubre de 2008 (2)
Septiembre de 2008 (2)
Agosto de 2008 (5)
Julio de 2008 (5)
Junio de 2008 (1)
Mayo de 2008 (3)
Abril de 2008 (2)
Marzo de 2008 (2)
Febrero de 2008 (2)
Enero de 2008 (5)
Noviembre de 2007 (2)
Octubre de 2007 (2)






Copyright © 2007 GuilleSQL, todos los derechos reservados.