【问题标题】:Using a Recursive CTE in SQL to get Cost Per Lbs在 SQL 中使用递归 CTE 获取每磅成本
【发布时间】:2016-10-24 13:58:59
【问题描述】:

我是一个相当不错的 C# 程序员,并且在 T-SQL 方面拥有丰富的经验,但是我在组合一个递归 CTE 来解决我的问题时遇到了麻烦。

我需要获取每磅食谱的成本。

食谱由成分组成,但成分也可以是其他食谱。在成分表中,有一个名为 associatedProductID 的字段。如果它为空,那么我们只需使用该成分的 CostPerLb(通过获取最新的成分库存 CostPerLb 来找到。如果它不为空,那么该成分实际上是另一个配方,我们需要首先找到该成分的每磅成本然后我们将该成分的总成本相加(配方中使用的成分数量 * 该成分的 costPerLb),然后最后除以配方的总 Lbs 得到配方的 CostPerLbl

这是一些简化的表格信息。

  • 表 [Recipes] 有一个 int ID
  • 表 [RecipeIngredints] 具有 int RecipeID、int IngredientID、decimal Quantity
  • 表 [Ingredients] 具有 int ID、int AssociatedProductID
  • 表 [IngredientStock] 具有 int ID、int IngredientID、十进制 CostPerLb

示例数据:

[Recipes]
+------+
| ID   |
+------+
| 11   |
+------+
| 465  |
+------+
| 356  |
+------+
| 1617 |
+------+

[Ingredients]
+--------------+--------------------+
| IngredientID | AssociatedPrductId |
+--------------+--------------------+
| 213          | NULL               |
+--------------+--------------------+
| 214          | NULL               |
+--------------+--------------------+
| 216          | NULL               |
+--------------+--------------------+
| 218          | NULL               |
+--------------+--------------------+
| 219          | 465                |
+--------------+--------------------+
| 225          | 356                |
+--------------+--------------------+
| 150          | NULL               |
+--------------+--------------------+
| 213          | NULL               |
+--------------+--------------------+
| 692          | 1617               |
+--------------+--------------------+
| 172          | NULL               |
+--------------+--------------------+
| 218          | NULL               |
+--------------+--------------------+
| 4            | NULL               |
+--------------+--------------------+
| 691          | NULL               |
+--------------+--------------------+

[RecipeIngredients]
+----------+--------------+-------------+
| RecipeID | IngredientID | Quanity     |
+----------+--------------+-------------+
| 11       | 213          | 2           |
+----------+--------------+-------------+
| 11       | 214          | 1           |
+----------+--------------+-------------+
| 11       | 216          | 4.31494     |
+----------+--------------+-------------+
| 11       | 218          | 10.4125     |
+----------+--------------+-------------+
| 11       | 219          | 10.37085    |
+----------+--------------+-------------+
| 11       | 225          | 3.141971875 |
+----------+--------------+-------------+
| 465      | 150          | 0.0995      |
+----------+--------------+-------------+
| 465      | 213          | 0.25        |
+----------+--------------+-------------+
| 465      | 692          | 6.752298547 |
+----------+--------------+-------------+
| 356      | 172          | 200         |
+----------+--------------+-------------+
| 356      | 218          | 249.9       |
+----------+--------------+-------------+
| 1617     | 4            | 26.59274406 |
+----------+--------------+-------------+
| 1617     | 691          | 0.743192188 |
+----------+--------------+-------------+

在我的脑海中,通用递归函数可能看起来像这样......(这显然不是 CTE,也不起作用......)

 FUNCTION [dbo].[fn_getRecipeCostPerLbs]
 (@ID INT = 0)
RETURNS DECIMAL(24,12)
AS
BEGIN select
 sum(
        --get the actual amount of ingredient used in the recipe (in lbs)
        ri.Quantity

        --get the cost of the ingredient (in $ / lbs)
        case 
          --when associated product id is null then use the latest ingredient stock cost / lbs
          --when its not null then use the cost / lbs of the recipe that it relates too
        when  i.AssociatedProductID is null then 
            --pull the cost/lbs of the newest stock in warehouse
            isNull((select top 1 ist.CostPerLb from IngredientStock ist where ist.IngredientID = i.id order by ist.id desc),0)
        else
            [dbo].[fn_getRecipeCostPerLbs](@ID)
        end
    )
    / 
    dbo.fn_FunctionToGetTotalRecipeLbs(@ID) -- divide by total lbs of the recipe to get cost Per Lbs
    as CostPerLbs


from RecipeIngredients ri
inner join Ingredients i on i.id = ri.IngredientID
where ri.RecipeID = @ID
)

提前致谢!

-大卫

【问题讨论】:

  • 您描述餐桌的方式 - 似乎食谱是由成分列表(RecipeIngredints)制成的,而这些(我猜)的成本保存在成分和IngredientStock - 您能否将 4 个表格链接在一起并汇总 Quantity * CostPerLb
  • 我知道您说“配料可以成为食谱”——不过,您的餐桌上是如何做到的?
  • 拥有一些表格和示例数据将大大有助于改善这个问题。 spaghettidba.com/2015/04/24/… 此外,这些标量函数可能会给您带来一些严重的性能问题。标量函数的表现是出了名的差。您在这里调用它们的方式意味着它们每行运行一次。
  • Andrew,在成分表中,有一个名为 AssociatedProductID 的字段。 (如问题中所述)如果这不为空,则该成分实际上是指另一个配方。就像物料清单是产品清单一样......但可能有另一个组件(物料清单)被引用为物料清单上的产品
  • 肖恩,感谢您的评论。这个查询本来是一个函数: [fn_getRecipeCostPerLbs] (因此使其递归)......但这只是假设......并且 fn_FunctionToGetTotalRecipeLbs 仅在总和之后调用一次。我的意图是根本不使用上面的查询……而是寻求帮助构建 CTE。

标签: sql-server recursion common-table-expression


【解决方案1】:

由于您可以在 SQL 中使用递归函数,因此请使用类似这样的函数 - 如果关联产品 ID 不为 NULL,则调用查询来评估其成本 - 可能将每磅成本除以/乘以 CASE 中的某个值声明

这是一个递归函数,而不是递归 CTE

CREATE FUNCTION [dbo].[fn_getRecipeCostPerLbs]
 (@ID INT)
RETURNS DECIMAL(24,12)
AS
BEGIN

DECLARE @COST DECIMAL(24,12) = 0;


SELECT @COST = SUM(CASE WHEN IngredientStock.AssociatedProductID IS NULL 
                                                    THEN 
                                                        IngredientStock.CostPerLb 
                                                    ELSE  
                                                        [dbo].[fn_getRecipeCostPerLbs](IngredientStock.AssociatedProductID)
                                                    END
                    )

                                     FROM RecipeIngredints  
                                      join Ingredients 
                                         on RecipeIngredints.RecipeID = @ID 
                                        AND 
                                        Ingredients.id = RecipeIngredints.IngredientID
                                     join IngredientStock 
                                         on 
                                         IngredientStock.ID = Ingredients.AssociatedProductID

    RETURN @COST;

)

我想您可能还需要找到每个配方的总成本和总重量,然后返回总成本/总重量 - 但这应该是可能的

编辑

也许像 -

您熟悉表之间的连接吗?我不确定您的表格是如何设计链接在一起的-您可能必须进行更改或提供更多信息-这样做的一个好方法是提供一个脚本来制作一小组示例表格(尝试剪下不必要的字段 id 太多了)并给出少量的代表性数据

SELECT @COST = SUM(RecipeIngredints.Quantity * (CASE WHEN Ingredients.AssociatedProductID IS NULL 
                                                    THEN 
                                                        IngredientStock.CostPerLb 
                                                    ELSE  
                                                        [dbo].[fn_getRecipeCostPerLbs](IngredientStock.AssociatedProductID)
                                                    END) )/ SUM(RecipeIngredints.Quantity)


                                     FROM RecipeIngredints  
                                      join Ingredients 
                                         on RecipeIngredints.RecipeID = @ID 
                                        AND 
                                        Ingredients.id = RecipeIngredints.IngredientID
                                     join IngredientStock 
                                         on 
                                         IngredientStock.ID = Ingredients.AssociatedProductID

【讨论】:

  • 安德鲁请看我对帖子的回复。您提供的这个查询不起作用,并且与表的工作方式并不接近。成分库存表有一个成分ID 的forienKey 将其链接回来。并且 AssociatedProductID 在成分表而不是库存表上......并将成分链接回配方。 (产品和配方在这里是一样的)......最后你是对的,我需要产品的总重量,然后除以原料的成本。这是由每磅原料的成本乘以 RecipeIngrediet.Quantity
  • @Javadave - 您是否尝试过更改 AssociatedProductID 的表名?我认为我只能勾勒出一个大致的想法,因为我们没有您想要使用的所有计算公式 - 在没有子食谱的基本食谱上尝试该函数 - 我会尝试使其平均重量/成本
  • 如果您假设食谱仅由基本成分制成,您是否能够编写 SQL 来执行此操作?我认为这是一个很好的起点。
【解决方案2】:

自己解决了。如果有人想对此进行改进,请告诉我...感谢您的帮助。此外,它的响应时间为 8 毫秒,比我的 C#/EF 2 秒要好。

CREATE FUNCTION [dbo].[fn_getRecipeCostPerPound]
(
@ID INT = 0
)
RETURNS DECIMAL(24,12)
AS
BEGIN

Declare @Ret decimal(24,12) = 0;
Declare @TotalLbs decimal(24,12) =  [dbo].[fn_RecipeLbs](@ID)


    DECLARE @MyCTETempTable TABLE (RecipeID int,
    IngredientID int,
    AssociatedProductID int,
    Quantity decimal(24,12),
    level int,
    CostPerLbs decimal(24,12)
    )


; with  CTE as 
        (
            select  
              ri.RecipeID
              ,ri.IngredientID
              ,i.AssociatedProductID
             ,dbo.fn_ConvertUomToPounds(ri.Quantity,ri.UnitOfMeasureID,i.specificgravity) as Quantity
              ,1 as level
            from    RecipeIngredients ri inner join Ingredients i on i.id = ri.IngredientID 
            where   ri.RecipeID = @ID
            union all
            select  
               ri_child.RecipeID
              ,ri_child.IngredientID
              ,i_child.AssociatedProductID
              ,dbo.fn_ConvertUomToPounds(ri_child.Quantity,ri_child.UnitOfMeasureID,i_child.specificgravity) as Quantity
              ,level + 1
            from     RecipeIngredients ri_child inner join Ingredients i_child on i_child.id = ri_child.IngredientID --and i_child.AssociatedProductID is null
            join    CTE parent
            on      parent.AssociatedProductID = ri_child.RecipeID
        )

INSERT INTO @MyCTETempTable (
RecipeID,
IngredientID,
AssociatedProductID,
Quantity,
level,
CostPerLbs
)  

select 
cte.*,  isNull((select top 1 ist.CostPerLb from IngredientStock ist where ist.IngredientID = cte_i.id order by ist.id desc),0) 
from    CTE
inner join Ingredients cte_i on cte_i.id = CTE.IngredientID

SET @Ret = (select sum(Quantity * CostPerLbs) / @TotalLbs
from @MyCTETempTable tt 
where RecipeID = @ID
group by RecipeID)



return @Ret

END

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-16
    相关资源
    最近更新 更多