Humdum
Humdum

Reputation: 113

How to get rows having sum equal to given value

There is table contain

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

Now if i had to choose rows where sum of Qty equals to 10, How can i do this ?

like 2+4+1 = 7 but if i add 5 then 12

so ignore 2, then 4+1+5 = 10

How can i achieve this ?

Edit:

I want any possible combination which sums up to gives value. suppose 7 then any rows which sums up to 7 like wise if 8 then any rows sums up to 8

want row/rows having combination equals to given value.

Upvotes: 9

Views: 12028

Answers (4)

balexandre
balexandre

Reputation: 75103

When you are dealing with such task in SQL you need to go to the cursors approach.

Cursors let you perform row by row operations and that's what you need, for example:

  • You want to make groups of where the summed quantity is 10

This are the tasks

  • select all the table into a temp table #TBL_ALL
  • loop row by row and when the rows sum up 10, delete from the temp table and insert into a final temp table #TBL_FINAL
  • do this until the #TBL_ALL sum cant be performed

An example:

Test table:

/****** 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

Test Data:

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;

Store procedure:

http://pastebin.com/EFeZcKXf

Results:

Grouped table:

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

Numbers not used:

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

Hope it helps


SP for those who cant access 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. I'm not a SQL Professional, so bear with me if I did something weird, and keep in mind that this is a performance waste, maybe someone can use the the Loop without Cursors, more in this page

Upvotes: 2

Martin B
Martin B

Reputation: 24170

The problem you want to solve is called the subset sum problem. Unfortunately, it is NP-complete.

This means that, whether you use SQL or any other language to solve it, you will only be able to solve very small instances of the problem, i.e. ones with only a few entries in the table. Otherwise, the runtime will become excessive, since it grows exponentially with the number of rows in the table. The reason for this is that there is essentially no better way of finding the solution than to try all possible combinations.

If an approximate solution is acceptable, there is a polynomial time algorithm, which is described on the Wikipedia page.

Upvotes: 6

gbn
gbn

Reputation: 432451

If you want it to be exactly always three numbers that add to 10, then this

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

If you want 2 or 4 or 5 numbers then you can't really do it in SQL

Edit:

  • Updated to ignore by ID not Qty
  • And again: If you want 2 or 4 or 5 numbers then you can't really do it in SQL

Upvotes: 3

Piotr Auguscik
Piotr Auguscik

Reputation: 3681

If u add always 3 numbers its like gbn said, if not then u have to check every combination of ur rows which gives u 2 to the number_of_rows power cobinations and i dont see how can it be done on sql in one query. If u use loop in sql sure its possible but u should find some good algorithm to finish this task.

Upvotes: -1

Related Questions