【发布时间】: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 倍。
同时连接Item 或PackageId 非常昂贵。在选择最便宜的包并加入源表后,我可以找到所需的包 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。
【问题讨论】:
-
嗯?您是通过将
PackageId或Itemid 相加来测量篮子中的物品数量吗?这对我来说没有意义。 -
'PackageId' 不重要
-
@MehmetOtkun 在我看来 PackageId 非常重要。因为 item 似乎是包裹中物品的数量。所以 PackageId 将是查询时如何识别该包。
-
只有当篮子中的 PakageID 数量有限时,它才能被 TSQL 解决。否则应该解决我的一些算法优化方法,如“单纯形法”
标签: sql sql-server group-by cube rollup