Untuk skema diasumsikan berikut dan contoh data
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
Baris ramuan ini terdiri dari 205.009 baris dan 42.613 resep. Ini akan sedikit berbeda setiap kali karena elemen acak.
Ini mengasumsikan dupes yang relatif sedikit (output setelah menjalankan contoh adalah 217 kelompok resep rangkap dengan dua atau tiga resep per kelompok). Kasus paling patologis berdasarkan angka-angka dalam OP akan menjadi 48.000 duplikat yang tepat.
Script untuk mengaturnya adalah
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)
Berikut ini diselesaikan dalam waktu kurang dari satu detik pada mesin saya untuk kedua kasus.
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
Satu peringatan
Saya berasumsi bahwa panjang string gabungan tidak akan melebihi 896 byte. Jika hal ini dilakukan, ini akan menimbulkan kesalahan pada saat dijalankan daripada gagal diam-diam. Anda harus menghapus kunci utama (dan indeks yang dibuat secara implisit) dari #temp
tabel. Panjang maksimum string gabungan dalam pengaturan pengujian saya adalah 125 karakter.
Jika string gabungan terlalu panjang untuk diindeks, maka kinerja XML PATH
kueri terakhir yang mengonsolidasikan resep yang sama bisa jadi buruk. Menginstal dan menggunakan agregasi string CLR khusus akan menjadi salah satu solusi karena dapat melakukan penggabungan dengan satu lintasan data daripada bergabung dengan diri yang tidak diindeks.
SELECT YourClrAggregate(RecipeId)
FROM #Concat
GROUP BY concatenated
Saya juga mencoba
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)
Ini bekerja dengan baik ketika ada duplikat yang relatif sedikit (kurang dari satu detik untuk data contoh pertama) tetapi berkinerja buruk dalam kasus patologis karena agregasi awal mengembalikan hasil yang persis sama untuk setiap RecipeID
dan karenanya tidak berhasil mengurangi jumlah perbandingan sama sekali.
Ini adalah generalisasi dari masalah pembagian relasional. Tidak tahu seberapa efisien ini:
Pendekatan (serupa) lainnya:
Dan satu lagi, yang berbeda:
Diuji di SQL-Fiddle
Menggunakan
CHECKSUM()
danCHECKSUM_AGG()
fungsi, tes di SQL-Fiddle-2 :( abaikan ini karena dapat memberikan hasil positif palsu )
sumber
CHECKSUM
danCHECKSUM_AGG
masih meninggalkan Anda perlu memeriksa positif palsu.Table 'RecipeIngredients'. Scan count 220514, logical reads 443643
dan 2Table 'RecipeIngredients'. Scan count 110218, logical reads 441214
. Yang ketiga tampaknya memiliki bacaan yang relatif lebih rendah dari dua tetapi masih terhadap data sampel lengkap saya membatalkan permintaan setelah 8 menit.