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:
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
Autres informations utiles:
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 des B_ID différents 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 l'a restaurée), 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.
Édité 3: fragmentation de l'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 aussi 7 vues sur A (toutes incluent le champ A.X), chacune avec son propre PK en cluster, et un autre index qui inclut également le champ A.X
Les vues sont filtrées (avec des champs qui ne sont pas dans cette équation), donc je doute qu'il y ait un moyen pour la MISE À JOUR A d'utiliser les vues elles-mêmes. Mais ils ont un index comprenant A.X, donc changer A.X signifie écrire les 7 vues et les 7 index dont ils disposent 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 suspendu au même point dans la boucle WHILE
. 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.
Édité 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?
Edit: Puisque je ne peux pas commenter votre post d'origine, je répondrai ici à votre question de Edit 4. Vous avez 7 index sur A.X. L'index est un arbre B , et chaque mise à jour de ce champ entraîne un rééquilibrage de l'arbre B. Il est plus rapide de reconstruire ces index à partir de zéro que de les rééquilibrer à chaque fois.
Le scénario de mise à jour est toujours plus rapide que l'utilisation d'une procédure.
Étant donné que vous mettez à jour la colonne X de toutes les lignes du tableau A, assurez-vous d'abord de supprimer l'index sur celle-ci. Assurez-vous également qu'aucun élément comme les déclencheurs et les contraintes n'est actif sur cette colonne.
La mise à jour des index est une activité coûteuse, tout comme la validation des contraintes et l'exécution de déclencheurs de niveau ligne qui effectuent une recherche dans d'autres données.
Une chose à regarder est les ressources système (mémoire, disque, CPU) pendant ce processus. J'ai tenté d'insérer 7 millions de lignes individuelles dans une seule table en un seul gros travail et mon serveur s'est bloqué d'une manière similaire à la vôtre.
Il s'avère que je n'avais pas assez de mémoire sur mon serveur pour exécuter ce travail d'insertion en masse. Dans des situations comme celle-ci, SQL aime conserver la mémoire et ne pas la laisser partir ... même après que ladite commande d'insertion ait pu ou non se terminer. Plus il y a de commandes traitées dans les gros travaux, plus la mémoire est consommée. Un redémarrage rapide a libéré ladite mémoire.
Ce que je ferais, c'est démarrer ce processus à partir de zéro avec votre gestionnaire de tâches en cours d'exécution. Si l'utilisation de la mémoire dépasse 75%, les chances que votre système/processus gèle de façon astronomique.
Si votre mémoire/ressources est en effet limitée comme indiqué ci-dessus, vos options sont de couper le processus en petits morceaux (avec le redémarrage occasionnel si l'utilisation de la mémoire est élevée) au lieu d'un gros travail ou de passer à un serveur 64 bits avec beaucoup de mémoire.