2013-10-07 80 views
2

我有兩個表,加上一個匹配表。爲了論證的緣故,我們稱之爲食譜和配料。每個食譜應該至少有一種成分,但可能有很多。每種成分都可以用於許多食譜。根據T-SQL中多個相關記錄的排序順序排列記錄?

Recipes   Ingredients  Match 
=============== =============== =============== 
ID int   ID int   RecipeID  int 
Name varchar  Name varchar  IngredientID int 

的樣本數據:

Recipes   Ingredients  Match (shown as CDL but stored as above) 
=============== =============== =============== 
Soup    Chicken   Soup:  Chicken, Tomatoes 
Pizza    Tomatoes   Pizza:  Cheese, Chicken, Tomatoes 
Chicken Sandwich Cheese   C. Sandwich: Bread, Chicken, Tomatoes 
Turkey Sandwich Bread    T. Sandwich: Bread, Cheese, Tomatoes, Turkey   
        Turkey 

這裏是問題:我需要根據自己成分的姓名(或名稱)來排序食譜。鑑於上述樣本數據,我需要的食譜此排序順序:

Turkey Sandwich (First ingredient bread, then cheese) 
Chicken Sandwich (First ingredient bread, then chicken) 
Pizza    (First ingredient cheese) 
Soup    (First ingredient chicken) 

第一成分排名的食譜很簡單:

WITH recipesranked AS (
    SELECT Recipes.ID, Recipes.Name, Recipes.Description, 
     ROW_NUMBER() OVER (ORDER BY Ingredients.Name) AS SortOrder 
    FROM 
     Recipes 
     LEFT JOIN Match ON Match.RecipeID = Recipes.ID 
     LEFT JOIN Ingredients ON Ingredients.ID = Match.IngredientID 
    ) 
SELECT ID, Name, Description, MIN(SortOrder) 
FROM  recipesranked 
GROUP BY ID, Name, Description; 

除此之外,我卡住了。在我上面的例子中,這幾乎可以工作,但將兩個三明治留在模糊的順序中。

我有一種感覺,MIN(SortOrder)應該被別的東西取代,也許是一個相關的子查詢,在同一個CTE中尋找不存在另一個記錄,但沒有弄清楚細節。

任何想法?

(有可能食譜有沒有成分。我不在乎他們來什麼樣的順序進行,但最終將是理想的。在這一點不是我的主要關注的問題。)

我m使用SQL Server 2008 R2。

更新:我增加了一個SQL搗鼓這一點,並在此更新的例子來匹配:

http://sqlfiddle.com/#!3/38258/2

更新:我有一個鬼鬼祟祟的懷疑,如果有一個解決方案,它涉及一個交叉連接來比較每個食譜/成分的每一個組合,然後以某種方式進行過濾。

+2

任何機會,你可以在SQL小提琴上設置此? – UnhandledExcepSean

+0

我並不真正追隨你想要達到的目標。 – DeadlyDan

+0

您可以使用此解決方案作爲您的起點http://stackoverflow.com/questions/19209768/sql-split-function-and-ordering-issue/19209883#19209883 –

回答

2

我覺得這會給你想要的東西(根據您提供的小提琴)

-- Show recipes ranked by all their ingredients alphabetically 
WITH recipesranked AS (
    SELECT Recipes.ID, Recipes.Name, SortedIngredients.SortOrder 
    FROM 
     Recipes 
     LEFT JOIN Match ON Match.RecipeID = Recipes.ID 
     LEFT JOIN 
     (
      SELECT ID, Name, POWER(2.0, ROW_NUMBER() OVER (ORDER BY Name Desc)) As SortOrder 
      FROM Ingredients) AS SortedIngredients 
     ON SortedIngredients.ID = Match.IngredientID 
    ) 
SELECT ID, Name, SUM(SortOrder) 
FROM  recipesranked 
GROUP BY ID, Name 
-- Sort by sum of the ingredients. Since the first ingredient for both kinds 
-- of sandwiches is Bread, this gives both of them the same sort order, but 
-- we need Turkey Sandwiches to come out first between them because Cheese 
-- is it's #2 sorted ingredient, but Chicken is the #2 ingredient for 
-- Chicken sandwiches. 
ORDER BY SUM(SortOrder) DESC; 

它只是使用POWER來確保最重要的成分首先被加權。

這適用於任何數量的配方和高達120種成份(共)工作

不會工作,如果食譜包含重複的成分,儘管你可以過濾那些出來,如果可能發生,他們

+0

如果我正確理解這一點,這將適用於每配方(在int爆炸之前)最多9個成分總數和不超過10每個查詢都有相同配料的配方(在配料的10個位置溢出之前)。 – richardtallent

+0

更改爲使用2的冪數。POWER以浮點形式運行,因此無論配方的數量如何,它總共可以處理多達120種成分。 –

+0

我最終在C#中使用了自定義IComparer <>,但此解決方案沒有相當適合我的應用程序(數據比配方複雜得多)。但這是最接近可行的解決方案,並且可能適用於其他情況,所以我接受它。 – richardtallent

0

這應該得到你所需要的:

WITH recipesranked AS (
    SELECT Recipes.ID, Recipes.Name, ROW_NUMBER() OVER (ORDER BY Ingredients.Name) AS SortOrder, 
    Rank() OVER (partition by Recipes.Name ORDER BY Ingredients.Name) as RankOrder 
    FROM 
     Recipes 
     LEFT JOIN Match ON Match.RecipeID = Recipes.ID 
     LEFT JOIN Ingredients ON Ingredients.ID = Match.IngredientID 
) 
    SELECT ID, Name,SortOrder, RankOrder 
    FROM  recipesranked 
Where RankOrder = 1 
ORDER BY SortOrder; 
+1

這不正確地命令雞肉三明治(麪包 - >雞)之前,土耳其三明治(麪包 - >奶酪) – Moho

1

二進制符號版本:

;with IngredientFlag(IngredientId, Flag) 
as 
(
    select 
     i.id Ingredient 
     , POWER(2, row_number() over (order by i.Name desc) - 1) 
    from 
     Ingredients i 
) 
, RecipeRank(RecipeId, Rank) 
as 
(
    select 
     m.RecipeID 
     , row_number() /* or rank() */ over (order by SUM(flag.Flag) desc) 
    from 
     Match m 
     inner join IngredientFlag flag 
     on m.IngredientID = flag.IngredientId 
    group by 
     m.RecipeID 
) 

select 
    RecipeId 
    , Name 
    , Rank 
from 
    RecipeRank rr 
    inner join Recipes r 
    on rr.RecipeId = r.id 

海峽的毗連版本:

-- order the ingredients per recipe 
;with RecipeIngredientOrdinal(RecipeId, IngredientId, Name, Ordinal) 
as 
(
    select 
    m.RecipeID 
    , m.IngredientID 
    , i.Name 
    , Row_Number() over (partition by m.RecipeId order by i.Name) Ordinal 
    from 
     Match m 
     inner join Ingredients i 
     on m.IngredientID = i.id 
) 
    -- get ingredient count per recipe 
, RecipeIngredientCount(RecipeId, IngredientCount) 
as 
(
    select 
    m.RecipeID 
    , count(1) 
    from 
     Match m 
    group by 
     m.RecipeID 
) 
    -- recursively build concatenated ingredient list per recipe 
    -- (note this will return incomplete lists which is why I include 
    -- 'generational' in the name) 
, GenerationalConcatenatedIngredientList(RecipeId, Ingredients, IngredientCount) 
as 
(
    select 
     rio.RecipeID 
     , cast(rio.Name as varchar(max)) 
     , rio.Ordinal 
    from 
     RecipeIngredientOrdinal rio 
    where 
     rio.Ordinal = 1 

    union all 

    select 
     rio.RecipeID 
     , cil.Ingredients + rio.Name 
     , rio.Ordinal 
    from 
     RecipeIngredientOrdinal rio 
     inner join GenerationalConcatenatedIngredientList cil 
     on rio.RecipeID = cil.RecipeId and rio.Ordinal = cil.IngredientCount + 1 
) 
    -- return row_number or rank ordered by the concatenated ingredients list 
-- (don't need to return Ingredients but shown for demonstrative purposes) 
, RecipeRankByIngredients(RecipeId, Rank, Ingredients) 
as 
(
    select 
     cil.RecipeId 
     , row_number() over (order by cil.Ingredients) -- or rank() 
     , cil.Ingredients 
    from 
     GenerationalConcatenatedIngredientList cil 
     inner join RecipeIngredientCount ric 
     on cil.RecipeId = ric.RecipeId 
        -- don't forget to filter for only the completed ingredient lists 
        -- and ignore all intermediate values 
        and cil.IngredientCount = ric.IngredientCount 
) 

select * from RecipeRankByIngredients 
+0

不幸的是字符串串聯不是一種選擇。這些「名字」實際上很長很多,並且已經被編入索引,並且性能很重要。 – richardtallent

+0

該算法有效;如何測試它,並讓我們知道 – Moho

+1

更新使用二進制標誌字段來訂購食譜 – Moho

0

唯一替代方式,我可以想到做到這一點,是使用動態SQL來生成一個關鍵點

這並沒有限制我的替代品的成分數量,但並不完全感到優雅!

DECLARE @MaxIngredients INT 

SELECT @MaxIngredients = MAX(IngredientCount) 
FROM 
(
    SELECT COUNT(*) AS IngredientCount 
    FROM Match 
    GROUP BY RecipeID 
) A 

DECLARE @COLUMNS nvarchar(max) 
SELECT @COLUMNS = N'[1]' 

DECLARE @COLUMN INT 
SELECT @COLUMN = 2 

WHILE (@COLUMN <= @MaxIngredients) 
BEGIN 
    SELECT @COLUMNS = @COLUMNS + N',[' + CAST(@COLUMN AS varchar(19)) + N']', @COLUMN = @COLUMN + 1 
END 

DECLARE @SQL nvarchar(max) 

SELECT @SQL = 
N'WITH recipesranked as(
SELECT * 
FROM 
( 
    SELECT M.RecipeID, 
      ROW_NUMBER() OVER (PARTITION BY M.RecipeID ORDER BY I.SortOrder) AS IngredientIndex, 
      I.SortOrder 
    FROM Match M 
    LEFT 
    JOIN 
    (
     SELECT *, ROW_NUMBER() OVER (ORDER BY Name) As SortOrder 
     FROM Ingredients 
    ) I 
     ON I.ID = M.IngredientID 
) AS SourceTable 
PIVOT 
(
    MIN(SortOrder) --min here is just for the syntax, there will only be one value 
    FOR IngredientIndex IN (' + @COLUMNS + N') 
) AS PivotTable) 
SELECT R.Name 
FROM RecipesRanked RR 
JOIN Recipes R 
    ON RR.RecipeID = R.ID 
ORDER BY ' + @COLUMNS 

EXEC SP_EXECUTESQL @SQL 
0

創建一個函數並使用它。

CREATE FUNCTION GetIngredients(@RecipeName varchar(200)) 
RETURNS VARCHAR(MAX) 
AS 
BEGIN 
    DECLARE @Ingredients VARCHAR(MAX) 
    SET @Ingredients=NULL 

    SELECT TOP 9999999 
     @Ingredients = COALESCE(@Ingredients + ', ', '') + Ingredients.Name 
    FROM Recipes 
    LEFT JOIN Match ON Match.RecipeID = Recipes.ID 
    LEFT JOIN Ingredients ON Ingredients.ID = Match.IngredientID 
    WHERE [email protected] 
    ORDER BY Ingredients.Name ASC 

    return @Ingredients 
END 
GO 


SELECT 
    Recipes.Name AS RecipeName, dbo.GetIngredients(Recipes.Name) [Ingredients] 
FROM Recipes 
ORDER BY [Ingredients]