UPDATE using a WITHIN GROUP - oracle

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
);

Related

execute immediate update returns NULL whilst the non string query works fine

I have an update query which works fine. once I put this in my PLSQL block it returns NULL and there is no error. Prior to this part of the block I insert a default value into the target table, therefore I know the update is actually updating the fields to NULL. Since this query is counting the rows of each column group by another column, then there is always a value for it.and I get the correct count from the query outside of the string..
when I place the query in my plsql Block I use the Execute immediate and the query would be a string in which my_table_name and my_column_names and Table_1 are variable.
I searched alot and found things like I should commit and etc. But problem still there.
for example:
Need help in execute immediate update query
update Table_FINAL r
set column_1 =
(
select max(totalcount)
from (
select 'my_table_name' as table_name, 'my_collumn_name' as column_name, column_3, count(*) as totalcount
from my_table_name a
where exists (select 1 from Table_2 where Table_2.column_x = a.column_x)
group by column_3
) s
where r.column_3 = s.column_3
)
;
and here the string:
execute immediate 'update Table_FINAL r
set column_1 =
(
select max(totalcount)
from (
select ''' || my_table_name || ''' as table_name, ''' || my_collumn_name || ''' as column_name, column_3, count(*) as totalcount
from ' || my_table_name || ' a
where exists (select 1 from Table_2 where Table_2.column_x = a.column_x)
group by column_3
) s
where r.column_3 = s.column_3
)'
;
it updates column_1 to NULL
If your dynamic SQL returns no rows, totalcount will be null, so max(totalcount) will be null and in that circumstance column_1 will be updated to null.
There are a couple of obvious solutions:
only execute the update if there's a value: ... where r.column_3 = s.column_3 and totalcount is not null.
handle the null : select max(nvl(totalcount,0)) …
Now, you assert that the update query "works fine". Does it work fine for all values you pass as my_table_name? Another reason why dynamic SQL is hard is that we can't look at the source code and know for certain what it's going to do at runtime. So you need to so some debugging. Run the dynamic SELECT statement without the update and see what you're actually executing:
execute immediate ' select max(totalcount) from (
select ''' || my_table_name || ''' as table_name, ''' || my_collumn_name || ''' as column_name, column_3, count(*) as totalcount
from ' || my_table_name || ' a
where exists (select 1 from Table_2 where Table_2.column_x = a.column_x)
group by column_3)' into l_total_count;
dbms_ouput.put_line(my_table_name ||'.'|| my_collumn_name ||' max(totalcount) = ' || l_total_count);
Remember to enable SERVEROUTPUT in whatever client you're using.
the update string is inside a loop and it seems that in each loop round it indeed does the correct update but then ir updates the rest to NULL … But I do not fully get what I should do since I hvae hundreds of fields.. (sic)
I guessed you were calling this code from a loop. So, it's not that you don't overwrite the "correct update" with null but that you don't want to over-write any value with any subsequent value. I can offer a few suggestions but really this is your data model and your business logic, so only you can decide the correct way to handle this.
Aggregate totalcount like this: set column_1 = column_1 + total_count. For this to work you'll need to apply nvl(max(totalcount),0).
Add a column to TABLE_FINAL to store values of my_collumn_value. Reference that column in the UPDATE statement's WHERE clause. (Obviously this supposes a completely different result set from the previous suggestion). You may need a column for values of my_table_name too.

How can i set a maxiumum value using list agg

i have read the other questions and answers and they do not help with my issue. i am asking if there is a way to set a limit on the number of results returned in listagg.
I am using this query
HR--Any baby with a HR<80
AS
SELECT fm.y_inpatient_dat, h.pat_id, h.pat_enc_csn_id,
LISTAGG(meas_value, '; ') WITHIN GROUP (ORDER BY fm.recorded_time)
abnormal_HR_values
from
ip_flwsht_meas fm
join pat_enc_hsp h on fm.y_inpatient_dat = h.inpatient_data_id
where fm.flo_meas_id in ('8' ) and (to_number(MEAS_VALUE) <80)
AND fm.recorded_time between (select start_date from dd) AND (select end_date from dd)
group by fm.y_inpatient_dat,h.pat_id, h.pat_enc_csn_id)
and I get the following error:
ORA-01489: result of string concatenation is too long
I have researched online how to set a size limit, but I can't seem to make it work. Can someone please advise how to set a limit so it does not exceed the 50 characters.
In Oracle 12.2, you can use ON OVERFLOW ERROR in the LISTAGG, like:
LISTAGG(meas_value, '; ' ON OVERFLOW ERROR) WITHIN GROUP (ORDER BY fm.recorded_time)
Then you can surround that with a SUBSTR() to get the first 50 characters.
Pre 12.2, you need to restructure the query to limit the number of rows that get seen by the LISTAGG. Here is an example of that that uses DBA_OBJECTS (so people without your tables can run it). It will only the 1st three values for each object type.
SELECT object_type,
listagg(object_name, ', ') within group ( order by object_name) first_three
FROM (
SELECT object_type,
object_name,
row_number() over ( partition by object_type order by object_name ) ord
FROM dba_objects
WHERE owner = 'SYS'
)
WHERE ord <= 3
GROUP BY object_type
ORDER BY object_type;
The idea is to number the row that you want to aggregate and then only aggregate the first X of them, where "X" is small enough not to overflow the max length on VARCHAR2. "X" will depend on your data.
Or, if you don't want the truncation at 50 characters to happen mid-values and/or you don't know how many values are safe to allow, you can replace the ord expression with a running_length expression to keep a running count of the length and cap it off before it gets to your limit (of 50 chars). That expression would be a SUM(length()) OVER (...). Like this:
SELECT object_type,
listagg(object_name, ', ') within group ( order by object_name) first_50_char,
FROM (
SELECT object_type,
object_name,
sum(length(object_name || ', '))
over ( partition by object_type order by object_name ) running_len
FROM dba_objects
WHERE owner = 'SYS'
)
WHERE running_len <= 50+2 -- +2 because the last one won't have a trailing delimiter
GROUP BY object_type
ORDER BY object_type;
With your query, all that put together would look like this:
SELECT y_inpatient_dat,
pat_id,
pat_enc_csn_id,
LISTAGG(meas_value, '; ') WITHIN GROUP ( ORDER BY fm.recorded_time ) abnormal_HR_values
FROM (
SELECT fm.y_inpatient_dat,
h.pat_id,
h.pat_enc_csn_id,
meas_value,
fm.recorded_time,
SUM(length(meas_value || '; ') OVER ( ORDER BY fm.recorded_time ) running_len
FROM ip_flwsht_meas fm
INNER JOIN pat_enc_hsp h on fm.y_inpatient_dat = h.inpatient_data_id
WHERE fm.flo_meas_id in ('8' ) and (to_number(MEAS_VALUE) <80)
AND fm.recorded_time BETWEEN
(SELECT start_date FROM dd) AND (SELECT end_date FROM dd)
)
WHERE running_len <= 50+2
GROUP BY fm.y_inpatient_dat,h.pat_id, h.pat_enc_csn_id;

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;

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;

Quering all columns of single table in where condition for same input data

If we want to fetch information based on condition on single column, we do like this
SELECT * FROM contact WHERE firstName = 'james'
IF we want to put conditions on multiple columns, we do this
SELECT * FROM contact WHERE firstName = 'james' OR lastName = 'james' OR businessName = 'james'
But What if we have more than 50 columns.
Is there better way other than WHERE Condition with OR Keyword?
The approach should not involve writing all column names.
There is a way to do this in MySql as shown here.
If you're wanting to search all VARCHAR2 columns, then the following script ought to help:
set pages 0;
set lines 200
select case when rn = 1 and rn_desc = 1 then 'select * from '||table_name||' where '||column_name||' = ''james'';'
when rn = 1 then 'select * from '||table_name||' where '||column_name||' = ''james'''
when rn_desc = 1 then ' and '||column_name||' = ''james'';'
else ' and '||column_name||' = ''james'''
end sql_stmt
from (select table_name,
column_name,
column_id,
row_number() over (partition by table_name order by column_id) rn,
row_number() over (partition by table_name order by column_id desc) rn_desc
from user_tab_columns
where data_type in ('VARCHAR2')
-- and table_name in (<list of tables>) -- uncomment and amend as appropriate!
)
order by table_name, column_id;
If you only want to search specific tables, you would have to put a filter in for the table_names you're after.
Running the above as a script will give you a script containing multiple queries that you can then run
There is no way you can avoid writing all the column names,
But you can use an IN condition to make writing this a bit shorter:
SELECT *
FROM contact
WHERE 'james' in (firstName, lastName, businessName)

Resources