根据T-SQL中多个相关记录的排序顺序对记录进行排名?

时间:2013-10-07 19:02:54

标签: tsql

我有两张桌子,还有一张匹配的桌子。为了论证,我们称之为食谱和配料。每个食谱应该至少有一种成分,但可能有很多。每种成分都可以用于许多食谱中。

 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中不存在的另一条记录,但还没有弄清楚细节。

有什么想法吗?

(食谱可能没有任何成分。我不关心它们出现的顺序,但最终会是理想的。此时不是我的主要关注点。)

我正在使用SQL Server 2008 R2。

更新:我为此添加了一个SQL小提琴,并在此处更新了示例以匹配:

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

更新:我有一种潜在的怀疑,如果有解决方案,它涉及交叉连接,以比较食谱/成分的每个组合,然后以某种方式过滤。

5 个答案:

答案 0 :(得分: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种成分(总计)

如果配方含有重复成分,则无效,但如果可以发生,可以将其过滤掉

答案 1 :(得分: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

Str Concat版本:

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

答案 2 :(得分: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;

答案 3 :(得分: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

答案 4 :(得分: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 Recipes.Name=@RecipeName
    ORDER BY Ingredients.Name ASC

    return @Ingredients
END
GO


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