Für das folgende angenommene Schema und Beispieldaten
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
Dies bevölkerte 205.009 Zutatenreihen und 42.613 Rezepte. Dies ist aufgrund des zufälligen Elements jedes Mal etwas anders.
Es werden relativ wenige Dupes angenommen (Ausgabe nach einem Beispiellauf waren 217 doppelte Rezeptgruppen mit zwei oder drei Rezepten pro Gruppe). Der pathologischste Fall, der auf den Zahlen im OP basiert, wären 48.000 exakte Duplikate.
Ein Skript zum Einrichten ist
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)
Das Folgende wurde in beiden Fällen in weniger als einer Sekunde auf meinem Computer abgeschlossen.
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
Eine Einschränkung
Ich nahm an, dass die Länge der verketteten Zeichenfolge 896 Bytes nicht überschreiten wird. Wenn dies der Fall ist, wird zur Laufzeit ein Fehler ausgegeben, anstatt stillschweigend fehlzuschlagen. Sie müssen den Primärschlüssel (und den implizit erstellten Index) aus der #temp
Tabelle entfernen . Die maximale Länge der verketteten Zeichenfolge in meinem Testaufbau betrug 125 Zeichen.
Wenn die verkettete Zeichenfolge zu lang ist, um sie zu indizieren, kann die Leistung der endgültigen XML PATH
Abfrage, die die identischen Rezepte konsolidiert, schlecht sein. Das Installieren und Verwenden einer benutzerdefinierten CLR-Zeichenfolgenaggregation wäre eine Lösung, da dies die Verkettung mit einem Durchgang der Daten anstelle eines nicht indizierten Self-Joins bewirken könnte.
SELECT YourClrAggregate(RecipeId)
FROM #Concat
GROUP BY concatenated
Ich habe es auch versucht
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)
Dies funktioniert akzeptabel, wenn relativ wenige Duplikate vorhanden sind (weniger als eine Sekunde für die ersten Beispieldaten), ist jedoch im pathologischen Fall schlecht, da die anfängliche Aggregation für alle genau die gleichen Ergebnisse liefert RecipeID
und es daher nicht schafft, die Anzahl der Duplikate zu verringern Vergleiche überhaupt.
Dies ist eine Verallgemeinerung des relationalen Teilungsproblems. Keine Ahnung, wie effizient dies sein wird:
Ein anderer (ähnlicher) Ansatz:
Und noch eine, andere:
Getestet bei SQL-Fiddle
Mit den
CHECKSUM()
undCHECKSUM_AGG()
Funktionen, Test auf SQL-Fiddle-2 :( ignorieren diese , wie es Fehlalarme geben kann )
quelle
CHECKSUM
undCHECKSUM_AGG
Sie müssen immer noch nach Fehlalarmen suchen.Table 'RecipeIngredients'. Scan count 220514, logical reads 443643
und Abfrage 2Table 'RecipeIngredients'. Scan count 110218, logical reads 441214
. Der dritte scheint relativ niedrigere Lesevorgänge zu haben als diese beiden, aber immer noch gegen die vollständigen Beispieldaten habe ich die Abfrage nach 8 Minuten abgebrochen.