Running total by grouped records in table - sql

I have a table like this (Oracle, 10)
Account Bookdate Amount
1 20080101 100
1 20080102 101
2 20080102 200
1 20080103 -200
...
What I need is new table grouped by Account order by Account asc and Bookdate asc with a running total field, like this:
Account Bookdate Amount Running_total
1 20080101 100 100
1 20080102 101 201
1 20080103 -200 1
2 20080102 200 200
...
Is there a simple way to do it?
Thanks in advance.

Do you really need the extra table?
You can get that data you need with a simple query, which you can obviously create as a view if you want it to appear like a table.
This will get you the data you are looking for:
select
account, bookdate, amount,
sum(amount) over (partition by account order by bookdate) running_total
from t
/
This will create a view to show you the data as if it were a table:
create or replace view t2
as
select
account, bookdate, amount,
sum(amount) over (partition by account order by bookdate) running_total
from t
/
If you really need the table, do you mean that you need it constantly updated? or just a one off? Obviously if it's a one off you can just "create table as select" using the above query.
Test data I used is:
create table t(account number, bookdate date, amount number);
insert into t(account, bookdate, amount) values (1, to_date('20080101', 'yyyymmdd'), 100);
insert into t(account, bookdate, amount) values (1, to_date('20080102', 'yyyymmdd'), 101);
insert into t(account, bookdate, amount) values (1, to_date('20080103', 'yyyymmdd'), -200);
insert into t(account, bookdate, amount) values (2, to_date('20080102', 'yyyymmdd'), 200);
commit;
edit:
forgot to add; you specified that you wanted the table to be ordered - this doesn't really make sense, and makes me think that you really mean that you wanted the query/view - ordering is a result of the query you execute, not something that's inherant in the table (ignoring Index Organised Tables and the like).

I'll start with this very important caveate: do NOT create a table to hold this data. When you do you will find that you need to maintain it which will become a never ending headache. Write a view to return the extra column if you want to do that. If you're working with a data warehouse then maybe you would do something like this, but even then err on the side of a view unless you simply can't get the performance that you need with indexes,decent hardware, etc.
Here's a query that will return the rows the way that you need them.
SELECT
Account,
Bookdate,
Amount,
(
SELECT SUM(Amount)
FROM My_Table T2
WHERE T2.Account = T1.Account
AND T2.Bookdate <= T1.Bookdate
) AS Running_Total
FROM
My_Table T1
Another possible solution is:
SELECT
T1.Account,
T1.Bookdate,
T1.Amount,
SUM(T2.Amount)
FROM
My_Table T1
LEFT OUTER JOIN My_Table T2 ON
T2.Account = T1.Account AND
T2.Bookdate <= T1.Bookdate
GROUP BY
T1.Account,
T1.Bookdate,
T1.Amount
Test them both for performance and see which works better for you. Also, I haven't thoroughly tested them beyond the example which you gave, so be sure to test some edge cases.

Use analytics, just like in your last question:
create table accounts
( account number(10)
, bookdate date
, amount number(10)
);
delete accounts;
insert into accounts values (1,to_date('20080101','yyyymmdd'),100);
insert into accounts values (1,to_date('20080102','yyyymmdd'),101);
insert into accounts values (2,to_date('20080102','yyyymmdd'),200);
insert into accounts values (1,to_date('20080103','yyyymmdd'),-200);
commit;
select account
, bookdate
, amount
, sum(amount) over (partition by account order by bookdate asc) running_total
from accounts
order by account,bookdate asc
/
output:
ACCOUNT BOOKDATE AMOUNT RUNNING_TOTAL
---------- -------- ---------- -------------
1 01-01-08 100 100
1 02-01-08 101 201
1 03-01-08 -200 1
2 02-01-08 200 200

Related

Find the reversal transactions in my table

I have a table data like the bellow
Date Amount
01022017 300
01052017 -300
03042016 200
06112016 400
05042016 -200
30012016 150
I need only the list like below (the negatives are reversals happen to those transactions).
My expected result like
Date Amount
06112016 400
30012016 150
I need to avoid the reversal transactions. But here in my table I don't have any reference column to indicate the transaction as reversal or normal transaction.
You can achieve this with a LEFT JOIN:
SELECT
m.Date
, m.Amount
FROM myTable m
LEFT JOIN myTable m2 on m.Amount = (-1 * m2.Amount)
WHERE m2.Date IS NULL
As noted in the comments, relying in the amount seems flaky to me but here is a try:
SELECT t.date, t.amount
FROM TableName t
WHERE NOT EXISTS (SELECT 1
FROM TableName t1 INNER JOIN TableName t2
ON T1.amount = -t2.amount and T1.date<=t2.date
WHERE (t.date=t1.date or t.date=t2.date) and t.amount=t1.amount
Note that this will not display amounts that appear more than twice.
based on your Assumed table data using Lag function we can do this
Sample Data :
DECLARE #Table1 TABLE
(Dates int, Amount int)
;
INSERT INTO #Table1
(Dates, Amount)
VALUES
(01022017, 300),
(01052017, -300),
(03042016, 200),
(06112016, 400),
(05042016, -200),
(30012016, 150)
;
Script :
Select t.Amount from (
select amount,
coalesce(lag(amount) over (PARTITION BY RIGHT (dates,4) order by (select null)), 0) + amount as diff
from #Table1) T
WHERE NOT EXISTS (select 1 from #Table1 where T.diff = Amount ) and T.diff <> 0
Try this one for oracle, The Problem with the below query is it reads the main table twice.
select Date,Amount from table where amount>0
minus
select Date,-1*amount as Amount from table where amount<0;

Operation on each row from select query

How execute additional query (UPDATE) on each row from SELECT?
I have to get amount from each row from select and send it to user's balance table.
Example:
status 0 - open
status 1 - processed
status 2 - closed
My select statement:
select id, user_id, sell_amount, sell_currency_id
from (select id, user_id, sell_amount, sell_currency_id,
sum(sell_amount)
over (order by buy_amount/sell_amount ASC, date_add ASC) as cumsell
from market t
where (status = 0 or status = 1) and type = 0
) t
where 0 <= cumsell and 7 > cumsell - sell_amount;
Select result from market table
id;user_id;amount;status
4;1;1.00000000;0
6;2;2.60000000;0
5;3;2.00000000;0
7;4;4.00000000;0
We get 7 amount and send it to user balance table.
id;user_id;amount;status
4;1;0.00000000;2 -- took 1, sum 1, status changed to 2
6;2;0.00000000;2 -- took 2.6, sum=3.6, status changed to 2
5;3;0.00000000;2 -- took 2, sum 5.6, status changed to 2
7;4;2.60000000;1 -- took 1.4, sum 7.0, status changed to 1 (because there left 2.6 to close)
User's balance table
user_id;balance
5;7 -- added 7 from previous operation
Postgres version 9.3
The general principle is to use UPDATE ... FROM over a subquery. Your example is too hard to turn into useful CREATE TABLE and SELECT statements, so I've made up a quick dummy dataset:
CREATE TABLE balances (user_id integer, balance numeric);
INSERT INTO balances (user_id, balance) VALUES (1,0), (2, 2.1), (3, 99);
CREATE TABLE transactions (user_id integer, amount numeric, applied boolean default 'f');
INSERT INTO transactions (user_id, amount) VALUES (1, 22), (1, 10), (2, -10), (4, 1000000);
If you wanted to apply the transactions to the balances you would do something like:
BEGIN;
LOCK TABLE balances IN EXCLUSIVE MODE;
LOCK TABLE transactions IN EXCLUSIVE MODE;
UPDATE balances SET balance = balance + t.amount
FROM (
SELECT t2.user_id, sum(t2.amount) AS amount
FROM transactions t2
GROUP BY t2.user_id
) t
WHERE balances.user_id = t.user_id;
UPDATE transactions
SET applied = true
FROM balances b
WHERE transactions.user_id = b.user_id;
The LOCK statements are important for correctness in the presence of concurrent inserts/updates.
The second UPDATE marks the transactions as applied; you might not need something like that in your design.

oracle sql how to get the unmatch positive records

hello guys I need a little help writing a sql statement that would basically point out the rows that don't have a corresponding negative matching number, based on my_id,report_id.
Here is the table declaration for better explanation.
CREATE TABLE ."TEST"
( "REPORT_ID" VARCHAR2(100 BYTE),
"AMOUNT" NUMBER(17,2),
"MY_ID" VARCHAR2(30 BYTE),
"FUND" VARCHAR2(20 BYTE),
"ORG" VARCHAR2(20 BYTE)
)
here is some sample data
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('1',50,'910','100000','67120');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('1',-50,'910','100000','67130');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('1',100,'910','100000','67150');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('2',200,'910','100000','67130');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('2',-200,'910','100000','67120');
INSERT INTO TEST (REPORT_ID, AMOUNT, MY_ID, FUND, ORG) VALUES ('1', '40.17', '910', '100000', '67150')
INSERT INTO TEST (REPORT_ID, AMOUNT, MY_ID, FUND, ORG) VALUES ('1', '-40.17', '910', '100000', '67150')
INSERT INTO TEST (REPORT_ID, AMOUNT, MY_ID, FUND, ORG) VALUES ('1', '40.17', '910', '100000', '67150')
if you create the table and look closely , you'll notice that by report_id and my_id most positive amounts have a direct negative amount. In the other hand, I need to identify those positive amounts that do not have a corresponding negative amount by my_id , and report_id.
expected result should look like this
"REPORT_ID" "FUND" "MY_ID" "ORG" "AMOUNT"
"1" "100000" "910" "67150" "40.17"
"1" "100000" "910" "67150" "100"
any ideas how can acomplish this.
EDIT:
Posted the wrong output result. Just to be clear the fund and org don't matter until after the match. For example if i was writing this using plsql i would find how many minuses do i have then how many pluses do i have compare each plus amount to each minus amount and delete them then i would be left with whatever plus amounts did not have negative amounts.
I apologize for the confusion. hope this makes it clearer now. once i have all my matches i should end up with only positive amounts that are left behind.
EDIT:
additional inserts
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('5',71,'911','100000','67150');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('5',71,'911','100000','67120');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('5',71,'911','100000','67140');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('5',71,'911','100000','67130');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('5',71,'911','100000','67130');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('5',71,'911','100000','67130');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('5',-71,'911','100000','67150');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('5',-71,'911','100000','67150');
Insert into TEST (REPORT_ID,AMOUNT,MY_ID,FUND,ORG) values ('5',-71,'911','100000','67150');
New Version
This should return just the rows that you want. If you are not concerned with org or fund then you can just use the query that is aliased x:
select distinct t1.report_id, t1.fund, t1.my_id, t1.org, t1.amount
from test t1,
(select distinct t.report_id, t.my_id, abs(amount) as amount
from test t
group by t.report_id, t.my_id, abs(amount)
having sum(t.amount) > 0) x
where t1.report_id = x.report_id
and t1.my_id = x.my_id
and t1.amount = x.amount;
Previous Version
select *
from test t
minus
select t1.*
from test t1,
test t2
where t1.amount = -1*t2.amount
and t1.report_id = t2.report_id
and t1.my_id = t2.my_id;
This just gives on row of output for the row with amt 100. I have asked you to clarify in the comments why any row with 200 should be included (if it should). I am also not sure whether you want one of the 47.17 values to be included. The difficulty with this is that the two positive values are identical in the example data you provided, is this correct?
a modified version query which works for me for me with your scenario. I used SQL SERVERZ
select *
from test t
EXCEPT
select t1.*
from test t1,
test t2
where t1.amount = -1*t2.amount
and t1.report_id = t2.report_id
and t1.my_id = t2.my_id;
Result with your Data
REPORT_ID AMOUNT MY_ID FUND ORG
1 100 910 100000 67150
the result after running this update query update test set AMOUNT=-500 where AMOUNT=-200
REPORT_ID AMOUNT MY_ID FUND ORG
1 100 910 100000 67150
2 -500 910 100000 67120
2 200 910 100000 67130
EDIT:
Here's an updated query based on the feedback and additional sample data provided. This query has the advantage of querying the TEST table just once, and it returns the expected results (3 rows of amount 71, one row of amount 100, and one row of amount 40.17).
SELECT
report_id, MAX(fund) fund, my_id, MAX(org) org, SUM(amount) amount
FROM (
SELECT
report_id, fund, my_id, org, amount
, ROW_NUMBER() OVER( PARTITION BY report_id, my_id, amount ) rn
FROM
test
) t
GROUP BY
rn, ABS(amount), report_id, my_id
HAVING
SUM(amount) > 0;
Results:
report_id fund my_id org amount
5 100000 911 67120 71.00
5 100000 911 67140 71.00
5 100000 911 67150 71.00
1 100000 910 67150 40.17
1 100000 910 67150 100.00
INITIAL ANSWER:
The below query should provide what you're looking for. I'm not sure what should be done if org and/or fund are different since you're not grouping on those values - I decided to use a MAX aggregate function on fund and org to select a single value without affecting the grouping. Maybe those columns should just be left out?
SELECT
report_id, MAX(fund) fund, my_id, MAX(org) org, SUM(amount) amount
FROM
test
GROUP BY
report_id, my_id, ABS(amount)
HAVING
SUM(amount) > 0;
Results:
report_id fund my_id org amount
1 100000 910 67150 40.17
1 100000 910 67150 100.00
Note that based on the sample data you provided, the expected result should not show 200 because there's a corresponding -200 for the same report_id (2) and my_id (910).

Creating a table with a sequence of values without inserting each one manually

I (think I) want to create a view or temporary table that contains a sequence.
The purpose of this table is simply to provide values needed to make a panel data set.
What I would like to do is create this sequence programmatically, using every value between two periods like 0 and 365, with gaps of 7 (say to make a weekly panel).
Here is how it can be done "manually", inserting each of the cutoff days by hand.
create table time_periods (day_cutoff int);
insert into time_periods values (7);
insert into time_periods values (14);
insert into time_periods values (28);
insert into time_periods values (35);
insert into time_periods values (42);
This table would then be used as so (doing a full cartesian join on an underling table of billing_records that contains ad hoc instances of when a billing was made.
select
buyer
, seller
, day_cutoff
, sum(case when billing_day < day_cutoff
then amount
else 0.0 end) as cumulative_spend
from time_periods
left join billing_records
on 1 = 1
group by buyer, seller, day_cutoff
You can just use generate_series:
select *
from generate_series(7, 42, 7);
It is documented here.
Here is one way to write your query:
select buyer, seller, day_cutoff,
sum(case when br.billing_day < day_cutoff then amount else 0.0 end) as cumulative_spend
from billing_records br cross join
generate_series(7, 42, 7) as day_cutoff
group by buyer, seller, day_cutoff ;

How to select info from row above?

I want to add a column to my table that is like the following:
This is just an example of how the table is structured, the real table is more than 10.000 rows.
No_ Name Account_Type Subgroup (New_Column)
100 Sales 3
200 Underwear 0 250 *100
300 Bikes 0 250 *100
400 Profit 3
500 Cash 0 450 *400
So for every time there is a value in 'Subgroup' I want the (New_Column) to get the value [No_] from the row above
No_ Name Account_Type Subgroup (New_Column)
100 Sales 3
150 TotalSales 3
200 Underwear 0 250 *150
300 Bikes 0 250 *150
400 Profit 3
500 Cash 0 450 *400
There are cases where the table is like the above, where two "Headers" are above. And in that case I also want the first above row (150) in this case.
Is this a case for a cursor or what do you recommend?
The data is ordered by No_
--EDIT--
Starting from the first line and then running through the whole table:
Is there a way I can store the value for [No_] where [Subgroup] is ''?
And following that insert this [No_] value in the (New_Column) in each row below having value in the [Subgroup] row.
And when the [Subgroup] row is empty the process will keep going, inserting the next [No_] value in (New_Column), that is if the next line has a value in [Subgroup]
Here is a better image for what I´m trying to do:
SQL Server 2012 suggests using Window Offset Functions.
In this case : LAG
Something like this:
SELECT [No_]
,[Name]
,[Account_Type]
,[Subgroup]
,LAG([No_]) OVER(PARTITION BY [Subgroup]
ORDER BY [No_]) as [PrevValue]
FROM table
Here is an example from MS:
http://technet.microsoft.com/en-us/library/hh231256.aspx
The ROW_NUMBER function will allow you to find out what number the row is, but because it is a windowed function, you will have to use a common table expression (CTE) to join the table with itself.
WITH cte AS
(
SELECT [No_], Name, Account_Type, Subgroup, [Row] = ROW_NUMBER() OVER (ORDER BY [No_])
FROM table
)
SELECT t1.*, t2.[No_]
FROM cte t1
LEFT JOIN cte t2 ON t1.Row = t2.Row - 1
Hope this helps.
Next query will return Name of the parent row instead of the row itself, i.e. Sales for both Sales, Underwear, Bikes; and Profit for Profit, Cash:
select ISNULL(t2.Name, t1.Name)
from table t1
left join table t2 on t1.NewColumn = t2.No
So in SQL Server 2008 i created test table with 3 values in it:
create table #ttable
(
id int primary key identity,
number int,
number_prev int
)
Go
Insert Into #ttable (number)
Output inserted.id
Values (10), (20), (30);
Insert in table, that does what you need (at least if understood correctly) looks like this:
declare #new_value int;
set #new_value = 13; -- NEW value
Insert Into #ttable (number, number_prev)
Values (#new_value,
(Select Max(number) From #ttable t Where t.number < #new_value))
[This part added] And to work with subgroup- just modify the inner select to filter out it:
Select Max(number) From #ttable t
Where t.number < #new_value And Subgroup != #Subgroup
SELECT
No_
, Name
, Account_Type
, Subgroup
, ( SELECT MAX(above.No_)
FROM TableX AS above
WHERE above.No_ < a.No_
AND above.Account_Type = 3
AND a.Account_Type <> 3
) AS NewColumn
FROM
TableX AS a