.....
temp Varchar2 (20);
e_name Varchar2 (255);
.....
Begin
e_name := e_name || temp;
Dbms_Output.Put_Line('names: ' || e_name);
Output result
-------------
names: 'John', 'Sam', 'David', 'Sam', 'John', 'Alex'
How do I format my e_name to remove the duplicate names so that I have the output result
required result
-------------
names: 'John', 'Sam', 'David', 'Alex'
Well you can modify your program like this
Begin
e_name := e_name || temp;
SELECT listagg ( names, ',' ) within GROUP (ORDER BY rn )
INTO e_name
FROM
(
SELECT level rn,
regexp_substr ( e_name, '[^,]+', 1, level ) names,
row_number ( ) over ( partition BY regexp_substr ( e_name, '[^,]+', 1, level ) order by level ) rnn
FROM dual
CONNECT BY regexp_substr ( e_name, '[^,]+', 1, level ) IS NOT NULL
)
WHERE rnn = 1;
Dbms_Output.Put_Line('names: ' || e_name);
The inner most query will convert the list into rows and then outer queries will filter and create the string again.
Replace WM_CONCAT with LISTAGG - I'm running Oragle 10g, LISTAGG is 11g:
SELECT wm_concat(ename) AS employees
FROM emp_test
WHERE deptno = 20
/
Output - SMITH repeats twice:
SMITH,JONES,SCOTT,ADAMS,FORD,SMITH
SELECT wm_concat(distinct ename) AS employees
FROM emp_test
WHERE deptno = 20
/
The distinct fixes the problem:
ADAMS,FORD,JONES,SCOTT,SMITH
This can be done in pure SQL, but it gets a bit messy, especially if you're pre-11gR2 listagg(). Since you're already in PL/SQL territory, here's a solution that eliminates the dups in a simple PL/SQL fashion:
declare e_name varchar2(255) := q'"'John', 'Sam', 'David', 'Sam', 'John', 'Alex'"';
new_ename varchar2(255) := substr(e_name,1,instr(e_name,'''',2));
begin
dbms_output.put_line ('e_name: ' || e_name);
for i in 1..length(e_name) - length(replace(e_name,',')) loop
if instr(new_ename,
substr(e_name,instr(e_name,', ',1,i),instr(e_name||', ',', ',1,i+1) - instr(e_name,', ',1,i))) = 0
then
new_ename := new_ename || substr(e_name,instr(e_name,', ',1,i),instr(e_name||', ',', ',1,i+1) - instr(e_name,', ',1,i));
end if;
end loop;
dbms_output.put_line ('new_ename: ' || new_ename);
end;
e_name: 'John', 'Sam', 'David', 'Sam', 'John', 'Alex'
new_ename: 'John', 'Sam', 'David', 'John', 'Alex'
There is something called ASSOCIATIVE array,which act as Hash table in Java. We can put delimted text into a hash table, and thus can eliminate the duplicates.
We use EXISTS method of collection here, to check if the value is already present!
This is a simple to read NO SQL type of solution. So, performing one.
DECLARE
e_name VARCHAR2 (4000) := 'the text goes here';
L_TEMP_TEXT VARCHAR2(4000);
V_LOOPCOUNT NUMBER :=0;
T_WORD VARCHAR2(4000);
T_FINAL_TEXT VARCHAR2(4000) := ' ';
--Declare a DICT like a Hash table
TYPE DICT IS TABLE OF VARCHAR2(4000) INDEX BY VARCHAR(4000);
MYDICT DICT;
BEGIN
L_TEMP_TEXT := regexp_replace(e_name,'[,]+',','); -- Replace multiple consecutive commas as single
LOOP
v_LOOPCOUNT := v_LOOPCOUNT+1;
T_WORD := REGEXP_SUBSTR(e_name, '[^,]+', 1, V_LOOPCOUNT);
--In a loop we tokenize the String using comma as delimiter
EXIT WHEN T_WORD IS NULL;
IF NOT MYDICT.EXISTS(T_WORD) THEN
-- It is like a Hash Table, if not exists add to it.
MYDICT(T_WORD) := T_WORD;
T_FINAL_TEXT : T_FINAL_TEXT || ',' || T_WORD;
END;
END LOOP;
T_FINAL_TEXT := TRIM(BOTH ',' FROM TRIM(T_FINAL_TEXT));
-- Trimming the unwanted commas
DBMS_OUTPUT.PUT_LINE('after removing duplicates : ' || T_FINAL_TEXT);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(sqlerrm||chr(10)||dbms_utility.format_error_backtrace);
END;
/
Related
How can I use columns dynamically with bulk collect?
The code shown here is throwing errors:
SET serverout ON
DECLARE
r_emp SYS.ODCINUMBERLIST := SYS.ODCINUMBERLIST();
t_emp SYS.ODCINUMBERLIST := SYS.ODCINUMBERLIST('CUST_ID');
v_array SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(
'CUST_TYPE',
'CERT_TYPE_NAME',
'CERT_NBR',
'NEW_PARENT_CUST_ID');
BEGIN
DBMS_OUTPUT.ENABLE;
FOR i IN 1..v_array.COUNT LOOP
r_emp.extend;
EXECUTE IMMEDIATE
'SELECT '||t_emp||' FROM CUSTOMER_PROFILE where '||v_array(i)||' is null'
BULK COLLECT INTO r_emp(i);
for k in 1..r_emp(i).count loop
dbms_output.put_line(v_array(i) || ': ' || r_emp(k));
end loop;
END LOOP;
END;
Error report:
ORA-06550: line 15, column 7:
PLS-00306: wrong number or types of arguments in call to '||'
r_emp is an array and not an array or arrays so you cannot BULK COLLECT INTO r_emp(i), instead, you need to BULK COLLECT INTO r_emp (and need to neither initialise nor extend it as that is done automatically by BULK COLLECT).
You also cannot concatenate a string with a VARRAY so 'SELECT '||t_emp will not work (and is where your error message comes from). Instead, you can concatenate two strings so 'SELECT '||t_emp(1) would work.
And you cannot initialise a SYS.ODCINUMBERLIST with the value 'CUST_ID' as it is a string and not a number.
Instead, you can use:
DECLARE
r_emp SYS.ODCINUMBERLIST;
t_emp SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST('CUST_ID');
v_array SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(
'CUST_TYPE',
'CERT_TYPE_NAME',
'CERT_NBR',
'NEW_PARENT_CUST_ID'
);
BEGIN
DBMS_OUTPUT.ENABLE;
FOR i IN 1..v_array.COUNT LOOP
FOR j IN 1..t_emp.COUNT LOOP
EXECUTE IMMEDIATE
'SELECT '||t_emp(j)||' FROM CUSTOMER_PROFILE where '||v_array(i)||' is null'
BULK COLLECT INTO r_emp;
FOR k IN 1..r_emp.COUNT LOOP
dbms_output.put_line(v_array(i) || ': ' || r_emp(k));
END LOOP;
END LOOP;
END LOOP;
END;
/
Which, for the sample data:
create table customer_profile (
cust_id NUMBER,
cust_type VARCHAR2(20),
cert_type_name VARCHAR2(20),
cert_nbr VARCHAR2(20),
new_parent_cust_id VARCHAR2(20)
);
INSERT INTO customer_profile
SELECT 1, NULL, 'a', 'a', 'a' FROM DUAL UNION ALL
SELECT 2, 'b', NULL, 'b', 'b' FROM DUAL UNION ALL
SELECT 3, 'c', 'c', NULL, 'c' FROM DUAL UNION ALL
SELECT 4, 'd', 'd', 'd', NULL FROM DUAL UNION ALL
SELECT 5, NULL, NULL, NULL, NULL FROM DUAL;
Outputs:
CUST_TYPE: 1
CUST_TYPE: 5
CERT_TYPE_NAME: 2
CERT_TYPE_NAME: 5
CERT_NBR: 3
CERT_NBR: 5
NEW_PARENT_CUST_ID: 4
NEW_PARENT_CUST_ID: 5
fiddle
I've used INITCAP to capitalize words in a string, but I've run into a small issue:
select initcap(q'[JOE'S CARD 'N' CANDY]') from dual;
It returns "Joe'S Card 'N' Candy", but I wonder if there is another way to capitalize the words so it will look like this "Joe's Card 'N' Candy" (notice the s is in lowercase)
In your place I would create a custom PL/SQL procedure of the kind:
create or replace function initcap_cust(p_input varchar2)
return varchar2
as
l_input varchar2(4000) := lower(p_input);
l_capitalize_first_letter boolean := true;
l_output varchar2(4000) := null;
l_curr_char char(1);
begin
-- here we iterate over the lowercased string characters
for i in 1..length(l_input) loop
l_curr_char := substr(l_input, i, 1);
-- if we find a space - OK, next alphabet letter should be capitalized
-- you can add here more delimiters, e.g.: l_curr_char in (' ', ',', etc)
if l_curr_char = ' ' then
l_capitalize_first_letter := true;
end if;
-- makes O'Sullivan look this way
if regexp_like(l_output, '(^| )O''$') then
l_capitalize_first_letter := true;
end if;
-- found the first letter after delimiter - OK, capitalize
if l_capitalize_first_letter and (l_curr_char between 'a' and 'z') then
l_curr_char := upper(l_curr_char);
l_capitalize_first_letter := false;
end if;
-- build the output string
l_output := l_output || l_curr_char;
end loop;
return l_output;
end;
It works in your case and similar ones. Also it can be customized depending on your needs without dealing with complex queries built using the only functions provided by Oracle out of the box.
N.B. Also there is an option to create equivalent java stored procedure, on the link provided by Edgars T. there is an example.
Adapted from this answer to use a single simpler regular expression to parse each word:
WITH names ( name ) AS (
SELECT 'FIRSTNAME O''MALLEY' FROM DUAL UNION
SELECT 'FIRST''NAME TEH''TE' FROM DUAL UNION
SELECT 'FORMAT ME BYGGER''N' FROM DUAL UNION
SELECT 'OLD MCDONALD' FROM DUAL UNION
SELECT 'EVEN OL''DER MACDONALD' FROM DUAL UNION
SELECT q'[JOE'S CARD 'N' CANDY]' FROM DUAL
)
SELECT name,
formatted_name
FROM names
MODEL
PARTITION BY (ROWNUM rn)
DIMENSION BY (0 dim)
MEASURES(name, CAST('' AS VARCHAR2(255)) word, CAST('' AS VARCHAR(255)) formatted_name)
RULES ITERATE(99) UNTIL (word[0] IS NULL)
(
word[0] = REGEXP_SUBSTR(name[0], '[^ ]+( *|$)', 1, ITERATION_NUMBER + 1),
formatted_name[0] = formatted_name[0]
-- Capitalise names starting with ', *', MC and MAC:
|| INITCAP(REGEXP_SUBSTR( word[0], '^([^'']?''|ma?c)?(.)(.*)$', 1, 1, 'i', 1 ) )
-- Capitalise the next letter of the word
|| UPPER( REGEXP_SUBSTR( word[0], '^([^'']?''|ma?c)?(.)(.*)$', 1, 1, 'i', 2 ) )
-- Lower case the rest of the word
|| LOWER( REGEXP_SUBSTR( word[0], '^([^'']?''|ma?c)?(.)(.*)$', 1, 1, 'i', 3 ) )
);
Output:
NAME FORMATTED_NAME
----------------------- ----------------------
EVEN OL'DER MACDONALD Even Ol'der MacDonald
OLD MCDONALD Old McDonald
FIRST'NAME TEH'TE First'name Teh'te
FORMAT ME BYGGER'N Format Me Bygger'n
JOE'S CARD 'N' CANDY Joe's Card 'N' Candy
FIRSTNAME O'MALLEY Firstname O'Malley
I am trying to make a function that has a variable of any data type passed in. I want to be able to do a if statement to see if it is a varchar2 or a number.
create or replace FUNCTION get_manager(pass in sys.ANYDATA )
RETURN varchar2
IS T1 varchar2(300);
I_var varchar2(300);
I_num number;
F_NAME STRING(300);
L_NAME STRING(300);
BEGIN
if pass%type = I_var%type then --Name
F_NAME := REGEXP_SUBSTR(PASS, '(\S*)(\s)'); --First Word
L_NAME := REGEXP_SUBSTR(PASS, '(\S*)(\s)', 1, 2); --Second Word
select FIRST_NAME || ' ' || LAST_NAME || ' ' || PHONE_NUMBER INTO T1 FROM EMPLOYEES WHERE EMPLOYEES.FIRST_NAME = F_NAME AND EMPLOYEES.LAST_NAME = L_NAME ;
elsif pass%type = I_num%type then --ID Number
select FIRST_NAME || ' ' || LAST_NAME || ' ' || PHONE_NUMBER INTO T1 FROM EMPLOYEES WHERE EMPLOYEES.EMPLOYEE_ID = pass;
end if;
RETURN(T1);
END;
I get these compiler errors:
Error(12,5): PL/SQL: Statement ignored
Error(12,13): PLS-00208: identifier 'TYPE' is not a legal cursor attribute
Any ideas as to what I am doing wrong?
As Amitabh said, the getTypeName function is the most important part. But there are lots of tricky parts involved with the ANY* types.
For example, it would be incredibly difficult to find the type of a local variable. Probably the best bet would be to use PL/SCOPE, but that's a lot of work. Since the variable must be hard-coded to assign to it, the below code also hard-codes the local variable type name for the comparison.
In practice you almost never want to use the ANY types. They are great for solving some hard problems. But you don't want to build a framework around them if you can avoid it. It's usually better to generate lots of simple code, or to generate dynamic SQL.
Function
create or replace FUNCTION get_manager(pass in sys.ANYDATA )
RETURN varchar2
IS T1 varchar2(300);
I_var varchar2(300);
I_num number;
F_NAME varchar2(300);
L_NAME varchar2(300);
V_STATUS pls_integer;
BEGIN
if PASS.getTypeName = 'SYS.VARCHAR2' then --Name
V_STATUS := PASS.GetVarchar2(F_NAME);
V_STATUS := PASS.GetVarchar2(L_NAME);
F_NAME := TRIM(REGEXP_SUBSTR(F_NAME, '(\S*)(\s)')); --First Word
L_NAME := TRIM(REGEXP_SUBSTR(L_NAME, '(\s)(\S*)')); --Second Word
select FIRST_NAME || ' ' || LAST_NAME || ' ' || PHONE_NUMBER INTO T1 FROM EMPLOYEES WHERE EMPLOYEES.FIRST_NAME = F_NAME AND EMPLOYEES.LAST_NAME = L_NAME ;
elsif PASS.getTypeName = 'SYS.NUMBER' then --ID Number
V_STATUS := PASS.GetNumber(I_NUM);
select FIRST_NAME || ' ' || LAST_NAME || ' ' || PHONE_NUMBER INTO T1 FROM EMPLOYEES WHERE EMPLOYEES.EMPLOYEE_ID = I_NUM;
end if;
RETURN(T1);
END;
/
Sample Schema
--drop table employees;
create table employees(employee_id number, first_name varchar2(300), last_name varchar2(300), phone_number varchar2(300));
insert into employees values(1, 'JOHN', 'SMITH', '867-5309');
commit;
Results
select get_manager(anydata.ConvertVarchar2('JOHN SMITH')) employee_data from dual;
EMPLOYEE_DATA
-------------
JOHN SMITH 867-5309
select get_manager(anydata.ConvertNumber(1)) employee_data from dual;
EMPLOYEE_DATA
-------------
JOHN SMITH 867-5309
I think you need to use GETTYPENAME function here to get the data type.
https://docs.oracle.com/cd/E18283_01/appdev.112/e16760/t_anydat.htm#i1000030
Thanks,
Amitabh
I want to use my result of function e.g. 'S500,S600,S700,S800' in a subquery in another script like:
where dept_no in (my result of function)
So I want to convert my string result to be like this ('S500','S600','S700','S800').
I tried to do this with dynamic SQL but I can't get it to work.
Hope below snipet suffice your requirement.
Approach 1 -> More effective
--Create a table type of VARCHAR
CREATE OR REPLACE type string_table
IS
TABLE OF VARCHAR2(100);
--Function to return tabl type
CREATE OR REPLACE
FUNCTION string_manipulate
RETURN string_table
AS
str_tab string_table;
BEGIN
SELECT 's00'||level bulk collect INTO str_tab FROM dual CONNECT BY level < 10;
RETURN str_tab;
end;
--Use function in the query
SELECT distinct 1
FROM
(SELECT 's001' dn FROM dual
UNION ALL
SELECT 's002' dn FROM dual
UNION ALL
SELECT 's003' dn FROM dual
UNION ALL
SELECT 's004' dn FROM dual
UNION ALL
SELECT 's005' dn FROM dual
UNION ALL
SELECT 's006' dn FROM dual
UNION ALL
SELECT 's007' dn FROM dual
UNION ALL
SELECT 's008' dn FROM dual
UNION ALL
SELECT 's009' dn FROM dual
)a
WHERE a.dn IN
(SELECT * FROM TABLE(string_manipulate)
);
--Approach 2
--Function to get output as mentioned.
CREATE OR REPLACE
FUNCTION string_manipulate
RETURN VARCHAR2
AS
BEGIN
RETURN 'S2009,S2020,S2021';
END;
-- Use function value in a query
SELECT 1
FROM dual
WHERE '''S2009'',''S2020'',''S2021''' = (''''
||REPLACE(string_manipulate,',',''',''')
||'''');
You need an iterator and text splitting by comma sign:
select empno,ename,sal,deptno
from emp
where empno in (
select to_number(
rtrim(
substr(emps,
instr(emps,',',1,iter.pos)+1,
instr(emps,',',1,iter.pos+1) -
instr(emps,',',1,iter.pos)),',')) emps
from (select ','||'7654,7698,7782,7788'||',' emps from t1) csv,
(select rownum pos from emp) iter
where iter.pos <= ((length(csv.emps) -
length(replace(csv.emps,',')))/length(','))-1
)
But better rewrite your function to return cursor.
you can use collection:
SELECT *
FROM YOUR_TABLE
WHERE DEPT_NO IN (SELECT *
FROM TABLE (SPLIT ('S500,S600,S700,S800')))--splits text with comma, for other chars use split(text, split_char)
With usage of MEMBER OF
SELECT *
FROM YOUR_TABLE
WHERE DEPT_NO MEMBER OF SPLIT ('S500,S600,S700,S800')--splits text with comma, for other chars use split(text, split_char)
the split fuction is:
CREATE OR REPLACE TYPE SPLIT_TBL AS TABLE OF VARCHAR2 (32767);
CREATE OR REPLACE FUNCTION SPLIT (P_LIST VARCHAR2, P_DEL VARCHAR2 := ',')
RETURN SPLIT_TBL
PIPELINED
IS
L_IDX PLS_INTEGER;
L_LIST VARCHAR2 (32767) := P_LIST;
BEGIN
LOOP
L_IDX := INSTR (L_LIST, P_DEL);
IF L_IDX > 0
THEN
PIPE ROW (SUBSTR (L_LIST, 1, L_IDX - 1));
L_LIST := SUBSTR (L_LIST, L_IDX + LENGTH (P_DEL));
ELSE
PIPE ROW (L_LIST);
EXIT;
END IF;
END LOOP;
RETURN;
END SPLIT;
FUNCTION GET_TS_EACH_DAY_DEPARTMENT (P_SER_NO VARCHAR2,
P_TS_DATE DATE
)
RETURN STRING_TABLE
IS
V_DEPT_NO VARCHAR2 (4000);
V_DEPT VARCHAR2(4000);
V_TABLE STRING_TABLE:=STRING_TABLE();
J NUMBER:=1;
BEGIN
for i in (select distinct ts_day dayy from WEB_TS_USER_LOCATIONS_V ) loop
V_TABLE.EXTEND;
V_TABLE(J):= WEB_TS_PKG.GET_TS_DAY_DEPARTMENT (P_SER_NO ,P_TS_DATE , i.dayy );
J:=J+1;
end loop;
RETURN V_TABLE;
END GET_TS_EACH_DAY_DEPARTMENT;
I have a table EMP with following definition:
EMP_ID NOT NULL NUMBER(6)
EMP_NAME NOT NULL VARCHAR2(25)
EMAIL NOT NULL VARCHAR2(25)
PHONE_NUMBER VARCHAR2(20)
HIRE_DATE NOT NULL DATE
JOB_ID NOT NULL VARCHAR2(10)
SALARY NUMBER(8,2)
If we want to count employees whose name not in 'King', 'Steve', 'John' we simply use this query :
SELECT count(*) FROM emp WHERE emp_name NOT IN('King','Steve','John');
Now Here is what I want above this:
What I want to do is, Create a PL/SQL Function which returns number of count according to the dynamic input, Like if we pass:
SELECT count_emp('King,Steve,John') FROM dual;
SELECT count_emp('William,Donald') FROM dual;
SELECT count_emp('Daniel') FROM dual;
needs to return appropriate count, how can I achieve this using PL/SQL FUNCTION
This is what I have tried and Needs guideline:
CREATE OR REPLACE FUNCTION count_emp(emp_nm IN varchar)
RETURN number
IS
cnt NUMBER;
BEGIN
SELECT count(*) INTO cnt FROM emp WHERE emp_name NOT IN(emp_nm);
RETURN cnt;
END;
it is giving result for single name, but how can I split/format multiple input(i.e. emp_nm) to pass in NOT IN()?
Try like this,
CREATE OR REPLACE
FUNCTION count_emp(emp_nm IN VARCHAR)
RETURN NUMBER
IS
cnt NUMBER;
BEGIN
SELECT count(*)
INTO cnt
FROM emp
WHERE ename NOT IN(
SELECT regexp_substr (emp_nm, '[^,]+', 1, ROWNUM)
FROM dual
CONNECT BY LEVEL <= LENGTH (regexp_replace (emp_nm, '[^,]+')) + 1);
RETURN cnt;
END;
You can try dynamic sql:
CREATE OR REPLACE FUNCTION count_emp(emp_nm IN varchar)
RETURN number
IS
cnt NUMBER;
BEGIN
Execute immediate 'SELECT count(*) FROM emp WHERE emp_name NOT IN(' || emp_nm || ')' returning into cnt;
RETURN cnt;
END;
You can also use MEMBER OF. Here is a snippet. Hope this helps!
-- Create a type in SQL
CREATE OR REPLACE TYPE t_emp_name AS TABLE OF VARCHAR2 (10);
-- use MEMBER OF to use your list as IN parameter
CREATE OR REPLACE FUNCTION count_emp (emp_nm IN t_emp_name)
RETURN NUMBER
IS
cnt NUMBER;
BEGIN
SELECT COUNT (*)
INTO cnt
FROM emp
WHERE emp_name NOT MEMBER OF (emp_nm);
RETURN cnt;
END;
-- Assign values to the list, you can do it dynamically as well. Call the function
DECLARE
l_emp_name_list t_emp_name;
lv_count NUMBER;
BEGIN
l_emp_name_list := t_emp_name ('King', 'Steve'); --add more names as needed
lv_count := count_emp (l_emp_name_list);
END;