Always return X number of rows - oracle

We have a set of values which we use to populate a bar chart. For this application, we will always need 5 years of data, we will always need 5 rows of data, even if the values are NULL.
See this query. Assume that the DATE column goes from 2017, 2016, 2015.........even those we may have no data for 2014 & 2013, I will need to return a 2014 & 2013 for, with a NULL as the other column.....
SELECT period_date, actual_eps
FROM (SELECT LAST_DAY(TO_DATE(TO_CHAR(period_date),'YYYYMM')) period_date, actual_eps
FROM period_data
WHERE ticker = 'ADRO'
AND period_type = 'A'
AND actual_eps IS NOT NULL
ORDER BY period_date DESC NULLS LAST)
WHERE rownum <= 5;
So, it will return what rows it has, up to 5, and NULL for the other rows which it does not have, up to 5.......
Thanks in advance

Try using a Common Table Expression/Subquery Factoring to generate rows for each year value. Use a RIGHT JOIN to generate NULLs for any missing rows.
Normally I would use a LEFT JOIN. But in this case I think it reads better this way.
Use NVL to substitute the year for NULL period_date values.
with years as
(
select to_char(sysdate, 'YYYY') as year from dual
UNION ALL
select to_char(add_months(sysdate,-12), 'YYYY') as year from dual
UNION ALL
select to_char(add_months(sysdate,-24), 'YYYY') as year from dual
UNION ALL
select to_char(add_months(sysdate,-36), 'YYYY') as year from dual
UNION ALL
select to_char(add_months(sysdate,-48), 'YYYY') as year from dual
)
SELECT
NVL(TO_CHAR(LAST_DAY(pd.period_date),'YYYYMM'),y.year) as period_date,
pd.actual_eps
FROM period_data pd
RIGHT JOIN years y ON y.year = to_char(pd.period_date,'YYYY')
AND pd.ticker = 'ADRO'
AND pd.period_type = 'A'
AND pd.actual_eps IS NOT NULL
WHERE rownum <= 5
ORDER BY period_date desc, actual_eps nulls last;
Output:
| PERIOD_DATE | ACTUAL_EPS |
|-------------|------------|
| 201902 | foo |
| 201802 | foo |
| 201702 | foo |
| 2016 | (null) |
| 2015 | (null) |
SQL Fiddle example

Related

Oracle - how to update a unique row based on MAX effective date which is part of the unique index

Oracle - Say you have a table that has a unique key on name, ssn and effective date. The effective date makes it unique. What is the best way to update a current indicator to show inactive for the rows with dates less than the max effective date? I can't really wrap my head around it since there are multiple rows with the same name and ssn combinations. I haven't been able to find this scenario on here for Oracle and I'm having developer's block. Thanks.
"All name/ssn having a max effective date earlier than this time yesterday:"
SELECT name, ssn
FROM t
GROUP BY name, ssn
HAVING MAX(eff_date) < SYSDATE - 1
Oracle supports multi column in, so
UPDATE t
SET current_indicator = 'inactive'
WHERE (name,ssn,eff_date) IN (
SELECT name, ssn, max(eff_date)
FROM t
GROUP BY name, ssn
HAVING MAX(eff_date) < SYSDATE - 1
)
Use a MERGE statement using an analytic function to identify the rows to update and then merge on the ROWID pseudo-column so that Oracle can efficiently identify the rows to update (without having to perform an expensive self-join by comparing the values):
MERGE INTO table_name dst
USING (
SELECT rid,
max_eff_date
FROM (
SELECT ROWID AS rid,
effective_date,
status,
MAX( effective_date ) OVER ( PARTITION BY name, ssn ) AS max_eff_date
FROM table_name
)
WHERE ( effective_date < max_eff_date AND status <> 'inactive' )
OR ( effective_date = max_eff_date AND status <> 'active' )
) src
ON ( dst.ROWID = src.rid )
WHEN MATCHED THEN
UPDATE
SET status = CASE
WHEN src.max_eff_date = dst.effective_date
THEN 'active'
ELSE 'inactive'
END;
So, for some sample data:
CREATE TABLE table_name ( name, ssn, effective_date, status ) AS
SELECT 'aaa', 1, DATE '2020-01-01', 'inactive' FROM DUAL UNION ALL
SELECT 'aaa', 1, DATE '2020-01-02', 'inactive' FROM DUAL UNION ALL
SELECT 'aaa', 1, DATE '2020-01-03', 'inactive' FROM DUAL UNION ALL
SELECT 'bbb', 2, DATE '2020-01-01', 'active' FROM DUAL UNION ALL
SELECT 'bbb', 2, DATE '2020-01-02', 'inactive' FROM DUAL UNION ALL
SELECT 'bbb', 3, DATE '2020-01-01', 'inactive' FROM DUAL UNION ALL
SELECT 'bbb', 3, DATE '2020-01-03', 'active' FROM DUAL;
The query only updates the 3 rows that need changing and:
SELECT *
FROM table_name;
Outputs:
NAME | SSN | EFFECTIVE_DATE | STATUS
:--- | --: | :------------- | :-------
aaa | 1 | 01-JAN-20 | inactive
aaa | 1 | 02-JAN-20 | inactive
aaa | 1 | 03-JAN-20 | active
bbb | 2 | 01-JAN-20 | inactive
bbb | 2 | 02-JAN-20 | active
bbb | 3 | 01-JAN-20 | inactive
bbb | 3 | 03-JAN-20 | active
db<>fiddle here

How to use Oracle's LISTAGG function with a multi values?

I have an 'ITEMS' table like below:
ITEM_NO ITEM_NAME
1 Book
2 Pen
3 Sticky Notes
4 Ink
5 Corrector
6 Ruler
In another 'EMP_ITEMS' table I have the below:
EMPLOYEE ITEMS_LIST
John 1,2
Mikel 5
Sophia 2,3,6
William 3,4
Daniel null
Michael 6
The output has to be like this:
EMPLOYEE ITEMS_LIST ITEM_NAME
John 1,2 Book,Pen
Mikel 5 Corrector
Sophia 2,3,6 Pen,Sticky Notes,Ruler
William 3,4 Sticky Notes,Ink
Daniel null null
Michael 6 Ruler
I used the below query:
SELECT e.EMPLOYEE,e.ITEMS_LIST, LISTAGG(i.ITEM_NAME, ',') WITHIN GROUP (ORDER BY i.ITEM_NAME) ITEM_DESC
FROM EMP_ITEMS e
INNER JOIN ITEMS i ON i.ITEM_NO = e.ITEMS_LIST
GROUP BY e.EMPLOYEE,e.ITEMS_LIST;
But there is an error:
ORA-01722: invalid number
But there is an error: ORA-01722: invalid number
That is because your ITEMS_LIST is a string composed of numeric and comma characters and is not actually a list of numbers and you are trying to compare a single item number to a list of items.
Instead treat it as a string a look for sub-string matches. To do this you will need to surround the strings in the delimiter character and compare to see if one is the substring of the other:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE Items ( ITEM_NO, ITEM_NAME ) As
SELECT 1, 'Book' FROM DUAL UNION ALL
SELECT 2, 'Pen' FROM DUAL UNION ALL
SELECT 3, 'Sticky Notes' FROM DUAL UNION ALL
SELECT 4, 'Ink' FROM DUAL UNION ALL
SELECT 5, 'Corrector' FROM DUAL UNION ALL
SELECT 6, 'Ruler' FROM DUAL;
CREATE TABLE emp_items ( EMPLOYEE, ITEMS_LIST ) AS
SELECT 'John', '1,2' FROM DUAL UNION ALL
SELECT 'Mikel', '5' FROM DUAL UNION ALL
SELECT 'Sophia', '3,2,6' FROM DUAL UNION ALL
SELECT 'William', '3,4' FROM DUAL UNION ALL
SELECT 'Daniel', null FROM DUAL UNION ALL
SELECT 'Michael', '6' FROM DUAL;
Query 1:
SELECT e.employee,
e.items_list,
LISTAGG( i.item_name, ',' )
WITHIN GROUP (
ORDER BY INSTR( ','||e.items_list||',', ','||i.item_no||',' )
) AS item_names
FROM emp_items e
LEFT OUTER JOIN
items i
ON ( ','||e.items_list||',' LIKE '%,'||i.item_no||',%' )
GROUP BY e.employee, e.items_list
Results:
| EMPLOYEE | ITEMS_LIST | ITEM_NAMES |
|----------|------------|------------------------|
| John | 1,2 | Book,Pen |
| Mikel | 5 | Corrector |
| Daniel | (null) | (null) |
| Sophia | 3,2,6 | Sticky Notes,Pen,Ruler |
| Michael | 6 | Ruler |
| William | 3,4 | Sticky Notes,Ink |

oracle concat timestmap from current_date

I hardly try to concat timestamps. Column time, defined as varchar2, contains values like 23:15. Now I want to create a timestamp with today's date and that time, in this example 23.03.18 23:15:00.00000 is expected. The way I'm doing this is
to_timestamp(to_char(trunc(current_date),'ddMMyyyy') ||
to_char(time),'dd.MM.yyyy hh24:mi:ss')
and it works. But when field time has value 06:15, I get the message "hour must be between 0 and 23 ". Whatever I try, it's always this message when time value has leading zero. How can that be corrected?
You don't need to use TRUNC on the current date as the TO_CHAR will extract only the year-to-day components and you also don't need TO_CHAR on the time column as it is already a string. Apart from those simplifications, your query works:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE times ( time ) AS
SELECT '00:00' FROM DUAL UNION ALL
SELECT '06:45' FROM DUAL UNION ALL
SELECT '12:00' FROM DUAL UNION ALL
SELECT '18:59' FROM DUAL UNION ALL
SELECT '23:15' FROM DUAL;
Query 1:
SELECT time,
TO_TIMESTAMP(
TO_CHAR( CURRENT_DATE, 'YYYY-MM-DD' ) || time,
'YYYY-MM-DDHH24:MI'
) AS current_day_time
FROM times
Results:
| TIME | CURRENT_DAY_TIME |
|-------|-----------------------|
| 00:00 | 2018-03-23 00:00:00.0 |
| 06:45 | 2018-03-23 06:45:00.0 |
| 12:00 | 2018-03-23 12:00:00.0 |
| 18:59 | 2018-03-23 18:59:00.0 |
| 23:15 | 2018-03-23 23:15:00.0 |
Works OK for me:
SQL> WITH test
2 AS (SELECT '23:15' time FROM DUAL
3 UNION
4 SELECT '06:15' time FROM DUAL)
5 SELECT TO_TIMESTAMP (
6 TO_CHAR (CURRENT_DATE, 'ddMMyyyy') || time,
7 'dd.MM.yyyy hh24:mi:ss') result
8 FROM test;
RESULT
----------------------------------------------------------------
23.03.18 06:15:00,000000000
23.03.18 23:15:00,000000000
SQL>
Please, post your SQL*Plus session so that we could see what you did.
try other way around: insert a leading 0 if hour < 10
to_timestamp(to_char(trunc(current_date),'ddMMyyyy') ||
to_char(
case when substr(time,1,instr(time,':',1,1)-1)<10
then'0'||time
else time
end),'dd.MM.yyyy hh24:mi:ss')

How do I conditionally group by two different columns in Oracle?

Suppose I have a table with the following data:
+----------+-----+--------+
| CLASS_ID | Day | Period |
+----------+-----+--------+
| 1 | A | CCR |
+----------+-----+--------+
| 1 | B | CCR |
+----------+-----+--------+
| 2 | A | 1 |
+----------+-----+--------+
| 2 | A | 2 |
+----------+-----+--------+
| 3 | A | 3 |
+----------+-----+--------+
| 3 | B | 4 |
+----------+-----+--------+
| 4 | A | 5 |
+----------+-----+--------+
As you could probably guess from the nature of the data, I'm working on an Oracle SQL query that pulls class schedule data from a Student Information System. I'm trying to pull a class's "period expression", a calculated value that contains the Day and Period fields into a single field. Let's get my expectation out of the way first:
If the Periods match, Period should be the GROUP BY field, and Day should be the aggregated field (via a LISTAGG function), so the calculated field would be CCR (A-B)
If the Days match, Day should be the GROUP BY field, and Period should be the aggregated field, so the calculated field would be 1-2 (A)
I'm only aware of how to do each GROUP BY individually, something like for where Days match:
SELECT
day,
LISTAGG(period, '-') WITHIN GROUP (ORDER BY period)
FROM schedule
GROUP BY day
and vice versa for matching Periods, but I'm not seeing how I could do that dynamically for Period and Day in the same query.
You'll also notice that the last row in the example data set doesn't span multiple days or periods, so I also need to account for classes that don't need a GROUP BY at all.
Edit
The end result should be:
+------------+
| Expression |
+------------+
| CCR(A-B) |
+------------+
| 1-2(A) |
+------------+
| 3-4(A-B) |
+------------+
| 5(A) |
+------------+
It is really not clear to me WHY you want output in that way. It doesn't provide any useful information (I don't think) - you can't tell, for example for class_id = 3, which combinations of day and period are actually used. There are four possible combinations (according to the output), but only two are actually in the class schedule.
Anyway - you may have your reasons. Here is how you can do it. You seem to want to LISTAGG both the day and the period (both grouped by class_id, they are not grouped by each other). The difficulty is that you want distinct values in the aggregate lists only - no duplicates. So you will need to select distinct, separately for period and for day, then to the list aggregations, and then concatenate the results in an inner join.
Something like this:
with
test_data ( class_id, day, period ) as (
select 1, 'A', 'CCR' from dual union all
select 1, 'B', 'CCR' from dual union all
select 2, 'A', '1' from dual union all
select 2, 'A', '2' from dual union all
select 3, 'A', '3' from dual union all
select 3, 'B', '4' from dual union all
select 4, 'A', '5' from dual
)
-- end of test data; the actual solution (SQL query) begins below this line
select a.class_id, a.list_per || '(' || b.list_day || ')' as expression
from ( select class_id,
listagg(period, '-') within group (order by period) as list_per
from ( select distinct class_id, period from test_data )
group by class_id
) a
inner join
( select class_id,
listagg(day, '-') within group (order by day) as list_day
from ( select distinct class_id, day from test_data )
group by class_id
) b
on a.class_id = b.class_id
;
CLASS_ID EXPRESSION
-------- ----------
1 CCR(A-B)
2 1-2(A)
3 3-4(A-B)
4 5(A)
How about union with having count(*) = 1?
select LISTAGG(period, '-') list WITHIN GROUP (ORDER BY period)
from schedule
group by CLASS_ID, day
having count(*) = 1
union all
select LISTAGG(day, '-') list WITHIN GROUP (ORDER BY day)
from schedule
group by CLASS_ID, period
having count(*) = 1

Subqueries in 2 tables

I have 2 tables: machine and work.
Table:machine
machine_no downtime location
A1-100-01 2 A1
A1-100 1.5 A1
A1-200 3 A1
CC3-100-01 0.5 CC3
CC3-100 1.5 CC3
Table:work
machine_no date
A1-100-01 2/4/14
A1-100 2/14/14
A1-200 2/6/14
CC3-100-01 3/15/14
CC3-100 3/2/14
I want the output to be like this:
machine_no total_downtime month
A1-100 3.5 (total of A1-100, A1-100-01) 02
A1-200 3 02
When location A1 is selected.
SELECT machine_no, SUM(downtime) as total_downtime
FROM (
SELECT
SUBSTR(machine_no, 1,
CASE WHEN INSTR(machine_no, '-', 1, 2) = 0
THEN LENGTH(machine_no)
ELSE INSTR(machine_no, '-', 1, 2)-1
END) as machine_no,
downtime
FROM machine
WHERE location='A1'
) InnerQuery
GROUP BY machine_no
How do I join table WORK and display the month? I'm using Oracle.
Thank you.
The month column's semantics in your expected query result is unclear. Assuming that it is another aggregation "key", then your query would be
select
regexp_substr(M.machine_no, '^[^-]+-[^-]+') as machine_no,
sum(downtime) as total_downtime,
to_char(W.date, 'mm') as month
from machine M
join work W
on W.machine_no = M.machine_no
group by
regexp_substr(M.machine_no, '^[^-]+-[^-]+'),
to_char(W.date, 'mm')
;
Assuming it is a (somehow) aggregated value, let's say via min() function, then your query would be
select
regexp_substr(M.machine_no, '^[^-]+-[^-]+') as machine_no,
sum(downtime) as total_downtime,
min(to_char(W.date, 'mm')) as month
from machine M
join work W
on W.machine_no = M.machine_no
group by
regexp_substr(M.machine_no, '^[^-]+-[^-]+')
;
Both of these, in addition, assume that the (total of A1-100, A1-100-01) in your expected result is just your note, not really a part of the result. But if not, then your query could be something along the lines of
select
regexp_substr(M.machine_no, '^[^-]+-[^-]+') as machine_no,
sum(downtime)||
case when count(1) > 1 then
' (total of '||
listagg(M.machine_no)
within group (order by M.machine_no)||
')'
end
as total_downtime,
to_char(W.date, 'mm') as month
from machine M
join work W
on W.machine_no = M.machine_no
group by
regexp_substr(M.machine_no, '^[^-]+-[^-]+'),
to_char(W.date, 'mm')
;
And even this works because of a few more assumptions about the (unsaid) properties of your machine and work tables, so I'm going to stop my answer here. :-)
User regular expression to take sub string of machine_no and to_char to get the month
WITH machine(machine_no, downtime, location) as (
select 'A1-100-01', 2, 'A1' from dual union all
select 'A1-100', 1.5, 'A1' from dual union all
select 'A1-200', 3, 'A1' from dual union all
select 'CC3-100-01', 0.5, 'CC3' from dual union all
select 'CC3-100', 1.5, 'CC3' from dual),
work(machine_no, ddate) as (
select 'A1-100-01', to_date('2/4/14', 'mm/dd/yyyy') from dual union all
select 'A1-100', to_date('2/14/14', 'mm/dd/yyyy') from dual union all
select 'A1-200', to_date('2/6/14', 'mm/dd/yyyy') from dual union all
select 'CC3-100-01', to_date('3/15/14', 'mm/dd/yyyy') from dual union all
select 'CC3-100', to_date('3/2/14', 'mm/dd/yyyy') from dual)
--End of data preparation
SELECT regexp_substr(m.machine_no, '^\w+-\w+') AS machine_no,
sum(m.downtime) downtime_sum,
to_char(w.ddate , 'MM') MONTH
FROM WORK w
JOIN machine m ON m.machine_no = w.machine_no
WHERE m.location = 'A1'
GROUP BY regexp_substr(m.machine_no, '^\w+-\w+'),
to_char(w.ddate , 'MM');
Output:
| MACHINE_NO | DOWNTIME_SUM | MONTH |
|------------|--------------|-------|
| A1-200 | 3 | 02 |
| A1-100 | 3.5 | 02 |

Resources