El escenario
Había una vez una base de datos provisional en una pequeña empresa que participaba en un proceso ETL, que actuaba como un catálogo de recepción para los diversos formatos de archivos de varias fuentes de terceros. La E se manejó a través de paquetes DTS, con pocas estructuras de control para auditoría o control, pero se consideró "lo suficientemente bueno" y, a todos los efectos, lo fue.
Los datos proporcionados por la parte E fueron destinados al consumo por una aplicación singular, desarrollada y administrada por un puñado de programadores jóvenes y capaces. Aunque carecían de experiencia o conocimiento de las técnicas de almacenamiento de datos de la época, establecieron y crearon sus propios procesos T y L a partir del código de la aplicación. Arrancando, estos ingenieros de software novatos inventaron lo que los extraños podrían llamar una "rueda menos que ideal", pero con "Lo suficientemente bueno" como un nivel de servicio siempre presente, pudieron proporcionar un marco operativo.
Durante un tiempo, todo fue bueno en el ámbito estrechamente acoplado, con el catálogo de Staging deleitándose con los datos de una docena de terceros, a su vez alimentados por la aplicación. A medida que la aplicación creció, también lo hizo su apetito, pero con los hábiles desarrolladores de caballeros blancos vigilando el sistema, estos apetitos se abordaron rápidamente y, en muchos casos, incluso bien.
Pero la edad de oro no podía durar para siempre, por supuesto. Con la prosperidad otorgada por la aplicación exitosa, el negocio creció y creció. A medida que crecía, el entorno y la aplicación de Staging se vieron obligados a crecer con él. A pesar de su vigilancia, el mero puñado de desarrolladores de héroes no pudo mantenerse al día con el sistema ahora expansivo, y los consumidores tenían derecho a sus datos. Ya no era una cuestión de lo que necesitaban o incluso querían, sino que la población sentía que simplemente lo merecían, exigiendo aún más.
Armado con poco más que cofres llenos de botín, la empresa llegó al mercado, contratando desarrolladores y administradores para ayudar a respaldar el sistema en constante crecimiento. Mercenarios de todos los valores se congregaron en la empresa, pero con este crecimiento repentino se produjo poca orientación experta disponible. Los nuevos desarrolladores y administradores lucharon por comprender las complejidades de la suite casera, hasta que las frustraciones resultaron en una guerra total. Cada departamento comenzó a intentar resolver cada problema solo, haciendo más para trabajar uno contra el otro que trabajar entre ellos. Se implementaría un solo proyecto o iniciativa de varias maneras diferentes, cada una ligeramente diferente de la siguiente. La tensión de todo resultó ser demasiado para algunos de los caballeros blancos y cuando cayeron, el imperio se derrumbó. Pronto, el sistema estaba en ruinas,
A pesar de la transformación de estos campos de promesa en sangrientos códigos de espagueti, la compañía aguantó. Era, después de todo, "lo suficientemente bueno".
El reto
Unos pocos cambios de régimen más y juergas de contratación más tarde, me encuentro en el empleo de la empresa. Han pasado muchos años desde las grandes guerras, pero el daño hecho aún es muy visible. Me las arreglé para abordar algunas de las debilidades en la parte E del sistema y agregar algunas tablas de control con el pretexto de actualizar los paquetes DTS a SSIS, que ahora están siendo utilizados por algunos profesionales reales de almacenamiento de datos a medida que crean una normalidad. y reemplazo de T y L documentado.
El primer obstáculo fue importar los datos de los archivos de terceros de una manera que no truncara los valores o cambiara los tipos de datos nativos, sino que también incluyera algunas teclas de control para recargas y purgas. Todo esto estaba muy bien, pero las aplicaciones necesitaban poder acceder a estas nuevas tablas de una manera transparente y transparente. Un paquete DTS puede llenar una tabla, que luego la aplicación lee directamente. Las actualizaciones de SSIS deben realizarse en paralelo por razones de control de calidad, pero estos nuevos paquetes incluyen varias claves de control y también aprovechan un esquema de partición, sin mencionar que los cambios de metadatos reales por sí solos pueden ser lo suficientemente significativos como para garantizar una nueva tabla por completo de todos modos, por lo que un Se utilizó una nueva tabla para los nuevos paquetes SSIS.
Con importaciones de datos confiables que ahora funcionan y están siendo utilizadas por el equipo de almacenamiento, el verdadero desafío es entregar los nuevos datos a las aplicaciones que acceden directamente al entorno de ensayo, con un impacto mínimo (también conocido como "No") en el código de la aplicación. Para esto, he elegido a puntos de vista de uso, cambiar el nombre de una tabla como dbo.DailyTransaction
a dbo.DailyTranscation_LEGACY
y reutilizar el dbo.DailyTransaction
nombre de objeto para una vista, que en efecto sólo selecciona todo, desde el momentoLEGACY
mesa designada. Dado que la recarga de los años de datos contenidos en estas tablas no es una opción desde la perspectiva del negocio, ya que las nuevas tablas particionadas y SSIS entran en producción, las antiguas importaciones de DTS se desactivan y las aplicaciones deben poder acceder a los nuevos datos en las nuevas tablas también. En este punto, las vistas se actualizan para seleccionar los datos de las nuevas tablas ( dbo.DailyTransactionComplete
por ejemplo, por ejemplo) cuando está disponible y seleccionar de las tablas heredadas cuando no lo está.
En efecto, se está haciendo algo como lo siguiente:
CREATE VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransaction_LEGACY l
WHERE NOT EXISTS ( SELECT 1
FROM dbo.DailyTransactionComplete t
WHERE t.FileDate = l.FileDate );
Si bien es lógico, esto no funciona en absoluto en una serie de casos de agregación, lo que generalmente resulta en un plan de ejecución que realiza una exploración de índice completa contra los datos en la tabla heredada. Esto probablemente esté bien para unas pocas docenas de millones de registros, pero no tanto para unas pocas docenas de cientos de millones de registros. Como este último es el caso, tuve que recurrir a ser ... "creativo", lo que me llevó a crear una vista indizada.
Este es el pequeño caso de prueba que he configurado, incluida la FileDate
clave de control que se ha portado al DateCode_FK
puerto compatible con Data Warehouse para ilustrar cuán poco me importa que las consultas sobre la nueva tabla sean modificables por el momento:
USE tempdb;
GO
SET NOCOUNT ON;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction_LEGACY'
AND type = 'U' )
BEGIN
--DROP TABLE dbo.DailyTransaction_LEGACY;
CREATE TABLE dbo.DailyTransaction_LEGACY
(
DailyTransaction_PK BIGINT IDENTITY( 1, 1 ) NOT NULL,
FileDate DATETIME NOT NULL,
Foo INT NOT NULL
);
INSERT INTO dbo.DailyTransaction_LEGACY ( FileDate, Foo )
SELECT DATEADD( DAY, ( 1 - ROW_NUMBER()
OVER( ORDER BY so1.object_id ) - 800 ) % 1000,
CONVERT( DATE, GETDATE() ) ),
so1.object_id % 1000 + so2.object_id % 1000
FROM sys.all_objects so1
CROSS JOIN sys.all_objects so2;
ALTER TABLE dbo.DailyTransaction_LEGACY
ADD CONSTRAINT PK__DailyTrainsaction
PRIMARY KEY CLUSTERED ( DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );
END;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransactionComplete'
AND type = 'U' )
BEGIN
--DROP TABLE dbo.DailyTransactionComplete;
CREATE TABLE dbo.DailyTransactionComplete
(
DailyTransaction_PK BIGINT IDENTITY( 1, 1 ) NOT NULL,
DateCode_FK INTEGER NOT NULL,
Foo INTEGER NOT NULL
);
INSERT INTO dbo.DailyTransactionComplete ( DateCode_FK, Foo )
SELECT TOP 100000
CONVERT( INTEGER, CONVERT( VARCHAR( 8 ), DATEADD( DAY,
( 1 - ROW_NUMBER() OVER( ORDER BY so1.object_id ) ) % 100,
GETDATE() ), 112 ) ),
so1.object_id % 1000
FROM sys.all_objects so1
CROSS JOIN sys.all_objects so2;
ALTER TABLE dbo.DailyTransactionComplete
ADD CONSTRAINT PK__DailyTransaction
PRIMARY KEY CLUSTERED ( DateCode_FK, DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );
END;
GO
En mi sandbox local, lo anterior me da una tabla heredada con aproximadamente 4.4 millones de filas y una nueva tabla que contiene 0.1 millones de filas, con cierta superposición de los valores DateCode_FK
/ FileDate
.
Se MAX( FileDate )
ejecuta una tabla contra el legado sin índices adicionales sobre lo que esperaría.
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput DATETIME;
SELECT @ConsumeOutput = MAX( FileDate )
FROM dbo.DailyTransaction_LEGACY;
SET STATISTICS IO, TIME OFF;
GO
Tabla 'DailyTransaction_LEGACY'. Cuenta de escaneo 1, lecturas lógicas 9228, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.
Tiempos de ejecución de SQL Server: tiempo de CPU = 889 ms, tiempo transcurrido = 886 ms.
Lanzar un índice simple sobre la mesa hace que las cosas sean mucho mejores. Sigue siendo un escaneo, pero escanea un registro en lugar de los 4,4 millones de registros. Estoy bien con eso.
CREATE NONCLUSTERED INDEX IX__DailyTransaction__FileDate
ON dbo.DailyTransaction_LEGACY ( FileDate );
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput DATETIME;
SELECT @ConsumeOutput = MAX( FileDate )
FROM dbo.DailyTransaction_LEGACY;
SET STATISTICS IO, TIME OFF;
GO
Tiempo de análisis y compilación de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 1 ms. Tabla 'DailyTransaction_LEGACY'. Cuenta de escaneo 1, lecturas lógicas 3, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.
Tiempos de ejecución de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 0 ms.
Y ahora, creando la vista para que los desarrolladores no tengan que cambiar ningún código porque aparentemente sería el fin del mundo tal como lo conocemos. Un cataclismo de tipo.
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate = CONVERT(
DATETIME, CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ), Foo
FROM dbo.DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransaction_LEGACY l
WHERE NOT EXISTS ( SELECT 1
FROM dbo.DailyTransactionComplete t
WHERE CONVERT( DATETIME, CONVERT( VARCHAR( 8 ),
t.DateCode_FK ), 112 ) = l.FileDate );
GO
Sí, la subconsulta es abismal, pero este no es el problema y probablemente simplemente crearé una columna computada persistente y arrojaré un índice para ese propósito cuando se resuelva el problema real. Así que sin más preámbulos,
El problema
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Tiempo de análisis y compilación de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 4 ms. Tabla 'DailyTransaction_LEGACY'. Recuento de escaneo 1, lecturas lógicas 11972, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0. 0. Tabla 'Mesa de trabajo'. Cuenta de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas lob de lectura anticipada 0. Tabla 'Archivo de trabajo'. Recuento de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura anticipada lob 0. Tabla 'DailyTransactionComplete'. Cuenta de escaneo 2, lecturas lógicas 620, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.
Tiempos de ejecución de SQL Server: tiempo de CPU = 983 ms, tiempo transcurrido = 983 ms.
Ah, ya veo, SQL Server está tratando de decirme que lo que estoy haciendo es idiota. Si bien estoy de acuerdo en gran medida, eso no cambia mi situación. En realidad, esto funciona de manera brillante para consultas en las que FileDate
la dbo.DailyTransaction
vista está incluida en el predicado, pero si bien el MAX
plan es lo suficientemente malo, el TOP
plan envía todo corriendo hacia el sur. Real sur.
SET STATISTICS IO, TIME ON;
SELECT TOP 10 FileDate
FROM dbo.DailyTransaction
GROUP BY FileDate
ORDER BY FileDate DESC
SET STATISTICS IO, TIME OFF;
GO
Tabla 'DailyTransactionComplete'. Cuenta de escaneo 2, lecturas lógicas 1800110, lecturas físicas 0, lecturas anticipadas 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0. Tabla 'DailyTransaction_LEGACY'. Cuenta de escaneo 1, lecturas lógicas 1254, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0. 0. Tabla 'Mesa de trabajo'. Cuenta de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas lob de lectura anticipada 0. Tabla 'Archivo de trabajo'. Recuento de exploración 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.
Tiempos de ejecución de SQL Server: tiempo de CPU = 109559 ms, tiempo transcurrido = 109664 ms.
Mencioné ser "creativo" antes, lo que probablemente fue engañoso. Lo que quise decir fue "más estúpido", por lo que mis intentos de hacer que esta vista funcione durante las operaciones de agregación han sido crear vistas en las tablas dbo.DailyTransactionComplete
y dbo.DailyTransaction_LEGACY
, vincular esquemas e indexar la última, luego usar esas vistas en otra vista con una NOEXPAND
pista en la vista heredada. Si bien está funcionando más o menos por lo que debe hacer por ahora, encuentro que la "solución" completa es bastante molesta, y que culmina con lo siguiente:
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'v_DailyTransactionComplete'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.v_DailyTransactionComplete AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.v_DailyTransactionComplete
AS SELECT DailyTransaction_PK, FileDate = CONVERT( DATETIME,
CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ),
Foo
FROM dbo.DailyTransactionComplete;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'v_DailyTransaction_LEGACY'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.v_DailyTransaction_LEGACY AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.v_DailyTransaction_LEGACY
WITH SCHEMABINDING
AS SELECT l.DailyTransaction_PK,
l.FileDate,
l.Foo,
CountBig = COUNT_BIG( * )
FROM dbo.DailyTransaction_LEGACY l
INNER JOIN dbo.DailyTransactionComplete n
ON l.FileDate <> CONVERT( DATETIME, CONVERT( VARCHAR( 8 ),
n.DateCode_FK ), 112 )
GROUP BY l.DailyTransaction_PK,
l.FileDate,
l.Foo;
GO
CREATE UNIQUE CLUSTERED INDEX CI__v_DailyTransaction_LEGACY
ON dbo.v_DailyTransaction_LEGACY ( FileDate, DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 80 );
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.v_DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.v_DailyTransaction_LEGACY WITH ( NOEXPAND );
GO
Obligar al optimizador a usar el índice proporcionado por la vista indexada hace que los problemas MAX
y TOP
desaparezcan, pero debe haber una mejor manera de lograr lo que estoy tratando de hacer aquí. Absolutamente cualquier sugerencia / regaño sería muy apreciada!
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Tabla 'v_DailyTransaction_LEGACY'. Cuenta de escaneo 1, lecturas lógicas 3, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura anticipada lob 0. Tabla 'DailyTransactionComplete'. Cuenta de escaneo 1, lecturas lógicas 310, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.
Tiempos de ejecución de SQL Server: tiempo de CPU = 31 ms, tiempo transcurrido = 36 ms.
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT TOP 10 @ConsumeOutput1 = FileDate
FROM dbo.DailyTransaction
GROUP BY FileDate
ORDER BY FileDate DESC
SET STATISTICS IO, TIME OFF;
GO
Tabla 'v_DailyTransaction_LEGACY'. Cuenta de escaneo 1, lecturas lógicas 101, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas lob de lectura anticipada 0. Tabla 'Mesa de trabajo'. Cuenta de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas lob de lectura anticipada 0. Tabla 'Archivo de trabajo'. Recuento de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura anticipada lob 0. Tabla 'DailyTransactionComplete'. Cuenta de escaneo 1, lecturas lógicas 310, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.
Tiempos de ejecución de SQL Server: tiempo de CPU = 63 ms, tiempo transcurrido = 66 ms.
TL; DR:
Ayúdame a comprender lo que necesito hacer para hacer consultas de agregación en la primera vista que mencioné que se ejecuta en cantidades razonables de tiempo con una utilización razonable de los recursos de E / S.