Complete datetime list despite missing values for analysis - sql

I have been a reader of StackOverflow for a long time already. Nearly always, I find my answers here. Great!
But now, I have a problem where I could not find a solution yet:
I have an Oracle table with an ID, a date and a value.
Think of it as a list of outstanding tasks (value) and project (ID). When the number of open tasks of a project changes, the list gets a new entry.
It looks like this:
ID month RemainingValue
1 01/01/2018 1000
1 01/03/2018 800
1 01/04/2018 600
1 01/07/2018 400
2 01/02/2018 700
2 01/03/2018 650
2 01/05/2018 600
3 01/02/2018 50
3 01/08/2018 40
4 01/01/2018 2000
(DateFormat DD/MM/YYYY)
Please note that not every month has a value!
I have to calculate the sum of all open tasks per month.
If there is no value for a month, that means that the number of open tasks has not decreased in that month, so the query should take the previous existing value of this project into account.
I want this result:
month result calculation remark
01/01/2018 3000 =1000 + 2000 ID 1+4
01/02/2018 3750 =1000 + 700 + 50 + 2000 ID 1[value of 01/01/2018]+2+3+4[value of 01/01/2018]
01/03/2018 3500 =800 + 650 + 50 + 2000 ID 1+2+3[value of 01/02/2018]+4[value of 01/01/2018]
What I did already:
I created a list of all months using the CONNECT BY LEVEL functionality, similar to this:
SELECT LEVEL AS NR
, ADD_MONTHS('01-JAN-2018', LEVEL) AS MONAT
FROM DUAL
CONNECT BY LEVEL <= (... SOME.SUBSELECT.TO.GET.THE.NUMBER.OF.LEVELS ...)
Then I can outer join this list of months to the table above based on the date.
The problem is, that the values of tasks of the unfilled months are NULL. But I don't want them to be NULL, I want the previous filled value in this case.
I tried with LAG functions, but without success so far.
I am hoping that there is some functionality in (Oracle) SQL which can do this where I don't know of.
Or maybe it's even simpler and I just don't get it...
The resulting query should also be performant, because the underlying table has millions of rows. So I'd like to avoid slow PL/SQL solutions...
Hope you can help!
Kind Regards,
Nadine

You could use an analytic query to get the latest value for each ID, up to and including that month (relying on the default windowing clause.
This uses your sample data in a CTE, and adds another one to provide your month generation (may not match your desired range of course):
-- first CTE to replictae your data
with my_table(ID, month, RemainingValue) as (
select 1, to_date('01/01/2018', 'DD/MM/YYYY'), 1000 from dual
union all select 1, to_date('01/03/2018', 'DD/MM/YYYY'), 800 from dual
union all select 1, to_date('01/04/2018', 'DD/MM/YYYY'), 600 from dual
union all select 1, to_date('01/07/2018', 'DD/MM/YYYY'), 400 from dual
union all select 2, to_date('01/02/2018', 'DD/MM/YYYY'), 700 from dual
union all select 2, to_date('01/03/2018', 'DD/MM/YYYY'), 650 from dual
union all select 2, to_date('01/05/2018', 'DD/MM/YYYY'), 600 from dual
union all select 3, to_date('01/02/2018', 'DD/MM/YYYY'), 50 from dual
union all select 3, to_date('01/08/2018', 'DD/MM/YYYY'), 40 from dual
union all select 4, to_date('01/01/2018', 'DD/MM/YYYY'), 2000 from dual
),
-- second CTE to generate all months, here based on full range in table
-- use whatever you currently have for this
all_months (month) as (
select add_months(min_month, + level - 1)
from (
select min(month) as min_month, max(month) as max_month from my_table
)
connect by level <= months_between(max_month, min_month) + 1
)
select am.month, mt.id,
max(mt.remainingvalue) keep (dense_rank last order by mt.month) as remainingvalue
from all_months am
left join my_table mt on mt.month <= am.month
group by am.month, mt.id
order by id, month;
which gets
MONTH ID REMAININGVALUE
---------- ---------- --------------
2018-01-01 1 1000
2018-02-01 1 1000
2018-03-01 1 800
2018-04-01 1 600
2018-05-01 1 600
2018-06-01 1 600
2018-07-01 1 400
2018-08-01 1 400
2018-02-01 2 700
2018-03-01 2 650
2018-04-01 2 650
...
And then use that as an inline view or another CTE, summing the values:
-- first CTE to replictae your data
with my_table(ID, month, RemainingValue) as (
select 1, to_date('01/01/2018', 'DD/MM/YYYY'), 1000 from dual
union all select 1, to_date('01/03/2018', 'DD/MM/YYYY'), 800 from dual
union all select 1, to_date('01/04/2018', 'DD/MM/YYYY'), 600 from dual
union all select 1, to_date('01/07/2018', 'DD/MM/YYYY'), 400 from dual
union all select 2, to_date('01/02/2018', 'DD/MM/YYYY'), 700 from dual
union all select 2, to_date('01/03/2018', 'DD/MM/YYYY'), 650 from dual
union all select 2, to_date('01/05/2018', 'DD/MM/YYYY'), 600 from dual
union all select 3, to_date('01/02/2018', 'DD/MM/YYYY'), 50 from dual
union all select 3, to_date('01/08/2018', 'DD/MM/YYYY'), 40 from dual
union all select 4, to_date('01/01/2018', 'DD/MM/YYYY'), 2000 from dual
),
-- second CTE to generate all months, here based on full range in table
-- use whatever you currently have for this
all_months (month) as (
select add_months(min_month, + level - 1)
from (
select min(month) as min_month, max(month) as max_month from my_table
)
connect by level <= months_between(max_month, min_month) + 1
),
-- third CTE to get the latest value for each ID up to that month
inter (month, id, remainingvalue) as (
select am.month, mt.id,
max(mt.remainingvalue) keep (dense_rank last order by mt.month)
from all_months am
left join my_table mt on mt.month <= am.month
group by am.month, mt.id
)
select month, sum(remainingvalue) as result,
listagg(remainingvalue, ' + ') within group (order by id) as calculation
from inter
group by month
order by month;
which gets:
MONTH RESULT CALCULATION
---------- ---------- ------------------------------
2018-01-01 3000 1000 + 2000
2018-02-01 3750 1000 + 700 + 50 + 2000
2018-03-01 3500 800 + 650 + 50 + 2000
2018-04-01 3300 600 + 650 + 50 + 2000
2018-05-01 3250 600 + 600 + 50 + 2000
2018-06-01 3250 600 + 600 + 50 + 2000
2018-07-01 3050 400 + 600 + 50 + 2000
2018-08-01 3040 400 + 600 + 40 + 2000
I assume the calculation and remark columns in your result are just for our benefit to understand the logic; if you do want them then calculation is easy to get as above, and if you want remark too then you just need to identify the month the value comes from too, and add another listagg:
...
-- third CTE to get the latest value for each ID up to that month
inter (month, id, remainingvalue, valuemonth) as (
select am.month, mt.id,
max(mt.remainingvalue) keep (dense_rank last order by mt.month),
max(mt.month)
from all_months am
left join my_table mt on mt.month <= am.month
group by am.month, mt.id
)
select month, sum(remainingvalue) as result,
'= ' || listagg(remainingvalue, ' + ') within group (order by id) as calculation,
'ID ' || listagg(id || case when month != valuemonth then '[' || valuemonth || ']' end, ' + ')
within group (order by id) as remark
from inter
group by month
order by month;
MONTH RESULT CALCULATION REMARK
---------- ---------- ------------------------ -----------------------------------------------------------------
2018-01-01 3000 = 1000 + 2000 ID 1 + 4
2018-02-01 3750 = 1000 + 700 + 50 + 2000 ID 1[2018-01-01] + 2 + 3 + 4[2018-01-01]
2018-03-01 3500 = 800 + 650 + 50 + 2000 ID 1 + 2 + 3[2018-02-01] + 4[2018-01-01]
2018-04-01 3300 = 600 + 650 + 50 + 2000 ID 1 + 2[2018-03-01] + 3[2018-02-01] + 4[2018-01-01]
2018-05-01 3250 = 600 + 600 + 50 + 2000 ID 1[2018-04-01] + 2 + 3[2018-02-01] + 4[2018-01-01]
2018-06-01 3250 = 600 + 600 + 50 + 2000 ID 1[2018-04-01] + 2[2018-05-01] + 3[2018-02-01] + 4[2018-01-01]
2018-07-01 3050 = 400 + 600 + 50 + 2000 ID 1 + 2[2018-05-01] + 3[2018-02-01] + 4[2018-01-01]
2018-08-01 3040 = 400 + 600 + 40 + 2000 ID 1[2018-07-01] + 2[2018-05-01] + 3 + 4[2018-01-01]

You seem to want to sum the most recent value for each project before a given month.
The following gets the remaining value for each id for each month:
with months as (
SELECT LEVEL AS NR, ADD_MONTHS(DATE '2018-01-01', LEVEL) AS MONTH
FROM DUAL
CONNECT BY LEVEL <= (... SOME.SUBSELECT.TO.GET.THE.NUMBER.OF.LEVELS ...)
)
select m.month, i.id,
(select max(t.remainingvalue) keep (dense_rank first order by month desc)
from t
where t.id = i.id and t.month <= m.month
) as remainingvalue
from months m cross join
(select distinct id from t) i;
Now let's just summarize this:
with months as (
SELECT LEVEL AS NR, ADD_MONTHS(DATE '2018-01-01', LEVEL) AS MONTH
FROM DUAL
CONNECT BY LEVEL <= (... SOME.SUBSELECT.TO.GET.THE.NUMBER.OF.LEVELS ...)
)
select month, sum(remainingvalue)
from (select m.month, i.id,
(select max(t.remainingvalue) keep (dense_rank first order by month desc)
from t
where t.id = i.id and t.month <= m.month
) as remaining_value
from months m cross join
(select distinct id from t) i
) mi
group by month;

Related

CONSECUTIVE DAYS QUERY [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 11 months ago.
Improve this question
I have an Oracle DB Connection that has data (SELECT * FROM SALES) as in the picture, i want a query that gives me which 3 consecutive days are those who have the sum of PREMIUM_TOTAL > 100.
I have tried with the method lead, lag , DATADIFF but failed. Also i'm new at this, if you can give me hints please.
If you want 3 rows from successive days then you can use a recursive query:
WITH successive_days (day, products, total, depth) AS (
SELECT entry_date,
TO_CHAR(product_id),
premium_total,
1
FROM table_name
UNION ALL
SELECT s.day + 1,
s.products || ',' || t.product_id,
s.total + t.premium_total,
s.depth + 1
FROM successive_days s
INNER JOIN table_name t
ON (s.day + 1 = t.entry_date)
WHERE s.depth < 3
)
SELECT day AS final_day, products, total
FROM successive_days
WHERE depth = 3
AND total >= 100;
Which, for the sample data:
CREATE TABLE table_name (product_id, entry_date, premium_total) AS
SELECT 1, DATE '2022-03-01', 1 FROM DUAL UNION ALL
SELECT 2, DATE '2022-03-01', 20 FROM DUAL UNION ALL
SELECT 4, DATE '2022-03-02', 30 FROM DUAL UNION ALL
SELECT 5, DATE '2022-03-03', 30 FROM DUAL UNION ALL
SELECT 10, DATE '2022-03-21', 12 FROM DUAL UNION ALL
SELECT 11, DATE '2022-03-31', 40.5 FROM DUAL UNION ALL
SELECT 13, DATE '2022-03-05', 70 FROM DUAL UNION ALL
SELECT 12, DATE '2022-03-05', 80 FROM DUAL UNION ALL
SELECT 14, DATE '2022-03-05', 10 FROM DUAL UNION ALL
SELECT 20, DATE '2022-03-06', 20 FROM DUAL UNION ALL
SELECT 21, DATE '2022-03-07', 30 FROM DUAL UNION ALL
SELECT 22, DATE '2022-03-07', 40 FROM DUAL UNION ALL
SELECT 30, DATE '2022-03-08', 20 FROM DUAL UNION ALL
SELECT 31, DATE '2022-03-09', 50 FROM DUAL UNION ALL
SELECT 40, DATE '2022-03-10', 2 FROM DUAL;
Outputs:
FINAL_DAY
PRODUCTS
TOTAL
2022-03-07 00:00:00
13,20,21
120
2022-03-07 00:00:00
13,20,22
130
2022-03-07 00:00:00
12,20,21
130
2022-03-07 00:00:00
12,20,22
140
2022-03-09 00:00:00
21,30,31
100
2022-03-09 00:00:00
22,30,31
110
If you want all the rows (at least 3) that are all within 3 successive days then you can use MATCH_RECOGNIZE:
SELECT MIN(entry_date) AS start_day,
MAX(entry_date) AS final_day,
LISTAGG(product_id, ',') WITHIN GROUP (ORDER BY entry_date) AS products,
SUM(premium_total) AS total
FROM table_name
MATCH_RECOGNIZE(
ORDER BY entry_date
MEASURES
MATCH_NUMBER() AS mno
ALL ROWS PER MATCH
AFTER MATCH SKIP TO NEXT ROW
PATTERN (first_day+ second_day+ third_day* final_day)
DEFINE
first_day AS FIRST(entry_date) = entry_date,
second_day AS FIRST(entry_date) + 1 = entry_date,
third_day AS FIRST(entry_date) + 2 = entry_date,
final_day AS FIRST(entry_date) + 2 = entry_date
AND SUM(premium_total) >= 100
)
GROUP BY mno;
Which, for the sample data, outputs:
START_DAY
FINAL_DAY
PRODUCTS
TOTAL
2022-03-05 00:00:00
2022-03-07 00:00:00
12,13,14,20,21,22
250
2022-03-05 00:00:00
2022-03-07 00:00:00
13,14,20,21,22
170
2022-03-05 00:00:00
2022-03-07 00:00:00
13,20,21,22
160
2022-03-06 00:00:00
2022-03-08 00:00:00
20,21,22,30
110
2022-03-07 00:00:00
2022-03-09 00:00:00
21,22,30,31
140
2022-03-07 00:00:00
2022-03-09 00:00:00
22,30,31
110
db<>fiddle here

I need data for all 24 months of data even with missing months

I need data for all 24 months of data even with missing months.
sample data
id custname reportdate sales
1 xx 31-JAN-17 1256
1 xx 31-MAR-17 3456
1 xx 30-JUN-17 5678
1 xx 31-DEC-17 6785
2 xx 31-JAN-17 1223
2 xx 31-APR-17 3435
2 xx 30-JUN-17 6777
2 xx 31-DEC-17 9643
what i need as a output
id custname reportdate sales
1 xx JAN-17 1256
1 xx FEB-17 <null>
1 xx MAR-17 3456
.....................................
.....................................
1 xx DEC-17 6785
And similarly for id 2 ....
Tried something like this without any luck
select CUSTNAME, reportdate, sales from
(
select TRIM( LEADING '0' FROM TO_CHAR( statementdate, 'YYYY-MM') ) AS REPORTDATE mm, CUSTNAME
froM MYTABLE) SALES,
(
select to_char(date '2017-01-01' + numtoyminterval(level,'month'), 'mm') MonthName
--i actually need format as MON-Last 2 digit of year eg:JAN-17
from dual
connect by level <= 24) ALLMONTHS
where mm = MonthName(+)
also tried with CTE and i cant use my_year.year_month CTE with outer join
my_year as (
select date '2017-01-31' start_date,date '2018-12-31' end_date from dual
)
select (to_char(add_months(trunc(start_date,'mm'),level - 1),'yyyy')||'-'||(to_char(add_months(trunc(start_date,'mm'),level - 1),'mm'))) year_month
from my_year
connect by trunc(end_date,'mm') >= add_months(trunc(start_date,'mm'),level - 1);
select id, customername, reportdate, sales,
TRIM( LEADING '0' FROM TO_CHAR( reportdate, 'YYYY-MM') ) AS stmntdate
from my_oracle_tbl a
where a.stmntdate = my_year.year_month (+)
also tried this as recommended by #Littlefoot, which isnt working
WITH mydates AS (
select LAST_DAY(add_months(date '2017-01-01', level - 1)) as mth, min_id,min_custname
from (
select min(id) as min_id, min(CUSTNAME) as min_custname
from my_oracle_tbl
)
connect by level <= 24)
select
nvl(t.id, a.min_id)id,
nvl(t.CUSTNAME,a.min_custname)CUSTNAME, a.mth, t.sales
from mydates a left join my_oracle_tbl t on a.mth= LAST_DAY(t.reporttdate)
where
t.id=2
;
You can use some old school tricks (UNION ALL IN COMBINATION WITH ADD_MONTHS FUNCTION AND SUM):
select id, custname,month,
decode(sum(sales),0,null,sum(sales)) sales from
(select id, custname, to_char(reportdate, 'mon-rrrr')
month,sales from my_oracle_tbl
UNION ALL
select a.*,b.*,0 sales from
(select distinct id, custname from my_oracle_tbl) a,
(
select to_char(sysdate,'mon')||'-2017' month from dual
UNION ALL
select ,to_char(add_months(sysdate,1),'mon')||'-2017' month from dual
UNION ALL
select ,to_char(add_months(sysdate,2),'mon')||'-2017' from dual
.......
UNION ALL
select ,to_char(add_months(sysdate,11),'mon')||'-2017' from dual) b
)
group by id, custname,month;
This is what i came up with do u see any concerns? is there a better way to write this? I need to get order by lowest to largest dates?? how can i achieve this. As of now it repeats order like this 12-2018,12-2017,11-2018,11-2017. I want 2017 dates first and then 2018
select CUSTNAME, reportdate, sum(sales), mth
from ( select to_char(add_months(date '2017-01-01', level - 1), 'mmyyyy') mth
from dual
connect by level <= 24)mo
left outer join oracle_tbl dc on mo.mth = to_char(reportdate, 'mmyyyy')
group by CUSTNAME, reportdate,mth
order by mth
Here's an example; see if it helps. It displays 12 months (you'd substitute it with 24 in line #21).
SQL> alter session set nls_date_format = 'dd.mm.yyyy';
Session altered.
SQL> with test (id, custname, reportdate, sales) as
2 (select 1, 'xx', date '2017-01-31', 1256 from dual union all
3 select 1, 'xx', date '2017-03-31', 3456 from dual union all
4 select 1, 'xx', date '2017-06-30', 5678 from dual union all
5 --
6 select 2, 'xx', date '2017-03-31', 1223 from dual union all
7 select 2, 'xx', date '2017-07-31', 3435 from dual union all
8 select 2, 'xx', date '2017-09-30', 6777 from dual
9 ),
10 all_dates as
11 (select add_months(min_repdate, column_value - 1) c_mon,
12 min_id,
13 min_custname
14 from (select min(reportdate) min_repdate,
15 id min_id,
16 min(custname) min_custname
17 from test
18 group by id
19 ),
20 table(cast(multiset(select level from dual
21 connect by level <= 12
22 ) as sys.odcinumberlist))
23 )
24 select nvl(t.id, a.min_id) id,
25 nvl(t.custname, a.min_custname) custname,
26 a.c_mon,
27 t.sales
28 from all_dates a left join test t on a.min_id = t.id and a.c_mon = t.reportdate
29 order by id, a.c_mon;
ID CU C_MON SALES
---------- -- ---------- ----------
1 xx 31.01.2017 1256
1 xx 28.02.2017
1 xx 31.03.2017 3456
1 xx 30.04.2017
1 xx 31.05.2017
1 xx 30.06.2017 5678
1 xx 31.07.2017
1 xx 31.08.2017
1 xx 30.09.2017
1 xx 31.10.2017
1 xx 30.11.2017
1 xx 31.12.2017
2 xx 31.03.2017 1223
2 xx 30.04.2017
2 xx 31.05.2017
2 xx 30.06.2017
2 xx 31.07.2017 3435
2 xx 31.08.2017
2 xx 30.09.2017 6777
2 xx 31.10.2017
2 xx 30.11.2017
2 xx 31.12.2017
2 xx 31.01.2018
2 xx 28.02.2018
24 rows selected.
SQL>

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

How can update a column based on the value of another column in SQL?

Basically I have Product table like this:
date price
--------- -----
02-SEP-14 50
03-SEP-14 60
04-SEP-14 60
05-SEP-14 60
07-SEP-14 71
08-SEP-14 45
09-SEP-14 45
10-SEP-14 24
11-SEP-14 60
I need to update the table in this form
date price id
--------- ----- --
02-SEP-14 50 1
03-SEP-14 60 2
04-SEP-14 60 2
05-SEP-14 60 2
07-SEP-14 71 3
08-SEP-14 45 4
09-SEP-14 45 4
10-SEP-14 24 5
11-SEP-14 60 6
What I have tried:
CREATE SEQUENCE user_id_seq
START WITH 1
INCREMENT BY 1
CACHE 20;
ALTER TABLE Product
ADD (ID number);
UPDATE Product SET ID = user_id_seq.nextval;
This is updating the ID in the usual way like 1,2,3,4,5..
I have no idea how to do it using basic SQL commands. Please suggest how can I make it. Thank you in advance.
Here is one way to create a view from your base data. I assume you have more than one product (identified by product id), and that the price dates aren't necessarily consecutive. The sequence is separate for each product id. (Also, product should be the name of a different table - where the product id is primary key, and you have other information such as product name, category, etc. The table in your post would be more properly called something like price_history.)
alter session set nls_date_format='dd-MON-rr';
create table product ( prod_id number, dt date, price number );
insert into product ( prod_id, dt, price )
select 101, '02-SEP-14', 50 from dual union all
select 101, '03-SEP-14', 60 from dual union all
select 101, '04-SEP-14', 60 from dual union all
select 101, '05-SEP-14', 60 from dual union all
select 101, '07-SEP-14', 71 from dual union all
select 101, '08-SEP-14', 45 from dual union all
select 101, '09-SEP-14', 45 from dual union all
select 101, '10-SEP-14', 24 from dual union all
select 101, '11-SEP-14', 60 from dual union all
select 102, '02-SEP-14', 45 from dual union all
select 102, '04-SEP-14', 45 from dual union all
select 102, '05-SEP-14', 60 from dual union all
select 102, '06-SEP-14', 50 from dual union all
select 102, '09-SEP-14', 60 from dual
;
commit;
create view product_vw ( prod_id, dt, price, seq ) as
select prod_id, dt, price,
count(flag) over (partition by prod_id order by dt)
from ( select prod_id, dt, price,
case when price = lag(price) over (partition by prod_id order by dt)
then null else 1 end as flag
from product
)
;
Now check what the view looks like:
select * from product_vw;
PROD_ID DT PRICE SEQ
------- ------------------- ---------- ----------
101 02/09/0014 00:00:00 50 1
101 03/09/0014 00:00:00 60 2
101 04/09/0014 00:00:00 60 2
101 05/09/0014 00:00:00 60 2
101 07/09/0014 00:00:00 71 3
101 08/09/0014 00:00:00 45 4
101 09/09/0014 00:00:00 45 4
101 10/09/0014 00:00:00 24 5
101 11/09/0014 00:00:00 60 6
102 02/09/0014 00:00:00 45 1
102 04/09/0014 00:00:00 45 1
102 05/09/0014 00:00:00 60 2
102 06/09/0014 00:00:00 50 3
102 09/09/0014 00:00:00 60 4
NOTE: This answers the question that was originally asked. The OP changed the data.
If your data is not too large, you can use a correlated subquery:
update product p
set id = (select count(distinct p2.price)
from product p2
where p2.date <= p.date
);
If your data is larger, then merge is more appropriate.
WITH cts AS
(
SELECT row_number() over (partition by price order by price ) as id
,date
,price
FROM Product
)
UPDATE p
set p.id = cts.id
from product p join cts on cts.id = p.id
This is the best way by which you try to do.
There is no another simple way to do this using simple statements

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

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