Aynı alt satır kümesine sahip üst satırları bulma


9

Diyelim ki böyle bir yapıya sahibim:

Tarifler tablosu

RecipeID
Name
Description

RecipeIngredients tablosu

RecipeID
IngredientID
Quantity
UOM

Tuşunun açık RecipeIngredientsolduğunu (RecipeID, IngredientID).

Yinelenen tarifleri bulmanın bazı iyi yolları nelerdir? Yinelenen bir tarif, her bir bileşen için tam olarak aynı bileşenlere ve miktarlara sahip olarak tanımlanır.

FOR XML PATHMalzemeleri tek bir sütunda birleştirmek için kullanmayı düşündüm . Bunu tam olarak araştırmadım, ancak malzemelerin / UOM'ların / miktarların aynı sırayla sıralandığından ve uygun bir ayırıcıya sahip olduğundan emin olursam işe yarayacaktır. Daha iyi yaklaşımlar var mı?

48K tarifler ve 200K içerik satırları vardır.

Yanıtlar:


7

Aşağıdaki varsayılan şema ve örnek veriler için

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 

Bu 205.009 madde satırları ve 42.613 tarifleri doldurdu. Bu, rastgele öğe nedeniyle her seferinde biraz farklı olacaktır.

Nispeten az sayıda çift olduğunu varsayar (örnek bir çalışmanın ardından çıktı, grup başına iki veya üç reçete içeren 217 çift reçete grubudur). OP'deki rakamlara dayanan en patolojik vaka 48.000 kesin kopya olacaktır.

Bunu ayarlamak için bir komut dosyası

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)

Her iki durumda da aşağıdakiler makinemde bir saniyeden daha kısa sürede tamamlandı.

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 

Bir uyarı

Birleştirilmiş dize uzunluğunun 896 baytı aşmayacağını varsaydım. Bunu yaparsa, sessizce başarısız olmak yerine çalışma zamanında bir hata ortaya çıkar. Birincil anahtarı (ve dolaylı olarak oluşturulmuş dizini) #temptablodan kaldırmanız gerekir . Test kurulumumda birleştirilmiş dizenin maksimum uzunluğu 125 karakterdi.

Birleştirilmiş dize dizine eklenemeyecek kadar uzunsa XML PATH, aynı tarifleri birleştiren son sorgunun performansı zayıf olabilir. Özel bir CLR dize toplama işlemi yüklemek ve kullanmak, bir dizine eklenmemiş kendi kendine birleştirme yerine verilerin bir geçişiyle birleştirmeyi yapabileceğinden bir çözüm olacaktır.

SELECT YourClrAggregate(RecipeId)
FROM #Concat
GROUP BY concatenated

Ben de denedim

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) 

Bu, nispeten az sayıda kopya (ilk örnek veriler için bir saniyeden az) olduğunda kabul edilebilir şekilde çalışır, ancak ilk toplama her RecipeIDbiri için tam olarak aynı sonuçları döndürdüğünden ve bu nedenle sayısını azaltmayı başaramadığından patolojik durumda kötü performans gösterir . karşılaştırmalar.


"Boş" tarifleri karşılaştırmak için çok mantıklı olup olmadığından emin değilim ama nihayet göndermeden önce, bu etki için benim sorgu değiştirdi, @ ypercube çözümleri yaptığı gibi görüyorum.
Andriy M

@AndriyM - Joe Celko onun içinde sıfıra bölme karşılaştırır ilişkisel bölünme makalesinde
Martin Smith

10

Bu ilişkisel bölünme sorununun genelleştirilmesidir. Bunun ne kadar verimli olacağı hakkında bir fikrim yok:

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

Başka (benzer) bir yaklaşım:

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

Ve başka, farklı bir tane:

; 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'da test edildi


CHECKSUM()Ve CHECKSUM_AGG()işlevlerini kullanarak SQL-Fiddle-2'de test edin :
( yanlış pozitifler verebileceğinden bunu dikkate almayın )

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  ;


Yürütme planları biraz korkutucu.
ypercubeᵀᴹ

Bu sorumun özünü oluşturuyor, bunu nasıl yapacağım. İcra planı, benim özel durumum için bir anlaşma kırıcı olabilir.
poke

1
CHECKSUMve CHECKSUM_AGGyine de yanlış pozitifleri kontrol etmene izin veriyor.
Martin Smith

470 tarifler ve 2057 içerik satırları ile cevabımdaki örnek verilerin kısaltılmış bir versiyonu için sorgu 1 Table 'RecipeIngredients'. Scan count 220514, logical reads 443643ve sorgu 2 vardır Table 'RecipeIngredients'. Scan count 110218, logical reads 441214. Üçüncüsü, bu ikisinden nispeten daha düşük okumalara sahip gibi görünüyor, ancak yine de tam örnek verilere karşı 8 dakika sonra sorguyu iptal ettim.
Martin Smith

Önce sayıları karşılaştırarak bunu hızlandırabilmelisiniz. Temel olarak, bileşenlerin sayısı aynı değilse, bir çift tarif tam olarak aynı bileşen setine sahip olamaz.
TomTom
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.