Oui, cela ressemble à un problème très générique, mais je n'ai pas encore été en mesure de le réduire.
J'ai donc une instruction UPDATE dans un fichier batch sql:
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
B a 40k enregistrements, A a 4M enregistrements et ils sont liés de 1 à n via A.B_ID, bien qu'il n'y ait pas de FK entre les deux.
Donc, fondamentalement, je pré-calcule un champ à des fins d'exploration de données. Bien que j'ai changé le nom des tables pour cette question, je n'ai pas changé la déclaration, c'est vraiment aussi simple que cela.
Cela prend des heures à fonctionner, j'ai donc décidé de tout annuler. La base de données a été corrompue, je l'ai donc supprimée, j'ai restauré une sauvegarde que j'ai faite juste avant d'exécuter l'instruction et j'ai décidé d'aller plus en détail avec un curseur:
DECLARE CursorB CURSOR FOR SELECT ID FROM B ORDER BY ID DESC -- Descending order
OPEN CursorB
DECLARE @Id INT
FETCH NEXT FROM CursorB INTO @Id
WHILE @@FETCH_STATUS = 0
BEGIN
DECLARE @Msg VARCHAR(50) = 'Updating A for B_ID=' + CONVERT(VARCHAR(10), @Id)
RAISERROR(@Msg, 10, 1) WITH NOWAIT
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = @Id
FETCH NEXT FROM CursorB INTO @Id
END
Maintenant, je peux le voir fonctionner avec un message avec l'ID décroissant. Ce qui se passe, c'est qu'il faut environ 5 minutes pour passer de id = 40k à id = 13
Et puis à l'id 13, pour une raison quelconque, il semble se bloquer. La base de données n'a aucune connexion en plus de SSMS, mais elle n'est pas réellement bloquée:
- le disque dur fonctionne en continu donc il fait définitivement quelque chose (j'ai vérifié dans Process Explorer que c'est bien le processus sqlserver.exe qui l'utilise)
J'ai exécuté sp_who2, trouvé le SPID (70) de la session SUSPENDUE, puis j'ai exécuté le script suivant:
sélectionnez * dans sys.dm_exec_requests r rejoignez sys.dm_os_tasks t sur r.session_id = t.session_id où r.session_id = 70
Cela me donne le wait_type, qui est PAGEIOLATCH_SH la plupart du temps mais change en fait parfois en WRITE_COMPLETION, ce qui, je suppose, se produit quand il vide le journal
- le fichier journal, qui était de 1,6 Go lorsque j'ai restauré la base de données (et quand il est arrivé à l'id 13), est maintenant de 3,5 Go
Autres informations utiles:
- le nombre d'enregistrements dans le tableau A pour B_ID 13 n'est pas grand (14)
- Mon collègue n'a pas le même problème sur sa machine, avec une copie de cette base de données (datant de quelques mois) avec la même structure.
- la table A est de loin la plus grande table de la DB
- Il a plusieurs index et plusieurs vues indexées l'utilisent.
- Il n'y a pas d'autre utilisateur sur la base de données, c'est local et aucune application ne l'utilise.
- La taille du fichier LDF n'est pas limitée.
- Le modèle de récupération est SIMPLE, le niveau de compatibilité est de 100
- Procmon ne me donne pas beaucoup d'informations: sqlserver.exe lit et écrit beaucoup à partir des fichiers MDF et LDF.
J'attends toujours qu'il se termine (cela fait 1h30) mais j'espérais que peut-être quelqu'un me donnerait une autre action que je pourrais essayer de résoudre ce problème.
Modifié: ajout d'extrait du journal procmon
15:24:02.0506105 sqlservr.exe 1760 ReadFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf SUCCESS Offset: 5,498,732,544, Length: 8,192, I/O Flags: Non-cached, Priority: Normal
15:24:02.0874427 sqlservr.exe 1760 WriteFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf SUCCESS Offset: 6,225,805,312, Length: 16,384, I/O Flags: Non-cached, Write Through, Priority: Normal
15:24:02.0884897 sqlservr.exe 1760 WriteFile C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA_1.LDF SUCCESS Offset: 4,589,289,472, Length: 8,388,608, I/O Flags: Non-cached, Write Through, Priority: Normal
En utilisant DBCC PAGE, il semble lire et écrire dans des champs qui ressemblent à la table A (ou à l'un de ses index), mais pour différents B_ID que 13. Reconstruire des index peut-être?
Édité 2: plan d'exécution
J'ai donc annulé la requête (en fait supprimé la base de données et ses fichiers, puis restauré), et vérifié le plan d'exécution pour:
UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = 13
Le plan d'exécution (estimé) est le même que pour n'importe quel B.ID et semble assez simple. La clause WHERE utilise une recherche d'index sur un index non cluster de B, la JOIN utilise une recherche d'index cluster sur les deux PK des tables. La recherche d'index cluster sur A utilise le parallélisme (x7) et représente 90% du temps CPU.
Plus important encore, l'exécution de la requête avec l'ID 13 est immédiate.
Modifié 3: fragmentation d'index
La structure des index est la suivante:
B a un PK en cluster (pas le champ ID) et un index unique non cluster, dont le premier champ est B.ID - ce deuxième index semble être toujours utilisé.
A possède un PK en cluster (champ non lié).
Il y a également 7 vues sur A (toutes incluent le champ AX), chacune avec son propre PK en cluster, et un autre index qui inclut également le champ AX
Les vues sont filtrées (avec des champs qui ne sont pas dans cette équation), donc je doute qu'il existe un moyen pour l'UPDATE A d' utiliser les vues elles-mêmes. Mais ils ont un index incluant AX, donc changer AX signifie écrire les 7 vues et les 7 index qu'ils ont qui incluent le champ.
Bien que la MISE À JOUR devrait être plus lente pour cela, il n'y a aucune raison pour laquelle un ID spécifique serait tellement plus long que les autres.
J'ai vérifié la fragmentation pour tous les index, tous étaient à <0,1%, sauf les index secondaires des vues , tous entre 25% et 50%. Les facteurs de remplissage pour tous les indices semblent corrects, entre 90% et 95%.
J'ai réorganisé tous les index secondaires et relancé mon script.
Il est toujours pendu, mais à un point différent:
...
(0 row(s) affected)
Updating A for B_ID=14
(4 row(s) affected)
Alors qu'auparavant, le journal des messages ressemblait à ceci:
...
(0 row(s) affected)
Updating A for B_ID=14
(4 row(s) affected)
Updating A for B_ID=13
C'est bizarre, car cela signifie qu'il n'est même pas pendu au même point de la WHILE
boucle. Le reste est identique: même ligne UPDATE en attente dans sp_who2, même type d'attente PAGEIOLATCH_EX et même utilisation HD intensive de sqlserver.exe.
La prochaine étape consiste à supprimer tous les index et vues et à les recréer, je pense.
Modifié 4: suppression puis reconstruction d'index
J'ai donc supprimé toutes les vues indexées que j'avais sur la table (7 d'entre elles, 2 index par vue, y compris celle en cluster). J'ai exécuté le script initial (sans curseur), et il a effectivement fonctionné en 5 minutes.
Mon problème provient donc de l'existence de ces index.
J'ai recréé mes index après avoir exécuté la mise à jour, et cela a pris 16 minutes.
Maintenant, je comprends que les index prennent du temps à reconstruire, et je suis en fait très bien avec la tâche complète qui prend 20 minutes.
Ce que je ne comprends toujours pas, c'est pourquoi lorsque j'exécute la mise à jour sans supprimer d'abord les index, cela prend plusieurs heures, mais lorsque je les supprime d'abord puis les recrée, cela prend 20 minutes. Cela ne devrait-il pas prendre à peu près le même temps?
DBCC PAGE
pour voir ce qui est écrit.