Rendimiento lento al insertar pocas filas en una tabla enorme

9

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
Paul Williams
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Paul White 9

Respuestas:

4

Parece que las divisiones de la página de índice en clúster serán dolorosas porque el índice en clúster contiene los datos reales y esto necesitará nuevas páginas para asignar y mover los datos a ellas. Es probable que esto provoque el bloqueo de página y, por lo tanto, el bloqueo.

Recuerde también que su clave de índice agrupado es de 21 bytes y esto deberá almacenarse en todos sus índices secundarios como un marcador.

¿Ha considerado convertir su columna de identidad de clave principal en su índice agrupado? Esto no solo reducirá el tamaño de sus otros índices, sino que también reducirá el número de divisiones de página en su índice agrupado. Vale la pena intentarlo si puedes soportar la reconstrucción de tus índices.

Steve
fuente
1

Con el enfoque de subprocesos múltiples, desconfío de la inserción en una tabla desde la que primero debe verificar la existencia previa de una clave. Eso me dice que hay un problema de concurrencia en ese índice PK de esa tabla, sin importar cuántos hilos haya. Por la misma razón, no me gusta la sugerencia de NOLOCK en la tabla de inventario porque parece que ocurriría un error si diferentes hilos pueden escribir la misma clave (¿el esquema de partición elimina esa posibilidad?). Tengo curiosidad acerca de cuán grande fue la aceleración en la introducción inicial de múltiples hilos porque debe haber funcionado bien en algún momento.

Algo para intentar es hacer que la consulta se lea más como una operación masiva y convertir el "donde no existe" en un "anti-join". (en última instancia, el optimizador puede optar por ignorar este esfuerzo). Como se mencionó anteriormente, eliminaría la sugerencia NOLOCK en la tabla de destino a menos que tal vez la partición no haya garantizado colisiones clave entre subprocesos.

 INSERT INTO i (...)
 SELECT DISTINCT ...             
   FROM tempdb..Update_Item_Work t (NOLOCK) -- nolock okay on read table
   left join Inventory i -- use without NOLOCK because PK is written inter-thread
     on i.Inv_Site_Key = t.UpdItemWrk_Site_Key
    and i.Inv_Item_Key = t.UpdItemWrk_Item_Key
    and i.Inv_Date = t.UpdItemWrk_Date
  where i.Inv_Site_Key is null   -- where not exist in inventory
    and UpdItemWrk_GUID = @GUID  -- for this thread

El tiempo que se ejecuta como base, puede volver a ejecutar con la sugerencia de combinación ("combinación izquierda" -> "combinación combinación izquierda") como otra posibilidad. Probablemente debería tener un índice en la tabla temporal (UpdItemWrk_Site_Key, UpdItemWrk_Item_Key, UpdItemWrk_Date) para la sugerencia de combinación.

No sé si las nuevas versiones no expresas de SQL Server 2008/2012 podrían paralelizar automáticamente las fusiones más grandes de este formulario, permitiéndole eliminar la partición basada en GUID.

Para alentar que la unión solo se produzca en los elementos distintos en lugar de todos los elementos, las cláusulas "seleccionar distinto ... de ..." se pueden convertir en "seleccionar * de (seleccionar distinto ... de ...)" antes continuando con la unión. Esto solo puede hacer una diferencia notable si el distinto está filtrando muchas filas. Nuevamente, el optimizador puede ignorar este esfuerzo.

crokusek
fuente