Para el siguiente esquema asumido y datos de ejemplo
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
Esto llenó 205.009 filas de ingredientes y 42.613 recetas. Esto será ligeramente diferente cada vez debido al elemento aleatorio.
Supone relativamente pocos engaños (el resultado después de una ejecución de ejemplo fue 217 grupos de recetas duplicadas con dos o tres recetas por grupo). El caso más patológico basado en las cifras en el OP sería 48,000 duplicados exactos.
Un script para configurar eso es
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)
Lo siguiente se completó en menos de un segundo en mi máquina para ambos casos.
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
Una advertencia
Supuse que la longitud de la cadena concatenada no excederá los 896 bytes. Si lo hace, generará un error en tiempo de ejecución en lugar de fallar silenciosamente. Deberá eliminar la clave primaria (y el índice creado implícitamente) de la #temp
tabla. La longitud máxima de la cadena concatenada en mi configuración de prueba fue de 125 caracteres.
Si la cadena concatenada es demasiado larga para indexar, el rendimiento de la XML PATH
consulta final que consolida las recetas idénticas podría ser pobre. Instalar y usar una agregación de cadena CLR personalizada sería una solución, ya que podría hacer la concatenación con una pasada de los datos en lugar de una autounión no indexada.
SELECT YourClrAggregate(RecipeId)
FROM #Concat
GROUP BY concatenated
También intenté
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)
Esto funciona de manera aceptable cuando hay relativamente pocos duplicados (menos de un segundo para los datos del primer ejemplo) pero funciona mal en el caso patológico ya que la agregación inicial devuelve exactamente los mismos resultados para cada uno RecipeID
y, por lo tanto, no logra reducir el número de comparaciones en absoluto.
Esta es una generalización del problema de división relacional. No tengo idea de cuán eficiente será esto:
Otro enfoque (similar):
Y otro, diferente:
Probado en SQL-Fiddle
Usando las funciones
CHECKSUM()
yCHECKSUM_AGG()
, pruebe en SQL-Fiddle-2 :( ignore esto ya que puede dar falsos positivos )
fuente
CHECKSUM
yCHECKSUM_AGG
aún así debes dejar de buscar falsos positivos.Table 'RecipeIngredients'. Scan count 220514, logical reads 443643
y la consulta 2Table 'RecipeIngredients'. Scan count 110218, logical reads 441214
. El tercero parece tener lecturas relativamente más bajas que esas dos, pero aún contra los datos de muestra completos, cancelé la consulta después de 8 minutos.