La présence du champ XML fait que la plupart des données de table se trouvent sur les pages LOB_DATA (en fait, ~ 90% des pages de table sont LOB_DATA).
Le simple fait d'avoir la colonne XML dans le tableau n'a pas cet effet. C'est la présence de données XML qui, dans certaines conditions , entraîne le stockage d'une partie des données d'une ligne hors ligne, sur les pages LOB_DATA. Et même si un (ou peut-être plusieurs ;-) pourrait faire valoir que duh, la XML
colonne implique qu'il y aura effectivement des données XML, il n'est pas garanti que les données XML devront être stockées hors ligne: à moins que la ligne soit à peu près déjà remplie en dehors de leur caractère XML, les petits documents (jusqu'à 8 000 octets) peuvent tenir en ligne et ne jamais aller sur une page LOB_DATA.
ai-je raison de penser que les pages LOB_DATA peuvent provoquer des analyses lentes non seulement en raison de leur taille, mais aussi parce que SQL Server ne peut pas analyser efficacement l'index cluster lorsqu'il y a beaucoup de pages LOB_DATA dans le tableau?
La numérisation consiste à regarder toutes les lignes. Bien sûr, lorsqu'une page de données est lue, toutes les données en ligne sont lues, même si vous avez sélectionné un sous-ensemble des colonnes. La différence avec les données LOB est que si vous ne sélectionnez pas cette colonne, les données hors ligne ne seront pas lues. Par conséquent, il n'est pas vraiment juste de tirer une conclusion sur l'efficacité avec laquelle SQL Server peut analyser cet index clusterisé, car vous n'avez pas exactement testé cela (ou vous en avez testé la moitié). Vous avez sélectionné toutes les colonnes, ce qui inclut la colonne XML, et comme vous l'avez mentionné, c'est là que se trouvent la plupart des données.
Nous savons donc déjà que le SELECT TOP 1000 *
test ne consistait pas simplement à lire une série de pages de données de 8k, toutes d'affilée, mais plutôt à sauter vers d'autres emplacements pour chaque ligne . La structure exacte de ces données LOB peut varier en fonction de leur taille. Sur la base des recherches présentées ici ( Quelle est la taille du pointeur LOB pour les types (MAX) comme Varchar, Varbinary, Etc? ), Il existe deux types d'allocations LOB hors ligne:
- Racine en ligne - pour les données comprises entre 8001 et 40 000 (vraiment 42 000) octets, si l'espace le permet, il y aura 1 à 5 pointeurs (24 à 72 octets) EN RANG qui pointent directement vers les pages LOB.
- TEXT_TREE - pour les données de plus de 42 000 octets, ou si les 1 à 5 pointeurs ne peuvent pas tenir en ligne, il n'y aura alors qu'un pointeur de 24 octets vers la page de départ d'une liste de pointeurs vers les pages LOB (c'est-à-dire le " page text_tree ").
L'une de ces deux situations se produit chaque fois que vous récupérez des données LOB qui dépassent 8 000 octets ou qui ne tiennent tout simplement pas en ligne. J'ai posté un script de test sur PasteBin.com (script T-SQL pour tester les allocations et les lectures LOB ) qui montre les 3 types d'allocations LOB (en fonction de la taille des données) ainsi que l'effet de chacune d'elles sur la logique et lectures physiques. Dans votre cas, si les données XML sont réellement inférieures à 42 000 octets par ligne, aucune d'entre elles (ou très peu) ne devrait se trouver dans la structure TEXT_TREE la moins efficace.
Si vous souhaitez tester la vitesse à laquelle SQL Server peut analyser cet index clusterisé, procédez comme suit, SELECT TOP 1000
mais spécifiez une ou plusieurs colonnes n'incluant pas cette colonne XML. Comment cela affecte-t-il vos résultats? Cela devrait être un peu plus rapide.
est-il jugé raisonnable d'avoir une telle structure de tableau / modèle de données?
Étant donné que nous avons une description incomplète de la structure réelle du tableau et du modèle de données, toute réponse peut ne pas être optimale en fonction de ces détails manquants. Dans cet esprit, je dirais qu'il n'y a rien de manifestement déraisonnable dans la structure de votre table ou le modèle de données.
Je peux (dans une application ac #) compresser XML de 20 Ko à ~ 2,5 Ko et le stocker dans la colonne VARBINARY, empêchant l'utilisation des pages de données LOB. Cela accélère les sélections 20 fois dans mes tests.
Cela a rendu la sélection de toutes les colonnes, ou même juste les données XML (maintenant VARBINARY
) plus rapide, mais cela bloque en fait les requêtes qui ne sélectionnent pas les données "XML". En supposant que vous avez environ 50 octets dans les autres colonnes et que vous en avez FILLFACTOR
100, alors:
Aucune compression: 15k de XML
données devraient nécessiter 2 pages LOB_DATA, ce qui nécessite alors 2 pointeurs pour la racine en ligne. Le premier pointeur fait 24 octets et le second 12, pour un total de 36 octets stockés en ligne pour les données XML. La taille totale des lignes est de 86 octets, et vous pouvez adapter environ 93 de ces lignes sur une page de données de 8060 octets. Par conséquent, 1 million de lignes nécessite 10 753 pages de données.
Compression personnalisée: 2,5 k de VARBINARY
données s'adapteront en ligne. La taille totale des lignes est de 2610 (2,5 * 1024 = 2560) octets, et vous ne pouvez insérer que 3 de ces lignes sur une page de données de 8060 octets. Par conséquent, 1 million de lignes nécessite 333 334 pages de données.
Ergo, l'implémentation d'une compression personnalisée entraîne une augmentation de 30 fois le nombre de pages de données pour l'index clusterisé. Autrement dit, toutes les requêtes utilisant une analyse d'index cluster ont maintenant environ 322 500 pages de données supplémentaires à lire. Veuillez consulter la section détaillée ci-dessous pour connaître les ramifications supplémentaires de ce type de compression.
Je mettrais en garde contre toute refactorisation basée sur les performances de SELECT TOP 1000 *
. Il est peu probable qu'il s'agisse d'une requête que l'application émettra, et ne doit pas être utilisée comme seule base pour des optimisations potentiellement inutiles.
Pour des informations plus détaillées et d'autres tests à essayer, veuillez consulter la section ci-dessous.
Cette question ne peut pas recevoir de réponse définitive, mais nous pouvons au moins faire des progrès et suggérer des recherches supplémentaires pour nous aider à nous rapprocher de la détermination du problème exact (idéalement basé sur des preuves).
Ce que nous savons:
- Le tableau comprend environ 1 million de lignes
- La taille de la table est d'environ 15 Go
- Le tableau contient une
XML
colonne et plusieurs autres colonnes de types: INT
, BIGINT
, UNIQUEIDENTIFIER
, « etc »
XML
la "taille" de la colonne est en moyenne d' environ 15k
- Après l'exécution
DBCC DROPCLEANBUFFERS
, il faut 20 à 25 secondes pour que la requête suivante se termine:SELECT TOP 1000 * FROM TABLE
- L'index cluster est en cours d'analyse
- La fragmentation sur l'indice clusterisé est proche de 0%
Ce que nous pensons savoir:
- Aucune autre activité de disque en dehors de ces requêtes. Êtes-vous sûr? Même s'il n'y a pas d'autres requêtes utilisateur, des opérations d'arrière-plan ont-elles lieu? Existe-t-il des processus externes à SQL Server exécutés sur la même machine qui pourraient prendre une partie des E / S? Il n'y en a peut-être pas, mais ce n'est pas clair sur la seule base des informations fournies.
- 15 Mo de données XML sont retournés. Sur quoi est basé ce nombre? Une estimation dérivée des 1 000 lignes multipliées par la moyenne de 15 000 données XML par ligne? Ou une agrégation programmatique de ce qui a été reçu pour cette requête? S'il ne s'agit que d'une estimation, je ne m'y fonderais pas car la distribution des données XML pourrait ne pas être identique à celle qu'implique une simple moyenne.
La compression XML pourrait vous aider. Comment feriez-vous exactement la compression dans .NET? Via les classes GZipStream ou DeflateStream ? Ce n'est pas une option à coût nul. Il compressera certainement certaines des données d'un grand pourcentage, mais il faudra également plus de CPU car vous aurez besoin d'un processus supplémentaire pour compresser / décompresser les données à chaque fois. Ce plan supprimerait également complètement votre capacité à:
- interroger les données XML via les
.nodes
, .value
, .query
et les .modify
fonctions XML.
indexer les données XML.
Veuillez garder à l'esprit (puisque vous avez mentionné que XML est "hautement redondant") que le XML
type de données est déjà optimisé en ce qu'il stocke les noms d'élément et d'attribut dans un dictionnaire, en attribuant un ID d'index entier à chaque élément, puis en utilisant cet ID entier dans tout le document (par conséquent, il ne répète pas le nom complet pour chaque utilisation, ni ne le répète à nouveau comme une balise de fermeture pour les éléments). Les données réelles ont également supprimé les espaces blancs superflus. C'est pourquoi les documents XML extraits ne conservent pas leur structure d'origine et pourquoi les éléments vides sont extraits comme <element />
s'ils étaient entrés en tant que<element></element>
. Ainsi, tout gain de compression via GZip (ou toute autre chose) ne sera trouvé qu'en compressant les valeurs des éléments et / ou des attributs, ce qui est une surface beaucoup plus petite qui pourrait être améliorée que la plupart ne le pensent et ne vaut probablement pas la perte de capacités comme indiqué ci-dessus.
Veuillez également garder à l'esprit que la compression des données XML et le stockage du VARBINARY(MAX)
résultat n'éliminera pas l'accès LOB, il le réduira simplement. Selon la taille du reste des données sur la ligne, la valeur compressée peut tenir dans la ligne, ou elle peut encore nécessiter des pages LOB.
Ces informations, bien qu'utiles, ne suffisent pas. Il existe de nombreux facteurs qui influencent les performances des requêtes, nous avons donc besoin d'une image beaucoup plus détaillée de ce qui se passe.
Ce que nous ne savons pas, mais devons:
- Pourquoi la performance de la
SELECT *
matière? Est-ce un modèle que vous utilisez dans le code. Si oui, pourquoi?
- Quelle est la performance de sélectionner uniquement la colonne XML? Quelles sont les statistiques et le calendrier si vous faites simplement
SELECT TOP 1000 XmlColumn FROM TABLE;
:?
La durée des 20 à 25 secondes nécessaires pour renvoyer ces 1 000 lignes est liée aux facteurs réseau (obtention des données via le câble) et la quantité est liée aux facteurs clients (soit environ 15 Mo plus le reste des non Données XML dans la grille dans SSMS, ou éventuellement sauvegarde sur disque)?
La prise en compte de ces deux aspects de l'opération peut parfois se faire simplement en ne renvoyant pas les données. Maintenant, on pourrait penser à sélectionner une table temporaire ou une variable de table, mais cela ne ferait qu'introduire quelques nouvelles variables (par exemple, les E / S de disque pour tempdb
, les écritures du journal des transactions, la possible croissance automatique des données tempdb et / ou du fichier journal, le besoin dans le pool de tampons, etc.). Tous ces nouveaux facteurs peuvent en fait augmenter le temps de requête. Au lieu de cela, je stocke généralement les colonnes dans des variables (du type de données approprié; non SQL_VARIANT
) qui sont écrasées à chaque nouvelle ligne (c'est-à-dire SELECT @Column1 = tab.Column1,...
).
CEPENDANT , comme l'a souligné @PaulWhite dans ce DBA.StackExchange Q & A, Logical lit différemment lors de l'accès aux mêmes données LOB , avec des recherches supplémentaires de moi-même publiées sur PasteBin ( script T-SQL pour tester divers scénarios pour les lectures LOB ) , LOB ne sont pas accessibles constamment entre SELECT
, SELECT INTO
, SELECT @XmlVariable = XmlColumn
, SELECT @XmlVariable = XmlColumn.query(N'/')
et SELECT @NVarCharVariable = CONVERT(NVARCHAR(MAX), XmlColumn)
. Nos options sont donc un peu plus limitées ici, mais voici ce qui peut être fait:
- Éliminez les problèmes de réseau en exécutant la requête sur le serveur exécutant SQL Server, dans SSMS ou SQLCMD.EXE.
- Éliminez les problèmes des clients dans SSMS en allant dans Options de requête -> Résultats -> Grille et en cochant l'option "Supprimer les résultats après exécution". Veuillez noter que cette option empêchera TOUTES les sorties, y compris les messages, mais peut toujours être utile pour exclure le temps nécessaire à SSMS pour allouer la mémoire par ligne, puis la dessiner dans la grille.
Sinon, vous pouvez exécuter la requête via Sqlcmd.exe et diriger la sortie pour aller nulle part via: -o NUL:
.
- Y a-t-il un type d'attente associé à cette requête? Si oui, quel est ce type d'attente?
Quelle est la taille réelle des données pour les XML
colonnes renvoyées ? La taille moyenne de cette colonne sur l'ensemble du tableau n'a pas vraiment d'importance si les lignes "TOP 1000" contiennent une partie disproportionnée du total des XML
données. Si vous voulez en savoir plus sur les 1000 premières lignes, regardez ces lignes. Veuillez exécuter ce qui suit:
SELECT TOP 1000 tab.*,
SUM(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [TotalXmlKBytes],
AVG(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [AverageXmlKBytes]
STDEV(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [StandardDeviationForXmlKBytes]
FROM SchemaName.TableName tab;
- Le schéma de table exact . Veuillez fournir la déclaration complète
CREATE TABLE
, y compris tous les index.
- Plan de requête? Est-ce quelque chose que vous pouvez publier? Cette information ne changera probablement rien, mais il vaut mieux savoir qu'elle ne changera pas que de deviner qu'elle ne le sera pas et qu'elle aura tort ;-)
- Y a-t-il une fragmentation physique / externe sur le fichier de données? Bien que cela ne soit peut-être pas un facteur important ici, puisque vous utilisez du "SATA de qualité grand public" et non un SSD ou même un SATA super-coûteux, l'effet des secteurs sous-optimisés sera plus visible, d'autant plus que le nombre de ces secteurs qui doit être lu augmente.
Quels sont les résultats exacts de la requête suivante:
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(),
OBJECT_ID(N'dbo.SchemaName.TableName'), 1, 0, N'LIMITED');
MISE À JOUR
Il m'est venu à l'esprit que je devrais essayer de reproduire ce scénario pour voir si je rencontre un comportement similaire. J'ai donc créé un tableau avec plusieurs colonnes (similaire à la description vague de la question), puis je l'ai rempli avec 1 million de lignes, et la colonne XML a environ 15k de données par ligne (voir le code ci-dessous).
Ce que j'ai trouvé, c'est que faire un SELECT TOP 1000 * FROM TABLE
terminé en 8 secondes la première fois, et 2 à 4 secondes à chaque fois par la suite (oui, exécuter DBCC DROPCLEANBUFFERS
avant chaque exécution de la SELECT *
requête). Et mon ordinateur portable de plusieurs années n'est pas rapide: SQL Server 2012 SP2 Developer Edition, 64 bits, 6 Go de RAM, double 2,5 Ghz Core i5 et un lecteur SATA à 5400 tr / min. J'utilise également SSMS 2014, SQL Server Express 2014, Chrome et plusieurs autres choses.
Sur la base du temps de réponse de mon système, je répéterai que nous avons besoin de plus d'informations (c'est-à-dire des détails sur le tableau et les données, les résultats des tests suggérés, etc.) afin d'aider à réduire la cause du temps de réponse de 20 à 25 secondes que vous voyez.
SET ANSI_NULLS, NOCOUNT ON;
GO
IF (OBJECT_ID(N'dbo.XmlReadTest') IS NOT NULL)
BEGIN
PRINT N'Dropping table...';
DROP TABLE dbo.XmlReadTest;
END;
PRINT N'Creating table...';
CREATE TABLE dbo.XmlReadTest
(
ID INT NOT NULL IDENTITY(1, 1),
Col2 BIGINT,
Col3 UNIQUEIDENTIFIER,
Col4 DATETIME,
Col5 XML,
CONSTRAINT [PK_XmlReadTest] PRIMARY KEY CLUSTERED ([ID])
);
GO
DECLARE @MaxSets INT = 1000,
@CurrentSet INT = 1;
WHILE (@CurrentSet <= @MaxSets)
BEGIN
RAISERROR(N'Populating data (1000 sets of 1000 rows); Set # %d ...',
10, 1, @CurrentSet) WITH NOWAIT;
INSERT INTO dbo.XmlReadTest (Col2, Col3, Col4, Col5)
SELECT TOP 1000
CONVERT(BIGINT, CRYPT_GEN_RANDOM(8)),
NEWID(),
GETDATE(),
N'<test>'
+ REPLICATE(CONVERT(NVARCHAR(MAX), CRYPT_GEN_RANDOM(1), 2), 3750)
+ N'</test>'
FROM [master].[sys].all_columns sac1;
IF ((@CurrentSet % 100) = 0)
BEGIN
RAISERROR(N'Executing CHECKPOINT ...', 10, 1) WITH NOWAIT;
CHECKPOINT;
END;
SET @CurrentSet += 1;
END;
--
SELECT COUNT(*) FROM dbo.XmlReadTest; -- Verify that we have 1 million rows
-- O.P. states that the "clustered index fragmentation is close to 0%"
ALTER INDEX [PK_XmlReadTest] ON dbo.XmlReadTest REBUILD WITH (FILLFACTOR = 90);
CHECKPOINT;
--
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 * FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 5676, lob physical reads 1, lob read-ahead reads 3967.
SQL Server Execution Times:
CPU time = 171 ms, elapsed time = 8329 ms.
*/
Et, parce que nous voulons prendre en compte le temps nécessaire pour lire les pages non LOB, j'ai exécuté la requête suivante pour sélectionner tout sauf la colonne XML (l'un des tests que j'ai suggéré ci-dessus). Cela revient en 1,5 seconde assez régulièrement.
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 ID, Col2, Col3, Col4 FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 1666 ms.
*/
Conclusion (pour le moment)
Sur la base de ma tentative de recréer votre scénario, je ne pense pas que nous puissions indiquer le lecteur SATA ou les E / S non séquentielles comme la principale cause des 20 à 25 secondes, surtout parce que nous Je ne sais pas à quelle vitesse la requête retourne sans inclure la colonne XML. Et je n'ai pas pu reproduire le grand nombre de lectures logiques (non LOB) que vous montrez, mais j'ai le sentiment que j'ai besoin d'ajouter plus de données à chaque ligne à la lumière de cela et de la déclaration de:
~ 90% des pages du tableau sont LOB_DATA
Ma table comporte 1 million de lignes, chacune sys.dm_db_index_physical_stats
contenant un peu plus de 15 000 données XML, et montre qu'il y a 2 millions de pages LOB_DATA. Les 10% restants seraient alors 222 000 pages de données IN_ROW, mais je n'en ai que 11 630. Donc, encore une fois, nous avons besoin de plus d'informations sur le schéma de table réel et les données réelles.