How to identify positive minimum or negative maximum in a column for a key? - sql

I have the following columns - Person_ID Days. For one person id, multiple days are possible. Something like this:
Person_Id Days
1000 100
1000 200
1000 -50
1000 -10
1001 100
1001 200
1001 50
1001 10
1002 -50
1002 -10
I need to address the following scenarios:
If all values for days column are positive, I need minimum of the days for a person_id. If the days column has both positive and negative, I need minimum of positive. If all negatives, I need maximum of negative.
The output like:
Person_id Days
1000 100
1001 10
1002 -10
I tried using case statement, but I am unable to use a same column in the condition as well as grouping.

Try this (Postgres 9.4+):
select person_id, coalesce(min(days) filter (where days > 0), max(days))
from a_table
group by 1
order by 1;

Oracle Setup:
CREATE TABLE table_name ( Person_Id, Days ) AS
SELECT 1000, 100 FROM DUAL UNION ALL
SELECT 1000, 200 FROM DUAL UNION ALL
SELECT 1000, -50 FROM DUAL UNION ALL
SELECT 1000, -10 FROM DUAL UNION ALL
SELECT 1001, 100 FROM DUAL UNION ALL
SELECT 1001, 200 FROM DUAL UNION ALL
SELECT 1001, 50 FROM DUAL UNION ALL
SELECT 1001, 10 FROM DUAL UNION ALL
SELECT 1002, -50 FROM DUAL UNION ALL
SELECT 1002, -10 FROM DUAL;
Query:
SELECT person_id, days
FROM (
SELECT t.*,
ROW_NUMBER() OVER ( PARTITION BY person_id
ORDER BY SIGN( ABS( days ) ),
SIGN( DAYS ) DESC,
ABS( DAYS )
) AS rn
FROM table_name t
)
WHERE rn = 1;
Output:
PERSON_ID DAYS
---------- ----------
1000 100
1001 10
1002 -10

Oracle solution:
with
input_data ( person_id, days) as (
select 1000, 100 from dual union all
select 1000, 200 from dual union all
select 1000, -50 from dual union all
select 1000, -10 from dual union all
select 1001, 100 from dual union all
select 1001, 200 from dual union all
select 1001, 50 from dual union all
select 1001, 10 from dual union all
select 1002, -50 from dual union all
select 1002, -10 from dual
)
select person_id,
NVL(min(case when days > 0 then days end), max(days)) as days
from input_data
group by person_id;
PERSON_ID DAYS
---------- ----------
1000 100
1001 10
1002 -10
For each person_id, if there is at least one days value that is strictly positive, then the min will be taken over positive days only and will be returned by NVL(). Otherwise the min() will return null, and NVL() will return max() over all days (all of which are, in this case, negative or 0).

select Person_id, min(abs(days)) * days/abs(days) from table_name
group by Person_id
-- + handle zero_divide .. SORRY.. the above works only in MySQL .
Something like this will work anywhere which is equivalent of above query:
select t.Person_id , min(t.days) from table_name t,
(select Person_id, min(abs(days)) as days from table_name group by Person_id) v
where t.Person_id = v.Person_id
and abs(t days) = v.days
group by Person_id;
OR
select id, min(Days) from (
select Person_id, min(abs(Days)) as Days from temp group by Person_id
union
select Person_id, max(Days) as Days from temp group by Person_id
) temp
group by Person_id;

You can do this by using GroupBy clause in sql server. Take a look into below query:-
CREATE TABLE #test(Person_Id INT, [Days] INT)
DECLARE #LargestNumberFromTable INT;
INSERT INTO #test
SELECT 1000 , 100 UNION
SELECT 1000 , 200 UNION
SELECT 1000 , -50 UNION
SELECT 1000 , -10 UNION
SELECT 1001 , 100 UNION
SELECT 1001 , 200 UNION
SELECT 1001 , 50 UNION
SELECT 1001 , 10 UNION
SELECT 1002 , -50 UNION
SELECT 1002 , -10
SELECT #LargestNumberFromTable = ISNULL(MAX([Days]), 0)
FROM #test
SELECT Person_Id
,CASE WHEN SUM(IIF([Days] > 0,[Days] , 0)) = 0 THEN MAX([Days]) -- All Negative
WHEN SUM([Days]) = SUM(IIF([Days] > 0, [Days], 0)) THEN MIN ([Days]) -- ALL Positive
WHEN SUM([Days]) <> SUM(IIF([Days] > 0, [Days], 0)) THEN MIN(IIF([Days] > 0, [Days], #LargestNumberFromTable)) --Mix (Negative And positive)
END AS [Days]
FROM #test
GROUP BY Person_Id
DROP TABLE #test

Related

Generate between two dates in different rows SQL

I am working on generating lines depending on the days (a row by day) between two dates. I would have in the first row the first date and in the second row the second date but it all depends on the ID and the money. I consider it is better to show you with an example:
The origin table:
My target:
Could be possible with a loop?
Thank you very much.
You can use a recursive sub-query factoring clause:
WITH dt_range ( id, dt, next_dt, money ) AS (
SELECT id,
dt,
LEAD(dt) OVER (PARTITION BY id ORDER BY dt),
money
FROM table_name
UNION ALL
SELECT id,
dt + INTERVAL '1' DAY,
next_dt,
money
FROM dt_range
WHERE dt + INTERVAL '1' DAY < next_dt
)
SEARCH DEPTH FIRST BY dt SET dt_order
SELECT id, dt, money FROM dt_range;
Which, for your sample data:
CREATE TABLE table_name ( id, dt, money ) AS
SELECT 500, DATE '2017-02-23', 3500 FROM DUAL UNION ALL
SELECT 500, DATE '2017-02-26', 35000 FROM DUAL UNION ALL
SELECT 500, DATE '2017-02-28', 50000 FROM DUAL UNION ALL
SELECT 200, DATE '2020-05-01', 8888 FROM DUAL UNION ALL
SELECT 200, DATE '2020-05-05', 999 FROM DUAL UNION ALL
SELECT 200, DATE '2020-05-09', 1000 FROM DUAL;
Outputs:
ID
DT
MONEY
500
23-FEB-17
3500
500
24-FEB-17
3500
500
25-FEB-17
3500
500
26-FEB-17
35000
500
27-FEB-17
35000
500
28-FEB-17
50000
200
01-MAY-20
8888
200
02-MAY-20
8888
200
03-MAY-20
8888
200
04-MAY-20
8888
200
05-MAY-20
999
200
06-MAY-20
999
200
07-MAY-20
999
200
08-MAY-20
999
200
09-MAY-20
1000
db<>fiddle here
If you are using Oracle 11g then it has bugs iterating over dates; this can be easily fixed by iterating over a number and then adding it to a date (rather than iterating directly on the date):
WITH dt_range ( id, dt, offset, next_dt, money ) AS (
SELECT id,
dt,
0,
LEAD(dt) OVER (PARTITION BY id ORDER BY dt),
money
FROM table_name
UNION ALL
SELECT id,
dt,
offset + 1,
next_dt,
money
FROM dt_range
WHERE dt + offset + 1 < next_dt
)
SEARCH DEPTH FIRST BY dt SET dt_order
SELECT id, dt + offset AS dt, money FROM dt_range;
db<>fiddle here
for example:
with
simple_data( id,dates,money) as
(
select 500, date '2017-02-23', 3500 from dual union all
select 500, date '2017-02-26', 35000 from dual union all
select 500, date '2017-02-28', 50000 from dual union all
select 200, date '2020-05-01', 8888 from dual union all
select 200, date '2020-05-05', 999 from dual union all
select 200, date '2020-05-09', 1000 from dual
)
,step1 as
(
select
sd.id,
sd.money,
sd.dates,
lead (dates,1,dates) over(partition by id order by dates)-1 lead_dts
from simple_data sd
)
select
st1.id,
st1.dates + to_number(t.column_value)-1 as dates,
st1.money
from step1 st1,table(cast(multiset(select level from dual connect by level<= st1.lead_dts-st1.dates+1) as ora_mining_varchar2_nt)) t
order by id desc,dates;
----OR------
with
simple_data( id,dates,money) as
(
select 500, date '2017-02-23', 3500 from dual union all
select 500, date '2017-02-26', 35000 from dual union all
select 500, date '2017-02-28', 50000 from dual union all
select 200, date '2020-05-01', 8888 from dual union all
select 200, date '2020-05-05', 999 from dual union all
select 200, date '2020-05-09', 1000 from dual
)
,step1 as
(
select
sd.id,
sd.money,
sd.dates,
lag (dates,1,dates) over(partition by id order by dates desc) lag_dts
from simple_data sd
)
select
st1.id,
st1.lag_dts - to_number(t.column_value) as dates,
st1.money
from step1 st1,table(cast(multiset(select st1.lag_dts-st1.dates - level +1 from dual connect by level<= st1.lag_dts-st1.dates) as ora_mining_varchar2_nt)) t
order by id desc,dates;
12c and later:
with
simple_data( id,dates,money) as
(
select 500, date '2017-02-23', 3500 from dual union all
select 500, date '2017-02-26', 35000 from dual union all
select 500, date '2017-02-28', 50000 from dual union all
select 200, date '2020-05-01', 8888 from dual union all
select 200, date '2020-05-05', 999 from dual union all
select 200, date '2020-05-09', 1000 from dual
)
,step1 as
(
select
sd.id,
sd.money,
sd.dates,
lead (dates,1,dates) over(partition by id order by dates)-1 lead_dts
from simple_data sd
)
select
st1.id,
st1.dates + t.lvl-1 as dates,
st1.money
from step1 st1,lateral(select level lvl from dual connect by level<= st1.lead_dts-st1.dates+1) t
order by id desc,dates;
---OR-----
with
simple_data( id,dates,money) as
(
select 500, date '2017-02-23', 3500 from dual union all
select 500, date '2017-02-26', 35000 from dual union all
select 500, date '2017-02-28', 50000 from dual union all
select 200, date '2020-05-01', 8888 from dual union all
select 200, date '2020-05-05', 999 from dual union all
select 200, date '2020-05-09', 1000 from dual
)
,step1 as
(
select
sd.id,
sd.money,
sd.dates,
lag (dates,1,dates) over(partition by id order by dates desc) lag_dts
from simple_data sd
)
select
st1.id,
st1.lag_dts - t.lvl as dates,
st1.money
from step1 st1,lateral(select st1.lag_dts-st1.dates - level +1 lvl from dual connect by level<= st1.lag_dts-st1.dates ) t
order by id desc,dates;

Oracle query to find if an account has been negative for some days

I have a table which has balance for accounts on a daily basis. Need to know how I can find all the accounts that have been negative for more than certain no.of days.
Sample data-
Accountid Date Balance
1000 01/01/2020 -1.00
1000 01/02/2020 -1.00
1000 01/03/2020 -1.00
1001 01/01/2020 -20.00
1001 01/02/2020 -20.00
1003 01/01/2020 15.00
1003 01/02/2020 16.00
I need to query all the accounts that have been negative for more than 2 days
You could query the days with negative balances, group by the account ID and then count how many rows you got in the having clause:
SELECT AccountID
FROM mytable
WHERE balance < 0
GROUP BY AccountID
HAVING COUNT(*) >= 2
If you want to consider only consecutive days then:
SELECT AccountId
FROM (
SELECT Accountid, DateTime, Balance,
SUM( has_changed_sign )
OVER ( PARTITION BY AccountId ORDER BY DateTime )
AS grp
FROM (
SELECT Accountid, DateTime, Balance,
CASE
WHEN SIGN( balance )
= LAG( SIGN( Balance ) )
OVER ( PARTITION BY AccountId ORDER BY DateTime )
THEN 0
ELSE 1
END AS has_changed_sign
FROM table_name t
)
WHERE Balance < 0
)
GROUP BY AccountID, grp
HAVING COUNT(*) > 2
So, for the test data:
CREATE TABLE table_name ( Accountid, DateTime, Balance ) AS
SELECT 1000, DATE '2020-01-01', -1.00 FROM DUAL UNION ALL -- 3 consecutive -ve days
SELECT 1000, DATE '2020-01-02', -1.00 FROM DUAL UNION ALL
SELECT 1000, DATE '2020-01-03', -1.00 FROM DUAL UNION ALL
SELECT 1000, DATE '2020-01-04', +1.00 FROM DUAL UNION ALL
SELECT 1001, DATE '2020-01-01', -20.00 FROM DUAL UNION ALL -- Only 2 negative
SELECT 1001, DATE '2020-01-02', -20.00 FROM DUAL UNION ALL
SELECT 1001, DATE '2020-01-03', +20.00 FROM DUAL UNION ALL
SELECT 1001, DATE '2020-01-04', +20.00 FROM DUAL UNION ALL
SELECT 1002, DATE '2020-01-01', -1.00 FROM DUAL UNION ALL -- 3 negative days but
SELECT 1002, DATE '2020-01-02', -1.00 FROM DUAL UNION ALL -- only 2 consecutive
SELECT 1002, DATE '2020-01-03', +1.00 FROM DUAL UNION ALL
SELECT 1002, DATE '2020-01-04', -1.00 FROM DUAL UNION ALL
SELECT 1003, DATE '2020-01-01', +15.00 FROM DUAL UNION ALL -- All positive
SELECT 1003, DATE '2020-01-02', +16.00 FROM DUAL UNION ALL
SELECT 1003, DATE '2020-01-03', +17.00 FROM DUAL UNION ALL
SELECT 1003, DATE '2020-01-04', +18.00 FROM DUAL;
This outputs:
| ACCOUNTID |
| --------: |
| 1000 |
If you only want more than 2 days then you could simply use LAG:
SELECT DISTINCT
AccountID
FROM (
SELECT AccountID,
balance,
LAG( balance, 1 ) OVER ( PARTITION BY AccountID ORDER BY DateTime )
AS balance_1_day_ago,
LAG( balance, 2 ) OVER ( PARTITION BY AccountID ORDER BY DateTime )
AS balance_2_days_ago
FROM table_name
)
WHERE balance < 0
AND balance_1_day_ago < 0
AND balance_2_days_ago < 0;
But that isn't going to scale well if you want to check over a larger period as the query is quickly going to become very large.
db<>fiddle here
Try this.
Select accountid, count(date) from table
Where balance < 0
Group by accountid
Having count(date) >2
Use a filter in a WHERE clause to get only the negative balances, then group by the account ID and in a HAVING clause check for the count of distinct days being greater than your limit of days.
SELECT accountid
FROM elbat
WHERE balance < 0
GROUP BY accountid
HAVING count(DISTINCT date) > 2;
If you want all columns then use partition by. The below solution would take count of date's order too
Select Accountid, Date, Balance,
row_number() over (Partition by
Accountid order by Date)
rn from table
Where balance<0 and rn>2 ;

Best 10 of 12 in SQL

Scoring for a running race series. They get points at each monthly race based on their finish. Their total score is their best 10 of 12 monthly races. How do I get that for each member?
tblRacePoints
memnum - Membership number
RaceNo - YYYYMM, e.g., 201910
Points
I want for each their total score of all races, total score of their best 10 of 12, and each of their lowest two scores for the year. Not everyone has done all the races so they may not have 12 entries for the year.
How do I write a query to do this, and then to rank them by their best 10/12 points?
If you are using MSSQL database, you can use ROW_NUMBER as below to achieve your required output. Same logic can be used for some other databases too.
Note: Table structure is just an assumption.
WITH your_table(player_id,dt,points)
AS
(
SELECT 1,'20190101', 100 UNION ALL SELECT 1,'20190201', 200 UNION ALL
SELECT 1,'20190301', 300 UNION ALL SELECT 1,'20190401', 400 UNION ALL
SELECT 1,'20190501', 500 UNION ALL SELECT 1,'20190601', 600 UNION ALL
SELECT 1,'20190701', 700 UNION ALL SELECT 1,'20190801', 800 UNION ALL
SELECT 1,'20190901', 900 UNION ALL SELECT 1,'20191001', 1000 UNION ALL
SELECT 1,'20191101', 1100 UNION ALL SELECT 1,'20191201', 1200 UNION ALL
SELECT 2,'20190101', 400 UNION ALL SELECT 2,'20190201', 200 UNION ALL
SELECT 2,'20190301', 300 UNION ALL SELECT 2,'20190401', 400 UNION ALL
SELECT 2,'20190501', 500 UNION ALL SELECT 2,'20190601', 600 UNION ALL
SELECT 2,'20190701', 700 UNION ALL SELECT 2,'20190801', 800 UNION ALL
SELECT 2,'20190901', 900 UNION ALL SELECT 2,'20191001', 1000 UNION ALL
SELECT 2,'20191101', 1100 UNION ALL SELECT 2,'20191201', 1200
)
SELECT
player_id,
YEAR(dt) Year,
SUM(Points) total_point
FROM
(
SELECT *,
ROW_NUMBER() OVER (PARTITION BY player_id, YEAR(dt) ORDER BY Points DESC) RN
FROM your_table
)A
WHERE RN <= 10
GROUP BY player_id, YEAR(dt)

How to calculate MTD and QTD by YTD value in Oracle

There are some data in my table t1 looks like below:
date dealer YTD_Value
2018-01 A 1100
2018-02 A 2000
2018-03 A 3000
2018-04 A 4200
2018-05 A 5000
2018-06 A 5500
2017-01 B 100
2017-02 B 200
2017-03 B 500
... ... ...
then I want to write a SQL to query this table and get below result:
date dealer YTD_Value MTD_Value QTD_Value
2018-01 A 1100 1100 1100
2018-02 A 2000 900 2000
2018-03 A 3000 1000 3000
2018-04 A 4200 1200 1200
2018-05 A 5000 800 2000
2018-06 A 5500 500 2500
2017-01 B 100 100 100
2017-02 B 200 100 200
2017-03 B 550 350 550
... ... ... ... ...
'YTD' means Year to date
'MTD' means Month to date
'QTD' means Quarter to date
So if I want to calculate MTD and QTD value for dealer 'A' in '2018-01', it should be the same as YTD.
If I want to calculate MTD value for dealer 'A' in '2018-06', MTD value should equal to YTD value in '2018-06' minus YTD value in '2018-05'. And the QTD value in '2018-06' should equal to YTD value in '2018-06' minus YTD value in '2018-03' or equal to sum MTD value in (2018-04,2018-05,2018-06)
The same rule for other dealers such as B.
How can I write the SQL to achieve this purpose?
The QTD calculation is tricky, but you can do this query without subqueries. The basic idea is to do a lag() for the monthly value. Then use a max() analytic function to get the YTD value at the beginning of the quarter.
Of course, the first quarter of the year has no such value, so a coalesce() is needed.
Try this:
with t(dte, dealer, YTD_Value) as (
select '2018-01', 'A', 1100 from dual union all
select '2018-02', 'A', 2000 from dual union all
select '2018-03', 'A', 3000 from dual union all
select '2018-04', 'A', 4200 from dual union all
select '2018-05', 'A', 5000 from dual union all
select '2018-06', 'A', 5500 from dual union all
select '2017-01', 'B', 100 from dual union all
select '2017-02', 'B', 200 from dual union all
select '2017-03', 'B', 550 from dual
)
select t.*,
(YTD_Value - lag(YTD_Value, 1, 0) over (partition by substr(dte, 1, 4) order by dte)) as MTD_Value,
(YTD_Value -
coalesce(max(case when substr(dte, -2) in ('03', '06', '09') then YTD_VALUE end) over
(partition by substr(dte, 1, 4) order by dte rows between unbounded preceding and 1 preceding
), 0
)
) as QTD_Value
from t
order by 1
Here is a db<>fiddle.
The following query should do the job. It uses a CTE that translates the varchar date column to dates, and then a few joins to recover the value to compare.
I tested it in this db fiddle and the output matches your expected results.
WITH cte AS (
SELECT TO_DATE(my_date, 'YYYY-MM') my_date, dealer, ytd_value FROM my_table
)
SELECT
TO_CHAR(ytd.my_date, 'YYYY-MM') my_date,
ytd.ytd_value,
ytd.dealer,
ytd.ytd_value - NVL(mtd.ytd_value, 0) mtd_value,
ytd.ytd_value - NVL(qtd.ytd_value, 0) qtd_value
FROM
cte ytd
LEFT JOIN cte mtd ON mtd.my_date = ADD_MONTHS(ytd.my_date, -1) AND mtd.dealer = ytd.dealer
LEFT JOIN cte qtd ON qtd.my_date = ADD_MONTHS(TRUNC(ytd.my_date, 'Q'), -1) AND mtd.dealer = qtd.dealer
ORDER BY dealer, my_date
PS : date is a reserved word in most RDBMS (including Oracle), I renamed that column to my_date in the query.
You can use lag() windows analytic and sum() over .. aggregation functions as :
select "date",dealer,YTD_Value,MTD_Value,
sum(MTD_Value) over (partition by qt order by "date")
as QTD_Value
from
(
with t("date",dealer,YTD_Value) as
(
select '2018-01','A',1100 from dual union all
select '2018-02','A',2000 from dual union all
select '2018-03','A',3000 from dual union all
select '2018-04','A',4200 from dual union all
select '2018-05','A',5000 from dual union all
select '2018-06','A',5500 from dual union all
select '2017-01','B', 100 from dual union all
select '2017-02','B', 200 from dual union all
select '2017-03','B', 550 from dual
)
select t.*,
t.YTD_Value - nvl(lag(t.YTD_Value)
over (partition by substr("date",1,4) order by substr("date",1,4) desc, "date"),0)
as MTD_Value,
substr("date",1,4)||to_char(to_date("date",'YYYY-MM'),'Q')
as qt,
substr("date",1,4) as year
from t
order by year desc, "date"
)
order by year desc, "date";
Rextester Demo

Oracle : Get average count for last 30 business days

Oracle version 11g.
My table has records similar to these.
calendar_date ID record_count
25-OCT-2017 1 20
25-OCT-2017 2 40
25-OCT-2017 3 60
24-OCT-2017 1 70
24-OCT-2017 2 50
24-OCT-2017 3 10
20-OCT-2017 1 35
20-OCT-2017 2 60
20-OCT-2017 3 90
18-OCT-2017 1 80
18-OCT-2017 2 50
18-OCT-2017 3 45
i.e for each ID, there is one record count for a given calendar day. The days are NOT continuous, i.e there may be missing records for weekends/holidays etc. On such days, there will not be records available for any ID. However on working days there are entries available for each ID .
I need to get the average record count for last 30 business days for each id
I want an output like this. ( Don't go by the values. It is just a sample )
ID avg_count_last_30
1 150
2 130
3 110
I am trying to figure out the most efficient way to do this. I thought of using RANGE BETWEEN , ROWS BETWEEN etc , but unsure it would work.
Off course a query like this won't help as there are holidays in between.
select id, AVG(record_count) FROM mytable
where calendar_date between SYSDATE - 30 and SYSDATE - 1
group by id;
what I need is something like
select id , AVG(record_count) FROM mytable
where calendar_date between last_30th_business_day and last_business_day
group by id;
last_30th_business_day will be count(DISTINCT business_days ) starting from most recent business day going backwards till I count 30.
last_business_day will be most recent business day
Would like to know experts opinion on this and best approach.
Based on your comment try this one:
WITH mytable (calendar_date, ID, record_count) AS (
SELECT TO_DATE('25-10-2017', 'DD-MM-YYYY'), 1, 20 FROM dual UNION ALL
SELECT TO_DATE('25-10-2017', 'DD-MM-YYYY'), 2, 40 FROM dual UNION ALL
SELECT TO_DATE('25-10-2017', 'DD-MM-YYYY'), 3, 60 FROM dual UNION ALL
SELECT TO_DATE('24-10-2017', 'DD-MM-YYYY'), 1, 70 FROM dual UNION ALL
SELECT TO_DATE('24-10-2017', 'DD-MM-YYYY'), 2, 50 FROM dual UNION ALL
SELECT TO_DATE('24-10-2017', 'DD-MM-YYYY'), 3, 10 FROM dual UNION ALL
SELECT TO_DATE('20-10-2017', 'DD-MM-YYYY'), 1, 35 FROM dual UNION ALL
SELECT TO_DATE('20-10-2017', 'DD-MM-YYYY'), 2, 60 FROM dual UNION ALL
SELECT TO_DATE('20-10-2017', 'DD-MM-YYYY'), 3, 90 FROM dual UNION ALL
SELECT TO_DATE('18-10-2017', 'DD-MM-YYYY'), 1, 80 FROM dual UNION ALL
SELECT TO_DATE('18-10-2017', 'DD-MM-YYYY'), 2, 50 FROM dual UNION ALL
SELECT TO_DATE('18-10-2017', 'DD-MM-YYYY'), 3, 45 FROM dual),
t AS (
SELECT calendar_date, ID, record_count,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY calendar_date desc) AS RN
FROM mytable)
SELECT ID, AVG(RECORD_COUNT)
FROM t
WHERE rn <= 30
group by ID;