La mise à jour SQL prend un temps très long / une utilisation élevée du disque pendant des heures

8

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

GFK
la source
1
Quelque chose dans le journal des erreurs SQL Server? De procmon également, quels sont les décalages dans le fichier dans lequel il écrit? Vous pouvez diviser par 8 192 pour obtenir la page, puis utiliser DBCC PAGEpour voir ce qui est écrit.
Martin Smith
3,5 Go ressemble à la quantité maximale de RAM qu'une station de travail Windows 32 bits peut gérer.
tschmit007
@MartinSmith Il n'y a absolument rien depuis que j'ai restauré dans les journaux SSMS SQL Server et rien non plus dans le journal des événements Windows
GFK
À quoi ressemblent vos index sur la table A (quelles colonnes, etc.)? Sont-ils fragmentés?
Stuart Ainsworth
@ tschmit007 Son édition de développement SQL 2008 R2 x64 sur Win Server 2008 R2 x64. Il s'agit d'une machine virtuelle s'exécutant sur Hyper-V (l'hôte est également 2008 R2 x64); la machine virtuelle a 4,2 Go de mémoire physique utilisée sur 5 Go et 4,6 Go de validation sur 10 Go maximum; l'hôte dispose d'une mémoire physique de 7,2 Go utilisée sur 8 Go et d'une validation de 7,8 sur 16 Go max. Les deux machines sont plus lentes en raison de l'utilisation HD mais ne sont pas obstruées.
GFK

Réponses:

0
  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 AX, déposez-la.
  3. Créez un index qui contiendra uniquement 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 augmenter au préalable 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: Étant donné que je ne peux pas commenter votre message d'origine, je répondrai ici à votre question de Edit 4. Vous avez 7 index sur AX 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.

bojan
la source
Pour le point 1, voir ma réponse à ik_zelf. Le curseur est là pour des raisons d'enquête et n'a pas beaucoup d'impact. Je vais mettre en œuvre le reste de vos suggestions, je pense que c'est tout ce qu'il me reste à faire. Si cela fonctionne, je serai toujours laissé sans explication sur ce qui se passe maintenant ...
GFK
Vous pouvez publier DDL pour vos tables (y compris tous les index, contraintes, etc.). Il y a peut-être quelque chose qui ralentit vos performances et cela vous manque.
bojan
1
Supprimer les index / Mettre à jour / Reconstruire les index fonctionne, et même si je préfère ne pas avoir à faire quelque chose d'aussi drastique, je ne vois pas que j'ai le choix. Merci!
GFK
0

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 se soit terminée ou non. 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.

Techie Joe
la source
0

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.

ik_zelf
la source
Je ne pense pas que ce soit le point. Je me rends compte que la mise à jour des enregistrements indexés prend du temps, et je sais que, dans l'ensemble, une partie du temps nécessaire est due à cela. Mais je m'attends à cela, et je suis d'accord: comme je l'ai dit, la mise à jour de 99% des lignes prend 5 minutes (même en utilisant le curseur), mais pour une raison quelconque, une ligne (et pas toujours la même) prend 5 heures. Ce qui m'inquiète, c'est ce comportement particulier.
GFK
les verrous ne sont pas un problème que vous avez dit .... qu'en est-il de l'utilisation du système de fichiers, atteignant 90% ou plus?
ik_zelf
non, c'est 31 Go gratuits sur 120 Go, donc je pense que c'est ok
GFK
que se passe-t-il si vous essayez de copier la table comme create table a_copy comme select * from a;
ik_zelf