Dla następujących założonych schematów i przykładowych danych
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
W tym zaludniono 205,009 rzędów składników i 42 613 przepisów. Będzie to nieco inne za każdym razem ze względu na losowy element.
Zakłada się względnie niewiele kopii (dane wyjściowe po przykładowym przebiegu wyniosły 217 zduplikowanych grup przepisów z dwoma lub trzema przepisami na grupę). Najbardziej patologicznym przypadkiem opartym na liczbach w PO byłoby 48 000 dokładnych duplikatów.
Skrypt do skonfigurowania to
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)
Poniższe czynności zostały wykonane w moim komputerze w niecałą sekundę w obu przypadkach.
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
Jedno zastrzeżenie
Zakładałem, że długość połączonego łańcucha nie przekroczy 896 bajtów. Jeśli to zrobi, spowoduje to błąd w czasie wykonywania, a nie cichą awarię. Musisz usunąć klucz podstawowy (i domyślnie utworzony indeks) z #temp
tabeli. Maksymalna długość połączonego łańcucha w moim zestawie testowym wynosiła 125 znaków.
Jeśli połączony ciąg jest zbyt długi, aby go zindeksować, wydajność ostatniego XML PATH
zapytania konsolidującego identyczne przepisy może być słaba. Zainstalowanie i użycie niestandardowej agregacji ciągów CLR byłoby jednym rozwiązaniem, ponieważ mogłoby to połączyć dane za pomocą jednego przejścia danych, a nie zindeksowanego nieindeksowanego połączenia.
SELECT YourClrAggregate(RecipeId)
FROM #Concat
GROUP BY concatenated
Też próbowałem
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)
Działa to akceptowalnie, gdy jest stosunkowo mało duplikatów (mniej niż sekunda w przypadku danych z pierwszego przykładu), ale działa źle w przypadku patologii, ponieważ początkowa agregacja zwraca dokładnie takie same wyniki dla każdego, RecipeID
a zatem nie udaje się zmniejszyć liczby porównania w ogóle.
Jest to uogólnienie problemu podziału relacyjnego. Nie mam pojęcia, jak efektywne to będzie:
Inne (podobne) podejście:
I inny, inny:
Testowane w SQL-Fiddle
Korzystając z funkcji
CHECKSUM()
iCHECKSUM_AGG()
, przetestuj w SQL-Fiddle-2 :( zignoruj to, ponieważ może to dać fałszywie pozytywne wyniki )
źródło
CHECKSUM
iCHECKSUM_AGG
nadal musisz sprawdzać, czy nie ma wyników fałszywie pozytywnych.Table 'RecipeIngredients'. Scan count 220514, logical reads 443643
zapytanie 2Table 'RecipeIngredients'. Scan count 110218, logical reads 441214
. Trzeci wydaje się mieć relatywnie niższe odczyty niż te dwa, ale nadal przy pełnych przykładowych danych anulowałem zapytanie po 8 minutach.