Supprimer les performances des données LOB dans SQL Server

16

Cette question est liée à ce fil de discussion .

Exécution de SQL Server 2008 Developer Edition sur mon poste de travail et un cluster de machines virtuelles à deux nœuds Enterprise Edition où je fais référence à "cluster alpha".

Le temps nécessaire pour supprimer des lignes avec une colonne varbinary (max) est directement lié à la longueur des données de cette colonne. Cela peut sembler intuitif au début, mais après enquête, cela ne correspond pas à ma compréhension de la façon dont SQL Server supprime réellement les lignes en général et traite ce type de données.

Le problème provient d'un problème de délai d'attente de suppression (> 30 secondes) que nous voyons dans notre application Web .NET, mais je l'ai simplifié pour le bien de cette discussion.

Lorsqu'un enregistrement est supprimé, SQL Server le marque comme un fantôme à nettoyer par une tâche de nettoyage des fantômes ultérieurement après la validation de la transaction (voir le blog de Paul Randal ). Dans un test supprimant trois lignes avec 16 Ko, 4 Mo et 50 Mo de données dans une colonne varbinary (max), respectivement, je vois cela se produire sur la page avec la partie en ligne des données, ainsi que dans la transaction Journal.

Ce qui me semble étrange, c'est que des verrous X sont placés sur toutes les pages de données LOB pendant la suppression et que les pages sont désallouées dans le PFS. Je vois cela dans le journal des transactions, ainsi qu'avec sp_locket les résultats du dm_db_index_operational_statsDMV ( page_lock_count).

Cela crée un goulot d'étranglement d'E / S sur mon poste de travail et notre cluster alpha si ces pages ne sont pas déjà dans le cache de tampon. En fait, le page_io_latch_wait_in_msmême DMV correspond pratiquement à toute la durée de la suppression et page_io_latch_wait_countcorrespond au nombre de pages verrouillées. Pour le fichier de 50 Mo sur mon poste de travail, cela se traduit par plus de 3 secondes lors du démarrage avec un cache tampon vide ( checkpoint/ dbcc dropcleanbuffers), et je ne doute pas que ce serait plus long pour une fragmentation importante et sous charge.

J'ai essayé de m'assurer qu'il ne s'agissait pas simplement d'allouer de l'espace dans le cache en prenant ce temps. J'ai lu 2 Go de données à partir d'autres lignes avant d'exécuter la suppression au lieu de la checkpointméthode, ce qui est plus que ce qui est alloué au processus SQL Server. Je ne sais pas si c'est un test valide ou non, car je ne sais pas comment SQL Server mélange les données. J'ai supposé que cela pousserait toujours l'ancien au profit du nouveau.

De plus, il ne modifie même pas les pages. Cela je peux voir avec dm_os_buffer_descriptors. Les pages sont propres après la suppression, tandis que le nombre de pages modifiées est inférieur à 20 pour les trois suppressions petites, moyennes et grandes. J'ai également comparé la sortie de DBCC PAGEpour un échantillonnage des pages recherchées, et il n'y a eu aucun changement (seul le ALLOCATEDbit a été supprimé de PFS). Il les désalloue simplement.

Pour prouver davantage que les recherches / désallocations de page sont à l'origine du problème, j'ai essayé le même test en utilisant une colonne filestream au lieu de varilla binaire (max). Les suppressions étaient à temps constant, quelle que soit la taille de LOB.

Donc, d'abord mes questions académiques:

  1. Pourquoi SQL Server doit-il rechercher toutes les pages de données LOB afin de les verrouiller X? Est-ce juste un détail de la façon dont les verrous sont représentés en mémoire (stockés avec la page en quelque sorte)? Cela fait que l'impact des E / S dépend fortement de la taille des données s'il n'est pas complètement mis en cache.
  2. Pourquoi le X se verrouille-t-il, juste pour les désallouer? N'est-il pas suffisant de verrouiller uniquement la feuille d'index avec la partie en ligne, car la désallocation n'a pas besoin de modifier les pages elles-mêmes? Existe-t-il un autre moyen d'accéder aux données LOB contre lesquelles le verrou protège?
  3. Pourquoi désallouer les pages à l'avance, étant donné qu'il existe déjà une tâche de fond dédiée à ce type de travail?

Et peut-être plus important, ma question pratique:

  • Existe-t-il un moyen de faire fonctionner les suppressions différemment? Mon objectif est de supprimer le temps constant quelle que soit la taille, similaire à filestream, où tout nettoyage se produit en arrière-plan après coup. Est-ce une chose de configuration? Suis-je en train de stocker des choses étrangement?

Voici comment reproduire le test décrit (exécuté via la fenêtre de requête SSMS):

CREATE TABLE [T] (
    [ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
    [Data] [varbinary](max) NULL
)

DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier

SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration

INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))

-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN

-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID

-- Do this after test
ROLLBACK

Voici quelques résultats du profilage des suppressions sur mon poste de travail:

| Type de colonne | Supprimer la taille | Durée (ms) | Lit | Écrit | CPU |
-------------------------------------------------- ------------------
| VarBinary | 16 KB | 40 | 13 | 2 | 0 |
| VarBinary | 4 Mo | 952 | 2318 | 2 | 0 |
| VarBinary | 50 Mo | 2976 | 28594 | 1 | 62 |
-------------------------------------------------- ------------------
| FileStream | 16 KB | 1 | 12 | 1 | 0 |
| FileStream | 4 Mo | 0 | 9 | 0 | 0 |
| FileStream | 50 Mo | 1 | 9 | 0 | 0 |

Nous ne pouvons pas nécessairement utiliser simplement filestream à la place parce que:

  1. Notre répartition de la taille des données ne le garantit pas.
  2. Dans la pratique, nous ajoutons des données dans de nombreux morceaux et filestream ne prend pas en charge les mises à jour partielles. Nous aurions besoin de concevoir autour de cela.

Mise à jour 1

Testé une théorie selon laquelle les données sont écrites dans le journal des transactions dans le cadre de la suppression, et cela ne semble pas être le cas. Suis-je en train de tester cela incorrectement? Voir ci-dessous.

SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001

BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID

SELECT
    SUM(
        DATALENGTH([RowLog Contents 0]) +
        DATALENGTH([RowLog Contents 1]) +
        DATALENGTH([RowLog Contents 3]) +
        DATALENGTH([RowLog Contents 4])
    ) [RowLog Contents Total],
    SUM(
        DATALENGTH([Log Record])
    ) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'

Pour un fichier de plus de 5 Mo, cela est retourné 1651 | 171860.

De plus, je m'attendrais à ce que les pages elles-mêmes soient sales si les données étaient écrites dans le journal. Seules les désallocations semblent être enregistrées, ce qui correspond à ce qui est sale après la suppression.

Update 2

J'ai reçu une réponse de Paul Randal. Il a affirmé qu'il doit lire toutes les pages pour parcourir l'arborescence et trouver les pages à désallouer, et a déclaré qu'il n'y avait pas d'autre moyen de rechercher quelles pages. Ceci est une demi-réponse à 1 & 2 (mais n'explique pas la nécessité de verrouiller les données hors ligne, mais ce sont de petites pommes de terre).

La question 3 est toujours ouverte: pourquoi désallouer les pages à l'avance s'il existe déjà une tâche en arrière-plan pour nettoyer les suppressions?

Et bien sûr, la question la plus importante: existe-t-il un moyen d'atténuer directement (c'est-à-dire de ne pas contourner) ce comportement de suppression dépendant de la taille? Je pense que ce serait un problème plus courant, à moins que nous ne soyons vraiment les seuls à stocker et supprimer des lignes de 50 Mo dans SQL Server? Est-ce que tout le monde travaille autour de cela avec une forme de travail de collecte des ordures?

Jeremy Rosenberg
la source
Je souhaite qu'il y ait une meilleure solution, mais je n'en ai pas trouvé. J'ai une situation de journalisation de gros volumes de lignes de taille variable, jusqu'à 1 Mo +, et j'ai un processus de «purge» pour supprimer les anciens enregistrements. Parce que les suppressions étaient si lentes, j'ai dû le diviser en deux étapes - d'abord supprimer les références entre les tables (ce qui est très rapide), puis supprimer les lignes orphelines. Le travail de suppression a duré en moyenne ~ 2,2 secondes / Mo pour supprimer les données. Alors bien sûr, j'ai dû réduire les conflits, j'ai donc une procédure stockée avec "DELETE TOP (250)" dans une boucle jusqu'à ce qu'aucune ligne ne soit supprimée.
Abacus du

Réponses:

5

Je ne peux pas dire pourquoi il serait tellement plus inefficace de supprimer un VARBINARY (MAX) que le flux de fichiers, mais une idée que vous pourriez envisager si vous essayez simplement d'éviter les délais d'attente de votre application Web lors de la suppression de ces LOBS. Vous pouvez stocker les valeurs VARBINARY (MAX) dans une table distincte (appelons-la tblLOB) référencée par la table d'origine (appelons-la tblParent).

À partir d'ici, lorsque vous supprimez un enregistrement, vous pouvez simplement le supprimer de l'enregistrement parent, puis avoir un processus de récupération de place occasionnel pour entrer et nettoyer les enregistrements dans la table LOB. Il peut y avoir une activité supplémentaire sur le disque dur au cours de ce processus de récupération de place, mais elle sera au moins distincte du site Web frontal et peut être effectuée pendant les heures creuses.

Ian Chamberland
la source
Merci. C'est exactement l'une de nos options au conseil d'administration. La table est un système de fichiers, et nous sommes actuellement en train de séparer les données binaires vers une base de données complètement distincte de la méta de la hiérarchie. Nous pouvons soit faire ce que vous avez dit et supprimer la ligne de hiérarchie, et demander à un processus GC de nettoyer les lignes LOB orphelines. Ou ayez un horodatage de suppression avec les données pour atteindre le même objectif. C'est la voie que nous pouvons emprunter s'il n'y a pas de réponse satisfaisante au problème.
Jeremy Rosenberg
1
Je serais prudent d'avoir juste un horodatage pour indiquer qu'il est supprimé. Cela fonctionnera, mais vous aurez finalement beaucoup d'espace utilisé occupé par des lignes actives. Vous devrez avoir une sorte de processus gc à un moment donné, selon la quantité supprimée, et il sera moins impactant de supprimer moins régulièrement plutôt que beaucoup de manière occasionnelle.
Ian Chamberland