Search code examples
sqlsql-server

Queries to sum two columns as row in the same table


I have two tables Customers and Operations; I want to sum the data Withdraw and Deposit from operations table along with customer name from customer table.

I have created a stored procedure:

select 
    customer_id, customer_fullname, 
    amount_withdraw, amount_deposit, entry_date 
from 
    Operations b 
join 
    Customers a on b.customer_id2 = a.customer_id
where  
    customer_id = 19

This is the result I got:

enter image description here

But the result I want should look like this

enter image description here

Do I need two procedures or what?


Solution

  • You can do this with UNION so effectively join multiple queries into the same resultset:

    http://sqlfiddle.com/#!18/6f74f/2

    select customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date 
    from Operations b 
    JOIN Customers a on b.customer_id2 = a.customer_id
    where customer_id = 19
    
    UNION
    
    SELECT customer_id ,customer_fullname , SUM(amount_withdraw), SUM(amount_deposit) ,null 
    FROM Operations b 
    JOIN Customers a on b.customer_id2 = a.customer_id
    WHERE customer_id = 19
    GROUP BY customer_id ,customer_fullname
    
    UNION 
    
    SELECT customer_id ,customer_fullname , SUM(amount_deposit) - SUM(amount_withdraw) ,null, null 
    FROM Operations b 
    JOIN Customers a on b.customer_id2 = a.customer_id
    WHERE customer_id = 19
    GROUP BY customer_id ,customer_fullname
    

    Is it a good idea to do this... Not usually. Formatting and aggregates like this are better left to the application or reporting layer. The first problem with the above query is the order of the results, the second problem is that you are using the same column to return different sets of information.

    If you really want to do this in SQL, we can fix the order based on the nullability of the date column, but the syntax looks a bit strange. We can't use functions in the order by clause for UNION queries, so to do this we have to wrap the rollup data into either a sub-query or a CTE

    http://sqlfiddle.com/#!18/6f74f/1

    SELECT * FROM (
      select customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date 
    from Operations b 
    JOIN Customers a on b.customer_id2 = a.customer_id
    where customer_id = 19
    
    UNION
    
    SELECT customer_id ,customer_fullname , SUM(amount_withdraw), SUM(amount_deposit) ,null 
    FROM Operations b 
    JOIN Customers a on b.customer_id2 = a.customer_id
    WHERE customer_id = 19
    GROUP BY customer_id ,customer_fullname
    
    UNION 
    
    SELECT customer_id ,customer_fullname , SUM(amount_deposit) - SUM(amount_withdraw) ,null, null 
    FROM Operations b 
    JOIN Customers a on b.customer_id2 = a.customer_id
    WHERE customer_id = 19
    GROUP BY customer_id ,customer_fullname
    ) RollupData
    ORDER BY ISNULL(entry_date,'2099-12-31'), ISNULL(amount_deposit, 999999999)
    

    The sorting can be fixed a different way by adding an arbitrary column instead of relying on nullability of the entry_date makes it a bit more obvious, we can then remove the column from the final output if you are using the result directly, but often we would leave it in so that the sorting can be re-affirmed in the reporting layer. This shows how to remove it:

    Another term for this order column is a Discriminator as it allows us to identify which of the underlying queries the results came from.

    http://sqlfiddle.com/#!18/7022a/7

    SELECT customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date 
    FROM
    (
        select 1 as [order], customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date 
        from Operations b 
        JOIN Customers a on b.customer_id2 = a.customer_id
        where customer_id = 19
    
        UNION
    
        SELECT 2 as [order], customer_id ,customer_fullname , SUM(amount_withdraw), SUM(amount_deposit) ,null 
        FROM Operations b 
        JOIN Customers a on b.customer_id2 = a.customer_id
        WHERE customer_id = 19
        GROUP BY customer_id ,customer_fullname
    
        UNION 
    
        SELECT 3 as [order], customer_id ,customer_fullname , SUM(amount_deposit) - SUM(amount_withdraw) ,null, null 
        FROM Operations b 
        JOIN Customers a on b.customer_id2 = a.customer_id
        WHERE customer_id = 19
        GROUP BY customer_id ,customer_fullname
      
    ) RollupData
    ORDER BY [order]
    
    customer_id customer_fullname amount_withdraw amount_deposit entry_date
    19 Ali Ahmed Omar 56000 0 2023-01-02
    19 Ali Ahmed Omar 64000 0 2023-04-08
    19 Ali Ahmed Omar 32000 0 2023-06-02
    19 Ali Ahmed Omar 0 65000 2023-09-04
    19 Ali Ahmed Omar 0 78200 2023-10-25
    19 Ali Ahmed Omar 0 32000 2023-11-05
    19 Ali Ahmed Omar 152000 175200 (null)
    19 Ali Ahmed Omar 23200 (null) (null)

    If you wanted to package this into a stored procedure, then use this syntax:

    http://sqlfiddle.com/#!18/8a727/1

      CREATE PROCEDURE CustomerOperations (
        @customerId INT
      )
      AS
      BEGIN
      
      SELECT customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date 
    FROM
    (
        select 1 as [order], customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date 
        from Operations b 
        JOIN Customers a on b.customer_id2 = a.customer_id
        where customer_id = @customerId
    
        UNION
    
        SELECT 2 as [order], customer_id ,customer_fullname , SUM(amount_withdraw), SUM(amount_deposit) ,null 
        FROM Operations b 
        JOIN Customers a on b.customer_id2 = a.customer_id
        WHERE customer_id = @customerId
        GROUP BY customer_id ,customer_fullname
    
        UNION 
    
        SELECT 3 as [order], customer_id ,customer_fullname , SUM(amount_deposit) - SUM(amount_withdraw) ,null, null 
        FROM Operations b 
        JOIN Customers a on b.customer_id2 = a.customer_id
        WHERE customer_id = @customerId
        GROUP BY customer_id ,customer_fullname
      
    ) RollupData
    ORDER BY [order], entry_date
    
    END
    

    A more standard solution would be to move the aggregates to other columns, you could also use window queries, but those would have a different layout to your request.

    A simple window query solution:

    http://sqlfiddle.com/#!18/6f74f/4

    select customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date 
         , SUM(amount_withdraw) OVER (PARTITION BY customer_id) as Total_withdraw
         , SUM(amount_deposit) OVER (PARTITION BY customer_id) as Total_deposit
         , SUM(amount_deposit - amount_withdraw) OVER (PARTITION BY customer_id) as Overall_Balance
    from Operations b 
    JOIN Customers a on b.customer_id2 = a.customer_id
    where customer_id = 19
    ORDER BY customer_id, entry_date
    
    customer_id customer_fullname amount_withdraw amount_deposit entry_date Total_withdraw Total_deposit Overall_Balance
    19 Ali Ahmed Omar 56000 0 2023-01-02 152000 175200
    19 Ali Ahmed Omar 64000 0 2023-04-08 152000 175200
    19 Ali Ahmed Omar 32000 0 2023-06-02 152000 175200
    19 Ali Ahmed Omar 0 65000 2023-09-04 152000 175200
    19 Ali Ahmed Omar 0 78200 2023-10-25 152000 175200
    19 Ali Ahmed Omar 0 32000 2023-11-05 152000 175200

    You can still use UNION to produce a similar result, however when we do this, it is really usefull to keep the discriminator column so we know what each row represents:

    http://sqlfiddle.com/#!18/11d586/4

    SELECT [Order] as LineType, customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date , Total_withdraw, Total_deposit, Overall_Balance 
    FROM
    (
        select 1 as [order], customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date, null as Total_withdraw, null as Total_deposit, null as Overall_Balance
        from Operations b 
        JOIN Customers a on b.customer_id2 = a.customer_id
        where customer_id = 19
    
        UNION
    
        SELECT 2 as [order], customer_id ,customer_fullname , null, null, null, SUM(amount_withdraw), SUM(amount_deposit) ,null 
        FROM Operations b 
        JOIN Customers a on b.customer_id2 = a.customer_id
        WHERE customer_id = 19
        GROUP BY customer_id ,customer_fullname
    
        UNION 
    
        SELECT 3 as [order], customer_id ,customer_fullname , null, null, null, null, null, SUM(amount_deposit) - SUM(amount_withdraw) 
        FROM Operations b 
        JOIN Customers a on b.customer_id2 = a.customer_id
        WHERE customer_id = 19
        GROUP BY customer_id ,customer_fullname
      
    ) RollupData
    ORDER BY [order], [entry_date]
    
    LineType customer_id customer_fullname amount_withdraw amount_deposit entry_date Total_withdraw Total_deposit Overall_Balance
    1 19 Ali Ahmed Omar 56000 0 2023-01-02 (null) (null) (null)
    1 19 Ali Ahmed Omar 37000 0 2023-03-02 (null) (null) (null)
    1 19 Ali Ahmed Omar 64000 0 2023-04-08 (null) (null) (null)
    1 19 Ali Ahmed Omar 32000 0 2023-06-02 (null) (null) (null)
    1 19 Ali Ahmed Omar 0 65000 2023-09-04 (null) (null) (null)
    1 19 Ali Ahmed Omar 0 78200 2023-10-25 (null) (null) (null)
    1 19 Ali Ahmed Omar 0 32000 2023-11-05 (null) (null) (null)
    2 19 Ali Ahmed Omar (null) (null) (null) 189000 175200 (null)
    3 19 Ali Ahmed Omar (null) (null) (null) (null) (null) -13800

    Finally, we need to talk abot efficiency. Given that your aggregate rows apply to the original query, and you are using stored procedures, we can use temporary tables to store the results of the initial query and then we can query against that recordset directly rather than back into the database.

    • This is can be thought of as similar to how Window Query functions work

    https://dbfiddle.uk/URG52cH1 (Dot Net Fiddle stopped working ;)

    CREATE PROCEDURE CustomerOperations (
        @customerId INT
    )
    AS
    BEGIN
      
      -- Select the record set into a temporary table
      SELECT customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date 
      INTO #CustomerOperationsTemp
      FROM Operations b 
      INNER JOIN Customers a on b.customer_id2 = a.customer_id
      WHERE customer_id = @customerId
        
      SELECT customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date 
      FROM
      (
        SELECT 1 as [order], customer_id ,customer_fullname , amount_withdraw ,amount_deposit ,entry_date 
        FROM #CustomerOperationsTemp
    
        UNION
    
        SELECT 2 as [order], customer_id ,customer_fullname , SUM(amount_withdraw), SUM(amount_deposit) ,null 
        FROM #CustomerOperationsTemp
        GROUP BY customer_id ,customer_fullname
    
        UNION 
    
        SELECT 3 as [order], customer_id ,customer_fullname , SUM(amount_deposit) - SUM(amount_withdraw) ,null, null 
        FROM #CustomerOperationsTemp
        GROUP BY customer_id ,customer_fullname 
      ) RollupData
      ORDER BY [order], entry_date
    
    END
    

    This is effectively the same workflow that a reporting or application layer would typically use:

    1. Query the data
    2. Apply Aggregates
    3. Format the data