Performances lentes en insérant quelques lignes dans une immense table

9

Nous avons un processus qui prend les données des magasins et met à jour une table d'inventaire à l'échelle de l'entreprise. Ce tableau contient des lignes pour chaque magasin par date et par article. Chez les clients avec de nombreux magasins, ce tableau peut devenir très volumineux - de l'ordre de 500 millions de lignes.

Ce processus de mise à jour de l'inventaire est généralement exécuté plusieurs fois par jour lorsque les magasins saisissent des données. Ces exécutions mettent à jour les données de seulement quelques magasins. Cependant, les clients peuvent également l'exécuter pour mettre à jour, par exemple, tous les magasins au cours des 30 derniers jours. Dans ce cas, le processus fait tourner 10 threads et met à jour l'inventaire de chaque magasin dans un thread séparé.

Le client se plaint que le processus prend du temps. J'ai profilé le processus et constaté qu'une requête insérée dans ce tableau prend beaucoup plus de temps que prévu. Cet INSERT se termine parfois en 30 secondes.

Lorsque j'exécute une commande SQL INSERT ad hoc sur cette table (délimitée par BEGIN TRAN et ROLLBACK), le SQL ad hoc se termine dans l'ordre des millisecondes.

La requête à exécution lente est ci-dessous. L'idée est d'insérer des enregistrements qui ne sont pas là et plus tard de les mettre à jour lorsque nous calculons différents bits de données. Une étape antérieure du processus a identifié les éléments à mettre à jour, effectué des calculs et inséré les résultats dans la table tempdb Update_Item_Work. Ce processus s'exécute dans 10 threads distincts, et chaque thread a son propre GUID dans 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)

Le tableau d'inventaire comprend 42 colonnes, dont la plupart suivent les quantités et les comptes pour divers ajustements d'inventaire. sys.dm_db_index_physical_stats dit que chaque ligne fait environ 242 octets, donc je m'attends à ce qu'environ 33 lignes tiennent sur une seule page de 8 Ko.

La table est regroupée sur la contrainte unique (Inv_Site_Key, Inv_Item_Key, Inv_Date). Toutes les clés sont DECIMAL (15,0) et la date est SMALLDATETIME. Il existe une clé primaire IDENTITY (non cluster) et 4 autres index. Tous les index et la contrainte cluster sont définis avec un explicite (FILLFACTOR = 90, PAD_INDEX = ON).

J'ai regardé dans le fichier journal pour compter les fractionnements de page. J'ai mesuré environ 1 027 fractionnements sur l'index clusterisé et 1 724 fractionnements sur un autre index, mais je n'ai pas enregistré sur quel intervalle ceux-ci se sont produits. Une heure et demie plus tard, j'ai mesuré 7 035 fractionnements de page sur l'index clusterisé.

Le plan de requête que j'ai capturé dans le profileur ressemble à ceci:

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                                                                                       

En regardant les requêtes par rapport à divers dmv, je vois que la requête attend sur PAGEIOLATCH_EX pour une durée de 0 sur une page de ce tableau d'inventaire. Je ne vois aucune attente ou blocage sur les serrures.

Cette machine a environ 32 Go de mémoire. Il exécute SQL Server 2005 Standard Edition, bien qu'il soit bientôt mis à niveau vers 2008 R2 Enterprise Edition. Je n'ai pas de chiffres sur la taille de la table d'inventaire en termes d'utilisation du disque, mais je peux l'obtenir, si nécessaire. C'est l'une des plus grandes tables de ce système.

J'ai exécuté une requête contre sys.dm_io_virtual_file_stats et j'ai vu que les attentes d'écriture moyennes contre tempdb étaient supérieures à 1,1 seconde . La base de données dans laquelle cette table est stockée a des temps d'attente d'écriture moyens de ~ 350 ms. Mais ils ne redémarrent leur serveur que tous les 6 mois environ, donc je ne sais pas si ces informations sont pertinentes. tempdb est réparti sur 4 fichiers différents Ils ont 3 fichiers différents pour la base de données qui contient la table d'inventaire.

Pourquoi cette requête prendrait-elle autant de temps à INSÉRER quelques lignes lorsqu'elle est exécutée avec de nombreux threads différents lorsqu'un seul INSERT est très rapide?

-- MISE À JOUR --

Voici les nombres de latence par lecteur, y compris les octets lus. Comme vous pouvez le voir, les performances de tempdb sont discutables. La table d'inventaire se trouve dans PDICompany_252_01.mdf, PDICompany_252_01_Second.ndf ou 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
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Paul White 9

Réponses:

4

Il semble que vos fractionnements de page d'index cluster seront pénibles car l'index cluster contient les données réelles et cela nécessitera l'allocation de nouvelles pages et le déplacement des données vers celles-ci. Cela est susceptible d'entraîner un verrouillage de page et donc un blocage.

N'oubliez pas également que votre clé d'index cluster est de 21 octets et que cela devra être stocké dans tous vos index secondaires en tant que signet.

Avez-vous envisagé de faire de votre colonne d'identité de clé primaire votre index cluster, non seulement cela réduira la taille de vos autres index, mais cela signifiera également que vous réduirez le nombre de fractionnements de page dans votre index cluster. Cela vaut la peine d'essayer si vous pouvez reconstruire vos index.

Steve
la source
1

Avec l'approche multi-thread, je me méfie de l'insertion dans une table à partir de laquelle vous devez d'abord vérifier l'existence préalable d'une clé. Cela me dit qu'il y a un problème de concurrence sur cet index PK à cette table, peu importe le nombre de threads. Pour la même raison, je n'aime pas le conseil NOLOCK sur la table d'inventaire car il semble qu'une erreur se produirait si différents threads sont capables d'écrire la même clé (le schéma de partitionnement supprime-t-il cette possibilité?). Je suis curieux de voir à quel point l'accélération a été importante lors de l'introduction initiale de plusieurs threads, car cela a dû bien fonctionner à un moment donné.

Quelque chose à essayer est de faire en sorte que la requête ressemble davantage à une opération en bloc et de convertir le «où n'existe pas» en un «anti-jointure». (en fin de compte, l'optimiseur peut choisir d'ignorer cet effort). Comme mentionné ci-dessus, je supprimerais l'indice NOLOCK sur la table de destination, à moins que le partitionnement n'ait garanti aucune collision de clés entre les threads.

 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

Timing qui s'exécute en tant que base, vous pouvez réexécuter avec l'indicateur de fusion ("jointure gauche" -> "jointure de fusion gauche") comme une autre possibilité. Vous devriez probablement avoir un index sur la table temporaire (UpdItemWrk_Site_Key, UpdItemWrk_Item_Key, UpdItemWrk_Date) pour le conseil de fusion.

Je ne sais pas si les nouvelles versions non express de SQL Server 2008/2012 seraient capables de paralléliser automatiquement des fusions plus importantes de ce formulaire vous permettant de supprimer le partitionnement basé sur GUID.

Pour encourager la jointure à se produire uniquement sur les éléments distincts plutôt que sur tous les éléments, les clauses "select distinct ... from ..." peuvent être converties en "select * from (select distinct ... from ...)" avant poursuivre la jointure. Cela ne peut faire une différence notable que si le filtre distinct filtre de nombreuses lignes. Encore une fois, l'optimiseur peut ignorer cet effort.

crokusek
la source