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
la source
Réponses:
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.
la source
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.
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.
la source