次のような構造があるとします。
RecipeID
Name
Description
RecipeID
IngredientID
Quantity
UOM
RecipeIngredients
のキーは(RecipeID, IngredientID)
です。
重複するレシピを見つけるための良い方法は何ですか?複製レシピは、各成分の成分と量がまったく同じセットとして定義されます。
FOR XML PATH
を使用して、材料を1つの列に結合することを考えました。私はこれを完全に調査していませんが、成分/ UOM /数量が同じ順序で並べ替えられていて、適切なセパレーターがあることを確認すれば機能するはずです。より良いアプローチはありますか?
48Kのレシピと200Kの材料の行があります。
以下の想定スキーマとサンプルデータの場合
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
これにより、205,009の原料行と42,613のレシピが入力されました。これはランダム要素のために毎回わずかに異なります。
これは、比較的少数のデュープを想定しています(サンプル実行後の出力は、グループごとに2つまたは3つのレシピを持つ217の重複するレシピグループでした)。 OPの数値に基づく最も病理的なケースは、48,000の完全な重複です。
それを設定するスクリプトは
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)
どちらの場合も、私のマシンでは以下が1秒未満で完了しました。
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
1つの警告
連結された文字列の長さは896バイトを超えないと想定しました。これを行うと、通知なしに失敗するのではなく、実行時にエラーが発生します。 #temp
テーブルから主キー(および暗黙的に作成されたインデックス)を削除する必要があります。テストセットアップでの連結文字列の最大長は125文字でした。
連結された文字列が長すぎてインデックスを作成できない場合、同じレシピを統合する最後のXML PATH
クエリのパフォーマンスが低下する可能性があります。カスタムCLR文字列集計のインストールと使用は、インデックス付けされていない自己結合ではなく、データの1つのパスで連結を行うことができるため、1つの解決策になります。
SELECT YourClrAggregate(RecipeId)
FROM #Concat
GROUP BY concatenated
私も試しました
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)
これは、重複が比較的少ない場合(最初の例のデータでは1秒未満)でも問題なく機能しますが、病理学的なケースでは、初期集計がすべてのRecipeID
に対してまったく同じ結果を返し、管理できないため、パフォーマンスが低下します。比較の数を減らすためです。
これは、関係分割問題の一般化です。これがどれほど効率的かはわかりません。
_; WITH cte AS
( SELECT RecipeID_1 = r1.RecipeID, Name_1 = r1.Name,
RecipeID_2 = r2.RecipeID, Name_2 = r2.Name
FROM Recipes AS r1
JOIN Recipes AS r2
ON r1.RecipeID <> r2.RecipeID
WHERE NOT EXISTS
( SELECT 1
FROM RecipeIngredients AS ri1
WHERE ri1.RecipeID = r1.RecipeID
AND NOT EXISTS
( SELECT 1
FROM RecipeIngredients AS ri2
WHERE ri2.RecipeID = r2.RecipeID
AND ri1.IngredientID = ri2.IngredientID
AND ri1.Quantity = ri2.Quantity
AND ri1.UOM = ri2.UOM
)
)
)
SELECT c1.*
FROM cte AS c1
JOIN cte AS c2
ON c1.RecipeID_1 = c2.RecipeID_2
AND c1.RecipeID_2 = c2.RecipeID_1
AND c1.RecipeID_1 < c1.RecipeID_2;
_
別の(同様の)アプローチ:
_SELECT RecipeID_1 = r1.RecipeID, Name_1 = r1.Name,
RecipeID_2 = r2.RecipeID, Name_2 = r2.Name
FROM Recipes AS r1
JOIN Recipes AS r2
ON r1.RecipeID < r2.RecipeID
AND NOT EXISTS
( SELECT IngredientID, Quantity, UOM
FROM RecipeIngredients AS ri1
WHERE ri1.RecipeID = r1.RecipeID
EXCEPT
SELECT IngredientID, Quantity, UOM
FROM RecipeIngredients AS ri2
WHERE ri2.RecipeID = r2.RecipeID
)
AND NOT EXISTS
( SELECT IngredientID, Quantity, UOM
FROM RecipeIngredients AS ri2
WHERE ri2.RecipeID = r2.RecipeID
EXCEPT
SELECT IngredientID, Quantity, UOM
FROM RecipeIngredients AS ri1
WHERE ri1.RecipeID = r1.RecipeID
) ;
_
そして別の、別のもの:
_; WITH cte AS
( SELECT RecipeID_1 = r.RecipeID, RecipeID_2 = ri.RecipeID,
ri.IngredientID, ri.Quantity, ri.UOM
FROM Recipes AS r
CROSS JOIN RecipeIngredients AS ri
)
, cte2 AS
( SELECT RecipeID_1, RecipeID_2,
IngredientID, Quantity, UOM
FROM cte
EXCEPT
SELECT RecipeID_2, RecipeID_1,
IngredientID, Quantity, UOM
FROM cte
)
SELECT RecipeID_1 = r1.RecipeID, RecipeID_2 = r2.RecipeID
FROM Recipes AS r1
JOIN Recipes AS r2
ON r1.RecipeID < r2.RecipeID
EXCEPT
SELECT RecipeID_1, RecipeID_2
FROM cte2
EXCEPT
SELECT RecipeID_2, RecipeID_1
FROM cte2 ;
_
SQL-Fiddleでテスト済み
CHECKSUM()
およびCHECKSUM_AGG()
関数を使用して、 SQL-Fiddle-2 でテストします。
(誤検知が発生する可能性があるため、これを無視してください)
_ALTER TABLE RecipeIngredients ADD ck AS CHECKSUM( IngredientID, Quantity, UOM ) PERSISTED ; CREATE INDEX ckecksum_IX ON RecipeIngredients ( RecipeID, ck ) ; ; WITH cte AS ( SELECT RecipeID, cka = CHECKSUM_AGG(ck) FROM RecipeIngredients AS ri GROUP BY RecipeID ) SELECT RecipeID_1 = c1.RecipeID, RecipeID_2 = c2.RecipeID FROM cte AS c1 JOIN cte AS c2 ON c1.cka = c2.cka AND c1.RecipeID < c2.RecipeID ;
_