Declare function in Oracle SQL temporary script - sql

I have a script that I run in the Toad for Oracle (or in the PL/SQL Developer):
select distinct AC.STATUS, S.DESCRIPTION, count(*) || ' / ' || (
select count(*)
from ACC AC
where AC.STATUS is not null
) "Count"
from ACC AC
join ACC_STATUS S
on AC.STATUS = S.STATUS
group by AC.STATUS, S.DESCRIPTION
order by AC.STATUS;
so it works pretty good and shows me some result like this:
-7 Status 1 30 / 174
-6 Status 2 124 / 174
-3 Status 3 6 / 174
-2 Status 4 4 / 174
-1 Status 5 10 / 174
I want to refactor this and extract the first select (in parentheses) into a function something like:
declare function zz
return number
is
declare res number;
begin
select count(*) into res
from ACC AC
where AC.STATUS is not null;
return res;
end zz;
and then use it somehow in the script.
Is it possible? And which syntax have I use? I do not want to save the function into database – only to run it for my own need in Toad or PL/SQL Developer. (We have Oracle version 12.1.0.2.0.)

In Oracle 12c or above you can utilize WITH factoring clause as
with
function f_test return number is
retval number;
begin
select count(*)
into retval
from acc
where status is not null;
return retval;
end;
select distinct
ac.status,
s.description,
count(*) || ' / ' || f_test
from acc ac join acc_status s on ac.status = s.status
group by ac.status, s.description
order by ac.status;
Otherwise, in lower versions, no - you can't do that and you'll have to create a stored function (the one that exists in the database).

Why? You can just use analytic functions:
select AC.STATUS, S.DESCRIPTION,
count(*) || ' / ' || sum(count(*)) over () as "count"
from ACC AC join
ACC_STATUS S
on AC.STATUS = S.STATUS
group by AC.STATUS, S.DESCRIPTION
order by AC.STATUS;
You don't have to worry about NULL values, because the JOIN filters them out.

Related

Multiple clause with lines SQL

I have implemented the following function in a functional way but with the problem that I had implemented it with temporary tables and inserts.
Since I want to optimize the code, I have decided to try the SQL with as .. statements.
The downside is that the way to implement it with SQL with as statements is different and I'm opting for this output:
ERROR: syntax error at end of input LINE 48: ...r = p_id_var AND
fvr.utz BETWEEN p_utz_begin AND p_utz_end);
^ SQL state: 42601 Character: 1754
This is the code:
CREATE OR REPLACE FUNCTION tlm.main_dash_tele_freq_blackout(
p_id_unit integer,
p_utz_begin timestamp without time zone,
p_utz_end timestamp without time zone)
RETURNS TABLE(can_freq interval, can_blackout interval, gps_freq interval, gps_blackout interval, chargeloss boolean)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
DECLARE
CAN_freq interval;
CAN_blackout interval;
CAN_chargeloss boolean;
GPS_freq interval;
GPS_blackout interval;
max_diff integer;
p_id_var integer;
BEGIN
p_id_var = 1001;
with main_dash_tele_freq_blackout_first_reading as (
SELECT fvr.utz, fvr.val FROM var.oper_readings fvr WHERE fvr.id_unit = p_id_unit AND fvr.id_var = p_id_var AND fvr.utz BETWEEN p_utz_begin AND p_utz_end
),
main_dash_tele_freq_blackout_second_reading as (
SELECT fr.utz , fr.val FROM main_dash_tele_freq_blackout_first_reading fr WHERE fr.id != 1),
main_dash_tele_freq_blackout_result_reading as (
SELECT ff.utz, ss.utz, (ss.utz - ff.utz), (ss.val - ff.val) FROM main_dash_tele_freq_blackout_first_reading ff FULL JOIN main_dash_tele_freq_blackout_second_reading ss ON ff.id = ss.id
)
;
CAN_freq = (SELECT AVG(diff) FROM main_dash_tele_freq_blackout_result_reading WHERE diff < '00:10:00');
CAN_blackout = (SELECT AVG(diff) FROM main_dash_tele_freq_blackout_result_reading WHERE diff > '00:10:00' AND (diff_val > 1 OR diff_val < -1));
CAN_chargeloss = (SELECT (MAX(diff_val)>10) FROM main_dash_tele_freq_blackout_result_reading WHERE diff > '00:10:00' AND (diff_val > 1 OR diff_val < -1));
------------------------------------------------------ Similar case for this variables ------------------------------------------------
with main_dash_tele_freq_blackout_first_GPS_reading as (
SELECT fvr.utz, fvr.lat, fvr.lon FROM var.oper_geo_readings fvr WHERE fvr.id_unit = p_id_unit AND fvr.utz BETWEEN p_utz_begin AND p_utz_end
),
main_dash_tele_freq_blackout_second_GPS_reading as (
SELECT fr.utz , fr.lat, fr.lon FROM main_dash_tele_freq_blackout_first_GPS_reading fr WHERE fr.id != 1
),
main_dash_tele_freq_blackout_result_GPS_reading as (
SELECT ff.utz, ss.utz, (ss.utz - ff.utz), (ss.lat - ff.lat), (ss.lon - ff.lon) FROM main_dash_tele_freq_blackout_first_GPS_reading ff FULL JOIN main_dash_tele_freq_blackout_second_GPS_reading ss ON ff.id = ss.id
);
GPS_freq = (SELECT AVG(diff) FROM main_dash_tele_freq_blackout_result_GPS_reading WHERE diff < '00:10:00');
GPS_blackout = (SELECT AVG(diff) FROM main_dash_tele_freq_blackout_result_GPS_reading WHERE diff > '00:10:00');
RETURN QUERY (SELECT CAN_freq, CAN_blackout, GPS_freq, GPS_blackout, CAN_chargeloss );
END
$BODY$;
You can use like as these syntaxes:
declare variable1 integer;
declare variable2 integer;
with
tb1(a) as (
select 1
union all
select 2
union all
select 3
),
tb2(a) as (
select 5
union all
select 10
union all
select 15
)
select (select sum(tb1.a) from tb1) into variable1, (select sum(tb2.a) from tb2) into variable2;
return query
select variable1, variable2
or
return query
with
tb1(a) as (
select 1
union all
select 2
union all
select 3
),
tb2(a) as (
select 5
union all
select 10
union all
select 15
)
select (select sum(tb1.a) from tb1), (select sum(tb2.a) from tb2);

concatenation code column not found in a table using sql

I have a table with few columns and I am trying to concatenate the code column with rank. But it throws me an error by saying code not found, this is in the concatenation I am doing with rank and code. The code column is a calculated column as you can see from the query. Could anyone please point me where I am doing a mistake?
table mm
ID cal_dtm rank milestone_hier_onb tr
CNT 1/31/2020 1 RFR EEZ
table dd
ID frs lst frst lst_s cal
CNT 6/20/2018 6/28/2018 6/28/2018 1/31/2020
Query
WITH r
AS (
SELECT dd.ID,
dd.frs,
dd.lst,
dd.frst,
dd.lst_s,
dd.cal_dtm,
mm.tr,
mm.ipf_rank,
mm.milestone_hier_onb
FROM dos dd
LEFT JOIN plw mm ON dd.ID = mm.ID
AND dd.cal_dtm = mm.cal_dtm
)
SELECT *
FROM (
SELECT r.ID,
r.cal_dtm,
'lp' AS code,
r.lst_s AS planned,
r.lst AS actual,
r.tr,
r.ipf_rank AS rank,
r.milestone_hier_onb AS onb,
* * CONCAT (
r.ipf_rank,
code
) AS milestone_label * *
FROM r
WHERE r.lst IS NOT NULL
UNION
SELECT r.ID,
r.cal_dtm,
'fp' AS code,
r.frst AS planned,
r.frs AS actual,
r.tr,
r.ipf_rank AS rank,
r.milestone_hier_onb AS onb,
* * CONCAT (
r.ipf_rank,
code
) AS milestone_label * *
FROM r
WHERE r.frs IS NOT NULL
)
ORDER BY ID,
cal_dtm,
code
Expected output
ID Cal code pl Al tr milestone_label rank onb
CNT 1/31/2020 lp 6/28/2018 6/28/2018 lp 1 1
CNT 1/31/2020 fp 6/20/2018
You can't use CODE like that; it is a constant so - concatenate that constant, e.g.
... 'lp' as code,
r.ipf_rank || 'lp' as milestone_label
...
How to add space?
r.ipf_rank ||' '|| 'lp' as milestone_label
or
r.ipf_rank ||' lp' as milestone_label
Let try:
remove order by
add alias **concat( r.ipf_rank, code) => **concat( r.ipf_rank, r.code)

Dynamically spilt rows in to mulitple comma-separated lists

I have a table that contains a list of users.
USER_TABLE
USER_ID DEPT
------- ----
USER1 HR
USER2 FINANCE
USER3 IT`
Using a SQL statement, I need to get the list of users as a delimited string returned as a varchar2 - this is the only datatype I can use as dictated by the application I'm using, e.g.
USER1, USER2, USER3
The issue I have is the list will exceed 4000 characters. I have the following which will manually chunk up the users in to lists of 150 users at a time (based on user_id max size being 20 characters plus delimiters safely fitting in to 4000 characters).
SELECT LISTAGG(USER_ID, ',') WITHIN GROUP (ORDER BY USER_ID)
FROM (SELECT DISTINCT USER_ID AS USER_ID, ROW_NUMBER() OVER (ORDER BY USER_ID) RN FROM TABLE_NAME)
WHERE RN <= 150
START WITH RN = 1
CONNECT BY PRIOR RN = RN - 1
UNION
SELECT LISTAGG(USER_ID, ',') WITHIN GROUP (ORDER BY USER_ID)
FROM (SELECT DISTINCT USER_ID AS USER_ID, ROW_NUMBER() OVER (ORDER BY USER_ID) RN FROM TABLE_NAME)
WHERE RN > 150 AND RN <= 300
START WITH RN = 1
CONNECT BY PRIOR RN = RN - 1
This is manual and would require an additional UNION for each chunk of 150 users and the total number of users could increase at a later date.
Is it possible to do this so the delimited strings of user_ids are generated dynamically so they fit in to multiple chunks of 4000 characters and no user_ids are split over multiple strings?
Ideally, I'd want the output to look like this:
USER1, USER2, USER3 (to) USER149
USER150, USER151, USER152 (to) USER300
USER301, USER302, USER303 (to) USER450`
The solution needs to be a SELECT statement as the schema is read-only and we aren't able to create any objects on the database. We're using Oracle 11g.
You can do this with a pipelined function:
create or replace function get_user_ids
return sys.dbms_debug_vc2coll pipelined
is
rv varchar2(4000) := null;
begin
for r in ( select user_id, length(user_id) as lng
from user_table
order by user_id )
loop
if length(rv) + r.lng + 1 > 4000
then
rv := rtrim(rv, ','); -- remove trailing comma
pipe row (rv);
rv := null;
end if;
rv := rv || r.user_id || ',';
end loop;
return;
end;
/
You would call it like this:
select column_value as user_id_csv
from table(get_user_ids);
Alternate way using below function :
create or replace FUNCTION my_agg_user
RETURN CLOB IS
l_string CLOB;
TYPE t_bulk_collect_test_tab IS TABLE OF VARCHAR2(4000);
l_tab t_bulk_collect_test_tab;
CURSOR user_list IS
SELECT USER_ID
FROM USER_TABLE ;
BEGIN
OPEN user_list;
LOOP
FETCH user_list
BULK COLLECT INTO l_tab LIMIT 1000;
FOR indx IN 1 .. l_tab.COUNT
LOOP
l_string := l_string || l_tab(indx);
l_string := l_string || ',';
END LOOP;
EXIT WHEN user_list%NOTFOUND;
END LOOP;
CLOSE user_list;
RETURN l_string;
END my_agg_user;
After function created ,
select my_agg_user from dual;
I believe the SQL I have below should work in most cases. I've hard-coded the SQL to break the strings up in to 150 entries of user id, but the rest is dynamic.
The middle part produces duplicates, which requires an additional distinct to eliminate, but I'm not sure if there is a better way to do this.
WITH POSITION AS ( SELECT ((LEVEL-1) * 150 + 1) FROM_POS, LEVEL * 150 TO_POS
FROM DUAL
CONNECT BY LEVEL <= (SELECT COUNT(DISTINCT( USER_ID)) / 150 FROM TABLE_NAME)
)
SELECT DISTINCT
LISTAGG(USER_ID, ',') WITHIN GROUP (ORDER BY USER_ID) OVER (PARTITION BY FROM_POS, TO_POS)
FROM
(SELECT DISTINCT USER_ID AS USER_ID, ROW_NUMBER() OVER (ORDER BY USER_ID) RN FROM TABLE_NAME) V0 ,
POSITION
WHERE V0.RN >= POSITION.FROM_POS
AND V0.RN <= POSITION.TO_POS

Dynamic Oracle Pivot_In_Clause

I'm kinda stuck. I want to do a user-role-relationship pivot table and my query so far looks like this:
WITH PIVOT_DATA AS (
SELECT *
FROM
(
SELECT USERNAME, GRANTED_ROLE
FROM DBA_USERS#DB_LINK U LEFT OUTER JOIN DBA_ROLE_PRIVS#DB_LINK R
ON U.USERNAME = R.GRANTEE
)
)
SELECT *
FROM PIVOT_DATA
PIVOT
(
COUNT(GRANTED_ROLE)
FOR GRANTED_ROLE
IN('CONNECT') -- Just an example
)
ORDER BY USERNAME ASC;
It works really fine and does the job, but I don't want to write to write any role I want to search for in the pivot_in_clause, because we got like tons of them and I don't want to check every time if there are any changes.
So is there a way to write a SELECT in the pivot_in_clause? I tried it myself:
[...]
PIVOT
(
COUNT(GRANTED_ROLE)
FOR GRANTED_ROLE
IN( SELECT ROLE FROM DBA_ROLES#DB_LINK )
)
[...]
But it always gives me an ORA-00936: "missing expression" in line 1 of the whole query and I don't know why. Can't there be a SELECT in the pivot_in_clause or am I doing it wrong?
You can build dynamic query in your script,
look at this example:
variable rr refcursor
declare
bb varchar2(4000);
cc varchar2( 30000 );
begin
WITH PIVOT_DATA AS (
SELECT *
FROM
(
SELECT USERNAME, GRANTED_ROLE
FROM DBA_USERS U LEFT OUTER JOIN DBA_ROLE_PRIVS R
ON U.USERNAME = R.GRANTEE
)
)
select ''''|| listagg( granted_role, ''',''' )
within group( order by granted_role ) || '''' as x
into bb
from (
select distinct granted_role from pivot_data
)
;
cc := q'[
WITH PIVOT_DATA AS (
SELECT *
FROM
(
SELECT USERNAME, GRANTED_ROLE
FROM DBA_USERS U LEFT OUTER JOIN DBA_ROLE_PRIVS R
ON U.USERNAME = R.GRANTEE
)
)
SELECT *
FROM PIVOT_DATA
PIVOT
(
COUNT(GRANTED_ROLE)
FOR GRANTED_ROLE
IN(]' || bb || q'[) -- Just an example
)
ORDER BY USERNAME ASC]';
open :rr for cc;
end;
/
SET PAGESIZE 200
SET LINESIZE 16000
print :rr
Here is the result (only small fragment, because it is very wide and long)
-----------------------------------------------------------------------------------------------------------------------------------
USERNAME 'ADM_PARALLEL_EXECUTE_TASK' 'APEX_ADMINISTRATOR_ROLE' 'AQ_ADMINISTRATOR_ROLE' 'AQ_USER_ROLE'
------------------------------ --------------------------- ------------------------- ----------------------- ----------------------
ANONYMOUS 0 0 0 0
APEX_030200 0 0 0 0
APEX_PUBLIC_USER 0 0 0 0
APPQOSSYS 0 0 0 0
..............
IX 0 0 1 1
OWBSYS 0 0 1 1

row to column conversion in sql

Suppose I am having 3 set of records like
Actno Sufix Amount
000005 230 101000
000005 535 100000
000005 630 -500000
000009 230 222000
000009 535 120000
000009 635 220000
I need to display it as
000005 230 101000 535 100000 630 -500000
000009 230 222000 535 120000 635 220000
Is that possible in SQL? Can anyone please help me regarding this?
If you know that you have three registrations for each Actno you can do this
;with cte as
(
select *,
row_number() over(partition by Actno order by Sufix) as rn
from [YourTable]
)
select
C.Actno,
min(C1.Sufix),
min(C1.Amount),
min(C2.Sufix),
min(C2.Amount),
min(C3.Sufix),
min(C3.Amount)
from cte as C
inner join cte as C1
on C.Actno = C1.Actno and C1.rn = 1
inner join cte as C2
on C.Actno = C2.Actno and C2.rn = 2
inner join cte as C3
on C.Actno = C3.Actno and C3.rn = 3
group by C.Actno
order by C.Actno
No, it's not possible in SQL.
It's also not what SQL is intended for; presentation is not SQL's job.
If you feel you need to get column data into rows, then there is something about your idea for organising your data that doesn't suit relational databases. That's where you should look.
If you don't mind that your aggregated rows are on string (and you're using Oracle), I used to do something like this:
CREATE OR REPLACE FUNCTION APPEND_FIELD(sqlstr in varchar2,
sep in varchar2)
return varchar2 is
ret varchar2(4000) := '';
TYPE cur_typ IS REF CURSOR;
rec cur_typ;
field varchar2(4000);
begin
OPEN rec FOR sqlstr;
LOOP
FETCH rec
INTO field;
EXIT WHEN rec%NOTFOUND;
ret := ret || field || sep;
END LOOP;
if length(ret) = 0 then
RETURN '';
else
RETURN substr(ret, 1, length(ret) - length(sep));
end if;
end;
/
select
actno,
sufix,
amount,
APPEND_FIELD('select sufix '''||' '||'''amount
from table tt
where tt.actno = '||t.actno||' and sufix != 230 ',' ')
from table t
where sufix=230;
HTH