Switch case expression for oracle database versions - oracle

I need to query the patch status on Oracle databases. Since Oracle version 12c the view sys.REGISTRY$HISTORY was replaced by the view DBA_REGISTRY_SQLPATCH. On older versions like 11g the view dba_registry_sqlpatch does not exist. The following query creates errors on oracle versions < 12c because the view dba_registry_sqlpatch does not exist. I need to build a query that runs on all oracle database versions. I can not use PL/SQL. I think it should be solved with a case expression.
/* Query for version < 11g: */
SELECT MIN (diff) diff, MIN (zeile) zeile
FROM (SELECT TO_CHAR (TRUNC (SYSDATE - TRUNC (action_time)), '9999') DIFF,
'DIFF : '
|| TO_CHAR (TRUNC (SYSDATE - TRUNC (action_time)), '9999')
|| ' DAYS '
|| 'ACTION='
|| action
|| ' VERSION='
|| version
|| ' DATE='
|| TO_CHAR (action_time, 'yyyymmdd')
|| ' ID='
|| TO_CHAR (id, '09')
|| ' COMMENTS='
|| comments
|| ' PORT='
|| (SELECT DBMS_UTILITY.port_string
FROM DUAL)
ZEILE
FROM sys.REGISTRY$HISTORY
WHERE action_time = (SELECT MAX (action_time)
FROM sys.REGISTRY$HISTORY
WHERE action IN ('APPLY', 'ROLLBACK'))
UNION ALL
/*Query for version 12c: */
(SELECT TO_CHAR (TRUNC (SYSDATE - TRUNC (action_time)), '9999') DIFF,
'DIFF : '
|| TO_CHAR (TRUNC (SYSDATE - TRUNC (action_time)), '9999')
|| ' DAYS '
|| 'ACTION='
|| action
|| ' VERSION='
|| version
|| ' DATE='
|| TO_CHAR (action_time, 'yyyymmdd')
|| ' ID='
|| TO_CHAR (patch_id)
|| ' COMMENTS='
|| description
|| ' PORT='
|| (SELECT DBMS_UTILITY.port_string
FROM DUAL)
ZEILE
FROM dba_registry_sqlpatch
WHERE action_time = (SELECT MAX (action_time)
FROM dba_registry_sqlpatch
WHERE action IN ('APPLY', 'ROLLBACK')))
UNION ALL
/* Query for no patch installed: */
SELECT (SELECT TO_CHAR (TRUNC (SYSDATE - TRUNC (created)), '9999')
FROM v$database)
DIFF,
'DIFF : '
|| (SELECT TO_CHAR (TRUNC (SYSDATE - TRUNC (created)), '9999')
FROM v$database)
|| ' DAYS ACTION=N./A. VERSION='
|| (SELECT SUBSTR (version, 1, 8)
FROM v$instance)
|| ' DATE='
|| (SELECT TO_CHAR (created, 'yyyymmdd')
FROM v$database)
|| ' ID= 99 COMMENTS='
|| (SELECT SUBSTR (version, 1, 8)
FROM v$instance)
|| ' PORT='
|| (SELECT DBMS_UTILITY.port_string
FROM DUAL)
ZEILE
FROM DUAL)
WHERE ROWNUM = 1;
Example output on an Oracle 12c database patched 11 days ago:
DIFF : 11 DAYS ACTION=APPLY VERSION=12.1.0.2 DATE=20160429 ID=22809813 COMMENTS=WINDOWS DB BUNDLE PATCH 12.1.0.2.160419(64bit):22809813 PORT=IBMPC/WIN_NT64-9.1.0

A case expression won't solve your problem. The tables being queried have to be known at parse time - you can't choose the table name dynamically when the query is executed, and a case will still get an ORA-00942 before the case is evaluated.
Assuming you only want the columns that exist in both the old table and the new view, you could use a bit of XML translation to get the data from whichever one exists:
select x.*
from (
select dbms_xmlgen.getxml(q'[select to_char(action_time, 'YYYY-MM-DD"T"HH24:MI:SS.FF9')
as action_time, action, version, id as patch_id, comments as description
from sys.REGISTRY$HISTORY]') as data
from dba_tables
where table_name = 'REGISTRY$HISTORY'
and not exists (select null from dba_views where view_name = 'DBA_REGISTRY_SQLPATCH')
union all
select dbms_xmlgen.getxml(q'[select to_char(action_time, 'YYYY-MM-DD"T"HH24:MI:SS.FF9')
as action_time, action, version, patch_id, description
from DBA_REGISTRY_SQLPATCH]') as data
from dba_views
where view_name = 'DBA_REGISTRY_SQLPATCH'
) t
cross join xmltable('/ROWSET/ROW' passing xmltype(t.data)
columns action_time timestamp path 'ACTION_TIME',
action varchar2(30) path 'ACTION',
version varchar2(30) path 'VERSION',
patch_id number path 'PATCH_ID',
comments varchar2(100) path 'DESCRIPTION'
) x;
And then replace select x.* with whatever you want to do with the data, essentially plugging that into you existing query, adding a union to get the unpatched version information:
...
union all
select vd.created as action_time, 'N/A' as action, substr(vi.version, 1, 8) as version,
99 as patch_id, substr(vi.version, 1, 8) as description
from v$database vd
cross join v$instance vi;
The to_char() is to get the timestamp value into the ISO format that is expected in XML. The dbms_xmlgen() calls convert the data from either table/view into an XML representation; and the XMLTable() converts it back. Which seems a bit pointless, but it lets you not know the object name until runtime.
As the columns are slightly different (ID, COMMENTS vs. PATCH_ID, DESCRIPTION) this gets separate XML from either the table or the view via a union all, but not from both as that would give an invalid XML document. In 12c it looks like REGISTRY$HISTORY will be empty, but in case it isn't it won't get any data from that if DBA_REGISTRY_SQLPATCH exists. (I'm being a bit lazy and not checking ownership, so someone else creating a table with that name would be an issue, but easily fixed). It aliases the columns names so they appear the same whichever table/view it ends up using, allowing the XML to be unpacked.
Putting that together with your string formatting, eliminating the subqueries, and using the last analytic function to only keep the most recent row, you can end up with something like:
select to_char (trunc (sysdate - trunc (max(action_time))), '9999') diff,
'DIFF : ' || to_char (trunc (sysdate - trunc (max(action_time))), '9999') || ' DAYS'
|| ' ACTION=' || max(action) keep (dense_rank last order by action_time)
|| ' VERSION=' || max(version) keep (dense_rank last order by action_time)
|| ' DATE=' || to_char (max(action_time), 'yyyymmdd')
|| ' ID=' || to_char (max(patch_id) keep (dense_rank last order by action_time), '09')
|| ' COMMENTS=' || max(comments) keep (dense_rank last order by action_time)
|| ' PORT=' || dbms_utility.port_string zeile
from (
select x.* from (
select dbms_xmlgen.getxml(q'[select to_char(action_time, 'YYYY-MM-DD"T"HH24:MI:SS.FF9')
as action_time, action, version, id as patch_id, comments as description
from sys.REGISTRY$HISTORY]') as data
from dba_tables
where table_name = 'REGISTRY$HISTORY'
and not exists (select null from dba_views where view_name = 'DBA_REGISTRY_SQLPATCH')
union all
select dbms_xmlgen.getxml(q'[select to_char(action_time, 'YYYY-MM-DD"T"HH24:MI:SS.FF9')
as action_time, action, version, patch_id, description
from DBA_REGISTRY_SQLPATCH]') as data
from dba_views
where view_name = 'DBA_REGISTRY_SQLPATCH'
) t
cross join xmltable('/ROWSET/ROW' passing xmltype(t.data)
columns action_time timestamp path 'ACTION_TIME',
action varchar2(30) path 'ACTION',
version varchar2(30) path 'VERSION',
patch_id number path 'PATCH_ID',
comments varchar2(100) path 'DESCRIPTION'
) x
union all
select vd.created as action_time, 'N./.A' as action, substr(vi.version, 1, 8) as version,
99 as patch_id, substr(vi.version, 1, 8) as comments
from v$database vd
cross join v$instance vi
);
Tested on 11.2.0.4 and 10.2.0.5, but I don't have an unpatched instance or a 12c instance to verify it behaves as you expect for those.

Edit: As Alex Poole shows in a comment (to his answer rather than to mine), what I describe below WILL NOT WORK. It is actually a good illustration of what will NOT work in this kind of situation.
I am leaving it here just so people who may have seen this already have a chance to see it's no good. I will delete the answer after a while.
Thank you Alex for pointing it out!
-
Clearly you can write your own queries, so I will only show here one way to do the "switch" expression you asked about. I only have version 11 (free edition) so I can't fully test, but this should work. To find the version of the Oracle DB your session is in, you can query the view V$VERSION. On my machine, I see the Oracle version is shown as:
SQL> select * from v$version where banner like 'Oracle%';
BANNER
--------------------------------------------------------------------------------
Oracle Database 11g Express Edition Release 11.2.0.2.0 - 64bit Production
Assuming v$version didn't change in Oracle 12c (that is: there is still a view v$version, the column is still called banner, and the Oracle DB version is shown like Oracle Database 12c ....), to get just the action_time you could do something like this:
select case
regexp_substr((select banner from v$version where banner like 'Oracle%'), '\d{1,2}')
when '11' then (select action_time from sys.REGISTRY$HISTORY)
when '12' then (select action_time from dba_registry_sqlpatch)
end as action_time ...
You don't need to write a case expression for the Oracle version for every bit of data from the "registry" table - you can build the full strings in the two branches of the case expression. You may tweak this to accommodate the "no patch installed" branch as well.
Good luck!

Related

How to use defined variable in Where clause

Defining and selecting variable works just fine in Oracle SQL Developer.
ALTER SESSION SET NLS_LANGUAGE=english; -- First day of week
--DEFINE SUMMER_START_DT = TO_CHAR(TO_DATE('03-24-2022', 'MM-DD-YYYY'),'yyyymmdd')
DEFINE SUMMER_START_DT = TO_CHAR(NEXT_DAY(LAST_DAY(TO_DATE(TO_CHAR('01/03/' || (EXTRACT(YEAR FROM SYSDATE)-1 + level) || '02:00:00'),'DD/MM/YYYY HH24:MI:SS')) - INTERVAL '7' DAY, 'SUNDAY'),'yyyymmdd') FROM DUAL CONNECT BY level <=1
SELECT &SUMMER_START_DT;
But I get an error when trying to use the variable in Select statement using it as filter in the Where clause.
SELECT a.* FROM TRADE a WHERE TO_CHAR(a.TRADE_DATE_TIME,'yyyymmdd') = &SUMMER_START_DT;
I get the error "SQL command not properly ended"
Hope someone can help me. Thanks
Kind regards
Soren Sig Mikkelsen
You substitution variable includes from dual, which is OK when you just prepend select in your first example; but in the second you end up with two from clauses:
SELECT a.*
FROM TRADE a
WHERE TO_CHAR(a.TRADE_DATE_TIME,'yyyymmdd') =
TO_CHAR(NEXT_DAY(LAST_DAY(TO_DATE(TO_CHAR('01/03/' || (EXTRACT(YEAR FROM SYSDATE)-1 + level) || '02:00:00'),'DD/MM/YYYY HH24:MI:SS')) - INTERVAL '7' DAY, 'SUNDAY'),'yyyymmdd')
FROM DUAL CONNECT BY level <=1
(You can see that in the generated column name/alias in the output grid; or set verify on and run as a script.)
If you really wanted to use that as the right-hand side of the filter then you could enclose it in parentheses:
SELECT a.* FROM TRADE a WHERE TO_CHAR(a.TRADE_DATE_TIME,'yyyymmdd') = (SELECT &SUMMER_START_DT);
which would become:
SELECT a.*
FROM TRADE a
WHERE TO_CHAR(a.TRADE_DATE_TIME,'yyyymmdd') =
(
SELECT TO_CHAR(NEXT_DAY(LAST_DAY(TO_DATE(TO_CHAR('01/03/' || (EXTRACT(YEAR FROM SYSDATE)-1 + level) || '02:00:00'),'DD/MM/YYYY HH24:MI:SS')) - INTERVAL '7' DAY, 'SUNDAY'),'yyyymmdd')
FROM DUAL CONNECT BY level <=1
)
But the connect by isn't doing anything here, so you can remove that; and if you remove from dual as well then you can run your first statement as:
SELECT &SUMMER_START_DT FROM DUAL;
and the second as it is.
You could simplify the calculation though. For a start you aren't using the time element, so you don't need to make it 2am; and you can truncate to the start of the year and add two months to get March 1st; as a string if that's really what you want:
to_char(next_day(last_day(add_months(trunc(sysdate, 'YYYY'), 2)) - 7, 'SUNDAY'), 'YYYYMMDD')
db<>fiddle
But you can keep it as a date; if you:
DEFINE SUMMER_START_DT = next_day(last_day(add_months(trunc(sysdate, 'YYYY'), 2)) - 7, 'SUNDAY')
then again you can do:
SELECT &SUMMER_START_DT FROM DUAL;
and your second query can be:
SELECT a.*
FROM TRADE a
WHERE a.TRADE_DATE_TIME >= &SUMMER_START_DT
AND a.TRADE_DATE_TIME < &SUMMER_START_DT + 1
which avoids converting every TRADE_DATE_TIME date value to a string to compare it, and allows an index on that date column to be used.

How to convert to_char ('DD/MM/YYYY HH24:MI') and to_date ('DD/MM/YYYY HH24:MI') using oracle or Plsql

select to_date((q.confirm_DATE || ' ' || q.confirm_time),'dd/mm/yyyy hh24:mi')
from qc_warning_record q
where q.warning_id = 125 ;
ineed show time
enter image description here
As per the info you provided it seems you are doing something similar.
Please find below a use case.
Create table qc_warning_record(id number,confirm_DATE DATE,confirm_time varchar2(10));
-----------------------------------
INSERT INTO qc_warning_record values(125,sysdate-3, '100012');
-------------------------------
select (to_char(q.confirm_DATE,'DD-MON-YYYY') || ' '|| to_char(to_date(q.confirm_time, 'hh24miss'),'hh24:mi:ss')) DATE_TIME
from qc_warning_record q where q.id = 125 ;
Assuming q.confirm_date is a date and q.confirm_time is a string (in format HH24:MI), and you need to create a value of date (date-time) data type, combining the date (truncated to midnight) from the first column with the time from the second, you could do this:
to_date( to_char(q.confirm_date, 'dd/mm/yyyy') || ' ' || q.confirm_time
, 'dd/mm/yyyy hh24:mi'
)
You won't be able to see time-of-day if you query dates and your NLS_DATE_FORMAT is set to dd-MON-rr. To change it, you must first run
alter session set nls_date_format='dd/mm/yyyy hh24:mi';

Showing Multiple rows against an ID in One Row - Oracle SQL

My question is somewhat related to :
Display multiple values of a column in one row (SQL Oracle)
However, I could not achieve desired results. Following is my Problem Statement;
I have a SQL Query ;
SELECT initiator_msisdn, trx_type || '/' || SUM(trx_amt/100) || '/' || SUM(merchant_comm_amt/100) agent_data
FROM LBI_DM_MK.T_M_INTERNAL_AUDIT_D
WHERE DATA_DATE = '20180401'
AND trx_status ='Completed'
GROUP BY initiator_msisdn, trx_type
;
That returns these rows;
The SQL That brings this data is ;
But, I want following result.
Please help to sort out this issue;
You could use LISTAGG:
WITH cte AS (
SELECT initiator_msisdn,
trx_type || '/' || SUM(trx_amt/100) || '/' ||
SUM(merchant_comm_amt/100) agent_data
FROM LBI_DM_MK.T_M_INTERNAL_AUDIT_D
WHERE DATA_DATE = '20180401'
AND trx_status ='Completed'
GROUP BY initiator_msisdn, trx_type
)
SELECT initiator_msisdn,
LISTAGG(agent_data, '|') WITHIN GROUP (ORDER BY agent_data) AS agent_data
FROM cte
GROUP BY initiator_msisdn;

Oracle Drop Table with YYYMMDD and YYYMMDD_HHMMSS

I have some tables end with date (YYYYMMDD), some end with HHMMSS:
INVENLEVEL_20160419
INVENLEVEL_20160419_120232 <-optional to exist
INVENLEVEL_20160425
INVENLEVEL_20160426
INVENLEVEL_20160426_032112 <-optional to exist
I need to keep tables within 7 days and drop other INVENLEVEL TABLES.
Expected Results, the following 2 tables deleted:
INVENLEVEL_20160419
INVENLEVEL_20160419_120232
Im able to drop for tables with date, but not the one with HHMMSS.
FOR x IN ( SELECT TABLE_NAME
FROM USER_TABLES
WHERE REGEXP_LIKE(TABLE_NAME, 'INVENLEVEL_[[:digit:]]{8}')
AND TO_DATE(SUBSTR(TABLE_NAME, -8), 'yyyymmdd') <= TRUNC(SYSDATE) - 7
) LOOP
EXECUTE IMMEDIATE 'DROP TABLE ' ||
x.TABLE_NAME ||
' PURGE';
How can i also drop for the table with HHMMSS also? Please note that tables with HHMMSS is optional to exist, means, sometimes we have it, sometime not.
Something like this, perhaps:
with sample_data as (select 'INVENLEVEL_20160419' table_name from dual union all
select 'INVENLEVEL_20160419_120232' table_name from dual union all
select 'INVENLEVEL_20160425' table_name from dual union all
select 'INVENLEVEL_20160426' table_name from dual union all
select 'INVENLEVEL_20160426_032112' table_name from dual union all
select 'NEW_20160426_032112' table_name from dual union all
select 'FRED' table_name from dual)
---- end of mimicking your data; see SQL below
select table_name,
to_date(substr(table_name, 12, 8), 'yyyymmdd') dt
from sample_data
where REGEXP_LIKE(TABLE_NAME, '^INVENLEVEL_[[:digit:]]{8}($|_[[:digit:]]{6})')
and to_date(substr(table_name, 12, 8), 'yyyymmdd') <= trunc(sysdate -7);
TABLE_NAME DT
-------------------------- ----------
INVENLEVEL_20160419 19/04/2016
INVENLEVEL_20160419_120232 19/04/2016
Obviously, you wouldn't need the sample_data subquery - I just used that in order to have data for the SQL to work against. You'd query your user_tables instead.
I amended your regexp to additionally check that it had either reached the end of the string after the 8 digits or there was another underscore followed by 6 digits.
Then I amended your substr to check for the 8 characters from the 12th position, in order to get the date - you have to do it like this, since if you use the end of the string as you had been doing, the date is not necessarily 8 characters from the end.

Using top N Employees

I need a query to get Top N employees working on every project in a specific month based on working hours. And I need the same query for all time (without specifying a month).
I get two hints first Hint: Use Substitution variables to prompt the user to enter values for N and a month.
Second Hint: Use the rank analytical function. If two employees tie they should get the same rank.
Just right now i have this not completed solution, and not sure if i should complete it:
SELECT BSR_PROJ.PROJECT_NAME,
BSR_TM.FNAME || ' ' || BSR_TM.MNAME || ' ' || BSR_TM.LNAME EMPLOYEE_NAME,
BSR_TRD.WORK_ITEM_DATE,
RANK() OVER (PARTITION BY BSR_PROJ.PROJECT_NAME ORDER BY BSR_TRD.WORK_ITEM_DATE ASC) EMPRANK
FROM BSR_TEAM_REPORT_DETAILS BSR_TRD,
BSR_PROJECTS BSR_PROJ,
BSR_TEAM_MEMBERS BSR_TM
WHERE BSR_TRD.BSR_TEAM_RES_ID = BSR_TM.ID
AND BSR_TRD.BSR_PRJ_ID = BSR_PROJ.ID
;
You need to include the month in the analytic function, as rank is calculated per project per month; truncating the date with a month format mask is an easy way to achieve this.
You also need to include that truncated month in the projection, so you can filter on it. I have chosen to present the month in the format 2015-12. You may wish to show it differently.
The query you have will generate the whole set of ranks for all employees across all assignments. You need an outer query to apply the filtering requirements. My solution uses SQL Plus substitution variables rather than bind variables, but the principle is the same:
select distinct project_name
, employee_name
from (
SELECT BSR_PROJ.PROJECT_NAME,
BSR_TM.FNAME || ' ' || BSR_TM.MNAME || ' ' || BSR_TM.LNAME EMPLOYEE_NAME,
to_char(trunc(bsr_trd.work_item_date, 'MM'), 'yyyy-mm') as project_month,
RANK() OVER (PARTITION BY BSR_PROJ.PROJECT_NAME,
trunc(bsr_trd.work_item_date, 'MM')
ORDER BY BSR_TRD.WORK_ITEM_DATE ASC) EMPRANK
FROM BSR_TEAM_REPORT_DETAILS BSR_TRD,
BSR_PROJECTS BSR_PROJ,
BSR_TEAM_MEMBERS BSR_TM
WHERE BSR_TRD.BSR_TEAM_RES_ID = BSR_TM.ID
AND BSR_TRD.BSR_PRJ_ID = BSR_PROJ.ID
)
where project_month = '&proj_month'
and emprank <= &rnk;
The answer i found as below :
SELECT *
FROM (SELECT RANK ()
OVER (PARTITION BY PROJ_NAME ORDER BY WORKING_HOURS DESC)
AS EMPRANK,
PROJ_NAME,
EMPLOYEE_NAME,
WORKING_HOURS
FROM ( SELECT BSR_PROJ.PROJECT_NAME AS PROJ_NAME,
BSR_TM.FNAME
|| ' '
|| BSR_TM.MNAME
|| ' '
|| BSR_TM.LNAME
EMPLOYEE_NAME,
SUM (WORK_ITEM_CONSUMED_HOURS) AS WORKING_HOURS
FROM BSR_TEAM_REPORT_DETAILS BSR_TRD,
BSR_PROJECTS BSR_PROJ,
BSR_TEAM_MEMBERS BSR_TM
WHERE BSR_TRD.BSR_TEAM_RES_ID = BSR_TM.ID
AND BSR_TRD.BSR_PRJ_ID = BSR_PROJ.ID
AND TO_CHAR (TRUNC (BSR_TRD.WORK_ITEM_DATE, 'MM'),
'YYYY-MM') = '&PROJ_MONTH'
GROUP BY BSR_PROJ.PROJECT_NAME,
BSR_TM.FNAME
|| ' '
|| BSR_TM.MNAME
|| ' '
|| BSR_TM.LNAME)) INNER_TABLE
WHERE EMPRANK <= &RNK;

Resources