在 SQL Server 中选择随机行,直到列的总和达到目标。

huangapple go评论64阅读模式
英文:

Select random rows till the sum of column reaches the target in sql server

问题

我需要随机显示的行,直到达到目标,如果有可能的组合,它应该达到目标。

如果目标是30,它应该返回:

id   price
1     20
5     10

或

id   price
2     30

如果目标超过总和或没有组合--在这种情况下为-120,它应该返回到最后的最大值:

Id  price 
1    20
2    30
3    40
4    15
5    10

如果目标小于总和--在这种情况下为5,它应该返回空:

Id  price

我尝试过的:

select t.* (select q.*, sum(q.price) over(order by newid()) as total from Orders q) t where t.total <= 45

我希望在达到确切的目标之前显示行,如果没有可能的组合可以达到确切的目标,那么它只能返回到最接近目标的值。

这个查询有时候没有给我确切的行。

英文:

I need the random rows to be displayed till the target reaches
it should reach the target if there is any possible combination is there.

    Id  price 
    1    20
    2    30
    3    40
    4    15
    5    10
.......200+ rows

If the target is 30 it should return with

id   price
1     20
5     10

or

id   price
2     30

If the target exceeds the sum or no combinations --120 in this case it should return till last maximum

Id  price 
1    20
2    30
3    40
4    15
5    10

If the target is less than the sum --5 in this case it should return nothing

Id  price

what i tried

select t.*(select q.*,sum(q.price) over(order by newid())as total from Orders q)t
where t.total&lt;=45

i want rows till the exact target is reached
if there is no possible combinations of getting the exact target then only it has to return till the nearest value to the target

this query is giving me the rows not exactly all the time

答案1

得分: 1

我同意@siggemannen的观点,你应该针对非常小的人口做这个操作。不管怎样,这是一个不错的练习,这里我正在处理四种情况。

  • 如果SUM(Price) < TargetPrice,则返回所有
  • 如果MIN(Price) < TargetPrice,则返回空
  • 计算可能在不同行之间的SUM(Price)的所有可能组合。如果有一个组合等于TargetPrice,则返回它。否则返回所有。

所以代码看起来像这样:

DECLARE @table TABLE (ID INT, Price MONEY)
INSERT INTO @table (ID, Price) VALUES
(1, 20), (2, 30), (3, 40), (4, 15), (5, 10)
DECLARE @TargetPrice MONEY = 30
IF @TargetPrice > (SELECT SUM(Price) FROM @table) --如果目标超过总和
BEGIN
    SELECT * 
    FROM @table
END
ELSE IF @TargetPrice < (SELECT MIN(Price) FROM @table) --如果目标小于最小值
BEGIN
    SELECT TOP 0 *
    FROM @table
END
ELSE 
BEGIN
    ;WITH cte AS ( --递归查找所有可能的组合
        SELECT id, Price, CONVERT(VARCHAR(MAX), Id) IdLst
        FROM @table
        UNION ALL 
        SELECT t.id, cte.Price + t.Price AS Price, cte.IdLst + '|' + CONVERT(VARCHAR(MAX), t.Id) IdLst
        FROM cte 
        INNER JOIN @table t 
            ON cte.ID < t.ID
    ), EqCombinations AS (  --仅保留匹配目标价格的第一个组合
        SELECT *, ROW_NUMBER() OVER (ORDER BY IDLst)  rn
        FROM cte
        WHERE cte.Price = @TargetPrice 
    ), Eq AS (  --选择第一个
        SELECT t.* 
        FROM EqCombinations
        CROSS APPLY (SELECT * FROM STRING_SPLIT(IDLst, '|')) p
        INNER JOIN @table t
            ON p.value = t.ID
        WHERE rn = 1
    )
    SELECT * , 'eq' why FROM Eq  --如果匹配到目标
    --否则返回所有
    UNION ALL SELECT * , 'all' why FROM @table WHERE NOT EXISTS(SELECT 1 FROM Eq)
END

希望这有助于你理解代码的翻译。

英文:

I aggre with @siggemannen that you should do this for very small populations.
Anyway is a nice excercise, here I'm addressing the 4 scenarios.

  • If SUM(Price) < TargetPrice return all
  • IF MIN(Price) < TargetPrice return nothing
  • Calculate all posible combinations of possible SUM(Price) between different rows. IF there is one combination equals to TargetPrice THEN return it. ELSE return all

So the code look like:

DECLARE @table TABLE (ID INT, Price money )--DECIMAL(10,4))
INSERT INTO @table (ID, Price) VALUES
(1, 20), (2, 30), (3, 40), (4, 15), (5, 10)
DECLARE @TargetPrice Money = 30
IF @TargetPrice &gt; (SELECT SUM(Price) FROM @table) --If the target exceeds the sum
BEGIN
SELECT * 
FROM @table
END
ELSE IF @TargetPrice &lt; (SELECT MIN(Price) FROM @table) --If the target is less than the sum
BEGIN
SELECT TOP 0 *
FROM @table
END
ELSE 
BEGIN
;WITH cte AS ( -- find all possible combinations recursively 
SELECT id, Price ,  CONVERT(VARCHAR(MAX),Id) IdLst
FROM @table
UNION ALL 
SELECT t.id, cte.Price + t.Price  as Price, cte.IdLst + &#39;|&#39; + CONVERT(VARCHAR(MAX),t.Id)   IDLst
FROM cte 
INNER JOIN @table t 
on cte.ID &lt; t.ID
), EqCombinations AS (  -- keep only the first one that matches the target price 
SELECT *, ROW_NUMBER() OVER (ORDER BY IDLst)  rn
FROM cte
WHERE cte.Price = @TargetPrice 
), Eq AS (  -- pick the first one
SELECT t.* 
FROM EqCombinations
CROSS APPLY (SELECT * FROM STRING_SPLIT(IDLst, &#39;|&#39;) parts) p
INNER JOIN @table t
ON p.value = t.ID
WHERE rn = 1
)
SELECT * , &#39;eq&#39; why FROM Eq  -- if the target was matched
-- else return all
UNION ALL SELECT * , &#39;all&#39; why FROM @table WHERE NOT EXISTS(SELECT 1 FROM Eq)
END

答案2

得分: 0

以下是翻译好的部分:

DECLARE @table TABLE (ID INT, Price DECIMAL(10,4))
INSERT INTO @table (ID, Price) VALUES
(1, 20), (2, 30), (3, 40), (4, 15), (5, 10)

这是将您的纯文本数据转换为可重现的DDL/DML语句非常有帮助。

这是一个有趣的挑战,我不确定我是否准确解释了您的要求,但这是我理解的内容:

选择一个随机行,检查新的累计总数是否小于某个值,如果小于,则获取另一行(但是必须是不同的行),然后再次检查。重复这个过程,直到第一行使累计总数超过检查值。

;WITH MinMax AS (
SELECT MIN(ID) AS minID, MAX(ID) AS maxID
  FROM @table
)

SELECT ID, Price	
  FROM (
        SELECT a.ID, a.Price, a.rn, SUM(Price) OVER (ORDER BY rn) AS rTotal
          FROM (
                SELECT t.ID, t.Price, ROW_NUMBER() OVER (ORDER BY RAND(CONVERT(VARBINARY,NEWID(),1))) AS rn
                  FROM (VALUES (
                                RAND(CONVERT(VARBINARY,NEWID(),1))
                			   )
                	   ) A(Rnd1)
                    CROSS APPLY (SELECT minID, maxID FROM MinMax) mm
                	CROSS APPLY (SELECT TOP 100 1 AS z FROM sys.system_objects a CROSS APPLY sys.system_objects) z
                	LEFT OUTER JOIN @table t
                	  ON ROUND(((maxID - minID) * Rnd1 + minID), 0) = t.ID
                 GROUP BY t.ID, t.Price
               ) a
       ) a
 WHERE rTotal &lt; = 50

在这里,我们使用一些神秘的方法来查找行并按随机顺序排列它们。接下来,我们应用窗口函数ROW_NUMBER,以便获得一个列来保留该顺序。然后,我们可以使用窗口函数SUM来获得在该随机顺序中这些行的累计总数,最后,我们可以比较该顺序中每行的累计总数与检查值。

示例输出:

ID Price
2 30.0000
4 15.0000
ID Price
5 10.0000
1 20.0000
4 15.0000
英文:

I took your plain text data and turned it into reproducible DDL/DML:

DECLARE @table TABLE (ID INT, Price DECIMAL(10,4))
INSERT INTO @table (ID, Price) VALUES
(1, 20), (2, 30), (3, 40), (4, 15), (5, 10)

It's really helpful to have this in your question.

This was an interesting challenge to solve, I'm not sure I interpreted your requirement exactly, but here's what I understood:

Select a random row, check the new running total against a value, and if it's less get another random (but distinct!) row and check again. Repeat until the first row which puts the running total over the check value.

;WITH MinMax AS (
SELECT MIN(ID) AS minID, MAX(ID) AS maxID
  FROM @table
)

SELECT ID, Price	
  FROM (
        SELECT a.ID, a.Price, a.rn, SUM(Price) OVER (ORDER BY rn) AS rTotal
          FROM (
                SELECT t.ID, t.Price, ROW_NUMBER() OVER (ORDER BY RAND(CONVERT(VARBINARY,NEWID(),1))) AS rn
                  FROM (VALUES (
                                RAND(CONVERT(VARBINARY,NEWID(),1))
                			   )
                	   ) A(Rnd1)
                    CROSS APPLY (SELECT minID, maxID FROM MinMax) mm
                	CROSS APPLY (SELECT TOP 100 1 AS z FROM sys.system_objects a CROSS APPLY sys.system_objects) z
                	LEFT OUTER JOIN @table t
                	  ON ROUND(((maxID - minID) * Rnd1 + minID), 0) = t.ID
                 GROUP BY t.ID, t.Price
               ) a
       ) a
 WHERE rTotal &lt; = 50

So here we use some voodoo to find rows, and put them into a random order. Next we apply the windowed function ROW_NUMBER to give is a column to preserve that order. Next we can use the windowed function SUM to get a running total, of those rows, in that random order and finally we can compare the running total of each row, in that order, to the check value.

Example output:

ID Price
2 30.0000
4 15.0000
ID Price
5 10.0000
1 20.0000
4 15.0000

huangapple
  • 本文由 发表于 2023年2月8日 23:54:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/75388379.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定