【问题标题】:How to get rows having sum equal to given value如何获得总和等于给定值的行
【发布时间】:2011-06-09 07:12:56
【问题描述】:

有表包含

ID     Qty
----------
1       2
2       4
3       1
4       5

现在,如果我必须选择数量总和等于 10 的行, 我该怎么做?

喜欢 2+4+1 = 7 但如果我加 5 那么 12

所以忽略 2,然后 4+1+5 = 10

我怎样才能做到这一点?

编辑:

我想要总结出价值的任何可能的组合。 假设 7 然后总和为 7 的任何行 同样,如果为 8,则任何行的总和为 8

希望行/行的组合等于给定值。

【问题讨论】:

  • 你想加多少个数字?总是 3?
  • 只有一种解决方案吗?如果有多个可能的解决方案,您是想要所有选项还是只想要遇到的第一个选项?
  • 你能解释一下你需要这个做什么吗?也许还有另一种方法可以实现您所描述的内容
  • 不太可能让 SQL Server 在合理的时间内解决NP-complete problems

标签: sql sql-server


【解决方案1】:

您要解决的问题称为subset sum 问题。不幸的是,它是NP-complete

这意味着,无论您使用 SQL 还是任何其他语言来解决它,您都只能解决问题的非常小的实例,即表中只有几个条目的实例。否则,运行时间将变得过多,因为它随着表中的行数呈指数增长。这样做的原因是,基本上没有比尝试所有可能的组合更好的方法来找到解决方案。

如果一个近似解是可以接受的,有一个多项式时间算法,在维基百科页面上有描述。

【讨论】:

    【解决方案2】:

    如果你希望它总是三个加 10 的数字,那么这个

    SELECT
      *
    FROM 
      MyTable t1
      JOIN
      MyTable t2 ON t1.ID <> t2.ID 
      JOIN
      MyTable t3 ON t1.ID <> t3.ID AND t2.ID <> t3.ID
    WHERE
      t1.Qty + t2.Qty + t3.Qty = 10
    

    如果你想要 2 个或 4 个或 5 个数字,那么你不能在 SQL 中真正做到

    编辑:

    • 更新为按 ID 而非数量忽略
    • 再说一遍:如果您想要 2 个或 4 个或 5 个数字,那么您无法在 SQL 中真正做到这一点

    【讨论】:

    • 并不总是 3 个数字,它们可以是任意组合,我想要总和等于用户传递的数字的行
    • 您可以编写一个存储过程,将上面的存储过程动态写入 (n) 个条目以相加然后执行它。从 1 个条目开始,检查是否返回任何行。如果没有行,则为 2 个条目动态生成 SQL。迭代到表中的行数。如果 (n) 项的总和有多个解决方案,不确定你会怎么做
    • @Humdum:除非指定上限,否则在 SQL 中确实不实用
    • @balexandre:我无法在工作中访问 pastebin,所以今晚我会尝试更多行...
    【解决方案3】:

    当您在 SQL 中处理此类任务时,您需要使用游标方法。

    游标让您可以逐行执行操作,而这正是您所需要的,例如:

    • 您想将总和为 10 的组进行分组

    这是任务

    • 选择所有表进入临时表#TBL_ALL
    • 逐行循环,当行总和为 10 时,从临时表中删除并插入到最终临时表中 #TBL_FINAL
    • 执行此操作直到无法执行 #TBL_ALL 总和

    一个例子:

    测试表:

    /****** Object:  Table [dbo].[tblExample]    Script Date: 06/09/2011 11:25:27 ******/
    SET ANSI_NULLS ON
    GO
    
    SET QUOTED_IDENTIFIER ON
    GO
    
    CREATE TABLE [dbo].[tblExample](
        [Id] [int] IDENTITY(1,1) NOT NULL,
        [Qty] [int] NOT NULL,
     CONSTRAINT [PK_tblExample] PRIMARY KEY CLUSTERED 
    (
        [Id] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    

    测试数据:

    INSERT INTO tblExample SELECT 2;
    INSERT INTO tblExample SELECT 4;
    INSERT INTO tblExample SELECT 1;
    INSERT INTO tblExample SELECT 5;
    INSERT INTO tblExample SELECT 5;
    INSERT INTO tblExample SELECT 11;
    INSERT INTO tblExample SELECT 1;
    INSERT INTO tblExample SELECT 2;
    INSERT INTO tblExample SELECT 3;
    INSERT INTO tblExample SELECT 4;
    INSERT INTO tblExample SELECT 7;
    INSERT INTO tblExample SELECT 9;
    INSERT INTO tblExample SELECT 1;
    INSERT INTO tblExample SELECT 2;
    

    存储过程:

    http://pastebin.com/EFeZcKXf

    结果:

    分组表:

    ids qty group
    12  9   1
    7   1   1
    11  7   2
    9   3   2
    4   5   3
    5   5   3
    2   4   4
    10  4   4
    14  2   4
    

    未使用的数字:

    id  qty 
    1   2
    8   2
    3   1
    13  1
    6   11
    

    希望对你有帮助


    SP 给那些无法访问 PasteBin 的人

    CREATE PROCEDURE getGroups
    (
        @groupByQty int, -- grouping number
        @numberRuns int  -- how many loops
    
        -- usage: getGroups 10, 10
    )
    AS
    
    SET NOCOUNT ON;
    
    -- declare all variables
    DECLARE  @rowId      int,
             @rowQty     int,
             @rowTotal   int,
             @groupId    int,
             @totalRuns  int,
             @continue   bit
    
    -- set up our final temporary table
    CREATE TABLE #TBL_COUNT
    (
      ids NVARCHAR(4000),
      qty int,
      [group] int
    )
    
    -- initializate variable
    SET @groupId = 1;
    SET @continue = 1;
    SET @totalRuns = 0;
    SELECT Id, Qty INTO #TBL_ALL FROM tblExample ORDER BY Qty DESC;
    
    WHILE @totalRuns <= @numberRuns 
    BEGIN
        -- declare the cursor
        DECLARE Product CURSOR FOR SELECT Id, Qty FROM #TBL_ALL ORDER BY Qty DESC;
    
        OPEN Product;
        FETCH Product INTO @rowId, @rowQty;
    
        PRINT ' ';
        PRINT '### Run: ' + CAST(@totalRuns AS nvarchar(10)) + ' #################################################################';
        PRINT 'Grouping Table by ' + CAST(@groupByQty AS nvarchar(10)) + ' | group id = ' + CAST(@groupId AS nvarchar(10));
    
        -- Retrieve and process the first row
    
        SELECT Top 1 @rowId = Id, @rowQty = Qty FROM #TBL_ALL ORDER BY Qty DESC;
        PRINT 'First Row: id = ' + CAST(@rowId AS nvarchar(10)) + ' | qty = ' + CAST(@rowQty AS nvarchar(10));
    
        -- sum it up and see if we have @groupByQty
        SELECT @rowTotal = ISNULL(SUM(qty),0) FROM #TBL_COUNT WHERE [group] = @groupId;
        PRINT 'Current sum in #TBL_COUNT: @groupId = '+ CAST(@groupId AS nvarchar(10)) +' | @rowTotal = ' + CAST(@rowTotal AS nvarchar(10)) + ' | (@rowTotal + @rowQty) = ' + CAST((@rowTotal + @rowQty) AS nvarchar(10));
    
        IF @rowQty > @groupByQty
        BEGIN
            PRINT '  x First row has an unused number';
        END
        ELSE
        BEGIN
          -- handle result
          IF (@rowTotal + @rowQty) = @groupByQty
          BEGIN
    
            PRINT '+++ Current sum is ' + CAST(@groupByQty AS nvarchar(10)) + ' +++';
    
            -- save number
            INSERT INTO #TBL_COUNT SELECT @rowId, @rowQty, @groupId;
            PRINT '### Inserted final # into #TBL_COUNT : id = ' + CAST(@rowId AS nvarchar(10)) + ' | qty = ' + CAST(@rowQty AS nvarchar(10)) + ' | group = ' + CAST(@groupId AS nvarchar(10));
    
            -- remove from table as we use it already
            DELETE FROM #TBL_ALL WHERE Id = @rowId;
    
            -- we got 10, let's change our Groupping
            SET @groupId = (@groupId + 1);
    
            PRINT 'New group id: ' + CAST(@groupId AS nvarchar(10));
    
          END
          ELSE 
          BEGIN   
            IF (@rowTotal + @rowQty) < @groupByQty
            BEGIN
                PRINT '### Inserted into #TBL_COUNT : id = ' + CAST(@rowId AS nvarchar(10)) + ' | qty = ' + CAST(@rowQty AS nvarchar(10)) + ' | group = ' + CAST(@groupId AS nvarchar(10));
    
                -- save number 
                INSERT INTO #TBL_COUNT SELECT @rowId, @rowQty, @groupId;
    
                -- remove from table as we use it already
                DELETE FROM #TBL_ALL WHERE Id = @rowId;
            END
            ELSE
            BEGIN
                PRINT '  x Unmatch number, will handle this latter';
            END
          END
        END 
    
        -- start the main processing loop
        WHILE @@Fetch_Status = 0
           BEGIN
    
              FETCH Product INTO @rowId, @rowQty;
              PRINT '@@Fetch_Status = ' + CAST(@@Fetch_Status AS nvarchar(100));
    
              IF @@Fetch_Status < 0
              BEGIN
                BREAK
              END
    
              -- we have the values of our row, let's use them
              PRINT 'Fetched Row: id = ' + CAST(@rowId AS nvarchar(10)) + ' | qty = ' + CAST(@rowQty AS nvarchar(10));
    
              -- sum it up and see if we have @groupByQty
              SELECT @rowTotal = ISNULL(SUM(qty),0) FROM #TBL_COUNT WHERE [group] = @groupId;
              PRINT 'Current sum in #TBL_COUNT: @groupId = '+ CAST(@groupId AS nvarchar(10)) +' | @rowTotal = ' + CAST(@rowTotal AS nvarchar(10)) + ' | (@rowTotal + @rowQty) = ' + CAST((@rowTotal + @rowQty) AS nvarchar(10));
    
              -- handle result
              IF (@rowTotal + @rowQty) = @groupByQty
              BEGIN
    
                PRINT '+++ Current sum is ' + CAST(@groupByQty AS nvarchar(10)) + ' +++';
    
                -- save number
                INSERT INTO #TBL_COUNT SELECT @rowId, @rowQty, @groupId;
                PRINT '### Inserted final # into #TBL_COUNT : id = ' + CAST(@rowId AS nvarchar(10)) + ' | qty = ' + CAST(@rowQty AS nvarchar(10)) + ' | group = ' + CAST(@groupId AS nvarchar(10));
    
                -- remove from table as we use it already
                DELETE FROM #TBL_ALL WHERE Id = @rowId;
    
                -- we got 10, let's change our Groupping
                SET @groupId = (@groupId + 1);
    
                PRINT 'New group id: ' + CAST(@groupId AS nvarchar(10));
    
                -- start again
                BREAK; 
    
              END
              ELSE 
              BEGIN   
                IF (@rowTotal + @rowQty) < @groupByQty
                BEGIN
                    PRINT '### Inserted into #TBL_COUNT : id = ' + CAST(@rowId AS nvarchar(10)) + ' | qty = ' + CAST(@rowQty AS nvarchar(10)) + ' | group = ' + CAST(@groupId AS nvarchar(10));
    
                    -- save number 
                    INSERT INTO #TBL_COUNT SELECT @rowId, @rowQty, @groupId;
    
                    -- remove from table as we use it already
                    DELETE FROM #TBL_ALL WHERE Id = @rowId;
                END
                ELSE
                BEGIN
                    PRINT '  x Unmatch number, will handle this latter';
                END
              END
    
           END -- END WHILE @@Fetch_Status = 0
    
           SET @totalRuns = @totalRuns + 1;
    
           -- Close and dealocate
           CLOSE Product;
           DEALLOCATE Product;
    
       END -- END WHILE totalRuns <= @numberRuns
    
       -- let's sum our last group and remove it if it's not @groupByQty
       SELECT @rowTotal = ISNULL(SUM(qty),0) FROM #TBL_COUNT WHERE [group] = @groupId;
       IF @rowTotal <> @groupByQty
       BEGIN
        SET IDENTITY_INSERT #TBL_ALL ON
        INSERT INTO #TBL_ALL (Id, Qty) SELECT Ids, Qty FROM #TBL_COUNT WHERE [group] = @groupId;
        DELETE FROM #TBL_COUNT WHERE [group] = @groupId;
       END
    
    SET NOCOUNT OFF;
    
    -- Show and Delete temp tables
    SELECT * FROM #TBL_COUNT;
    SELECT * FROM #TBL_ALL;
    DROP TABLE #TBL_COUNT;
    DROP TABLE #TBL_ALL;
    

    P.S.我不是 SQL 专业人士,所以如果我做了一些奇怪的事情,请多多包涵,并记住这是一种性能浪费,也许有人可以使用没有游标的循环, more in this page

    【讨论】:

    • 我不完全理解代码,但我有点怀疑它是否能在一般情况下工作,因为它似乎使用了一种贪婪的策略,可能会误导它做出错误的决定(也因为它似乎是一个多项式时间算法,并且子集和是 NP 完全的)。例如,它是否正确处理以下数字序列(期望总和为 10):2、7、5、3?
    • 试试你自己,打开一个数据库,创建一个表,插入数据并运行SP,这不是100%,因为我没有花太多时间,但它是可行的。对于您的示例:是的,因为它按qty 对数据进行排序,所以数字将是 7、5、3、2:processing: keep 7; 7+5 > 10 = 放电 5; 7+3 = 10 -> 复制到临时; 0+2 保持 2;环形; 2+5 保持 5;结尾;如果总和为 10,则删除最后一组;
    • 我错过了关于排序的一点......但我仍然认为“贪婪”会产生不必要的影响。再举一个例子,这个序列(同样期望总和为 10)如何:5、4、4、2?据我了解该算法,它将选择并保留 5 和 4(因为它们的总和仍小于 10),然后拒绝其他 4 和 2,因为它们都会导致总和溢出......
    • 你说得对,现在它不会这样做,但我可以轻松更改流程以释放5 并继续使用第二个数字,依此类推。我确实认为这是一项容易的任务。再说一次,我不是 SQL Pro。
    【解决方案4】:

    如果你总是添加 3 个数字,就像 gbn 说的那样,如果不是,那么你必须检查你的行的每个组合,这给你 2 到 number_of_rows 电源组合,我不知道如何在一个查询中在 sql 上完成它。如果你在 sql 中使用循环确定它是可能的,但你应该找到一些好的算法来完成这个任务。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-12-28
      • 1970-01-01
      • 2023-01-18
      • 2019-09-23
      • 1970-01-01
      • 2016-08-26
      • 1970-01-01
      相关资源
      最近更新 更多