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:
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:
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:
L'estimation de l'entrée de tri est moins précise:
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):
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:
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 Persons
table 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' QUERYTRACEON
indicateur produit ce plan:
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 :