동일한 자식 행 집합이있는 부모 행 찾기


9

다음과 같은 구조가 있다고 가정하십시오.

요리법 테이블

RecipeID
Name
Description

레시피 성분 표

RecipeID
IngredientID
Quantity
UOM

열쇠는 RecipeIngredients입니다 (RecipeID, IngredientID).

중복 레시피를 찾는 좋은 방법은 무엇입니까? 중복 레시피는 각 성분에 대해 정확히 동일한 성분 세트와 수량을 갖는 것으로 정의됩니다.

FOR XML PATH재료를 하나의 열로 결합하는 데 사용 하려고 생각했습니다 . 나는 이것을 완전히 탐구하지는 않았지만 재료 / UOM / 수량이 동일한 순서로 정렬되고 적절한 분리기가 있는지 확인해야 작동합니다. 더 나은 접근법이 있습니까?

48K 레시피와 200K 재료 행이 있습니다.

답변:


7

다음 가정 된 스키마 및 예제 데이터

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 개의 레시피를 채웠습니다. 임의 요소로 인해 매번 약간 씩 다릅니다.

이 예제에서는 상대적으로 적은 속임수를 가정합니다 (예제 실행 후 출력은 그룹당 두세 개의 레시피를 가진 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 

하나의 경고

연결된 문자열의 길이가 896 바이트를 초과하지 않는다고 가정했습니다. 그렇게하면 런타임에 자동 실패가 아닌 오류가 발생합니다. #temp테이블 에서 기본 키 (및 내재적으로 작성된 인덱스)를 제거해야 합니다. 테스트 설정에서 연결된 문자열의 최대 길이는 125 자입니다.

연결된 문자열이 색인하기에 너무 긴 XML PATH경우 동일한 레시피를 통합하는 최종 쿼리의 성능 이 좋지 않을 수 있습니다. 사용자 지정 CLR 문자열 집계를 설치하고 사용하는 것은 인덱싱되지 않은 자체 조인이 아닌 한 번의 데이터 통과로 연결을 수행 할 수있는 솔루션 중 하나입니다.

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) 

이 상대적으로 적은 (이하 첫 번째 예제 데이터에 대한 초 이상) 중복하지만 수행은 모든 정확히 같은 결과 초기 집계를 반환과 병적 인 경우에 몹시 때 수용 가능한 작동 RecipeID등의 수를 줄이기 위해 관리하지 않습니다 전혀 비교하지 마십시오.


"빈"레시피를 비교하는 것이 의미가 있는지는 확실하지 않지만 @ypercube의 솔루션이 무엇을했는지 알기 때문에 마지막으로 게시하기 전에 해당 효과로 쿼리를 변경했습니다.
Andriy M

@AndriyM-Joe Celko는 그의 관계 부서 기사
Martin Smith

10

이것은 관계 분할 문제의 일반화입니다. 이것이 얼마나 효율적인지 모릅니다.

; 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 에서 테스트


은 Using CHECKSUM()CHECKSUM_AGG()에서 기능 시험을 SQL-바이올린-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  ;


실행 계획은 무섭습니다.
ypercubeᵀᴹ

이것은 내 질문의 중심에 있으며, 이것을 수행하는 방법. 그러나 실행 계획은 내 특정 상황에 대한 거래 차단기 일 수 있습니다.
찌르다

1
CHECKSUM그리고 CHECKSUM_AGG아직도 당신이 오 탐지를 확인하기 위해 필요 둡니다.
Martin Smith

470 가지 레시피와 2057 재료 행을 가진 내 대답에있는 예제 데이터의 컷 다운 버전의 경우 쿼리 1에는 Table 'RecipeIngredients'. Scan count 220514, logical reads 443643쿼리 2가 Table 'RecipeIngredients'. Scan count 110218, logical reads 441214있습니다. 세 번째는 두 개보다 읽기가 상대적으로 적지 만 여전히 전체 샘플 데이터에 대해 8 분 후에 쿼리를 취소했습니다.
Martin Smith

카운트를 먼저 비교하여 속도를 높일 수 있어야합니다. 기본적으로 재료의 개수가 동일하지 않으면 한 쌍의 레시피에 정확히 동일한 재료 세트를 설정할 수 없습니다.
TomTom 2016 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.