sql pivot function with a defined variable in the IN statement - sql

Can anyone tell me how I can make this work? I either get "ORA-56901: non-constant expression is not allowed for pivot|unpivot values" or I get "ORA-00917: missing comma" depending on whether I put &mb1 in quotes or not in the IN statement. Thank you!
define mb1= ADD_MONTHS(TRUNC(SYSDATE,'MM'),-1);
select *
from
( select COLLECTOR
,Month
,low_activity_days
from dwh_prod.low_activity_days_collect_t) src
pivot
(
sum(low_activity_days)
for month in ('&mb1')
) piv;

You could set your substitution to a fixed value instead, so it isn't trying to to a calculation inside the pivot clause. As you're working with dates you can generate a date literal:
set termout off
column x_mb1 new_value mb1
select 'date ''' || TO_CHAR(ADD_MONTHS(TRUNC(SYSDATE,'MM'),-1),
'YYYY-MM-DD') || '''' as x_mb1
from dual;
set termout on
select * from (
select COLLECTOR ,Month ,low_activity_days
from dwh_prod.low_activity_days_collect_t
) src
pivot ( sum(low_activity_days) for month in (&mb1) ) piv;
The first query - with the output hidden by termout generates a string:
select 'date ''' || TO_CHAR(ADD_MONTHS(TRUNC(SYSDATE,'MM'),-1),
'YYYY-MM-DD') || '''' as x_mb1
from dual;
X_MB1
-----------------
date '2017-03-01'
The column ... new_value command then sets &mb to that. The substitution in your really query then becomes:
select * from (
select COLLECTOR ,Month ,low_activity_days
from dwh_prod.low_activity_days_collect_t
) src
pivot ( sum(low_activity_days) for month in (date '2017-03-01') ) piv
The problem with your original code is that you're ending up with a reference to sysdate inside the substituted pivot clause, which throws the ORA-56901 exception (since sysdate isn't constant). You can use any fixed value in the pivot, so you could do:
define mb1=''01-APR-17''
... for month in (&mb1) ) piv;
with the qoutes around the string as part of the definition, or
define mb1='01-APR-17'
... for month in ('&mb1') ) piv;
without, or other variations. But because those end up as string you're relying on implicit date conversion, and relying on your NLS settings; with the date literal I'm generating that isn't an issue. If you really wanted to use that date format you could still do:
define mb1='01-APR-17'
... for month in (to_date('&mb1', 'DD-MON-RR')) ) piv;
or select the date in that specific format with the new_values method:
column x_mb1 new_value mb1
select to_char(add_months(trunc(sysdate, 'MM'), -1), 'DD-MON-RR') as x_mb1 from dual;
... for month in (to_date('&mb1', 'DD-MON-RR')) ) piv;
Using the date literal format still seems simpler to me though.
I'm not sure why pivoting a single value is particularly useful though. Maybe your real query is doing more, but simple aggregation and a filter look more appropriate here:
select collector, sum(low_activity_days)
from low_activity_days_collect_t
where month = add_months(trunc(sysdate, 'MM'), -1)
group by collector;
If you want a rolling 12-month summary you don't need to build the pivot quite that dynamically; you can generate a month-offset number based on the current date, which will give you a fixed range of numeric values you can pivot on (rather than dates), something like:
select * from (
select collector, low_activity_days,
months_between(trunc(sysdate, 'MM'), month) as month_offset
from low_activity_days_collect_t
where month >= add_months(trunc(sysdate, 'MM'), -13)
and month < trunc(sysdate, 'MM')
) src
pivot (sum(low_activity_days) as months_ago
for month_offset in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
which will give columns collector, 1_months_ago, 2_months_ago etc.
What you can't easily do is make the column headings represent the actual month (e.g. 2017-04, 2017-03, ...), but you don't seem to have been trying to do that anyway; and unless you alias the pivoted column terms the ones generated in this example have to be treated as quoted identifiers (because they start with numbers).
If you do want month headers you could generate those with some more substitution variable shenanigans, with something like this before the query:
set termout off
column x_m1 new_value m1
column x_m2 new_value m2
...
column x_m12 new_value m12
select to_char(add_months(trunc(sysdate, 'MM'), -1), 'YYYY-MM') as x_m1,
to_char(add_months(trunc(sysdate, 'MM'), -2), 'YYYY-MM') as x_m2,
...
to_char(add_months(trunc(sysdate, 'MM'), -12), 'YYYY-MM') as x_m12
from dual;
column 1_months_ago heading &m1
column 2_months_ago heading &m2
...
column 12_months_ago heading &m12
set termout on
You might find it easier to use a proper reporting tool to query and format/display the results for you though.

Related

How to convert a date to a string

I want yo get only the 'date hours:minutes:seconds' from the Date column
Date
10/11/22 12:14:01,807000000
11/12/22 13:15:46,650000000
29/12/22 14:30:46,501000000
and I want to get a string column with date hours:minutes:seconds
Date_string
10/11/22 12:14:01
11/12/22 13:15:46
29/12/22 14:30:46
I tried this code but it doesn't work:
select*, TO_CHAR(extract(hour from (Date)))||':'||TO_CHAR(extract(minute from (Date)))||':'||TO_CHAR(extract(second from (Date))) as Date_string
from table;
If this is a date column, you could use to_char directly:
SELECT m.*, TO_CHAR(my_date_column, 'dd/mm/yy hh24:mi:ss')
FROM mytable m
You can use REGEX SUBSTRING function to get the date string on the left.
SELECT REGEXP_SUBSTR (Date_string, '[^,]+', 1, 1)
AS left_part
FROM Table1;
where ^, means look for chars that are NOT comma on 1st position
and get the first occurrence (on the left)
Result:
LEFT_PART
10/11/22 12:14:01
11/12/22 13:15:46
29/12/22 14:30:46
reference:
https://docs.oracle.com/cd/B12037_01/server.101/b10759/functions116.htm
Just do it with the TO_DATE() and TO_CHAR() function pair, both operating on the Oracle date format strings:
Building the scenario:
-- your input ..
WITH indata(dt) AS (
SELECT '10/11/22 12:14:01,807000000' FROM dual UNION ALL
SELECT '11/12/22 13:15:46,650000000' FROM dual UNION ALL
SELECT '29/12/22 14:30:46,501000000' FROM dual
)
-- end of your input. Real query starts here.
-- Change following comma to "WITH" ..
,
-- Now convert to TIMESTAMP(9) ...
as_ts AS (
SELECT
TO_TIMESTAMP(dt ,'DD/MM/YY HH24:MI:SS,FF9') AS ts
FROM indata
)
SELECT
ts
, CAST(ts AS TIMESTAMP(0)) AS recast -- note: this is rounded
, TO_CHAR(ts,'DD/MM/YY HH24:MI:SS') AS reformatted -- this is truncated
FROM as_ts
Result:
TS
RECAST
REFORMATTED
10-NOV-22 12.14.01.807000000
10-NOV-22 12.14.02
10/11/22 12:14:01
11-DEC-22 13.15.46.650000000
11-DEC-22 13.15.47
11/12/22 13:15:46
29-DEC-22 14.30.46.501000000
29-DEC-22 14.30.47
29/12/22 14:30:46
Going by what you have in your question, it appears that the data in the field Date is a timestamp. This isn't a problem, but the names of the table (TABLE) and field (Date) present some challenges.
In Oracle, TABLE is a reserved word - so to use it as the name of a table it must be quoted by putting it inside double-quotes, as "TABLE". Similarly, Date is a mixed-case identifier and must likewise be quoted (e.g. "Date") every time it's used.
Given the above your query becomes:
SELECT TO_CHAR("Date", 'DD/MM/YY HH24:MI:SS') AS FORMATTED_DATE
FROM "TABLE"
and produces the desired results. db<>fiddle here
Generally, it's best in Oracle to avoid using reserved words as identifiers, and to allow the database to convert all names to upper case - if you do that you don't have to quote the names, and you can refer to them by upper or lower case as the database automatically converts all unquoted names to upper case internally.

How can I get the previous month if the month is kind of number, not a format of time

Now, my case is I have two kinds of variable:
LOG_DTM
LOG_DTM_ID
For the 1): It is store the data about month.
For the 2): It is the data about turning LOG_DTM into number, so it is not an expression of time, just a number.
For example, if the LOG_DTM = OCT 6 2022, then LOG_DTM_ID = 20221006.
The Question is I want to find the last month data from database,
For the LOG_DTM, I am doing in this way(it is working):
select *
from table
where
LOG_DTM between TRUNC(ADD_MONTHS(SYSDATE, -1),'MM')
and LAST_DAY(ADD_MONTHS(TRUNC(SYSDATE,'mm'),-1))
However, for the LOG_DTM_ID, it cannot work:
select *
from table
where
LOG_DTM_ID between to_number(to_charc(TRUNC(ADD_MONTHS(SYSDATE, -1), 'MM')))
and to_number(to_charc(LAST_DAY(ADD_MONTHS(TRUNC(SYSDATE, 'mm'), -1))))
May I know whats wrong with me? Is my logic flow wrong or syntax wrong? Thanks very much.
Use TO_CHAR and not TO_CHARC; and
Include a format model as the second argument to TO_CHAR.
You can simplify the upper bound to TRUNC(SYSDATE,'mm')-INTERVAL '1' SECOND
Select *
from table_name
Where LOG_DTM_ID between to_number(to_char(TRUNC(ADD_MONTHS(SYSDATE, -1),'MM'), 'YYYYMMDDHH24'))
AND to_number(to_char(TRUNC(SYSDATE,'mm')-INTERVAL '1' SECOND, 'YYYYMMDDHH24'))
Which, for the sample data:
CREATE TABLE table_name (
LOG_DTM DATE,
LOG_DTM_ID NUMBER(10,0)
GENERATED ALWAYS AS (TO_NUMBER(TO_CHAR(log_dtm, 'YYYYMMDDHH24')))
);
INSERT INTO table_name (log_dtm)
SELECT ADD_MONTHS(SYSDATE, -1) FROM DUAL;
Outputs:
LOG_DTM
LOG_DTM_ID
2022-09-06 10:15:39
2022090610
fiddle

Selecting YYYYMM of the previous month in HIVE

I am using Hive, so the SQL syntax might be slightly different. How do I get the data from the previous month? For example, if today is 2015-04-30, I need the data from March in this format 201503? Thanks!
select
employee_id, hours,
previous_month_date--YYYYMM,
from
employees
where
previous_month_date = cast(FROM_UNIXTIME(UNIX_TIMESTAMP(),'yyyy-MM-dd') as int)
From experience, it's safer to use DATE_ADD(Today, -1-Day(Today)) to compute last-day-of-previous-month without having to worry about edge cases. From there you can do what you want e.g.
select
from_unixtime(unix_timestamp(), 'yyyy-MM-dd') as TODAY,
date_add(from_unixtime(unix_timestamp(), 'yyyy-MM-dd'), -1-cast(from_unixtime(unix_timestamp(), 'd') as int)) as LAST_DAY_PREV_MONTH,
substr(date_add(from_unixtime(unix_timestamp(), 'yyyy-MM-dd'), -1-cast(from_unixtime(unix_timestamp(), 'd') as int)), 1,7) as PREV_MONTH,
cast(substr(regexp_replace(date_add(from_unixtime(unix_timestamp(), 'yyyy-MM-dd'), -1-cast(from_unixtime(unix_timestamp(), 'd') as int)), '-',''), 1,6) as int) as PREV_MONTH_NUM
from WHATEVER limit 1
-- today last_day_prev_month prev_month prev_month_num
-- 2015-08-13 2015-07-30 2015-07 201507
See Hive documentation about date functions, string functions etc.
below works across year boundaries w/o complex calcs:
date_format(add_months(current_date, -1), 'yyyyMM') --previous month's yyyyMM
in general,
date_format(add_months(current_date, -n), 'yyyyMM') --previous n-th month's yyyyMM
use proper sign for needed direction (back/ahead)
You could do (year('2015-04-30')*100+month('2015-04-30'))-1 for the above mentioned date, it will return 201503 or something like (year(from_unixtime(unix_timestamp()))*100+month(from_unixtime(unix_timestamp())))-1 for today's previous month. Assuming your date column is in 'yyyy-mm-dd' format you can use the first example and substitute the date string with your table column name; for any other format the second example will do, add the column name in the unix_timestamp() operator.
Angelo's reply is a good start but it returns 201500 if the original date was 2015-01-XX. Building on his answer, I suggest using the following:
IF(month(${DATE}) = 1,
(year(${DATE})-1)*100 + 12,
year(${DATE})*100 + month(${DATE})-1
) as month_key
provided you get rid of those hyphens in your input string , previous date's month id in YYYYMM format you can get by:-
select if( ((${hiveconf:MonthId}-1)%100)=0 ,${hiveconf:MonthId}-89,${hiveconf:MonthId}-1 ) as PreviousMonthId;

select only specific dates oracle sql

I have got an example query like
select cu.customer_no
from customers.cu
where cu.audit_date like '01-FEB-14'
The problem is that I would like to retrieve data only for specific dates which are the first day of the month and skip all of them in between. The desired query should be something like
select cu.customer_no
from customers.cu
where cu.audit_date like ('01-FEB-14', '01-JAN-14', '01-MAY-14')
Assuming that audit_date is a date, not a varchar2, if you are certain that the time component is always midnight, you can do
WHERE cu.audit_date = trunc(cu.audit_date, 'MM')
If there may be a non-midnight time component and you want to ignore the time component, you can do
WHERE trunc(cu.audit_date) = trunc(cu.audit_date, 'MM')
Alternately, you can use to_char
WHERE to_number( to_char( cu.audit_date, 'DD' ) ) = 1
select cu.customer_no
from customers.cu
where cu.audit_date
in (select distinct trunc(audit_date ,'MM') from customers);

PLSQL 'IN' Operator

I'm trying to select rows based on a range of 'dates' determined by the following PLSQL query, which currently delivers the results I need - being the 'date' object of the last 10 weeks of the day of the week when the script is run. Eg. running it on the 22th of May would yield, 15th May, 8th May and so on.
SELECT SYSDATE-(level*7) as DateRange
FROM DUAL
CONNECT BY LEVEL <=10
This generates a list of dates. Then I try and combine this with a parent select statement to get rows with the dates outputted by the above that are in the 'DAY' (of Oracle type DATE) column.
SELECT * FROM NEM_RM16
WHERE NEM_RM16.DAY IN (
SELECT SYSDATE-(level*7) as DateRange
FROM DUAL
CONNECT BY LEVEL <=10);
Which gives no results, despite knowing that there are rows that have the dates generated by the above.
I've read that when using the 'IN' operator, values must be enclosed in single quotes, but I'm not sure about how to do this with the query above.
Am I going about this the right way by using the IN operator, or should I be doing a different type of nested query?
use trunc for truncate time component from sysdate
SELECT * FROM NEM_RM16
WHERE NEM_RM16.DAY IN (
SELECT trunc(SYSDATE)-(level*7) as DateRange
FROM DUAL
CONNECT BY LEVEL <=10);
Maybe the format of the date returned by nested query does not match with the date format of the column NEM_RM16.DAY
Probably, if the dates are compared after making them of the same format, they will match properly
Like this
SELECT *
FROM NEM_RM16
WHERE TO_DATE(TO_CHAR(NEM_RM16.DAY, 'DD/MM/YYYY'), 'DD/MM/YYYY') IN
(SELECT TO_DATE(TO_CHAR(SYSDATE - (level * 7), 'DD/MM/YYYY'),
'DD/MM/YYYY') as DateRange
FROM DUAL
CONNECT BY LEVEL <= 10);
Hope it helps