Mise à jour efficace d'une table à l'aide de JOIN


8

J'ai un tableau qui contient les détails des ménages et un autre qui contient les détails de toutes les personnes associées aux ménages. Pour la table des ménages, j'ai une clé primaire définie à l'aide de deux colonnes - [tempId,n]. Pour la table personne, j'ai une clé primaire définie à l'aide de 3 de ses colonnes[tempId,n,sporder]

En utilisant le tri dicté par l'indexation en cluster sur les clés primaires, j'ai généré un ID unique pour chaque enregistrement de ménage [HHID]et de personne [PERID](l'extrait ci-dessous sert à générer PERID]:

 ALTER TABLE dbo.persons
 ADD PERID INT IDENTITY
 CONSTRAINT [UQ dbo.persons HHID] UNIQUE;

Maintenant, ma prochaine étape consiste à associer chaque personne aux ménages correspondants, à savoir; mappez de a [PERID]à a [HHID]. Le croisement entre les deux tables est basé sur les deux colonnes [tempId,n]. Pour cela, j'ai la déclaration de jointure interne suivante.

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n;

J'ai un total de 1928783 dossiers de ménage et 5239842 dossiers de personne. Le temps d'exécution est actuellement très élevé.

Maintenant, mes questions:

  1. Est-il possible d'optimiser davantage cette requête? Plus généralement, quelles sont les règles générales pour optimiser une requête de jointure?
  2. Existe-t-il une autre construction de requête qui peut atteindre le résultat souhaité avec un meilleur temps d'exécution?

J'ai téléchargé le plan d'exécution généré par SQL Server 2008 pour l'ensemble du script sur SQLPerformance.com

Réponses:


19

Je suis à peu près certain que les définitions des tableaux sont proches de ceci:

CREATE TABLE dbo.households
(
    tempId  integer NOT NULL,
    n       integer NOT NULL,
    HHID    integer IDENTITY NOT NULL,

    CONSTRAINT [UQ dbo.households HHID] 
        UNIQUE NONCLUSTERED (HHID),

    CONSTRAINT [PK dbo.households tempId, n]
    PRIMARY KEY CLUSTERED (tempId, n)
);

CREATE TABLE dbo.persons
(
    tempId  integer NOT NULL,
    sporder integer NOT NULL,
    n       integer NOT NULL,
    PERID   integer IDENTITY NOT NULL,
    HHID    integer NOT NULL,

    CONSTRAINT [UQ dbo.persons HHID]
        UNIQUE NONCLUSTERED (PERID),

    CONSTRAINT [PK dbo.persons tempId, n, sporder]
        PRIMARY KEY CLUSTERED (tempId, n, sporder)
);

Je n'ai pas de statistiques pour ces tableaux ou vos données, mais ce qui suit définira au moins la cardinalité du tableau correctement (le nombre de pages est une supposition):

UPDATE STATISTICS dbo.persons 
WITH 
    ROWCOUNT = 5239842, 
    PAGECOUNT = 100000;

UPDATE STATISTICS dbo.households 
WITH 
    ROWCOUNT = 1928783, 
    PAGECOUNT = 25000;

Analyse du plan de requête

La requête que vous avez maintenant est:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n;

Cela génère le plan plutôt inefficace:

Plan par défaut

Les principaux problèmes de ce plan sont la jonction de hachage et le tri. Les deux nécessitent une allocation de mémoire (la jointure de hachage doit créer une table de hachage, et le tri a besoin d'espace pour stocker les lignes pendant le tri). L'explorateur de plans indique que cette requête a obtenu 765 Mo:

Octroi de mémoire

C'est beaucoup de mémoire serveur à consacrer à une seule requête! Plus précisément, cette allocation de mémoire est fixée avant le début de l'exécution en fonction du nombre de lignes et des estimations de taille.

Si la mémoire s'avère insuffisante au moment de l'exécution, au moins certaines données pour le hachage et / ou le tri seront écrites sur le disque tempdb physique . C'est ce qu'on appelle un «déversement» et cela peut être une opération très lente. Vous pouvez suivre ces déversements (dans SQL Server 2008) à l'aide des événements de profilage Hash Warnings et Sort Warnings .

L'estimation de l'entrée de génération de la table de hachage est très bonne:

Entrée de hachage

L'estimation de l'entrée de tri est moins précise:

Trier l'entrée

Vous devrez utiliser Profiler pour vérifier, mais je soupçonne que le tri se répandra dans tempdb dans ce cas. Il est également possible que la table de hachage se répande également, mais cela est moins clair.

Notez que la mémoire réservée à cette requête est divisée entre la table de hachage et le tri, car elles s'exécutent simultanément. La propriété de plan Memory Fractions affiche la quantité relative de l'allocation de mémoire qui devrait être utilisée par chaque opération.

Pourquoi trier et hacher?

Le tri est introduit par l'optimiseur de requête pour garantir que les lignes parviennent à l'opérateur de mise à jour d'index en cluster dans l'ordre des clés en cluster. Cela favorise l'accès séquentiel à la table, ce qui est souvent beaucoup plus efficace que l'accès aléatoire.

La jointure par hachage est un choix moins évident, car ses entrées sont de tailles similaires (en première approximation, de toute façon). La jointure par hachage est préférable lorsqu'une entrée (celle qui construit la table de hachage) est relativement petite.

Dans ce cas, le modèle de calcul des coûts de l'optimiseur détermine que la jointure par hachage est la moins chère des trois options (hachage, fusion, boucles imbriquées).

Amélioration des performances

Le modèle de coût n'est pas toujours correct. Il a tendance à surestimer le coût de la jointure de fusion parallèle, d'autant plus que le nombre de threads augmente. Nous pouvons forcer une jointure de fusion avec un indice de requête:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (MERGE JOIN);

Cela produit un plan qui ne nécessite pas autant de mémoire (car la jointure de fusion n'a pas besoin d'une table de hachage):

Plan de fusion

Le tri problématique est toujours là, car la jointure de fusion conserve uniquement l'ordre de ses clés de jointure (tempId, n) mais les clés en cluster le sont (tempId, n, sporder). Vous pouvez constater que le plan de jointure de fusion ne fonctionne pas mieux que le plan de jointure de hachage.

Joindre les boucles imbriquées

Nous pouvons également essayer une jointure de boucles imbriquées:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (LOOP JOIN);

Le plan de cette requête est le suivant:

Plan de boucles imbriquées en série

Ce plan de requête est considéré comme le pire par le modèle de calcul des coûts de l'optimiseur, mais il possède certaines fonctionnalités très souhaitables. Tout d'abord, la jointure de boucles imbriquées ne nécessite pas d'allocation de mémoire. Deuxièmement, il peut conserver l'ordre des clés de la Personstable afin qu'un tri explicite ne soit pas nécessaire. Vous trouverez peut-être que ce plan fonctionne relativement bien, peut-être même assez bien.

Boucles imbriquées parallèles

Le gros inconvénient du plan de boucles imbriquées est qu'il s'exécute sur un seul thread. Il est probable que cette requête bénéficie du parallélisme, mais l'optimiseur décide qu'il n'y a aucun avantage à le faire ici. Ce n'est pas nécessairement correct non plus. Malheureusement, il n'y a pas d'indication de requête intégrée pour obtenir un plan parallèle, mais il existe un moyen non documenté:

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n
OPTION (LOOP JOIN, QUERYTRACEON 8649);

L'activation de l'indicateur de trace 8649 avec l' QUERYTRACEONindicateur produit ce plan:

Plan de boucles imbriquées parallèles

Nous avons maintenant un plan qui évite le tri, ne nécessite aucune mémoire supplémentaire pour la jointure et utilise efficacement le parallélisme. Vous devriez trouver que cette requête fonctionne bien mieux que les alternatives.

Plus d'informations sur le parallélisme dans mon article Forcing a Parallel Query Execution Plan :


1

En regardant votre plan de requête, il est possible que votre véritable problème ne soit pas la jointure en soi, mais plutôt le processus réel de mise à jour.

D'après ce que je peux voir, vous êtes susceptible de mettre à jour tous les enregistrements de personne dans votre base de données et de mettre à jour les index (je ne peux pas voir quels autres index cela a donc je ne sais pas si cela pourrait être un facteur)

S'il s'agit d'une tâche ponctuelle, pourriez-vous désactiver les index, exécuter la mise à jour et reconstruire les index?

Une fois les données renseignées, vous pouvez ajouter une clause where à votre requête pour ne mettre à jour que les enregistrements à mettre à jour

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.