Say I have a list of dates & prices:
20170322 109.89
20170321 107.02
20170320 109.25
20170317 108.44
20170316 108.53
20170315 107.94
20170314 106.83
20170313 110.02
20170310 107.31
20170309 107.54
20170308 107.67
20170307 108.98
What I need is, from the most recent date: 20170322 (109.89), what is the FIRST date / price value that is HIGHER than the original value, which is 20170313 (110.02). Note these are in DESC order of the date
Been at this ALL day.
Assuming the columns are called DT and PRICE, and assuming there is only one "thing" whose price you monitor (otherwise you would need a GROUP BY clause):
select min(dt) as dt, min(price) keep (dense_rank first order by dt) as price
from your_table
where price > ( select min(price) keep (dense_rank first order by dt)
from your_table
)
https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions056.htm
Solution using the MATCH_RECOGNIZE clause (requires Oracle 12 and above).
I created the test data in a WITH clause. That is NOT PART OF THE SOLUTION; the SQL query begins after the WITH clause, at SELECT TICKER, ....
The question mark in the reluctant match in the PATTERN trips the JDBC driver, so this query can't be run from SQL Developer; it needs to be run in SQL*Plus or similar front-end. (The workaround is to change b*? to b* and to add to the DEFINE clause: b as b.price <= a.price.)
To illustrate more of the flexibility of MATCH_RECOGNIZE, I assumed there may be several "tickers", each with its inception date (earliest date with a price), and the query looks for the first occurrence of a price higher than the original one, per ticker.
with
test_data ( ticker, dt, price ) as (
select 'XYZ', to_date('20170322', 'yyyymmdd'), 109.89 from dual union all
select 'XYZ', to_date('20170321', 'yyyymmdd'), 107.02 from dual union all
select 'XYZ', to_date('20170320', 'yyyymmdd'), 109.25 from dual union all
select 'XYZ', to_date('20170317', 'yyyymmdd'), 108.44 from dual union all
select 'XYZ', to_date('20170316', 'yyyymmdd'), 108.53 from dual union all
select 'XYZ', to_date('20170315', 'yyyymmdd'), 107.94 from dual union all
select 'XYZ', to_date('20170314', 'yyyymmdd'), 106.83 from dual union all
select 'XYZ', to_date('20170313', 'yyyymmdd'), 110.02 from dual union all
select 'XYZ', to_date('20170310', 'yyyymmdd'), 107.31 from dual union all
select 'XYZ', to_date('20170309', 'yyyymmdd'), 107.54 from dual union all
select 'XYZ', to_date('20170308', 'yyyymmdd'), 107.67 from dual union all
select 'XYZ', to_date('20170307', 'yyyymmdd'), 108.98 from dual
)
select ticker, dt, price
from test_data
match_recognize (
partition by ticker
order by dt
measures c.dt as dt, c.price as price
one row per match
pattern ( ^ a b*? c )
define c as c.price > a.price
)
;
TICKER DT PRICE
------ ---------- -------
XYZ 2017-03-13 110.02
1 row selected.
Related
How to translate the following query to Oracle SQL, as Oracle doesn't support distinct on()?
select distinct on (t.transaction_id) t.transaction_id as transactionId ,
t.transaction_status as transactionStatus ,
c.customer_id as customerId ,
c.customer_name as customerName,
You can use ANY_VALUE with group by for this:
https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/any_value.html
Example: https://dbfiddle.uk/WUxvjv5J
with t (a,b,c) as (
select 1,10,1 from dual union all
select 1,10,2 from dual union all
select 1,10,3 from dual union all
select 1,20,4 from dual union all
select 1,20,5 from dual union all
select 1,30,7 from dual
)
select a,b,any_value(c)
from t
group by a,b;
Yes, Oracle has a full set of windowing functions you can use for this. The simplest is ROW_NUMBER:
SELECT *
FROM (SELECT x.col1,
x.col2,
x.col3,
ROW_NUMBER() OVER (PARTITION BY x.col1 ORDER BY x.col2 DESC) seq
FROM table x)
WHERE seq = 1
for each distinct col1, it will number the highest col2 value as seq=1, the next highest as seq=2, etc... so you can filter on 1 to get the desired row. You can used as complex ORDER BY logic as you need to pick the row you want. The key thing is that the ORDER BY goes inside the ROW_NUMBER OVER clause along with the distinct (PARTITION BY) definition, not outside in the main query block.
(SELECT LISTAGG(EVENT_DESC, ',') WITHIN GROUP (ORDER BY EVENT_DESC) FROM EVENT_REF WHERE EVENT_ID IN
( SELECT REGEXP_SUBSTR(AFTER_VALUE,'[^,]+', 1, level) FROM DUAL
CONNECT BY REGEXP_SUBSTR(AFTER_VALUE, '[^,]+', 1, level) IS NOT NULL
)
)
A table from which I am fetching AFTER_VALUE has values of integer which is comma seperated like
AFTER_VALUE data
Expected output
1
Event1
1,2
Event1,Event2
1,12,2,5
Event1,Event12,Event2,Event5
15,13
Event15,Event13
these are Ids in EVENT_REF table which have some description. I am trying to basically present
ex. 1,2 as Event1, Event2 and send back from query. There are multiple events so using REPLACE would be very tedious.
When using above query I'm getting error as “ORA-01722: invalid number” whenever there is more than one value in AFTER_VALUE column Ex. if there exists only one id , then the query works but for values like 1,2 or 1,13 etc it throws invalid number error.
PS: The event names are not Event1,Event2 etc , I have just put for reference.
You don't even need regular expressions for this assignment. Standard string function replace() can do the same thing, and faster. You only need an extra 'Event' at the beginning of the string, since that one doesn't "replace" anything.
Like this: (note that you don't need the with clause; I included it only for quick testing)
with
event_ref (after_value) as (
select '1' from dual union all
select '1,2' from dual union all
select '1,12,2,5' from dual union all
select '15,13' from dual
)
select after_value,
'Event' || replace(after_value, ',', ',Event') as desired_output
from event_ref
;
AFTER_VALUE DESIRED_OUTPUT
----------- -----------------------------
1 Event1
1,2 Event1,Event2
1,12,2,5 Event1,Event12,Event2,Event5
15,13 Event15,Event13
Ah,ok, looks, like you have other characters in your comma-separated list, so you can use this query:
with EVENT_REF(EVENT_ID,EVENT_DESC) as (
select 1, 'Desc 1' from dual union all
select 2, 'Desc 2' from dual union all
select 3, 'Desc 3' from dual union all
select 4, 'Desc 4' from dual union all
select 5, 'Desc 5' from dual union all
select 12, 'Desc12' from dual union all
select 13, 'Desc13' from dual union all
select 15, 'Desc15' from dual
)
select
(SELECT LISTAGG(EVENT_DESC, ',')
WITHIN GROUP (ORDER BY EVENT_DESC)
FROM EVENT_REF
WHERE EVENT_ID IN
( SELECT to_number(REGEXP_SUBSTR(AFTER_VALUE,'\d+', 1, level))
FROM DUAL
CONNECT BY level<=REGEXP_COUNT(AFTER_VALUE, '\d+')
)
)
from (
select '1' AFTER_VALUE from dual union all
select '1,2' AFTER_VALUE from dual union all
select '1,12,2,5' AFTER_VALUE from dual union all
select '15,13' AFTER_VALUE from dual
);
PS. And do not forget that to_number has 'default on conversion error' now: https://docs.oracle.com/en/database/oracle/oracle-database/12.2/sqlrf/TO_NUMBER.html
There is no need to split and concatenate substrings, just use regexp_replace:
with EVENT_REF (AFTER_VALUE) as (
select '1' from dual union all
select '1,2' from dual union all
select '1,12,2,5' from dual union all
select '15,13' from dual
)
select regexp_replace(AFTER_VALUE,'(\d+)','Event\1') from EVENT_REF;
REGEXP_REPLACE(AFTER_VALUE,'(\D+)','EVENT\1')
-----------------------------------------------
Event1
Event1,Event2
Event1,Event12,Event2,Event5
Event15,Event13
Thank you in advance for your help.
I have a table that holds itinerary information for drivers. There will be times when the itinerary seems to have the same stop (but is several days apart). I'd like to be able to query the table and filter out any record where the address is the same AND the dates are consecutive.
Is this possible?
Thanks again,
josh
with tst as(
select timestamp '2020-08-01 00:00:00' dt, '123 street' loc from dual
union all
select timestamp '2020-08-01 00:00:00', '89 street' from dual
union all
select timestamp '2020-08-02 00:00:00', '456 airport' from dual
union all
select timestamp '2020-08-04 00:00:00', '456 airport' from dual
union all
select timestamp '2020-08-05 00:00:00', '67 street' from dual
union all
select timestamp '2020-08-06 00:00:00', '89 street' from dual
union all
select timestamp '2020-08-07 00:00:00', '123 street' from dual
)
select dt, loc
from (
select dt, loc, nvl(lag(loc) over(order by dt), 'FIRST_ROW') prev_loc
from tst
) where loc <> prev_loc;
fiddle
Another approach would be to use Tabibitosan method which assign consecutive rows a group number and then count number of rows per group.(found in asktom website).
with test_data as(
select date'2020-08-01' dt, '123 street' loc from dual
union all
select date '2020-08-01', '89 street' from dual
union all
select date '2020-08-02', '456 airport' from dual
union all
select date '2020-08-04', '456 airport' from dual
union all
select date '2020-08-05', '67 street' from dual
union all
select date '2020-08-06', '89 street' from dual
union all
select date '2020-08-07', '123 street' from dual
)
select max(dt),loc
from
(
select t.*
,row_number() over (order by dt) -
row_number() over (partition by loc order by dt) grp
from test_data t
)
group by grp,loc
having count(*) > 1;
Another approach using match_recognize available from 12c onwards.patter used {1,} says repeated one or more times
more to learn match_recognize here
with test_data as(
select date'2020-08-01' dt, '123 street' loc from dual
union all
select date '2020-08-01', '89 street' from dual
union all
select date '2020-08-02', '456 airport' from dual
union all
select date '2020-08-04', '456 airport' from dual
union all
select date '2020-08-05', '67 street' from dual
union all
select date '2020-08-06', '89 street' from dual
union all
select date '2020-08-07', '123 street' from dual
)
select *
from test_data
match_recognize (
order by dt
all rows per match
pattern (equal{1,})
define
equal as loc = prev(loc)
);
Playground: Dbfiddle
I am trying to implement the same in redshift and i am finding it little difficult to do that. Since redshift is in top of postgresql engine, if any one can do it in postgresql it would be really helpfull. Basically the code gets the count for previous two month at column level. If there is no count for exact previous month then it gives 0.
This is my code:
with abc(dateval,cnt) as(
select 201908, 100 from dual union
select 201907, 200 from dual union
select 201906, 300 from dual union
select 201904, 600 from dual)
select dateval, cnt,
last_value(cnt) over (order by dateval
range between interval '1' month preceding
and interval '1' month preceding ) m1,
last_value(cnt) over (order by dateval
range between interval '2' month preceding
and interval '2' month preceding ) m2
from (select to_date(dateval, 'yyyymm') dateval, cnt from abc)
I get error in over by clause. I tried to give cast('1 month' as interval) but still its failing. Can someone please help me with this windows function.
expected output:
Regards
This is how I would do it. In Redshift there's no easy way to generate sequences, do I select row_number() from an arbitrary table to create a sequence:
with abc(dateval,cnt) as(
select 201908, 100 union
select 201907, 200 union
select 201906, 300 union
select 201904, 600),
cal(date) as (
select
add_months(
'20190101'::date,
row_number() over () - 1
) as date
from <an arbitrary table to generate a sequence of rows> limit 10
),
with_lag as (
select
dateval,
cnt,
lag(cnt, 1) over (order by date) as m1,
lag(cnt, 2) over (order by date) as m2
from abc right join cal on to_date(dateval, 'YYYYMM') = date
)
select * from with_lag
where dateval is not null
order by dateval
Here is my Table EMP_EARN_DETAILS.
Emp_Ern_No is the primary key.
I need to get the amount for each emp_no for each earn_no where the emp_earn_no is the maximum.
The output should be as follows.
0004321 ERN001 2345 11
0004321 ERN002 345 10
0004321 ERN003 345 9
000507 ER-01 563 4
000732 ERN001 2345 12
000732 ERN002 9 13
000732 ERN003 678 8
Please help me with the query
You can aggregate by the fields you need and, at the same time, order by the EMP_EARN_NO value; this can be a solution, by analytic functions:
WITH TEST(emp_no, earn_no, amount, emp_earn_no) AS
(
SELECT '0004321' , 'ERN001' ,2345 ,11 FROM DUAL UNION ALL
SELECT '0004321' , 'ERN002' ,345 , 10 FROM DUAL UNION ALL
SELECT '0004321' , 'ERN003' ,345 ,9 FROM DUAL UNION ALL
SELECT '000507' , 'ER-01' ,56 ,1 FROM DUAL UNION ALL
SELECT '000507' , 'ER-01' ,563 , 2 FROM DUAL UNION ALL
SELECT '000507' , 'ER-01' ,563 ,3 FROM DUAL UNION ALL
SELECT '000507' , 'ER-01' ,563 ,4 FROM DUAL UNION ALL
SELECT '00732' , 'ERN001' ,123 ,7 FROM DUAL UNION ALL
SELECT '00732' , 'ERN001' ,2345 ,12 FROM DUAL UNION ALL
SELECT '00732' , 'ERN002' ,9 ,13 FROM DUAL UNION ALL
SELECT '00732' , 'ERN003' ,67 ,5 FROM DUAL UNION ALL
SELECT '00732' , 'ERN003' ,456 ,6 FROM DUAL UNION ALL
SELECT '00732' , 'ERN003' ,678 ,8 FROM DUAL
)
SELECT emp_no, earn_no, amount, emp_earn_no
FROM (
SELECT emp_no,
earn_no,
amount,
emp_earn_no, ROW_NUMBER() OVER ( PARTITION BY EMP_NO, EARN_NO ORDER BY emp_earn_no DESC) AS ROW_NUM
FROM TEST
)
WHERE ROW_NUM = 1
Give this a shot,
SELECT EMP_NO, SUM(AMOUNT)
FROM EMP_EARN_DETAILS
GROUP BY EMP_NO
HAVING EMP_EARN_NO = MAX(EMP_EARN_NO)
Try this query:
select emp_no, earn_no,
sum(amount) keep (dense_rank last order by emp_earn_no) as sum_amount
from emp_earn_details
group by emp_no, earn_no
First by following query , your conditions achieved :
select t.emp_no a ,t.earn_no b ,max(t.amount) c
from EMP_EARN_DETAILS t
group by t.emp_no,t.earn_no
order by t.emp_no
Only things that you must specify , in a same record with different EMP_EARN_NO. You have to specify in same record which must be in result.
So if you want maximum EMP_EARN_NO be in result you can use following query as final query (exactly your target in question):
select t.emp_no a ,t.earn_no b ,max(t.amount) c, max(t.emp_earn_no) emp_earn_no
from EMP_EARN_DETAILS t
group by t.emp_no,t.earn_no
order by t.emp_no
If you want minimum or others EMP_EARN_NO be in result you can above query replace max function by your conditions.