Optimisation de la jointure sur une grande table

10

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.

Quick Joe Smith
la source
Pourriez-vous inclure le résultat de sp_spaceused 'dbo.hugetable', s'il vous plaît?
Will A
Terminé, ajouté juste au-dessus du début des définitions de table.
Quick Joe Smith,
Tout à fait. Sa taille ridicule est la raison pour laquelle je m'intéresse à cela.
Quick Joe Smith

Réponses:

5

Votre ix_hugetablelook est tout à fait inutile car:

  • il est l'index cluster (PK)
  • INCLUDE ne fait aucune différence car un index clusterisé INCLUT toutes les colonnes non clés (valeurs non clés à la feuille la plus basse = INCLUDEd = ce qu'est un index clusterisé)

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 supprimer ix_hugetable. Vous avez déjà essayé (fk, added, id). Si rien d'autre, vous économiserez beaucoup d'espace disque et la maintenance des index

Une 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é

create nonclustered index ix_hugetable on dbo.hugetable (
    fk asc, added asc
) include(value)

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.

gbn
la source
Hah, je pensais que les index cluster et non cluster avaient fk et ajoutés dans un ordre différent. Je ne peux pas croire que je n'ai pas remarqué cela, presque autant que je ne peux pas croire que cela a été configuré de cette façon en premier lieu. Je changerai l'index groupé demain, puis je descendrai la rue pour un café pendant qu'il reconstruit.
Quick Joe Smith
J'ai modifié l'indexation et j'ai eu un bash avec FORCE ORDER dans le but de réduire le nombre de recherches sur la grande table, mais en vain. Ma question a été mise à jour.
Quick Joe Smith
@Quick Joe Smith: mise à jour de ma réponse
gbn
Oui, j'ai essayé ça peu de temps après. Parce que la reconstruction d'index prend tellement de temps, je l'ai oublié et j'ai d'abord pensé que je l'avais accéléré en faisant quelque chose de complètement indépendant.
Quick Joe Smith
2

Définissez un index hugetablesur juste la addedcolonne.

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 fkdans la clause where de la première requête, elle ignore donc l'index.

bohémien
la source
Le plan d'exécution montre que l'index (ix_hugetable) est recherché . Ou dites-vous que cet index n'est pas approprié pour la requête?
Quick Joe Smith
L'index n'est pas approprié. Qui sait comment il "utilise l'index". L'expérience me dit que c'est votre problème. Essayez-le et dites-nous comment ça se passe.
Bohemian
@Quick Joe Smith - avez-vous essayé la suggestion de @ Bohemian? Quels sont les résultats?
Lieven Keersmaekers
2
Je ne suis pas d'accord: la clause ON est d'abord traitée logiquement et est effectivement un WHERE dans la pratique, donc OP doit d'abord essayer les deux colonnes. Aucune indexation sur fk du tout = analyse d'index en cluster ou recherche de clé pour obtenir la valeur fk pour JOIN. Pouvez-vous également ajouter quelques références au comportement que vous avez décrit? Surtout pour SQL Server étant donné que vous avez peu d'historique de réponse à ce SGBDR. En fait, -1 rétrospectivement comme je tape ce commentaire
gbn
2

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).

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_spaceusedsortie «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.

Will A
la source
Ma compréhension est qu'il existe 3 types d'algorithmes de jointure et que la jointure de fusion a les meilleures performances lorsque les deux entrées sont ordonnées par le prédicat de jointure. À tort ou à raison, c'est le résultat que j'essaie d'obtenir.
Quick Joe Smith
2
Mais il y a plus que cela. Si #smalltable avait un grand nombre de lignes, une jointure de fusion peut être appropriée. Si, comme son nom l'indique, elle a un petit nombre de lignes, une jointure en boucle pourrait être le bon choix. Imaginez que #smalltable avait une ou deux lignes et correspondait à une poignée de lignes de l'autre table - il serait difficile de justifier une fusion ici.
Will A
J'ai pensé qu'il y avait plus à cela; Je ne savais tout simplement pas ce que cela pouvait être. L'optimisation de la base de données n'est pas exactement mon point fort, comme vous l'avez probablement déjà deviné.
Quick Joe Smith
@Quick Joe Smith - merci pour le sp_spaceused. 75 Go d'index et 18 Go de données - ix_hugetable n'est-il pas le seul index de la table?
Will A
1
+1 Volonté. Le planificateur fait actuellement ce qu'il faut. Le problème réside dans la recherche aléatoire de disques en raison de la façon dont vos tables sont regroupées.
Denis de Bernardy
1

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.

Denis de Bernardy
la source
Merci pour ce lien. J'essaierai cela quand j'arriverai au travail demain.
Quick Joe Smith