Tenemos un proceso que toma datos de las tiendas y actualiza una tabla de inventario de toda la empresa. Esta tabla tiene filas para cada tienda por fecha y por artículo. En los clientes con muchas tiendas, esta tabla puede ser muy grande, del orden de 500 millones de filas.
Este proceso de actualización de inventario generalmente se ejecuta muchas veces al día a medida que las tiendas ingresan datos. Estas ejecuciones actualizan los datos de solo unas pocas tiendas. Sin embargo, los clientes también pueden ejecutar esto para actualizar, por ejemplo, todas las tiendas en los últimos 30 días. En este caso, el proceso hace girar 10 hilos y actualiza el inventario de cada tienda en un hilo separado.
El cliente se queja de que el proceso lleva mucho tiempo. He perfilado el proceso y descubrí que una consulta que INSERTA en esta tabla consume mucho más tiempo del que esperaba. Este INSERT a veces se completa en 30 segundos.
Cuando ejecuto un comando SQL INSERT ad-hoc contra esta tabla (limitado por BEGIN TRAN y ROLLBACK), el SQL ad-hoc se completa en el orden de milisegundos.
La consulta de ejecución lenta está debajo. La idea es INSERTAR registros que no están allí y luego ACTUALIZARlos mientras calculamos varios bits de datos. Un paso previo en el proceso identificó los elementos que deben actualizarse, realizó algunos cálculos y guardó los resultados en la tabla tempdb Update_Item_Work. Este proceso se ejecuta en 10 subprocesos separados, y cada subproceso tiene su propio GUID en Update_Item_Work.
INSERT INTO Inventory
(
Inv_Site_Key,
Inv_Item_Key,
Inv_Date,
Inv_BusEnt_ID,
Inv_End_WtAvg_Cost
)
SELECT DISTINCT
UpdItemWrk_Site_Key,
UpdItemWrk_Item_Key,
UpdItemWrk_Date,
UpdItemWrk_BusEnt_ID,
(CASE UpdItemWrk_Set_WtAvg_Cost WHEN 1 THEN UpdItemWrk_WtAvg_Cost ELSE 0 END)
FROM tempdb..Update_Item_Work (NOLOCK)
WHERE UpdItemWrk_GUID = @GUID
AND NOT EXISTS
-- Only insert for site/item/date combinations that don't exist
(SELECT *
FROM Inventory (NOLOCK)
WHERE Inv_Site_Key = UpdItemWrk_Site_Key
AND Inv_Item_Key = UpdItemWrk_Item_Key
AND Inv_Date = UpdItemWrk_Date)
La tabla de inventario tiene 42 columnas, la mayoría de las cuales registran cantidades y cuentan para varios ajustes de inventario. sys.dm_db_index_physical_stats dice que cada fila tiene aproximadamente 242 bytes, por lo que espero que quepan unas 33 filas en una sola página de 8 KB.
La tabla se agrupa en la restricción única (Inv_Site_Key, Inv_Item_Key, Inv_Date). Todas las claves son DECIMALES (15,0) y la fecha es SMALLDATETIME. Hay una clave primaria IDENTITY (no agrupada) y otros 4 índices. Todos los índices y la restricción agrupada se definen con un explícito (FILLFACTOR = 90, PAD_INDEX = ON).
Miré en el archivo de registro para contar divisiones de página. Medí aproximadamente 1,027 divisiones en el índice agrupado y 1,724 divisiones en otro índice, pero no registré en qué intervalo ocurrieron. Una hora y media más tarde, medí 7.035 divisiones de página en el índice agrupado.
El plan de consulta que capturé en el generador de perfiles se ve así:
Rows Executes StmtText
---- -------- --------
490 1 Sequence
0 1 |--Index Update
0 1 | |--Collapse
0 1 | |--Sort
0 1 | |--Filter
996 1 | |--Table Spool
996 1 | |--Split
498 1 | |--Assert
0 0 | |--Compute Scalar
498 1 | |--Clustered Index Update(UK_Inventory)
498 1 | |--Compute Scalar
0 0 | |--Compute Scalar
0 0 | |--Compute Scalar
498 1 | |--Compute Scalar
498 1 | |--Top
498 1 | |--Nested Loops
498 1 | |--Stream Aggregate
0 0 | | |--Compute Scalar
498 1 | | |--Clustered Index Seek(tempdb..Update_Item_Work)
498 498 | |--Clustered Index Seek(Inventory)
0 1 |--Index Update(UX_Inv_Exceptions_Date_Site_Item)
0 1 | |--Collapse
0 1 | |--Sort
0 1 | |--Filter
996 1 | |--Table Spool
490 1 |--Index Update(UX_Inv_Date_Site_Item)
490 1 |--Collapse
980 1 |--Sort
980 1 |--Filter
996 1 |--Table Spool
Al mirar las consultas frente a varios dmv, veo que la consulta está esperando en PAGEIOLATCH_EX por una duración de 0 en una página en esta tabla de Inventario. No veo ninguna espera o bloqueo en las cerraduras.
Esta máquina tiene aproximadamente 32 GB de memoria. Está ejecutando SQL Server 2005 Standard Edition, aunque pronto se actualizarán a 2008 R2 Enterprise Edition. No tengo números sobre el tamaño de la tabla de inventario en términos de uso del disco, pero puedo obtener eso, si es necesario. Es una de las tablas más grandes de este sistema.
Ejecuté una consulta contra sys.dm_io_virtual_file_stats y vi que las esperas de escritura promedio contra tempdb superaban los 1,1 segundos . La base de datos en la que se almacena esta tabla tiene esperas de escritura promedio de ~ 350 ms. Pero solo reinician su servidor cada 6 meses más o menos, así que no tengo idea si esta información es relevante. tempdb se distribuye en 4 archivos diferentes. Tienen 3 archivos diferentes para la base de datos que contiene la tabla de inventario.
¿Por qué esta consulta tardaría tanto en INSERTAR algunas filas cuando se ejecuta con muchos subprocesos diferentes cuando un solo INSERTAR es muy rápido?
- ACTUALIZACIÓN -
Aquí están los números de latencia por unidad, incluidos los bytes leídos. Como puede ver, el rendimiento de tempdb es cuestionable. La tabla de inventario se encuentra en PDICompany_252_01.mdf, PDICompany_252_01_Second.ndf o PDICompany_252_01_Third.ndf.
ReadLatencyWriteLatencyLatencyAvgBPerRead AvgBPerWriteAvgBPerTransferDriveDB physical_name
42 1112 623 62171 67654 65147R: tempdb R:\Microsoft SQL Server\Tempdb\tempdev1.mdf
38 1101 615 62122 67626 65109S: tempdb S:\Microsoft SQL Server\Tempdb\tempdev2.ndf
38 1101 615 62136 67639 65123T: tempdb T:\Microsoft SQL Server\Tempdb\tempdev3.ndf
38 1101 615 62140 67629 65119U: tempdb U:\Microsoft SQL Server\Tempdb\tempdev4.ndf
25 341 71 92767 53288 87009X: PDICompany X:\Program Files\PDI\Enterprise\Databases\PDICompany_Third.ndf
26 339 71 90902 52507 85345X: PDICompany X:\Program Files\PDI\Enterprise\Databases\PDICompany_Second.ndf
10 231 90 98544 60191 84618W: PDICompany_FRx W:\Program Files\PDI\Enterprise\Databases\PDICompany_FRx.mdf
61 137 68 9120 9181 9125W: model W:\Microsoft SQL Server\MSSQL.3\MSSQL\Data\modeldev.mdf
36 113 97 9376 5663 6419V: model V:\Microsoft SQL Server\Logs\modellog.ldf
22 99 34 92233 52112 86304W: PDICompany W:\Program Files\PDI\Enterprise\Databases\PDICompany.mdf
9 20 10 25188 9120 23538W: master W:\Microsoft SQL Server\MSSQL.3\MSSQL\Data\master.mdf
20 18 19 53419 10759 40850W: msdb W:\Microsoft SQL Server\MSSQL.3\MSSQL\Data\MSDBData.mdf
23 18 19 947956 58304 110123V: PDICompany_FRx V:\Program Files\PDI\Enterprise\Databases\PDICompany_FRx_1.ldf
20 17 17 828123 55295 104730V: PDICompany V:\Program Files\PDI\Enterprise\Databases\PDICompany.ldf
5 13 13 12308 4868 5129V: master V:\Microsoft SQL Server\Logs\mastlog.ldf
11 13 13 22233 7598 8513V: PDIMaster V:\Program Files\PDI\Enterprise\Databases\PDIMaster.ldf
14 11 13 13846 9540 12598W: PDIMaster W:\Program Files\PDI\Enterprise\Databases\PDIMaster.mdf
13 11 11 22350 1107 1110V: msdb V:\Microsoft SQL Server\Logs\MSDBLog.ldf
17 9 9 745437 11821 23249V: PDIFoundation V:\Program Files\PDI\Enterprise\Databases\PDIFoundation.ldf
34 8 31 29490 33725 30031W: PDIFoundation W:\Program Files\PDI\Enterprise\Databases\PDIFoundation.mdf
5 8 8 61560 61236 61237V: tempdb V:\Microsoft SQL Server\Logs\templog.ldf
13 6 11 8370 35087 16785W: SAHost_Company01 W:\Program Files\PDI\Enterprise\Databases\SAHostCompany.mdf
2 6 5 56235 33667 38911W: SAHost_Company01 W:\Program Files\PDI\Enterprise\Databases\SAHost_Company_01_log.LDF