web-dev-qa-db-fra.com

Instruction de mise à jour SQL prenant un temps très long / utilisation élevée du disque pendant des heures

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 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?

8
GFK
  1. Restez avec la commande UPDATE. CURSOR sera plus lent pour ce que vous essayez de faire.
  2. Supprimez/désactivez tous les index, y compris ceux des vues indexées. Si vous avez une clé étrangère sur A.X, déposez-la.
  3. Créez un index qui ne contiendra que A.B_ID et un autre pour B.ID.
  4. Même si vous utilisez le modèle de récupération simple, la dernière transaction sera toujours dans le journal des transactions avant d'être vidée sur le disque. C'est pourquoi vous devez pré-agrandir votre journal des transactions et le configurer pour qu'il augmente pour une plus grande quantité (par exemple, 100 Mo).
  5. Définissez également la croissance du fichier de données sur une quantité plus importante.
  6. Assurez-vous que vous disposez de suffisamment d'espace disque pour la croissance future des fichiers journaux et de données.
  7. Une fois la mise à jour terminée, recréez/activez les index que vous avez supprimés/désactivés à l'étape 2.
  8. Si vous n'en avez plus besoin, supprimez les index créés à l'étape 3.

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.

0
bojan

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.

0
ik_zelf

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.

0
Techie Joe