Dans ma candidature, plusieurs fois, je dois montrer des résultats qui sont paginés et triés par certains champs.
Par exemple, une simple liste d'utilisateurs triée par nom de famille. À cause de cela et parce que j'ai aussi une suppression logique et c'est une application multi-location, j'utilise généralement un index en clusters comme celui-ci:
CREATE CLUSTERED INDEX [idx] ON [Users]
(
IsDeleted ASC,
[AccountId] ASC,
[LastName] ASC
)
Cela signifie qu'une requête comme paginé comme SELECT TOP(20) * FROM Users WHERE IsDeleted = 0 AND AccountId = xxx
est triée par nom de famille. Je sais que ce n'est pas garanti d'être trié, mais dans la pratique, c'est toujours.
Cependant, lire ici Kimberly Tripp Publication de blog sur index en cluster , elle dit que c'est une idée horrible de le faire comme ça. Et c'est encore pire parce que le champ isdeletted (bit) ne me permet pas de régler le
Toutefois, si je modifie l'index en cluster sur seulement l'ID unique, je devrais commencer à utiliser ORDER BY LastName
, ce qui est très lent.
Ma table comporte quelques millions d'enregistrements (dizaines de millions au plus) et il est utilisé en général comme suit:
IsDeleted = 0, AccountId = xxxx
(seules données non supprimées pour un seul compte sont mises à jour en vrac).Question:
Quel serait l'index recommandé (et comment trier) pour ce type de tables?
n autre exemple pour Ces types de tables serait un tableau des résultats de l'enquête qui possède les colonnes suivantes IsDeleted (BIT), AccountId (FK GUID), UserId (FK GUID), QuestionKey (NVARCHAR), AnswerValue (TEXT)
, où ma clé en cluster serait probablement (IsDeleted, AccountId, UserId, QuestionKey)
et 99% du temps, je interrogerais la table ou la mise à jour en vrac par les 3 premiers champs
WHERE IsDeleted = 0
AND AccountId = xxx
AND UserId = yyy
ou même les 4 champs: ... AND QuestionKey = 'country'
EDIT:
L'une des principales raisons pour lesquelles j'ai fait cela est que les mises à jour en vrac et les requêtes sont toujours limitées à 1 ou à un petit nombre de pages. Avoir une colonne Identity
nécessiterait les requêtes et les mises à jour de lire/écrire dans la plupart des pages.
Edit 2:
Exemple de Joe'Blard:
Cette requête:
SELECT TOP (20) *
FROM Users2
WHERE IsDeleted = 0
AND AccountId = '46FC5693-7446-415A-8626-8937365460D1'
ORDER BY [LastName];
(IsDeleted, AccountId, LastName)
Résultats à cette:Temps CPU = 3 ms, temps écoulé = 3 ms.
Table 'Utilisateurs2'. Numérisation Nombre 1, Lectures logiques 5, Lectures physiques 4, Read-aveugle Reads 0, LOB Logique Lit 0, LOB Physical Reads 0, LOB Read-avance lit 0.
(IsDeleted, AccountId, LastName)
non regroupé à ce sujet:Temps CPU = 16 ms, temps écoulé = 18 ms.
Table 'Utilisateurs2'. Numérisation Nombre 1, Lectures logiques 533, Lectures physiques 5, Read-aveugle Reads 1240, LOB Logique Lit 0, LOB Physical Reads 0, LOB Read-aveugle lit 0.
Notez le IO et heure. C'est bien plus lent et nécessite plus d'IO si les données ne sont pas stockées ensemble. Cela pourrait prendre plus d'espace, mais la différence de vitesse est notable. Ai-je tort de le faire comme ça?
Je dois d'abord réitérer quoi RDFOZZ dit dans sa réponse à propos d'une explicite ORDER BY
. Si SQL Server effectue un Scan d'ordre d'allocation Vous pouvez obtenir les mauvais résultats. Comprenant ORDER BY
Dans la requête ne provoque pas de succès. Pourquoi ne pas le faire?
À partir d'une perspective de performance de la requête, l'indice que vous souhaitez dépend du nombre de rangées que vous revenez à la fois et du nombre de colonnes nécessaires à la table.
D'abord, je vais jeter environ 6,5 millions de rangées dans une table avec six locataires:
CREATE TABLE dbo.Users2 (
IsDeleted Bit NOT NULL,
[AccountId] UNIQUEIDENTIFIER NOT NULL,
[LastName] NVARCHAR(50) NOT NULL,
[UsefulColumn] NVARCHAR(20) NOT NULL,
[OtherColumns] NVARCHAR(100) NOT NULL
);
CREATE CLUSTERED INDEX [idx] ON [Users2]
(
IsDeleted ASC,
[AccountId] ASC,
[LastName] ASC
);
CREATE TABLE #ids (id INT NOT NULL IDENTITY (0, 1), [AccountId] UNIQUEIDENTIFIER NOT NULL);
INSERT INTO #ids
SELECT TOP 6 NEWID()
FROM master..spt_values;
INSERT INTO [Users2] WITH (TABLOCK)
SELECT
CASE WHEN t1.number % 10 = 1 THEN 1 ELSE 0 END
, #ids.[AccountId]
, LEFT(REPLACE(CONVERT(NVARCHAR(50), NEWID()), '-', ''), 12)
, REPLICATE(N'Z', 20)
, REPLICATE(N'Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
LEFT OUTER JOIN #ids ON ABS(t1.number % 6) = #ids.id;
DROP TABLE #ids;
-- get an ID: FFA7D6D8-63E8-422B-B5E7-F7020871CDB4
SELECT TOP 1 [AccountId] FROM Users2
WHERE IsDeleted = 0
ORDER BY [AccountId] DESC;
Lors de la query similaire à la vôtre:
SELECT TOP (20) *
FROM Users2
WHERE IsDeleted = 0
AND AccountId = 'FFA7D6D8-63E8-422B-B5E7-F7020871CDB4'
ORDER BY [LastName];
Je reçois un indice en clustere recherché comme prévu:
Utilise l'index en cluster la meilleure option? Ça dépend. Si vous n'avez pas besoin de sélectionner toutes les colonnes dans la table, vous pouvez définir un index de revêtement plus petit qui renvoie les données dont vous avez besoin sans tri explicite. Avoir un index de revêtement plus petit est bon pour la performance que vous avez plus loin dans les données. Supposons que vous n'ayez besoin que de UsefulColumn
et non de la colonne OtherColumns
. Vous pouvez définir l'index suivant:
CREATE NONCLUSTERED INDEX [idx_1] ON [Users2]
(
[AccountId] ASC,
[LastName] ASC
)
INCLUDE ([UsefulColumn])
WHERE IsDeleted = 0;
C'est assez grand. Pour mon cas de test, il s'agit d'environ 28% de la taille des données. Pour cet indice, il est important de noter que la modification de la clé en cluster de la table n'aura pas un impact important sur sa taille. SQL Server stocke les colonnes de touches en cluster dans le nœud feuille de l'index, à moins qu'elles ne soient déjà incluses dans l'index. Cela peut être démontré avec un test simple:
CREATE TABLE dbo.IX_TEST (
COL1 BIGINT NOT NULL,
COL2 BIGINT NOT NULL,
FILLER VARCHAR(6) NOT NULL,
PRIMARY KEY (COL1)
);
INSERT INTO dbo.IX_TEST WITH (TABLOCK)
SELECT TOP (1000000)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, REPLICATE('Z', 6)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
EXEC sp_spaceused 'IX_TEST'; -- 96 KB
CREATE INDEX COL1 ON dbo.IX_TEST (COL1)
EXEC sp_spaceused 'IX_TEST'; -- 14032 KB
CREATE INDEX COL2 ON dbo.IX_TEST (COL2)
EXEC sp_spaceused 'IX_TEST'; --- 35920 KB
Retour à notre table, si j'ai créé un index sur juste la colonne UsefulColumn
, il y aurait une surcharge (sans compression) d'environ 1 octet pour la colonne IsDeleted
, 16 octets pour le AccountId
, 2 * la longueur moyenne de la colonne LastName
et 0 ou 4 octets pour l'uniquificateur interne (ne se produit que pour des deux noms de famille en double). Pour mes données de test, un peu de frais généraux:
1 + 16 + 2 * 12 + 0 = 41 octets
Cependant pour le idx_1
Index I Défini ci-dessus, il ne figure que 1 octets (1 pour IsDeleted
et 0 pour l'uniquificateur, en supposant de ne pas avoir de très nombreux noms de famille). L'index est principalement important car j'utilise des colonnes larges comme colonnes clés. N'oubliez pas que les index auront la même taille avec votre clé de clustering actuelle, mais la modification de la clé de clustering de la table en un ensemble plus mince de colonnes diminuera considérablement la taille de l'index uniquement défini sur UsefulColumn
.
Pour toute valeur TOP
, je devrais obtenir un bel index de recherche sur l'index de couverture. Cette requête:
SELECT TOP (200000) [LastName], [UsefulColumn]
FROM Users2
WHERE IsDeleted = 0 AND AccountId = 'FFA7D6D8-63E8-422B-B5E7-F7020871CDB4'
ORDER BY [LastName];
A le plan suivant:
Même si vous avez besoin de chaque colonne de la table, l'index défini ci-dessus peut toujours donner une bonne performance. Vous obtiendrez une recherche clé pour chaque rangée que vous revenez. Pour un seul utilisateur final, effectuez une recherche clé sur 20 rangées, cela ne devrait pas être notable. Dans votre test, vous avez vu des temps d'exécution de 3 ms et 18 ms. Cependant, si vous avez une charge de travail très concurrente, cela pourrait faire la différence. Seulement vous pouvez évaluer cela correctement.
Même sans les mises en garde ci-dessus, lors de la sélection de nombreuses lignes, vous remarquerez peut-être une grande différence de performance:
La première requête a un indice d'index pour forcer l'utilisation de l'index. L'optimiseur de requêtes pense que le plan sera beaucoup plus coûteux que le plan qui utilise l'index en cluster.
Si je devais deviner, je dirais que vous n'avez probablement pas besoin d'utiliser un index en cluster pour effectuer votre pagination. Si vous avez beaucoup d'index sur la table, vous pouvez bénéficier de la création d'un nouvel indice de couverture pour vos requêtes de pagination et en définissant l'index en cluster sur un jeu de colonnes plus petit et unique.
Comme cette question concerne un système multi-locataires, veuillez consulter la réponse plus détaillée que j'ai fournie à cette question DBA.SE:
clé primaire composite dans la base de données SQL Server multi-locataire
Mais quelques notes rapides:
Il est probablement préférable de non avoir une colonne de l'indice en clustered pouvant modifier la valeur due si vous avez des index non clusters (et si cet index en cluster est un PK qui a des fks pointant dessus). Étant donné que les colonnes de clés d'index en cluster sont copiées dans tous les index non clusters, la modification de la valeur dans l'une de ces colonnes nécessite une opération d'écriture sur tous les index non clusters sur ces tables (sans parler de l'effet d'ondulation si c'est un PK qui a Fks pointant dessus car ils auraient également besoin d'être mis à jour). Non seulement c'est un problème pour les E/S, mais cela signifie tirer dans des pages de données pour des index dans le pool tampon (c'est-à-dire la mémoire) que vous n'avez peut-être pas même utiliser. Cela augmente également la portée du blocage et des blocages car de plus en plus d'éléments seront verrouillés lors de cette opération.
Pour les multi-locataires, je préfère utiliser le TenantID
d'abord puisque toutes vos requêtes d'application devraient avoir cette colonne dans la clause WHERE. Seules les requêtes qui ne constitueraient pas la maintenance de la Back-terminale ou la société interne (c'est-à-dire des rapports de Cross-Client) et vous pouvez faire appel à des requêtes légèrement plus lentes là-bas OR créer un indice non clusterné pour traiter ceux-ci.
Si vous n'avez pas de petit nombre de colonnes non changeantes pour créer un index clustered unique/pk, ajoutez une colonne IDENTITY
colonne de type INT
ou si vous avez besoin d'être, BIGINT
. Rarement est là un besoin d'utiliser UNIQUEIDENTIFIER
pour de telles choses.
Premièrement: les raisons pour lesquelles une large index en cluster est une mauvaise idée est la suivante:
Donc, dans des termes purement pratiques, peut-être la première chose à poser est-ce un problème pour vous?
Bien qu'il soit toujours préférable de résoudre des problèmes potentiels avant de devenir des problèmes réels, je suppose que votre DB n'est pas une nouvelle. Si vous n'êtes pas attendu une croissance significative et que tout fonctionne correctement aujourd'hui; Vous n'exécutez pas dans une situation où l'espace disque est serré et que l'augmentation est coûteuse; Et, si vous vous attendez à une refonte de causer des problèmes de performance, envisagez si une modification est la meilleure solution à ce moment-là.
S'il y a d'autres conducteurs à changer, surtout tout ce qui permettrait d'effort de refactorisation général, puis expérimentalez des alternatives est une tâche logique et devrait probablement être faite. Cependant, si le seulement Motivation de changement est "le faire de cette façon est mauvais", alors (quelle que soit la correction de la déclaration), vous risquez d'investir beaucoup de temps et d'efforts dans la fabrication Un changement qui n'a aucun avantage visible évident. À moins que vous soyez le patron à ce sujet, votre patron est susceptible de penser que c'est une mauvaise idée.
Cela de côté: votre principale préoccupation concernant le changement est que vous utilisez actuellement des indices en cluster pour trier vos données dans l'ordre le plus pratique. Et, que vous enregistrez actuellement (une quantité notable de) temps, car vous ne mettez pas de ORDER BY
sur vos requêtes et comptez sur les index en cluster pour renvoyer les données dans le bon ordre, car c'est comme ça que c'est comment stocké. Et jusqu'à présent, cela fonctionne pour vous.
Premièrement, notez que cela pourrait changer. Je n'ai pas trouvé de page Microsoft réelle qui dit: "Les données renvoyées à partir d'une table avec un index en clustered peuvent être renvoyées dans une autre chose que l'ordre d'index en cluster," mais j'ai trouvé multiplePages qui expliquent que [~ # ~ ~] pas [~ # ~] Utiliser un ORDER BY
signifie qu'il n'y a pas de garantie de l'ordre de vos données, index en cluster ou pas. Actuellement, le moteur SQL Server peut optimiser sa requête d'une manière qui arrive à renvoyer ses données dans la commande triée; Modifications de vos données (un grand afflux de nouvelles données), votre serveur (déplacement de votre dB sur un serveur avec plus de CPU) ou vos routines de maintenance (la reconstruction de l'indice en cluster plus ou moins fréquemment) pourraient entraîner une modification de ce comportement. .
Cependant, j'ai aussi trouvé n article qui peut fournir un conseil de savoir pourquoi ORDER BY LastName
fonctionne si mal pour vous. À première vue, cela peut sembler comme s'il soutient ce que vous faites maintenant. Il ne mentionne pas spécifiquement qu'un index en cluster peut aider à trier les performances avec une clause ORDER BY
.
Cependant, ORDER BY LastName
ne correspond pas à la commande de votre index en cluster. Je vous encourage à essayer d'ajouter ORDER BY Is_Deleted, AccountID, LastName
à votre requête existante. Puisque le ORDER BY
correspond à l'index, y compris, il ne devrait pas imposer une pénalité notable sur la requête. Je me trompe peut-être - l'optimiseur peut reconnaître que Is_Deleted
_ et AccountID
_ _ _ _ sera le même pour toutes les lignes retournées et ont utilisé l'ordre d'index dans vos tests passés, mais le meilleur moyen de Garantie L'utilisation d'un index consiste à vous assurer que vous incluez toutes les colonnes, ce qui peut être vrai ici aussi.
Étape suivante - Si vous souhaitez vraiment envisager de modifier votre index en cluster sur un plus étroit, j'essayais cela et voyez comment il fonctionne pour vous (dans votre système de test, bien sûr). Modifier l'index en cluster (en UserId
, je vais assumer0 et ajouter un index non clusterné avec la définition de l'actuelle en clustere. Testez la même requête - celle où le ORDER BY
Correspond aux colonnes (et la direction de tri, qui est importante) du nouvel index. Voyez maintenant si votre requête fonctionne bien ou non.
Les résultats dépendront probablement de la façon dont vous faites la pagination. Si cela est finalement intégré à la requête (pour SQL 2012 et plus tard, la méthode OFFSET
/FETCH
méthode obtient des critiques raves), alors il y a de bonnes chances que l'optimiseur se rendra compte que cela va être plus rapide Trouvez les bonnes lignes avec l'indice non clusterné qu'avec une autre méthode et rechercherez les données restantes sur les 20-30 lignes que vous souhaitez réellement quand elles trouvent lesquelles elles sont.
Si la requête n'utilise pas l'index, vous pouvez utiliser des indices de table pour essayer de le forcer à et voir si cela fonctionne mieux que celui de l'optimiseur, cependant, remplace l'appel de l'optimiseur l'un de la meilleure façon de ramener vos données. Cela devrait être fait avec soin, car il semble que vos données changent ....
Vous voudrez également voir l'impact réel sur vos mises à jour en vrac. Cependant, dans la plupart des cas, les requêtes que l'utilisateur doit attendre sont plus critiques que celles qui arrivent des heures d'heure, du temps sage; Si ce ne sont pas aussi rapides, à moins qu'ils forcent votre travail nocturne normal à pousser au-delà de ses fenêtres disponibles, c'est généralement OK.