2013-10-07 81 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]