【问题标题】:SQL Server - Grouping Combination of possibilities by fixed valueSQL Server - 按固定值分组可能性组合
【发布时间】:2017-01-27 16:43:38
【问题描述】:

我必须创建包含固定物品的最便宜的篮子。

例如对于有 (5) 件商品的购物篮

1 和 4 = (1 * 50) + (1 * 100) = 150

2 和 3 = (1 * 60) + (1 * 80) = 140 -- 这是我的家伙

2 和 2 和 1 = (1 * 60) + (1 * 60) + (1 * 50) = 170

3 和 3 = (1 * 80) + (1 * 80) = 160 **** 这 6 个项目,但总项目可以超过最少项目。重要的是总成本... ....

这也适用于一个篮子可能有的任意数量的项目。还有很多商店,每个商店都有不同的包装,可能包括几个项目。

如何用 SQL 处理这个问题?

更新

这是示例数据生成代码。递归 CTE 解决方案更昂贵。我应该在 500-600 毫秒内完成 600-700 家商店的工作。这是一个包搜索引擎。使用“#temp”表或“UNUION”手动创建场景比递归 CTE 便宜 15-20 倍。

同时连接ItemPackageId 非常昂贵。在选择最便宜的包并加入源表后,我可以找到所需的包 ID 或项目。

我期待一个可以超快并获得正确选项的神奇解决方案。 每个商店只需要最便宜的篮子。手动创建场景非常快,但有时会因为正确的最便宜的篮子而失败。

                CREATE TABLE #storePackages(
                StoreId int not null,
                PackageId int not null,
                ItemType int not null, -- there are tree item type 0 is normal item, 1 is item has discount 2 is free item
                ItemCount int not null,
                ItemPrice decimal(18,8) not null,
                MaxItemQouta int not null, -- in generaly a package can have between 1 and 6 qouata but in rare can up to 20-25
                MaxFullQouta int not null -- sometimes a package can have additional free or discount item qouta. MaxFullQouta will always greater then MaxItemQouta
            )

            declare @totalStores int
            set @totalStores = (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN 200 AND 400 ORDER BY NEWID())

            declare @storeId int;
            declare @packageId  int;
            declare @maxPackageForStore int;
            declare @itemMinPrice decimal(18,8);
            set @storeId = 1;
            set @packageId = 1

            while(@storeId <= @totalStores)
                BEGIN
                    set @maxPackageForStore = (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN 2 AND 6 ORDER BY NEWID())
                    set @itemMinPrice = (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN 40 AND 100 ORDER BY NEWID())
                        BEGIN
                            INSERT INTO #storePackages
                            SELECT DISTINCT 
                             StoreId = @storeId
                            ,PackageId = CAST(@packageId + number AS int) 
                            ,ItemType = 0
                            ,ItemCount = number
                            ,ItemPrice = @itemMinPrice + (10 * (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN pkgNo.number AND pkgNo.number + 2  ORDER BY NEWID()))
                            ,MaxItemQouta = @maxPackageForStore
                            ,MaxFullQouta =  @maxPackageForStore + (CASE WHEN number > 1 AND number < 4 THEN 1 ELSE 0 END)
                            FROM master..[spt_values] pkgNo
                            WHERE number BETWEEN 1 AND @maxPackageForStore
                            UNION ALL
                            SELECT DISTINCT 
                             StoreId = @storeId
                            ,PackageId = CAST(@packageId + number AS int) 
                            ,ItemType = 1
                            ,ItemCount = 1
                            ,ItemPrice = (@itemMinPrice / 2) + (10 * (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN pkgNo.number AND pkgNo.number + 2  ORDER BY NEWID()))
                            ,MaxItemQouta = @maxPackageForStore
                            ,MaxFullQouta =  @maxPackageForStore  + (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN 0 AND 2 ORDER BY NEWID())
                            FROM master..[spt_values] pkgNo
                            WHERE number BETWEEN 2 AND (CASE WHEN @maxPackageForStore > 4 THEN 4 ELSE @maxPackageForStore END)


                        set @packageId = @packageId + @maxPackageForStore;
                        END
                set @storeId =@storeId + 1;
                END

            SELECT * FROM #storePackages
            drop table #storePackages

我的解决方案

首先,我感谢所有试图帮助我的人。然而,所有建议的解决方案都基于 CTE。正如我之前所说,当考虑数百个商店时,递归 CTE 会导致性能问题。一次还要求多个包裹。这意味着,我要求可以包含多个篮子。一个是 5 个项目,另一个是 3 个项目,另一个是 7 个项目......

最后的解决方案

首先,我按项目大小在表格中生成所有可能的场景...通过这种方式,我可以选择消除不需要的场景。

CREATE TABLE ItemScenarios(
   Item int,
   ScenarioId int,
   CalculatedItem int  --this will be joined with Store Item
)

然后我生成了从 2 项到 25 项的所有可能场景并插入到ItemScenarios 表中。场景可以通过使用 WHILE 或递归 CTE 生成一次。这种方式的好处是,场景只生成一次。

结果如下。

Item          |   ScenarioId       |     CalculatedItem
--------------------------------------------------------
2                   1                     2
2                   2                     3
2                   3                     1
2                   3                     1
3                   4                     5
3                   5                     4
3                   6                     3
3                   7                     2
3                   7                     2
3                   8                     2
3                   8                     1
3                   9                     1
3                   9                     1
3                   9                     1
....
.....
......
25                  993                   10

通过这种方式,我可以限制场景大小、最大不同商店、最大不同包等。

我还可以排除一些在数学上不可能最便宜的场景。例如对于 4 个项目的请求,一些场景

场景一:2+2

场景二:2+1+1

场景三:1+1+1+1

在这些场景中;方案 2 不可能是最便宜的篮子。因为,

如果 场景 2 场景 3 --> 场景 1 会低于 场景 2。因为降低成本的东西是 2 件价格,**场景 1* 有双 2 件

如果 场景 2 场景 1 --> 场景 3 会低于 场景 2

现在,如果我删除像 Scenario 2 这样的场景,我会获得一些性能优势。

现在我可以在商店中选择最便宜的商品价格

DECLARE @requestedItems int;
SET @requestedItems = 5;

CREATE TABLE #JoinedPackageItemWithScenarios(
   StoreId int not null,
   PackageId int not null,
   ItemCount int not null,
   ItemPrice decimal(18,8) 
   ScenarioId int not null,
)
INSERT INTO #JoinedPackageItemWithScenarios
 SELECT
    SPM.StoreId  
   ,SPM.PackageId 
   ,SPM.ItemCount 
   ,SPM.ItemPrice 
   ,SPM.ScenarioId
   FROM (
      SELECT 
            SP.StoreId  
           ,SP.PackageId 
           ,SP.ItemCount 
           ,SP.ItemPrice 
           ,SC.ScenarioId
           ,RowNumber = ROW_NUMBER() OVER (PARTITION BY SP.StoreId,SC.ScenarioId,SP.ItemCount ORDER BY SP.ItemPrice) 
      FROM ItemScenarios SC
      LEFT JOIN StorePackages AS SP ON SP.ItemCount = SC.CalculatedItem
      WHERE SC.Item = @requestedItems
 ) SPM
 WHERE SPM.RowNumber = 1

-- NOW I HAVE CHEAPEST PRICE FOR EACH ITEM, I CAN CREATE BASKET

 CREATE TABLE #selectedScenarios(
   StoreId int not null,
   ScenarioId int not null,
   TotalItem int not null,
   TotalCost decimal(18,8) 
)
 INSERT INTO #selectedScenarios
 SELECT 
      StoreId
     ,ScenarioId
     ,TotalItem 
     ,TotalCost 
  FROM (
     SELECT 
           StoreId
          ,ScenarioId
          --,PackageIds = dbo.GROUP_CONCAT(CAST(PackageId AS nvarchar(20))) -- CONCATENING PackageId decreasing performance here. We can joing seleceted scenarios with #JoinedPackageItemWithScenarios after selection complated.
          ,TotalItem = SUM(ItemCount)
          ,TotalCost = SUM(ItemPrice)
          ,RowNumber = ROW_NUMBER() OVER (PARTITION BY StoreId ORDER BY SUM(ItemPrice))
        FROM #JoinedPackageItemWithScenarios JPS
        GROUP BY StoreId,ScenarioId
        HAVING(SUM(ItemCount) >= @requestedItems)
     ) SLECTED
     WHERE RowNumber = 1

  -- NOW WE CAN POPULATE PackageIds if needed

  SELECT 
      SS.StoreId
     ,SS.ScenarioId
     ,TotalItem = MAX(SS.TotalItem)
     ,TotalCost = MAX(SS.TotalCost)
     ,PackageIds = dbo.GROUP_CONCAT(CAST(JPS.PackageId AS nvarchar(20)))
    FROM #selectedScenarios SS
    JOIN #JoinedPackageItemWithScenarios AS JPS ON JPS.StoreId = SS.StoreId AND JPS.ScenarioId = SS.ScenarioId
    GROUP BY SS.StoreId,SS.ScenarioId

总和

在我的测试中,这种方式至少比递归 CTE 快 10 倍,尤其是在商店数量和请求商品数量增加的情况下。它也得到 100% 正确的结果。因为当商店和请求的项目数量增加时,递归 CTE 尝试了数百万次不需要的 JOIN。

【问题讨论】:

  • 嗯?您是通过将PackageIdItem id 相加来测量篮子中的物品数量吗?这对我来说没有意义。
  • 'PackageId' 不重要
  • @MehmetOtkun 在我看来 PackageId 非常重要。因为 item 似乎是包裹中物品的数量。所以 PackageId 将是查询时如何识别该包。
  • 只有当篮子中的 PakageID 数量有限时,它才能被 TSQL 解决。否则应该解决我的一些算法优化方法,如“单纯形法”

标签: sql sql-server group-by cube rollup


【解决方案1】:

如果您想要组合,则需要递归 CTE。防止无限递归是一个挑战。这是一种方法:

with cte as (
      select cast(packageid as nvarchar(4000)) as packs, item, cost
      from t
      union all
      select concat(cte.packs, ',', t.packageid), cte.item + t.item, cte.cost + t.cost
      from cte join
           t
           on cte.item + t.item < 10  -- some "reasonable" stop condition
     )
select top 1 cte.*
from cte
where cte.item >= 5
order by cost desc;

我不能 100% 确定 SQL Server 会接受连接条件,但这应该可以。

【讨论】:

  • 1. concat 返回 nvarchar(4000),因此您应该将 packageId 转换为相同的类型。 2. cte 有名称包的列,但你在 concat 中使用了 packageId。 3. 你的递归 cte 没有停止,因为它总是会尝试加入 1 和 1 项......你应该添加条件 where packs concat(cte.packs,',', t.packageid)
【解决方案2】:

假设您想要比较所有可能的项目排列,直到篮子中的总项目超过您的总篮子数量,类似以下的操作可以满足您的需求。

DECLARE @N INT = 1;

DECLARE @myTable TABLE (storeID INT DEFAULT(1), packageID INT IDENTITY(1, 1), item INT, cost INT);
INSERT @myTable (item, cost) VALUES (1, 50), (2, 60), (3, 80), (4, 100), (5, 169), (5, 165), (4, 101), (2, 61);

WITH CTE1 AS (
    SELECT item, cost
    FROM (
        SELECT item, cost, ROW_NUMBER() OVER (PARTITION BY item ORDER BY cost) RN
        FROM @myTable) T
    WHERE RN = 1)
, CTE2 AS (
    SELECT CAST('items'+CAST(C1.item AS VARCHAR(10)) AS VARCHAR(4000)) items, C1.cost totalCost, C1.item totalItems
    FROM CTE1 C1
    UNION ALL
    SELECT CAST(C2.items + ' + items' + CAST(C1.item AS VARCHAR(10)) AS VARCHAR(4000)), C1.cost + C2.totalCost, C1.item + C2.totalItems
    FROM CTE2 C2
    CROSS JOIN CTE1 C1
    WHERE C2.totalItems < @N)
SELECT TOP 1 *
FROM CTE2
WHERE totalItems >= @N
ORDER BY totalCost, totalItems DESC;

编辑处理@Matt 提到的问题。

【讨论】:

  • 如果您认为最小数量的项目可以在 1 行内完全表示,例如(100,1) 或 (50,2) 它最终会复制项目和成本。
  • @Matt 我以为它永远不会这么简单,但实际上这只是在 CTE2 的第一部分中不使用交叉连接的问题。
  • 现在 (1,1) 为 totalCost 和 totalItems 生成 1 和 1。我喜欢使用递归和交叉连接来创建更无限的组合。实际上,我仍在尝试围绕这个方向而不是我之前的方向来思考,但我认为其中一些问题是在一个地方寻找总项目 = @N。因为最终它不应该被限制在交叉连接中,但我认为你的回答引发了一些事情,所以我可以找到我正在做的事情的解决方案。欢呼
  • @Matt CTE 递归部分中的&lt; @N 是因为一旦达到@N,您添加的任何更多项目都会(必然)产生更大的成本。您只想在少于篮子总数时继续执行递归循环。末尾的&gt;= @N 是为了让您只获得至少达到@N 项的结果(这似乎是OP 要求的)。
  • 得到了你要去的地方,但成本是约束而不是物品的数量,可能有很多物品但成本最低。我正在尝试在我的脑海中找出一个例子,当我欢呼时我会回到它
【解决方案3】:

首先我们要找到所有的组合,然后选择一个价格最低的组合来寻求价值

DECLARE @Table as TABLE (StoreId INT, PackageId INT, Item INT, Cost INT)
INSERT INTO @Table VALUES (1,1,1,50),(1,2,2,60),(1,3,3,80),(1,4,4,100)

DECLARE @MinItemCount INT = 5;

WITH cteCombinationTable AS (
    SELECT  cast(PackageId as NVARCHAR(4000)) as Package, Item, Cost
    FROM @Table
    UNION ALL
    SELECT CONCAT(o.Package,',',c.PackageId), c.Item + o.Item, c.Cost + o.Cost FROM @Table as c join cteCombinationTable as o on CONCAT(o.Package,',',c.PackageId) <> Package
    where o.Item < @MinItemCount
)

select top 1 * 
from cteCombinationTable
where item >= @MinItemCount
order by cast(cost as decimal)/@MinItemCount

【讨论】:

    【解决方案4】:
    IF OBJECT_ID('tempdb..#TestResults') IS NOT NULL
        BEGIN
            DROP TABLE #TestResults
        END
    
    DECLARE @MinItemCount INT = 5
    
    ;WITH cteMaxCostToConsider AS (
        SELECT
           StoreId
           ,CASE
              WHEN (SUM(ItemCount) >= @MinItemCount) AND
              SUM(ItemPrice) < MIN(((@MinItemCount / ItemCount) + IIF((@MinItemCount % ItemCount) > 0, 1,0)) * ItemPrice) THEN SUM(ItemPrice)
              ELSE MIN(((@MinItemCount / ItemCount) + IIF((@MinItemCount % ItemCount) > 0, 1,0)) * ItemPrice)
           END AS MaxCostToConsider
        FROM
           storePackages
        GROUP BY
           StoreId
    )
    
    , cteRecursive AS (
        SELECT
          StoreId
          ,'<PackageId>' + CAST(PackageId AS VARCHAR(MAX)) + '</PackageId>' AS PackageIds
          ,ItemCount AS CombinedItemCount
          ,CAST(ItemPrice AS decimal(18,8)) AS CombinedCost
        FROM
           storePackages
    
        UNION ALL
    
        SELECT
          r.StoreId
          ,r.PackageIds + '<PackageId>' + CAST(t.PackageId AS VARCHAR(MAX)) + '</PackageId>'
          ,r.CombinedItemCount + t.ItemCount
          ,CAST(r.CombinedCost + t.ItemPrice AS decimal(18,8))
        FROM
           cteRecursive r
           INNER JOIN storePackages t
           ON r.StoreId = t.StoreId
           INNER JOIN cteMaxCostToConsider m
           ON r.StoreId = m.StoreId
          AND r.CombinedCost + t.ItemPrice <= m.MaxCostToConsider
    )
    
    , cteCombinedCostRowNum AS (
        SELECT
           StoreId
           ,CAST(PackageIds AS XML) AS PackageIds
           ,CombinedCost
           ,CombinedItemCount
           ,DENSE_RANK() OVER (PARTITION BY StoreId ORDER BY CombinedCost) AS CombinedCostRowNum
           ,ROW_NUMBER() OVER (PARTITION BY StoreId ORDER BY CombinedCost) AS PseudoCartId
        FROM
           cteRecursive
        WHERE
           CombinedItemCount >= @MinItemCount
    )
    
    SELECT DISTINCT
        c.StoreId
        ,x.PackageIds
        ,c.CombinedItemCount
        ,c.CombinedCost
    INTO #TestResults
    FROM
        cteCombinedCostRowNum  c
        CROSS APPLY (
           SELECT( STUFF ( (
              SELECT ',' + PackageId
              FROM
                 (SELECT T.N.value('.','VARCHAR(100)') as PackageId FROM c.PackageIds.nodes('PackageId') as T(N)) p
              ORDER BY
                 PackageId
              FOR XML PATH(''), TYPE ).value('.','NVARCHAR(MAX)'), 1, 1, '')
        ) as PackageIds
        ) x
    WHERE
        CombinedCostRowNum = 1
    
    
    SELECT *
    FROM
        #TestResults
    

    大约需要 1000-2000 MS 的时间差异很大,具体取决于测试数据中必须考虑的组合(例如,有时您的脚本会生成更多或更少的数据)。

    这个答案无疑看起来比 Gordon 的或 ZLK 的要复杂一些,但它可以处理 Ties、重复值、1 个符合标准的包以及其他一些事情。然而,主要区别实际上是在最后一个查询中,我将在递归查询期间构建的 XML 拆分,然后按顺序重新组合,以便您可以使用 DISTINCT 并获得唯一的配对,例如包 2 + 包 3 = 140 & 包 3 + 包 2 = 140 将是所有查询中的前 2 个结果,因此使用 XML 拆分然后重新组合允许它成为单行。但是假设您还有另一行,例如 (1,5,2,60),它有 2 个项目,成本为 60,此查询也将返回该组合。

    您可以在答案之间进行挑选,并使用他们的方法来获得组合和我的方法来获得最终结果等......但是要解释我的查询过程。

    cteMaxCostToConsider - 这只是一种获取包含递归查询的成本的方法,因此必须考虑更少的记录。它的作用是确定所有包裹的成本,或者您购买所有相同包裹以满足最低数量的成本。

    cteRecursive - 这类似于 ZLK 的答案和 Gordon 的答案,但它所做的是出去并继续添加项目和项目组合,直到达到 MaxCostToConsider。如果我限制查看项目数量,它可能会错过 7 个项目比 5 个便宜的情况,因此通过限制到确定的组合成本,它会限制递归并表现更好。

    cteCombinedCostRowNum - 这只是找到最低的综合成本和至少最少的项目数。

    最后的查询有点棘手,但交叉应用将递归 cte 中构建的 XML 字符串拆分为不同的行,对这些行重新排序,然后再次将它们连接起来,以便反向组合,例如Package 2 & Package 3 逆向 Package 3 & Package 2 变成同一个记录,然后调用distinct。

    这比 SELECT top N 灵活一点。要查看差异,请将以下测试用例添加到您的测试数据中,一次添加 1 个: (StoreId、PackageId、Item、Cost)

    • (1,5,2,60)
    • (1,6,1,1),(1,7,1,1)
    • (1,8,50,1)

    已编辑。以上将为您提供综合成本最低的商店的所有组合。您注意到的错误是由于cteMaxCostToConsider。我使用的是SUM(ItemPrice),但有时与它相关的SUM(ItemCount) 没有足够的项目来考虑MaxCostToConsider。我修改了 case 语句来纠正这个问题。

    我还修改了您提供的数据示例。请注意,您应该将其中的 PackageId 更改为 IDENTITY 列,因为我使用您使用的方法在商店中获取了重复的 PackageId。

    这是你的脚本的修改版本,看看我在说什么:

    IF OBJECT_ID('storePackages') IS NOT NULL
        BEGIN
            DROP TABLE storePackages
        END
    
    CREATE TABLE storePackages(
        StoreId int not null,
        PackageId int not null IDENTITY(1,1),
        ItemType int not null, -- there are tree item type 0 is normal item, 1 is item has discount 2 is free item
        ItemCount int not null,
        ItemPrice decimal(18,8) not null,
        MaxItemQouta int not null, -- in generaly a package can have between 1 and 6 qouata but in rare can up to 20-25
        MaxFullQouta int not null -- sometimes a package can have additional free or discount item qouta. MaxFullQouta will always greater then MaxItemQouta
    )
    
    declare @totalStores int
    set @totalStores = (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN 200 AND 400 ORDER BY NEWID())
    
    declare @storeId int;
    declare @packageId  int;
    declare @maxPackageForStore int;
    declare @itemMinPrice decimal(18,8);
    set @storeId = 1;
    set @packageId = 1
    
    while(@storeId <= @totalStores)
        BEGIN
            set @maxPackageForStore = (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN 2 AND 6 ORDER BY NEWID())
            set @itemMinPrice = (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN 40 AND 100 ORDER BY NEWID())
                BEGIN
                    INSERT INTO storePackages (StoreId, ItemType, ItemCount, ItemPrice, MaxFullQouta, MaxItemQouta)
                    SELECT DISTINCT 
                        StoreId = @storeId
                    --,PackageId = CAST(@packageId + number AS int) 
                    ,ItemType = 0
                    ,ItemCount = number
                    ,ItemPrice = @itemMinPrice + (10 * (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN pkgNo.number AND pkgNo.number + 2  ORDER BY NEWID()))
                    ,MaxItemQouta = @maxPackageForStore
                    ,MaxFullQouta =  @maxPackageForStore + (CASE WHEN number > 1 AND number < 4 THEN 1 ELSE 0 END)
                    FROM master..[spt_values] pkgNo
                    WHERE number BETWEEN 1 AND @maxPackageForStore
                    UNION ALL
                    SELECT DISTINCT 
                        StoreId = @storeId
                    --,PackageId = CAST(@packageId + number AS int) 
                    ,ItemType = 1
                    ,ItemCount = 1
                    ,ItemPrice = (@itemMinPrice / 2) + (10 * (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN pkgNo.number AND pkgNo.number + 2  ORDER BY NEWID()))
                    ,MaxItemQouta = @maxPackageForStore
                    ,MaxFullQouta =  @maxPackageForStore  + (SELECT TOP 1 n = number FROM master..[spt_values] WHERE number BETWEEN 0 AND 2 ORDER BY NEWID())
                    FROM master..[spt_values] pkgNo
                    WHERE number BETWEEN 2 AND (CASE WHEN @maxPackageForStore > 4 THEN 4 ELSE @maxPackageForStore END)
    
    
                --set @packageId = @packageId + @maxPackageForStore;
                END
        set @storeId =@storeId + 1;
        END
    
    SELECT * FROM storePackages
    --drop table #storePackages
    

    没有 PackageId 只需 StoreId 和最低的组合成本 - 约 200-300 毫秒,具体取决于数据 接下来,如果您不关心里面有什么包并且您只希望每个商店有 1 行,您可以执行以下操作:

    IF OBJECT_ID('tempdb..#TestResults') IS NOT NULL
        BEGIN
            DROP TABLE #TestResults
        END
    
    DECLARE @MinItemCount INT = 5
    
    ;WITH cteMaxCostToConsider AS (
        SELECT
           StoreId
           ,CASE
              WHEN (SUM(ItemCount) >= @MinItemCount) AND
              SUM(ItemPrice) < MIN(((@MinItemCount / ItemCount) + IIF((@MinItemCount % ItemCount) > 0, 1,0)) * ItemPrice) THEN SUM(ItemPrice)
              ELSE MIN(((@MinItemCount / ItemCount) + IIF((@MinItemCount % ItemCount) > 0, 1,0)) * ItemPrice)
           END AS MaxCostToConsider
        FROM
           storePackages
        GROUP BY
           StoreId
    )
    
    , cteRecursive AS (
        SELECT
          StoreId
          ,ItemCount AS CombinedItemCount
          ,CAST(ItemPrice AS decimal(18,8)) AS CombinedCost
        FROM
           storePackages
    
        UNION ALL
    
        SELECT
          r.StoreId
          ,r.CombinedItemCount + t.ItemCount
          ,CAST(r.CombinedCost + t.ItemPrice AS decimal(18,8))
        FROM
           cteRecursive r
           INNER JOIN storePackages t
           ON r.StoreId = t.StoreId
           INNER JOIN cteMaxCostToConsider m
           ON r.StoreId = m.StoreId
          AND r.CombinedCost + t.ItemPrice <= m.MaxCostToConsider
    )
    
    SELECT
        StoreId
        ,MIN(CombinedCost) as CombinedCost
        INTO #TestResults
    FROM
        cteRecursive
    WHERE
        CombinedItemCount >= @MinItemCount
    GROUP BY
        StoreId
    
    SELECT *
    FROM
        #TestResults
    

    每个 StoreId 仅具有 PackageIds 1 条记录 - 根据要考虑的测试数据/组合而有很大差异 ~600-1300MS 或者,如果您仍然想要包 ID,但您不在乎选择哪种组合并且只想要 1 条记录,那么您可以这样做:

    IF OBJECT_ID('tempdb..#TestResults') IS NOT NULL
        BEGIN
            DROP TABLE #TestResults
        END
    
    DECLARE @MinItemCount INT = 5
    
    ;WITH cteMaxCostToConsider AS (
        SELECT
           StoreId
           ,CASE
              WHEN (SUM(ItemCount) >= @MinItemCount) AND
              SUM(ItemPrice) < MIN(((@MinItemCount / ItemCount) + IIF((@MinItemCount % ItemCount) > 0, 1,0)) * ItemPrice) THEN SUM(ItemPrice)
              ELSE MIN(((@MinItemCount / ItemCount) + IIF((@MinItemCount % ItemCount) > 0, 1,0)) * ItemPrice)
           END AS MaxCostToConsider
        FROM
           storePackages
        GROUP BY
           StoreId
    )
    
    , cteRecursive AS (
        SELECT
          StoreId
          ,CAST(PackageId AS VARCHAR(MAX)) AS PackageIds
          ,ItemCount AS CombinedItemCount
          ,CAST(ItemPrice AS decimal(18,8)) AS CombinedCost
        FROM
           storePackages
    
        UNION ALL
    
        SELECT
          r.StoreId
          ,r.PackageIds + ',' + CAST(t.PackageId AS VARCHAR(MAX))
          ,r.CombinedItemCount + t.ItemCount
          ,CAST(r.CombinedCost + t.ItemPrice AS decimal(18,8))
        FROM
           cteRecursive r
           INNER JOIN storePackages t
           ON r.StoreId = t.StoreId
           INNER JOIN cteMaxCostToConsider m
           ON r.StoreId = m.StoreId
          AND r.CombinedCost + t.ItemPrice <= m.MaxCostToConsider
    )
    
    , cteCombinedCostRowNum AS (
        SELECT
           StoreId
           ,PackageIds
           ,CombinedCost
           ,CombinedItemCount
           ,ROW_NUMBER() OVER (PARTITION BY StoreId ORDER BY CombinedCost) AS RowNumber
        FROM
           cteRecursive
        WHERE
           CombinedItemCount >= @MinItemCount
    )
    
    SELECT DISTINCT
        c.StoreId
        ,c.PackageIds
        ,c.CombinedItemCount
        ,c.CombinedCost
    INTO #TestResults
    FROM
        cteCombinedCostRowNum  c
    WHERE
        RowNumber = 1
    
    
    SELECT *
    FROM
        #TestResults
    

    请注意,所有基准测试都是在使用 4 年的笔记本电脑 Intel i7-3520M CPU 2.9 GHz、8 GB RAM 和 SAMSUNG 500 GB EVO SSD 上完成的。因此,如果您在资源适当的服务器上运行它,我预计会成倍增长。毫无疑问,在 storePackages 上添加索引也会加快答案。

    【讨论】:

    • 此解决方案比其他解决方案更快,但还不够。它也有一些错误。部分店铺未列出,部分店铺复制。
    • @MehmetOtkun 我发现了您提到的错误,并且商店没有列出因此。重复存储,因为 2 个东西 1 多于 1 行可能具有相同的最低成本每个存储和 2 个您生成的数据中的 packageid 在不应该被重复使用/重复的地方。至于您的 600-700 MS 2 以上答案在我使用的旧笔记本电脑的范围内。仅取决于您实际上想要查看多少数据,如果您想要的只是 storeid 和最低成本 200-300 MS。
    • @MehmetOtkun 如果我的回答或其他回答为您提供了所需的结果,请考虑接受它,以便问题显示为已回答并获得声誉积分。另外我很好奇,因为当我给你一些基准毫秒时,你从来没有提供过反馈......
    • 非常感谢您所做的一切。但是递归 CTE 在很多情况下会导致巨大的性能问题。我在上面写了自己的解决方案。
    【解决方案5】:

    我的解决方案

    首先,我感谢所有试图帮助我的人。然而,所有建议的解决方案都基于 CTE。正如我之前所说,当考虑数百个商店时,递归 CTE 会导致性能问题。一次还要求多个包裹。这意味着,一个请求可以包含多个篮子。一个是 5 个项目,另一个是 3 个项目,另一个是 7 个项目......

    最后的解决方案

    首先,我按项目大小在表格中生成所有可能的场景...通过这种方式,我可以选择消除不需要的场景。

    CREATE TABLE ItemScenarios(
       Item int,
       ScenarioId int,
       CalculatedItem int  --this will be joined with Store Item
    )
    

    然后我生成了从 2 项到 25 项的所有可能场景并插入到ItemScenarios 表中。场景可以通过使用 WHILE 或递归 CTE 生成一次。这种方式的好处是,场景只生成一次。

    结果如下。

    Item          |   ScenarioId       |     CalculatedItem
    --------------------------------------------------------
    2                   1                     2
    2                   2                     3
    2                   3                     1
    2                   3                     1
    3                   4                     5
    3                   5                     4
    3                   6                     3
    3                   7                     2
    3                   7                     2
    3                   8                     2
    3                   8                     1
    3                   9                     1
    3                   9                     1
    3                   9                     1
    ....
    .....
    ......
    25                  993                   10
    

    通过这种方式,我可以限制场景大小、最大不同商店、最大不同包等。

    我还可以排除一些在数学上不可能最便宜的场景。例如对于 4 个项目的请求,一些场景

    场景一:2+2

    场景二:2+1+1

    场景三:1+1+1+1

    在这些场景中;方案 2 不可能是最便宜的篮子。因为,

    如果 场景 2 场景 3 --> 场景 1 会低于 场景 2。因为降低成本的东西是 2 件价格,**场景 1* 有双 2 件

    如果 场景 2 场景 1 --> 场景 3 会低于 场景 2

    现在,如果我删除像 Scenario 2 这样的场景,我会获得一些性能优势。

    现在我可以在商店中选择最便宜的商品价格

    DECLARE @requestedItems int;
    SET @requestedItems = 5;
    
    CREATE TABLE #JoinedPackageItemWithScenarios(
       StoreId int not null,
       PackageId int not null,
       ItemCount int not null,
       ItemPrice decimal(18,8) 
       ScenarioId int not null,
    )
    INSERT INTO #JoinedPackageItemWithScenarios
     SELECT
        SPM.StoreId  
       ,SPM.PackageId 
       ,SPM.ItemCount 
       ,SPM.ItemPrice 
       ,SPM.ScenarioId
       FROM (
          SELECT 
                SP.StoreId  
               ,SP.PackageId 
               ,SP.ItemCount 
               ,SP.ItemPrice 
               ,SC.ScenarioId
               ,RowNumber = ROW_NUMBER() OVER (PARTITION BY SP.StoreId,SC.ScenarioId,SP.ItemCount ORDER BY SP.ItemPrice) 
          FROM ItemScenarios SC
          LEFT JOIN StorePackages AS SP ON SP.ItemCount = SC.CalculatedItem
          WHERE SC.Item = @requestedItems
     ) SPM
     WHERE SPM.RowNumber = 1
    
    -- NOW I HAVE CHEAPEST PRICE FOR EACH ITEM, I CAN CREATE BASKET
    
     CREATE TABLE #selectedScenarios(
       StoreId int not null,
       ScenarioId int not null,
       TotalItem int not null,
       TotalCost decimal(18,8) 
    )
     INSERT INTO #selectedScenarios
     SELECT 
          StoreId
         ,ScenarioId
         ,TotalItem 
         ,TotalCost 
      FROM (
         SELECT 
               StoreId
              ,ScenarioId
              --,PackageIds = dbo.GROUP_CONCAT(CAST(PackageId AS nvarchar(20))) -- CONCATENING PackageId decreasing performance here. We can joing seleceted scenarios with #JoinedPackageItemWithScenarios after selection complated.
              ,TotalItem = SUM(ItemCount)
              ,TotalCost = SUM(ItemPrice)
              ,RowNumber = ROW_NUMBER() OVER (PARTITION BY StoreId ORDER BY SUM(ItemPrice))
            FROM #JoinedPackageItemWithScenarios JPS
            GROUP BY StoreId,ScenarioId
            HAVING(SUM(ItemCount) >= @requestedItems)
         ) SLECTED
         WHERE RowNumber = 1
    
      -- NOW WE CAN POPULATE PackageIds if needed
    
      SELECT 
          SS.StoreId
         ,SS.ScenarioId
         ,TotalItem = MAX(SS.TotalItem)
         ,TotalCost = MAX(SS.TotalCost)
         ,PackageIds = dbo.GROUP_CONCAT(CAST(JPS.PackageId AS nvarchar(20)))
        FROM #selectedScenarios SS
        JOIN #JoinedPackageItemWithScenarios AS JPS ON JPS.StoreId = SS.StoreId AND JPS.ScenarioId = SS.ScenarioId
        GROUP BY SS.StoreId,SS.ScenarioId
    

    总和

    在我的测试中,这种方式至少比递归 CTE 快 10 倍,尤其是在商店数量和请求商品数量增加的情况下。它也得到 100% 正确的结果。因为当商店和请求的项目数量增加时,递归 CTE 尝试了数百万次不需要的 JOIN。

    【讨论】:

    • hmmm,我很好奇您的实际代码,这只是对步骤的描述,但并未显示完成某些关键代码的代码。因此,寻找类似问题的其他人无法复制该解决方案。另外,我相信我和其他回答的人都很想学习你的技术,如果它快得多的话。就目前而言,这是一个不完整的答案。
    • 如果您对我如何生成预定义场景感到好奇,这很容易。可以通过简单的递归 CTE 或 WHILE 子句场景来生成。其他步骤在我的解决方案中进行了完整编码。但是我的实际代码非常复杂。它有很多不同的参数。不过我会在短时间内添加场景生成代码。
    • 大声笑使用递归 cte... 我的主要好奇心是你如何确定通过代码消除哪些场景。我们的方法和您的方法之间的主要区别在于我们的方法不限于潜在的最大 N 个场景,因为您的方法受您尽可能预先定义的最大场景数量的限制。如果你在 1 家商店里有 50 件商品,而且都是 1 美元的最便宜的,那该怎么办?这意味着所有 50 件商品都可以作​​为最便宜的组合,并且 50 到最少商品的每个组合都可以使用。您在答案中允许了您的问题不允许的约束。
    • |o|我不反对递归 CTE。但就我而言,它的工作速度非常慢。此外,在我的项目中,95% 的请求项目在 2 到 7 之间。出于这个原因,约束对我很有用。而且我认为一切都有一些限制。通过订购价格确定最便宜的方案取决于“''ROW_NUMBER'''”。每个商店对于同一商品可以有不同的价格。意味着它将具有不同的 PackageId。在第一次订购时,我发现一件商品的价格最便宜。在第二阶段,我将商店和场景分组并按总价订购
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-12-28
    • 1970-01-01
    • 2016-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多