Considérez ce tableau:
create table Books
(
Id bigint not null primary key identity(1, 1),
UniqueToken varchar(100) not null,
[Text] nvarchar(max) not null
)
Imaginons que nous ayons plus de 100 000 livres dans ce tableau.
Nous avons maintenant 10 000 livres à insérer dans ce tableau, dont certains sont en double. Nous devons donc d'abord filtrer les doublons, puis insérer de nouveaux livres.
Une façon de vérifier les doublons est la suivante:
select UniqueToken
from Books
where UniqueToken in
(
'first unique token',
'second unique token'
-- 10,000 items here
)
L'existence de la colonne Text
affecte-t-elle les performances de cette requête? Si oui, comment pouvons-nous l'optimiser?
P.S. J'ai la même structure, pour d'autres données. Et ça ne marche pas bien. Un ami m'a dit que je devrais diviser ma table en deux tables comme suit:
create table BookUniqueTokens
(
Id bigint not null primary key identity(1, 1),
UniqueToken varchar(100)
)
create table Books
(
Id bigint not null primary key,
[Text] nvarchar(max)
)
Et je dois faire mon algorithme de recherche de doublons sur la première table uniquement, puis insérer des données dans les deux. De cette façon, il a affirmé que les performances s'améliorent beaucoup, car les tables sont physiquement séparées. Il a affirmé que la colonne [Text]
Affecte toute requête select
sur la colonne UniqueToken
.
Considérez votre requête avec 8 prédicats de filtre dans votre clause IN
sur un ensemble de données de 10 000 enregistrements.
select UniqueToken
from Books
where UniqueToken in
(
'Unique token 1',
'Unique token 2',
'Unique token 3',
'Unique token 4',
'Unique token 5',
'Unique token 6',
'Unique token 9999',
'Unique token 5000'
-- 10,000 items here
);
Un scan d'index cluster est utilisé, il n'y a pas d'autres index présents sur cette table de test
Avec une taille de données de 216 octets .
Vous devez également noter que même avec 8 enregistrements, les filtres OR
s'empilent.
Les lectures qui se sont produites sur cette table:
Crédits à statistiquesparser.
Lorsque vous incluez la colonne Text
dans la partie sélectionnée de votre requête, la taille réelle des données change radicalement:
select UniqueToken,Text
from Books
where UniqueToken in
(
'Unique token 1',
'Unique token 2',
'Unique token 3',
'Unique token 4',
'Unique token 5',
'Unique token 6',
'Unique token 9999',
'Unique token 5000'
-- 10,000 items here
);
Encore une fois, le scan d'index cluster avec un prédicat résiduel:
Mais avec un ensemble de données de 32 Ko .
Comme il y a près de 1000 lectures logiques lob:
Maintenant, lorsque nous créons les deux tables en question, et les remplissons avec les mêmes enregistrements de 10k
Exécuter la même sélection sans Text
. N'oubliez pas que nous avons eu 99 lectures logiques lors de l'utilisation de la table Books
.
select UniqueToken
from BookUniqueTokens
where UniqueToken in
(
'Unique token 1',
'Unique token 2',
'Unique token 3',
'Unique token 4',
'Unique token 5',
'Unique token 6',
'Unique token 9999',
'Unique token 5000'
-- 10,000 items here
)
Les lectures sur BookUniqueTokens
sont inférieures, 67 au lieu de 99.
Nous pouvons retracer cela aux pages de la table d'origine Books
et aux pages de la nouvelle table sans Text
.
Table _ Books
d'origine:
Nouvelle table BookUniqueTokens
Ainsi, toutes les pages + (2 pages supplémentaires?) Sont lues à partir de l'index clusterisé.
Pourquoi y a-t-il une différence et pourquoi la différence n'est-elle pas plus grande? Après tout, la différence de taille de données est énorme (données Lob <> pas de données Lob)
Books
Espace de données
Espace de données BooksWithText
La raison en est ROW_OVERFLOW_DATA .
Lorsque les données dépassent 8 Ko, les données sont stockées en tant que ROW_OVERFLOW_DATA sur différentes pages.
Ok, si les données lob sont stockées sur différentes pages, pourquoi les tailles de page de ces deux index clusterisés ne sont-elles pas les mêmes?
En raison du pointeur de 24 octets ajouté à l'index clusterisé pour suivre chacune de ces pages. Après tout, le serveur sql doit savoir où il peut trouver les données lob.
Il a affirmé que la colonne [Texte] affecte toute requête de sélection sur la colonne UniqueToken.
Et
L'existence de la colonne Texte affecte-t-elle les performances de cette requête? Si oui, comment pouvons-nous l'optimiser?
Si les données stockées sont en réalité des données Lob et que la requête fournie dans la réponse est utilisée
Il apporte des frais généraux en raison des pointeurs de 24 octets.
Selon que les exécutions/min ne sont pas folles, je dirais que c'est négligeable, même avec 100 000 enregistrements.
N'oubliez pas que cette surcharge ne se produit que si un index comprenant Text
est utilisé, tel que l'index cluster.
Mais, que se passe-t-il si l'analyse d'index cluster est utilisée et que les données lob ne dépassent pas 8 Ko?
Si les données ne dépassent pas 8 Ko et que vous n'avez pas d'index sur UniqueToken
, la surcharge pourrait être plus importante. même si vous ne sélectionnez pas la colonne Text
.
Logical lit sur 10 000 enregistrements lorsque le texte ne comporte que 137 caractères (tous les enregistrements):
Tableau "Livres2". Nombre de numérisations 1, lectures logiques 419
En raison de toutes ces données supplémentaires se trouvant sur les pages d'index en cluster.
Encore une fois, un index sur UniqueToken
(sans inclure la colonne Text
) résoudra ce problème.
Comme l'a souligné @David Browne - Microsoft, vous pouvez également stocker les données hors ligne, afin de ne pas ajouter cette surcharge sur l'index clusterisé lorsque vous ne sélectionnez pas cette colonne de texte.
De plus, si vous souhaitez que le texte soit stocké hors ligne, vous pouvez le forcer sans utiliser de tableau séparé. Définissez simplement l'option "types de valeur importants hors ligne" avec sp_tableoption. docs.Microsoft.com/en-us/sql/relational-databases
TL; DR
En fonction de la requête fournie, l'indexation de UniqueToken
sans inclure TEXT
devrait résoudre vos problèmes. De plus, j'utiliserais une table temporaire ou un type de table pour effectuer le filtrage au lieu de l'instruction IN
.
MODIFIER:
oui il y a un index non cluster sur UniqueToken
Votre exemple de requête ne touche pas la colonne Text
et, sur la base de la requête, il doit s'agir d'un index de couverture.
Si nous testons cela sur les trois tables que nous avons utilisées précédemment (UniqueToken
+ données Lob, uniquement UniqueToken
, UniqueToken
+ 137 données Char dans la colonne nvarchar (max))
CREATE INDEX [IX_Books_UniqueToken] ON Books(UniqueToken);
CREATE INDEX [IX_BookUniqueTokens_UniqueToken] ON BookUniqueTokens(UniqueToken);
CREATE INDEX [IX_Books2_UniqueToken] ON Books2(UniqueToken);
Les lectures restent les mêmes pour ces trois tables, car l'index non cluster est utilisé.
Table 'Books'. Scan count 8, logical reads 16, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'BookUniqueTokens'. Scan count 8, logical reads 16, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Books2'. Scan count 8, logical reads 16, physical reads 5, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Détails supplémentaires
par @David Browne - Microsoft
De plus, si vous souhaitez que le texte soit stocké hors ligne, vous pouvez le forcer sans utiliser de tableau séparé. Définissez simplement l'option "types de valeur importants hors ligne" avec sp_tableoption. docs.Microsoft.com/en-us/sql/relational-databases/
N'oubliez pas que vous devez reconstruire vos index pour que cela prenne effet sur les données déjà remplies.
Par @Erik Darling
Sur
Le filtrage sur les données Lob est nul.
Vos allocations de mémoire peuvent passer par le toit lorsque vous utilisez des types de données plus gros, ce qui affecte les performances.
Techniquement parlant, tout ce qui occupe plus d'espace sur une page de données, de sorte que les données nécessitent plus de pages de données, diminuera les performances, même si elles sont si petites qu'elles ne peuvent pas être facilement mesurées. Mais plus de pages de données signifient plus d'opérations nécessaires pour lire plus de pages, et plus de mémoire requise pour contenir plus de pages de données, etc.
Ainsi, si vous analysez un segment de mémoire ou un index, la présence d'une colonne NVARCHAR(MAX)
peut affecter les performances, même si vous ne sélectionnez pas il. Par exemple, si vous disposez de 5 000 à 7 000 octets par ligne, alors dans le schéma présenté dans la question, ce serait stocké en ligne, créant ainsi le besoin de plus de pages de données. Pourtant, 8100 octets (environ) ou plus garantiront que les données sont stockées hors ligne avec juste un pointeur vers la ou les pages LOB, donc ce ne serait pas si mal.
Mais, dans votre cas, puisque vous avez mentionné avoir un index non clusterisé sur UniqueToken
, alors cela ne devrait vraiment pas avoir autant d'importance (ou même pas du tout), s'il y a une NVARCHAR(MAX)
colonne de 5 000 à 7 000 octets (provoquant 1 page par ligne) car la requête doit rechercher l'index qui ne contient que les colonnes Id
et UniqueToken
. Et, l'opération devrait faire une recherche au lieu d'une analyse, donc ne pas lire toutes les pages de données de l'index.
Considération finale: à moins que vous n'ayez un matériel vraiment ancien (c'est-à-dire pas de RAM et/ou autres processus monopolisant le disque/CPU/RAM, auquel cas la plupart des requêtes seraient affectées, pas seulement celle-ci), 100 000 lignes, ce n'est pas beaucoup de lignes. En fait, ce n'est même pas près de beaucoup de lignes. 1 million de lignes ne serait même pas beaucoup de lignes pour faire une énorme différence ici.
Donc, en supposant que votre requête utilise effectivement l'index non cluster, je pense que nous devrions chercher quelque part en dehors de la colonne NVARCHAR(MAX)
pour le problème. Cela ne veut pas dire que parfois diviser une table en deux tables n'est pas le meilleur choix, il est juste douteux que cela aiderait ici étant donné les informations fournies .
Les trois points que je voudrais examiner pour améliorer sont:
Noms de schémas explicites: Ceci est mineur, mais toujours toujours préfixe les objets basés sur le schéma avec leur nom de schéma. Vous devez donc utiliser dbo.Books
Au lieu de simplement Books
. Non seulement cela aidera dans les cas où plusieurs schémas sont utilisés et différents utilisateurs ont des schémas par défaut différents, mais cela réduit également certains verrouillages qui se produisent lorsque le schéma n'est pas explicitement indiqué et que SQL Server doit le vérifier à quelques endroits.
La liste IN
: Celles-ci sont pratiques, mais ne sont pas connues pour leur évolutivité. IN
les listes se développent en une condition OR
pour chaque élément de la liste. Sens:
where UniqueToken in
(
'first unique token',
'second unique token'
-- 10,000 items here
)
devient:
where UniqueToken = 'first unique token'
OR UniqueToken = 'second unique token'
-- 10,000 items here (9,998 more OR conditions)
Lorsque vous ajoutez d'autres éléments à la liste, vous obtenez davantage de conditions OR
.
Au lieu de créer dynamiquement une liste IN
, créez une table temporaire locale et créez la liste des instructions INSERT
. En outre, enveloppez-les tous dans une transaction pour éviter les frais généraux de transaction qui se produiraient autrement pour chaque INSERT
(réduisant ainsi 10 000 transactions à 1):
CREATE TABLE #UniqueTokens
(
UniqueToken VARCHAR(100) NOT NULL
COLLATE Latin1_General_100_BIN2
PRIMARY KEY
);
BEGIN TRAN;
..dynamically generated INSERT INTO #UniqueTokens (UniqueToken) VALUES ('...');
COMMIT TRAN;
Maintenant que cette liste est chargée, vous pouvez l'utiliser comme suit pour obtenir le même ensemble de jetons en double:
SELECT bk.[UniqueToken]
FROM dbo.Books bk
INNER JOIN #UniqueTokens tmp
ON tmp.[UniqueToken] = bk.[UniqueToken];
Ou, étant donné que vous voulez savoir laquelle des 10 000 nouvelles entrées vous pouvez charger, vous voulez vraiment la liste des non - jetons en double afin que vous puissiez insérer ceux-ci, non? Dans ce cas, vous feriez ce qui suit:
SELECT tmp.[UniqueToken]
FROM #UniqueTokens tmp
WHERE NOT EXISTS(SELECT *
FROM dbo.Books bk
WHERE bk.[UniqueToken] = tmp.[UniqueToken]);
Comparaison de chaînes: S'il n'y a pas de besoin spécifique de comparaisons insensibles à la casse et/ou insensibles à l'accent en ce qui concerne UniqueToken
, et en supposant que la base de données dans laquelle vous avez créé cette table (qui n'utilise pas la clause COLLATE
pour la colonne [UniqueToken]
) n'a pas de classement binaire par défaut, vous pouvez alors améliorer les performances de la correspondance des valeurs UniqueToken
en utilisant une comparaison binaire . Les comparaisons non binaires doivent créer une clé de tri pour chaque valeur, et cette clé de tri est basée sur des règles linguistiques pour une culture spécifique (par exemple Latin1_General
, French
, Hebrew
, Syriac
, etc.). C'est beaucoup de traitement supplémentaire si les valeurs doivent simplement être exactement identiques. Faites donc ce qui suit:
UniqueToken
UniqueToken
pour qu'elle soit VARCHAR(100) NOT NULL COLLATE Latin1_General_100_BIN2
(comme dans le tableau temporaire illustré ci-dessus)UniqueToken