Tìm các hàng cha mẹ có các bộ hàng con giống hệt nhau


9

Giả sử tôi có một cấu trúc như thế này:

Bảng công thức

RecipeID
Name
Description

Bảng RecipeIngred cống

RecipeID
IngredientID
Quantity
UOM

Chìa khóa trên RecipeIngredients(RecipeID, IngredientID).

Một số cách tốt để tìm công thức nấu ăn trùng lặp là gì? Một công thức trùng lặp được định nghĩa là có cùng một bộ thành phần và số lượng chính xác cho mỗi thành phần.

Tôi đã nghĩ đến việc sử dụng FOR XML PATHđể kết hợp các thành phần thành một cột duy nhất. Tôi chưa khám phá đầy đủ điều này nhưng nó sẽ hoạt động nếu tôi đảm bảo các thành phần / UOM / số lượng được sắp xếp theo cùng một trình tự và có một dấu tách thích hợp. Có cách tiếp cận tốt hơn?

Có 48K công thức nấu ăn và 200K hàng thành phần.

Câu trả lời:


7

Đối với dữ liệu ví dụ và lược đồ giả định sau đây

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 

Điều này bao gồm 205.009 hàng thành phần và 42.613 công thức nấu ăn. Điều này sẽ hơi khác nhau mỗi lần do yếu tố ngẫu nhiên.

Nó giả định tương đối ít bản sao (đầu ra sau khi chạy ví dụ là 217 nhóm công thức trùng lặp với hai hoặc ba công thức mỗi nhóm). Trường hợp bệnh lý nhất dựa trên các số liệu trong OP sẽ là 48.000 bản sao chính xác.

Một kịch bản để thiết lập đó là

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)

Sau đây hoàn thành trong chưa đầy một giây trên máy của tôi cho cả hai trường hợp.

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 

Một caveat

Tôi giả sử rằng độ dài của chuỗi được nối sẽ không vượt quá 896 byte. Nếu nó làm điều này sẽ gây ra lỗi trong thời gian chạy chứ không phải âm thầm thất bại. Bạn sẽ cần xóa khóa chính (và chỉ mục được tạo ngầm) khỏi #tempbảng. Độ dài tối đa của chuỗi được nối trong thiết lập thử nghiệm của tôi là 125 ký tự.

Nếu chuỗi kết nối quá dài để lập chỉ mục thì hiệu năng của XML PATHtruy vấn cuối cùng hợp nhất các công thức giống hệt nhau có thể kém. Cài đặt và sử dụng tổng hợp chuỗi CLR tùy chỉnh sẽ là một giải pháp vì có thể thực hiện nối với một lần truyền dữ liệu thay vì tự tham gia không được lập chỉ mục.

SELECT YourClrAggregate(RecipeId)
FROM #Concat
GROUP BY concatenated

Tôi cũng đã thử

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) 

Điều này hoạt động có thể chấp nhận được khi có tương đối ít trùng lặp (ít hơn một giây đối với dữ liệu ví dụ đầu tiên) nhưng hoạt động kém trong trường hợp bệnh lý vì tập hợp ban đầu trả về kết quả chính xác cho mỗi RecipeIDvà do đó không quản lý để cắt giảm số lượng so sánh cả.


Tôi không chắc liệu có hợp lý khi so sánh các công thức "trống" hay không nhưng tôi cũng đã thay đổi truy vấn của mình thành hiệu ứng đó trước khi cuối cùng đăng nó, xem đó là những gì các giải pháp của @ ypercube đã làm.
Andriy M

@AndriyM - Joe Celko so sánh nó với số 0 trong bài viết phân chia quan hệ
Martin Smith

10

Đây là một khái quát của vấn đề phân chia quan hệ. Không biết làm thế nào hiệu quả này sẽ được:

; 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;

Một cách tiếp cận (tương tự) khác:

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
        ) ;

Và một cái khác, khác:

; 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 ;

Đã thử nghiệm tại SQL-Fiddle


Sử dụng các hàm CHECKSUM()CHECKSUM_AGG()hàm, kiểm tra tại SQL-Fiddle-2 :
( bỏ qua điều này vì nó có thể cho kết quả dương tính giả )

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  ;


Các kế hoạch thực hiện là loại đáng sợ.
ypercubeᵀᴹ

Điều này là cốt lõi của câu hỏi của tôi, làm thế nào để làm điều này. Kế hoạch thực hiện có thể là một phá vỡ thỏa thuận cho tình huống cụ thể của tôi, mặc dù.
chọc

1
CHECKSUMCHECKSUM_AGGvẫn để lại cho bạn cần kiểm tra dương tính giả.
Martin Smith

Đối với phiên bản rút gọn của dữ liệu mẫu trong câu trả lời của tôi với 470 công thức nấu ăn và 2057 hàng thành phần truy vấn 1 có Table 'RecipeIngredients'. Scan count 220514, logical reads 443643và truy vấn 2 Table 'RecipeIngredients'. Scan count 110218, logical reads 441214. Cái thứ ba dường như có số lần đọc tương đối thấp hơn hai cái đó nhưng vẫn trái với toàn bộ dữ liệu mẫu tôi đã hủy truy vấn sau 8 phút.
Martin Smith

Bạn sẽ có thể tăng tốc độ này bằng cách so sánh số lượng đầu tiên. Về cơ bản, một cặp công thức nấu ăn không thể có cùng một bộ thành phần nếu số lượng thành phần không giống nhau.
TomTom
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.