Pour le schéma supposé suivant et les exemples de données
CREATE TABLE dbo.RecipeIngredients
(
RecipeId INT NOT NULL ,
IngredientID INT NOT NULL ,
Quantity INT NOT NULL ,
UOM INT NOT NULL ,
CONSTRAINT RecipeIngredients_PK
PRIMARY KEY ( RecipeId, IngredientID ) WITH (IGNORE_DUP_KEY = ON)
) ;
INSERT INTO dbo.RecipeIngredients
SELECT TOP (210000) ABS(CRYPT_GEN_RANDOM(8)/50000),
ABS(CRYPT_GEN_RANDOM(8) % 100),
ABS(CRYPT_GEN_RANDOM(8) % 10),
ABS(CRYPT_GEN_RANDOM(8) % 5)
FROM master..spt_values v1,
master..spt_values v2
SELECT DISTINCT RecipeId, 'X' AS Name
INTO Recipes
FROM dbo.RecipeIngredients
Cela comprenait 205 009 lignes d'ingrédients et 42 613 recettes. Ce sera légèrement différent à chaque fois en raison de l'élément aléatoire.
Il suppose relativement peu de dupes (la sortie après un exemple de série était de 217 groupes de recettes en double avec deux ou trois recettes par groupe). Le cas le plus pathologique basé sur les chiffres du PO serait 48 000 doublons exacts.
Un script pour le configurer est
DROP TABLE dbo.RecipeIngredients,Recipes
GO
CREATE TABLE Recipes(
RecipeId INT IDENTITY,
Name VARCHAR(1))
INSERT INTO Recipes
SELECT TOP 48000 'X'
FROM master..spt_values v1,
master..spt_values v2
CREATE TABLE dbo.RecipeIngredients
(
RecipeId INT NOT NULL ,
IngredientID INT NOT NULL ,
Quantity INT NOT NULL ,
UOM INT NOT NULL ,
CONSTRAINT RecipeIngredients_PK
PRIMARY KEY ( RecipeId, IngredientID )) ;
INSERT INTO dbo.RecipeIngredients
SELECT RecipeId,IngredientID,Quantity,UOM
FROM Recipes
CROSS JOIN (SELECT 1,1,1 UNION ALL SELECT 2,2,2 UNION ALL SELECT 3,3,3 UNION ALL SELECT 4,4,4) I(IngredientID,Quantity,UOM)
Les opérations suivantes ont été effectuées en moins d'une seconde sur ma machine dans les deux cas.
CREATE TABLE #Concat
(
RecipeId INT,
concatenated VARCHAR(8000),
PRIMARY KEY (concatenated, RecipeId)
)
INSERT INTO #Concat
SELECT R.RecipeId,
ISNULL(concatenated, '')
FROM Recipes R
CROSS APPLY (SELECT CAST(IngredientID AS VARCHAR(10)) + ',' + CAST(Quantity AS VARCHAR(10)) + ',' + CAST(UOM AS VARCHAR(10)) + ','
FROM dbo.RecipeIngredients RI
WHERE R.RecipeId = RecipeId
ORDER BY IngredientID
FOR XML PATH('')) X (concatenated);
WITH C1
AS (SELECT DISTINCT concatenated
FROM #Concat)
SELECT STUFF(Recipes, 1, 1, '')
FROM C1
CROSS APPLY (SELECT ',' + CAST(RecipeId AS VARCHAR(10))
FROM #Concat C2
WHERE C1.concatenated = C2.concatenated
ORDER BY RecipeId
FOR XML PATH('')) R(Recipes)
WHERE Recipes LIKE '%,%,%'
DROP TABLE #Concat
Une mise en garde
J'ai supposé que la longueur de la chaîne concaténée ne dépasserait pas 896 octets. Si tel est le cas, une erreur sera générée au moment de l'exécution plutôt que d'échouer silencieusement. Vous devrez supprimer la clé primaire (et l'index créé implicitement) de la #temp
table. La longueur maximale de la chaîne concaténée dans ma configuration de test était de 125 caractères.
Si la chaîne concaténée est trop longue pour être indexée, les performances de la XML PATH
requête finale consolidant les recettes identiques pourraient bien être médiocres. L'installation et l'utilisation d'une agrégation de chaînes CLR personnalisée serait une solution car cela pourrait faire la concaténation avec un seul passage des données plutôt qu'une auto-jointure non indexée.
SELECT YourClrAggregate(RecipeId)
FROM #Concat
GROUP BY concatenated
J'ai aussi essayé
WITH Agg
AS (SELECT RecipeId,
MAX(IngredientID) AS MaxIngredientID,
MIN(IngredientID) AS MinIngredientID,
SUM(IngredientID) AS SumIngredientID,
COUNT(IngredientID) AS CountIngredientID,
CHECKSUM_AGG(IngredientID) AS ChkIngredientID,
MAX(Quantity) AS MaxQuantity,
MIN(Quantity) AS MinQuantity,
SUM(Quantity) AS SumQuantity,
COUNT(Quantity) AS CountQuantity,
CHECKSUM_AGG(Quantity) AS ChkQuantity,
MAX(UOM) AS MaxUOM,
MIN(UOM) AS MinUOM,
SUM(UOM) AS SumUOM,
COUNT(UOM) AS CountUOM,
CHECKSUM_AGG(UOM) AS ChkUOM
FROM dbo.RecipeIngredients
GROUP BY RecipeId)
SELECT A1.RecipeId AS RecipeId1,
A2.RecipeId AS RecipeId2
FROM Agg A1
JOIN Agg A2
ON A1.MaxIngredientID = A2.MaxIngredientID
AND A1.MinIngredientID = A2.MinIngredientID
AND A1.SumIngredientID = A2.SumIngredientID
AND A1.CountIngredientID = A2.CountIngredientID
AND A1.ChkIngredientID = A2.ChkIngredientID
AND A1.MaxQuantity = A2.MaxQuantity
AND A1.MinQuantity = A2.MinQuantity
AND A1.SumQuantity = A2.SumQuantity
AND A1.CountQuantity = A2.CountQuantity
AND A1.ChkQuantity = A2.ChkQuantity
AND A1.MaxUOM = A2.MaxUOM
AND A1.MinUOM = A2.MinUOM
AND A1.SumUOM = A2.SumUOM
AND A1.CountUOM = A2.CountUOM
AND A1.ChkUOM = A2.ChkUOM
AND A1.RecipeId <> A2.RecipeId
WHERE NOT EXISTS (SELECT *
FROM (SELECT *
FROM RecipeIngredients
WHERE RecipeId = A1.RecipeId) R1
FULL OUTER JOIN (SELECT *
FROM RecipeIngredients
WHERE RecipeId = A2.RecipeId) R2
ON R1.IngredientID = R2.IngredientID
AND R1.Quantity = R2.Quantity
AND R1.UOM = R2.UOM
WHERE R1.RecipeId IS NULL
OR R2.RecipeId IS NULL)
Cela fonctionne de manière acceptable quand il y a relativement peu de doublons (moins d'une seconde pour le premier exemple de données) mais fonctionne mal dans le cas pathologique car l'agrégation initiale renvoie exactement les mêmes résultats pour chaque RecipeID
et ne parvient donc pas à réduire le nombre de comparaisons du tout.
Il s'agit d'une généralisation du problème de la division relationnelle. Aucune idée de l'efficacité de cette opération:
Une autre approche (similaire):
Et un autre, différent:
Testé chez SQL-Fiddle
En utilisant les fonctions
CHECKSUM()
etCHECKSUM_AGG()
, testez à SQL-Fiddle-2 :( ignorez cela car cela peut donner des faux positifs )
la source
CHECKSUM
etCHECKSUM_AGG
vous laisse toujours besoin de vérifier les faux positifs.Table 'RecipeIngredients'. Scan count 220514, logical reads 443643
et la requête 2Table 'RecipeIngredients'. Scan count 110218, logical reads 441214
. Le troisième semble avoir des lectures relativement inférieures à ces deux-là, mais malgré les données d'échantillonnage complètes, j'ai annulé la requête après 8 minutes.