web-dev-qa-db-fra.com

Optimisation de la jointure sur une grande table

J'essaie de convaincre certaines performances supplémentaires 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 à l'envers, 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 comprend valeur.

Cependant, lorsque je fais cela, 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 importants 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 se présente actuellement comme 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é allant 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 ma prochaine ligne de conduite. 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 sur 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 DBA précédent qui a insisté pour que toutes les tables partout aient un GUID, sans exception .

10
Quick Joe Smith

Votre ix_hugetable Semble tout à fait inutile car:

  • it is l'index clusterisé (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 remplacer la clé en cluster par (added, fk, id) Et supprimez 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 peut consister à essayer le conseil FORCE ORDER avec des manières boh de l'ordre des tables et aucun conseil JOIN/INDEX. J'essaie de ne pas utiliser personnellement les conseils JOIN/INDEX car vous supprimez les options de l'optimiseur. Il y a plusieurs 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, elle est identique à COUNT(*) sémantiquement. Mais pour SUM, il a besoin de la valeur réelle, pas existence.

Par exemple, si vous changez COUNT(value) en COUNT(DISTINCT value)sans en changeant l'index, il devrait à nouveau interrompre la requête car il doit traiter la valeur comme une valeur, pas comme 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.

5
gbn

Définissez un index sur hugetable uniquement sur la colonne added.

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.

2
Bohemian

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êtes à 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 . A en juger par le sp_spaceused la 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.

2
Will A

Votre index est incorrect. Voir index dos et donts .

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 désordre avec l'énorme.

Essayez d'ajouter un index cluster sur hugetable(added, fk). Cela devrait obliger le planificateur à rechercher les lignes applicables de la grande table et à imbriquer la boucle ou à les fusionner avec la petite table.

1