Using top N Employees - oracle

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;

Related

Oracle Number type without precision - how do I know if it is a whole number or not

Our vendor's database has Number types for all numbers including whole numbers and decimal numbers. Literally, every numeric type column is created as NUMBER without precision and scale.
This is a big problem as we need to map these columns to proper data types on our target system, we are loading data from these tables into.
We need to know if a number is an integer or decimal.
Other than doing a random sampling/data profiling, is it possible to infer proper data types?
UPDATE:
I accepted the answer below and suggestion from #Bohemian. In addition to that, I will use SAMPLE clause that will do a random sampling of the table since my source tables are huge (many billions of rows).
SELECT
MAX(CASE WHEN col1 IS NOT NULL AND col1 <> round(col1, 0) then 1 else 0 end) as col1,
MAX(CASE WHEN col2 IS NOT NULL AND col2 <> round(col2, 0) then 1 else 0 end) as col2
FROM TABLE
SAMPLE(0.05)
If I want to sample only X rows, use formula below to SAMPLE(N):
Xrows*100/table_rows_total
You can try selecting each FIELD, and seeing if all values of FIELD are equal to ROUND(FIELD, 0). If they are, then that field should be integer. If not, decimal.
I have answered it in this other post and the query that you would use to find the maximum number of decimal places in all of the number columns is the same as that one.
To identify the columns with the their maximum decimal position digits, you can run the SQL below after substituting the MY_SCHEMA, MY_TABLE and the number 10 with say 25 to identify columns that have values over 25 decimal places. This SQL will generate a SQL that should be run to get your result.
SELECT 'SELECT ' || LISTAGG('MAX(LENGTH(TO_CHAR(ABS(' || column_name || ') - FLOOR(ABS(' || column_name || '))))) - 1 AS decimals_' || column_name || CHR(13)
, CHR(9)|| ', ') WITHIN GROUP (ORDER BY rn) ||
' FROM ' || owner || '.' || table_name || CHR(13) ||
' WHERE ' || CHR(13) ||
LISTAGG('(LENGTH(TO_CHAR(ABS(' || column_name || ') - FLOOR(ABS(' || column_name || ')))) - 1) > 10 ' || CHR(13)
, CHR(9)|| ' OR ')
WITHIN GROUP (ORDER BY rn) AS Nasty_Numbers_Finder_Query
FROM
(
SELECT owner, table_name, column_name,
row_number() OVER ( PARTITION BY table_name ORDER BY rownum) rn
FROM dba_tab_columns
WHERE
OWNER = 'MY_SCHEMA'
AND table_name = 'MY_TABLE'
AND (data_type LIKE '%FLOAT%'
OR data_type LIKE '%NUMERIC%')
) a
GROUP BY owner, table_name
For more information, I have blogged about it here.

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;

How to speed up this oracle select query?

I have following sql query in code, which is makes query go slow:
SELECT a.*,
(SELECT CASE
WHEN (score IS NOT NULL OR comments IS NOT NULL)
THEN
' ( score : ' || TO_CHAR (SCORE) || ' )'
ELSE
' ( hələ )'
END
FROM t_lecture_task_present b
WHERE b.t_idx = a.t_idx AND B.STUDENT_ID = '{$member_code}')
AS task_score
FROM t_lecture_task a
WHERE a.lec_open_idx = '24422'
ORDER BY s_date ASC, t_idx ASC
(16 seconds)
If I try query without
(SELECT CASE
WHEN (score IS NOT NULL OR comments IS NOT NULL)
THEN
' ( score : ' || TO_CHAR (SCORE) || ' )'
ELSE
' ( hələ )'
END
FROM t_lecture_task_present b
WHERE b.t_idx = a.t_idx AND B.STUDENT_ID = '{$member_code}')
as task_score
it works faster.
So, I tried removing or comments is not null, and it works got 2 seconds faster.
You can't compare the performance of queries that produce different results :)
Depending on data distribution in your tables, you would likely benefit from these indexes:
t_lecture_task(lec_open_idx)
t_lecture_task_present(t_idx, student_id)
Try to re-write your query to use joins instead of scalar sub queries (select as a column). Not only are they more awkward to read, they are more difficult to optimize.
select a.*
,case when score is not null
or comments is not null then ' ( score : ' || to_char (score) || ' )'
else ' ( hələ )'
end as task_score
from t_lecture_task a
left join t_lecture_task_present b on(
b.t_idx = a.t_idx
and b.student_id = '{$member_code}')
)
where a.lec_open_idx = '24422';

UPDATE using a WITHIN GROUP

I've written the below statement which returns the data in the format i need to update another table with however i'm struggling with the update
SELECT element_id,
LISTAGG(cast(0 as varchar2(20))||', '|| VALUE, ' | ') WITHIN GROUP (ORDER BY display_order)
FROM EDRN.MD$$_ELEMENT_VALUES
WHERE element_id IN
(SELECT element_id FROM EDRN_NEW.DATA_DICTIONARY)
GROUP BY element_id;
I done a basic convert into an UPDATE statement
UPDATE EDRN_NEW.DATA_DICTIONARY
SET Choices = (LISTAGG(CAST(0 AS VARCHAR2(20))||', '|| VALUE, ' | ') WITHIN GROUP (ORDER BY display_order)
FROM EDRN.MD$$_ELEMENT_VALUES
WHERE element_id IN
(SELECT element_id FROM EDRN_NEW.DATA_DICTIONARY)
GROUP BY element_id);
This received a "ORA-00934: group function is not allowed here" error. I'm unsure how to remove the group function but retain the data format i require?
You need a subquery to use listagg(). In this case, a correlated subquery:
update EDRN_NEW.DATA_DICTIONARY dd
set choices = (SELECT LISTAGG(cast(0 as varchar2(20))||', '|| VALUE, ' | ') WITHIN GROUP (ORDER BY display_order)
FROM EDRN.MD$$_ELEMENT_VALUES ev
WHERE ev.element_id = dd.element_id
)
where exists (select 1
from EDRN.MD$$_ELEMENT_VALUES ev
where ev.element_id = dd.element_id
);

Switch case expression for oracle database versions

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!

Resources