Para o seguinte esquema assumido e dados de exemplo
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
Este povoou 205.009 linhas de ingredientes e 42.613 receitas. Isso será ligeiramente diferente a cada vez devido ao elemento aleatório.
Pressupõe relativamente poucos enganos (a saída após uma execução de exemplo foi de 217 grupos de receitas duplicados com duas ou três receitas por grupo). O caso mais patológico baseado nos números do PO seria 48.000 duplicatas exatas.
Um script para configurar isso é
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)
O seguinte foi concluído em menos de um segundo na minha máquina nos dois 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
Uma ressalva
Eu assumi que o comprimento da seqüência concatenada não excederá 896 bytes. Se isso ocorrer, ocorrerá um erro no tempo de execução, em vez de falhar silenciosamente. Você precisará remover a chave primária (e o índice criado implicitamente) da #temp
tabela. O comprimento máximo da sequência concatenada na minha configuração de teste era de 125 caracteres.
Se a sequência concatenada for muito longa para indexar, o desempenho da XML PATH
consulta final consolidando as receitas idênticas poderá ser ruim. Instalar e usar uma agregação de cadeia CLR personalizada seria uma solução, pois isso poderia concatenar com uma passagem dos dados, em vez de uma associação automática não indexada.
SELECT YourClrAggregate(RecipeId)
FROM #Concat
GROUP BY concatenated
Eu também tentei
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)
Isso funciona de maneira aceitável quando há relativamente poucas duplicatas (menos de um segundo para o primeiro exemplo de dados), mas apresenta um desempenho ruim no caso patológico, pois a agregação inicial retorna exatamente os mesmos resultados para todos RecipeID
e, portanto, não consegue reduzir o número de comparações.
Esta é uma generalização do problema de divisão relacional. Não faço ideia de quão eficiente isso será:
Outra abordagem (semelhante):
E outro, diferente:
Testado no SQL-Fiddle
Usando as funções
CHECKSUM()
eCHECKSUM_AGG()
, teste no SQL-Fiddle-2 :( ignore isso, pois pode dar falsos positivos )
fonte
CHECKSUM
eCHECKSUM_AGG
ainda assim você precisa verificar se há falsos positivos.Table 'RecipeIngredients'. Scan count 220514, logical reads 443643
e a consulta 2Table 'RecipeIngredients'. Scan count 110218, logical reads 441214
. O terceiro parece ter leituras relativamente mais baixas do que as duas, mas ainda com os dados completos da amostra, cancelei a consulta após 8 minutos.