J'essaie d'amadouer des performances supplémentaires à partir d'une requête qui accède à une table avec environ 250 millions d'enregistrements. D'après ma lecture du plan d'exécution réel (non estimé), le premier goulot d'étranglement est une requête qui ressemble à ceci:
select
b.stuff,
a.added,
a.value
from
dbo.hugetable a
inner join
#smalltable b on a.fk = b.pk
where
a.added between @start and @end;
Voir plus bas pour les définitions des tables et index impliqués.
Le plan d'exécution indique qu'une boucle imbriquée est utilisée sur #smalltable et que l'analyse d'index sur hugetable est exécutée 480 fois (pour chaque ligne de #smalltable). Cela me semble en arrière, j'ai donc essayé de forcer une jointure de fusion à utiliser à la place:
select
b.stuff,
a.added,
a.value
from
dbo.hugetable a with(index = ix_hugetable)
inner merge join
#smalltable b with(index(1)) on a.fk = b.pk
where
a.added between @start and @end;
L'index en question (voir ci-dessous pour la définition complète) couvre les colonnes fk (le prédicat de jointure), ajouté (utilisé dans la clause where) & id (inutile) dans l'ordre croissant et inclut la valeur .
Lorsque je fais cela, cependant, la requête passe de 2 1/2 minutes à plus de 9. J'aurais espéré que les indications forceraient une jointure plus efficace qui ne ferait qu'un seul passage sur chaque table, mais clairement pas.
Toute orientation est la bienvenue. Informations supplémentaires fournies si nécessaire.
Mise à jour (2011/06/02)
Après avoir réorganisé l'indexation sur la table, j'ai réalisé des progrès significatifs en matière de performances, mais j'ai rencontré un nouvel obstacle en ce qui concerne la synthèse des données dans l'énorme table. Le résultat est un résumé par mois, qui ressemble actuellement à ce qui suit:
select
b.stuff,
datediff(month, 0, a.added),
count(a.value),
sum(case when a.value > 0 else 1 end) -- this triples the running time!
from
dbo.hugetable a
inner join
#smalltable b on a.fk = b.pk
group by
b.stuff,
datediff(month, 0, a.added);
À l'heure actuelle, hugetable a un index clusterisé pk_hugetable (added, fk)
(la clé primaire) et un index non clusterisé dans l'autre sens ix_hugetable (fk, added)
.
Sans la 4e colonne ci-dessus, l'optimiseur utilise une jointure de boucle imbriquée comme auparavant, en utilisant #smalltable comme entrée externe, et une recherche d'index non groupée comme boucle interne (exécutant à nouveau 480 fois). Ce qui me préoccupe, c'est la disparité entre les lignes estimées (12 958,4) et les lignes réelles (74 668 468). Le coût relatif de ces recherches est de 45%. Le temps de course est cependant inférieur à une minute.
Avec la 4ème colonne, le temps de course passe à 4 minutes. Il recherche cette fois sur l'index clusterisé (2 exécutions) pour le même coût relatif (45%), agrège via une correspondance de hachage (30%), puis effectue une jointure de hachage sur #smalltable (0%).
Je ne suis pas sûr de mon prochain plan d'action. Ma préoccupation est que ni la recherche de plage de dates ni le prédicat de jointure ne sont garantis ou même tout ce qui est susceptible de réduire considérablement l'ensemble de résultats. Dans la plupart des cas, la plage de dates ne coupera que 10 à 15% des enregistrements, et la jointure interne sur fk peut filtrer peut-être 20 à 30%.
Comme l'a demandé Will A, les résultats de sp_spaceused
:
name | rows | reserved | data | index_size | unused
hugetable | 261774373 | 93552920 KB | 18373816 KB | 75167432 KB | 11672 KB
#smalltable est défini comme:
create table #endpoints (
pk uniqueidentifier primary key clustered,
stuff varchar(6) null
);
Alors que dbo.hugetable est défini comme:
create table dbo.hugetable (
id uniqueidentifier not null,
fk uniqueidentifier not null,
added datetime not null,
value decimal(13, 3) not null,
constraint pk_hugetable primary key clustered (
fk asc,
added asc,
id asc
)
with (
pad_index = off, statistics_norecompute = off,
ignore_dup_key = off, allow_row_locks = on,
allow_page_locks = on
)
on [primary]
)
on [primary];
Avec l'index suivant défini:
create nonclustered index ix_hugetable on dbo.hugetable (
fk asc, added asc, id asc
) include(value) with (
pad_index = off, statistics_norecompute = off,
sort_in_tempdb = off, ignore_dup_key = off,
drop_existing = off, online = off,
allow_row_locks = on, allow_page_locks = on
)
on [primary];
Le champ id est redondant, un artefact d'un ancien DBA qui a insisté pour que toutes les tables partout aient un GUID, sans exception.
la source
Réponses:
Votre
ix_hugetable
look est tout à fait inutile car:De plus: - ajouté ou fk doit être le premier - ID est le premier = pas beaucoup d'utilisation
Essayez de changer la clé en cluster
(added, fk, id)
et de la supprimerix_hugetable
. Vous avez déjà essayé(fk, added, id)
. Si rien d'autre, vous économiserez beaucoup d'espace disque et la maintenance des indexUne autre option pourrait être d'essayer l'indicateur FORCE ORDER avec des manières boh de l'ordre des tables et aucun indice JOIN / INDEX. J'essaie de ne pas utiliser personnellement les astuces JOIN / INDEX car vous supprimez les options de l'optimiseur. Il y a de nombreuses années, on m'a dit (séminaire avec un gourou SQL) que l'indication FORCE ORDER peut aider lorsque vous avez une grande table JOIN petite table: YMMV 7 ans plus tard ...
Oh, et dites-nous où réside le DBA afin que nous puissions organiser un ajustement de la percussion
Modifier, après la mise à jour du 02 juin
La 4e colonne ne fait pas partie de l'index non cluster, elle utilise donc l'index cluster.
Essayez de changer l'index NC pour INCLURE la colonne de valeur afin qu'il n'ait pas à accéder à la colonne de valeur pour l'index clusterisé
Remarque: Si la valeur n'est pas nullable, c'est la même chose que
COUNT(*)
sémantiquement. Mais pour SUM, il faut la valeur réelle , pas l' existence .Par exemple, si vous passez
COUNT(value)
àCOUNT(DISTINCT value)
sans modifier l'index, il doit à nouveau interrompre la requête car il doit traiter la valeur en tant que valeur et non en tant qu'existence.La requête nécessite 3 colonnes: ajoutée, fk, valeur. Les 2 premiers sont filtrés / joints, tout comme les colonnes clés. la valeur est juste utilisée et peut donc être incluse. Utilisation classique d'un indice de couverture.
la source
Définissez un index
hugetable
sur juste laadded
colonne.Les bases de données utiliseront un index en plusieurs parties (plusieurs colonnes) uniquement à l'extrême droite de la liste des colonnes car elles ont des valeurs comptées à partir de la gauche. Votre requête ne spécifie pas
fk
dans la clause where de la première requête, elle ignore donc l'index.la source
C'est l'ordre que j'attendrais de l'optimiseur de requête à utiliser, en supposant qu'une boucle se joint au bon choix. L'alternative est de boucler 250 millions de fois et d'effectuer une recherche dans la table #temp à chaque fois - ce qui pourrait bien prendre des heures / jours.
L'index que vous forcez à utiliser dans la jointure MERGE est à peu près 250 millions de lignes * «la taille de chaque ligne» - pas petit, au moins quelques Go. À en juger par la
sp_spaceused
sortie «quelques Go» pourrait être un euphémisme - la jointure MERGE nécessite que vous parcouriez l'index qui va être très intensif en E / S.la source
Votre index est incorrect. Voir les index dos et ne pas faire .
Dans l'état actuel des choses, votre seul index utile est celui de la clé primaire de la petite table. Le seul plan raisonnable est donc de seq balayer la petite table et d'emboîter en boucle le gâchis avec l'énorme.
Essayez d'ajouter un index clusterisé sur
hugetable(added, fk)
. Cela devrait obliger le planificateur à rechercher les lignes applicables de l'énorme table, et à imbriquer la boucle ou à les fusionner avec la petite table.la source