This question already has answers here:
PL/SQL rewrite concatenated query with 'IN' clause
(2 answers)
Closed 9 years ago.
I want to build query dynamically as per criterias selected from gui. Here is my oracle package,
CREATE OR REPLACE PACKAGE TestPkg
AS
g_lastnamelist VARCHAR2(50);
FUNCTION getLastName return VARCHAR2;
FUNCTION buildQuery(p_lastnamelist VARCHAR2);
END;
CREATE OR REPLACE PACKAGE BODY TestPkg
AS
FUNCTION getLastName return VARCHAR2
IS
BEGIN
RETURN replace(g_lastnamelist, '''', '');
END;
FUNCTION buildQuery(p_lastnamelist VARCHAR2);
IS
m_query varchar2(1000);
BEGIN
g_lastnamelist := p_lastnamelist;
m_query := 'SELECT * FROM emp WHERE last_name IN(TestPkg.getLastName)';
END;
END;
here if i use 'SELECT * FROM emp WHERE last_name IN('||p_lastnamelist||')'; then it returns me correct record but if i use 'SELECT * FROM emp WHERE last_name IN(TestPkg.getLastName)'; like this, then it fails. Whats the reason.
Thanks in advance.
I'm assuming you're passing in a comma-separated list of quoted values, for example p_lastnamelist is 'Smith','Jones', as that's the only way your first query will work, and explains the replace call. (It helps if you show the input, output and any errors you get, so we don't have to guess and assume). When that's used in the queries they end up looking significantly different. The first one:
'SELECT * FROM emp WHERE last_name IN('||p_lastnamelist||')'
becomes:
SELECT * FROM emp WHERE last_name IN('Smith','Jones')
... which will match on any records with either of those last names. But the second one:
'SELECT * FROM emp WHERE last_name IN(TestPkg.getLastName)'
becomes, after you've stripped the quotes from the value:
SELECT * FROM emp WHERE last_name IN('Smith,Jones')
... which is looking for a single value, and would only match if you had a record with last name Smith,Jones, which is unlikely. Whatever is returned by TestPkg.getLastName will be treated a single string value. It has to be since the function returns a varchar2. But that's also the case if you use a bind variable as Egor suggested.
Related
I am new to learning SQL and currently learning it in class. I am trying to write a code that fulfills the following requirements:
Make a reservation: Input parameters: Hotel, guest’s name, start date, end dates, room type, date of reservation. Output: reservation ID. NOTE: Only one guest per reservation. However, the same guest can make multiple reservations.
Find a reservation: Input is the guest’s name and date, hotel ID. The output is reservation ID
I am still somewhat new to deciphering error codes and tried to look up what they mean. However, I'm still not quite sure why my code is wrong.
CREATE OR REPLACE PACKAGE hotelmanagement AS
FUNCTION make(rsrv_id VARCHAR2
,hotel_name VARCHAR2
,guest VARCHAR2
,start_date VARCHAR2
,end_date VARCHAR2
,room_type VARCHAR2
,rsrv_date VARCHAR2)
RETURN NUMBER IS
rsrv_id NUMBER;
BEGIN
SELECT rsrv_seq.nextval INTO reserve_id FROM dual;
INSERT INTO reservations
VALUES
(reserve_id, 'Four Seasons', 'Amanda', 'July-30-2019', 'Aug-8-2019',
'King', 'July-18-2019');
tot_rsrv := tot_rsrv + 1;
RETURN(rsrv_id);
END;
FUNCTION find(guest VARCHAR2
,rsrv_date VARCHAR2)
RETURN NUMBER IS
rsrv_id NUMBER;
BEGIN
SELECT rsrv_id
INTO guest
FROM reservations
WHERE rsrv_date = find_rsrv_date;
END;
RETURN(rsrv_id);
END hotelmanagement;
I have these error messages(two of them look the same?):
Error(4,1): PLS-00103: Encountered the symbol "SELECT" when expecting one of the following: begin function pragma procedure subtype type current cursor delete exists prior The symbol "begin" was substituted for "SELECT" to continue.
Error(5,1): PLS-00103: Encountered the symbol "RSRV_ID" when expecting one of the following: language
Error(5,1): PLS-00103: Encountered the symbol "RSRV_ID" when expecting one of the following: language
The requirements are a quite vague, so it is difficult to provide the desired procedure. Anyway, your code has some weak items.
Your procedure has several input parameters but you don't use them inside the procedure.
Use appropriate data types, i.e. never(!) store DATE or number values in string, i.e. VARCHAR2. Use VARCHAR2 only for string data. At least you use four digit year, which is the proper way of doing it.
RETURN command does not use brackets.
Taking all this into account your code should be more or less like this:
CREATE OR REPLACE PACKAGE BODY hotelmanagement AS
FUNCTION make( -- you can't declare variable "rsrv_id" twice
hotel_name IN VARCHAR2
,guest IN VARCHAR2
,start_date IN DATE
,end_date IN DATE
,room_type IN VARCHAR2
,rsrv_date IN DATE)
RETURN NUMBER IS
rsrv_id NUMBER;
BEGIN
INSERT INTO reservations
VALUES
(rsrv_seq.nextval, hotel_name, guest, start_date, end_date,
room_type, rsrv_date)
RETURNING reserve_id INTO rsrv_id ;
-- tot_rsrv := tot_rsrv + 1; -> I don't see any use for it, variable tot_rsrv is not declared
RETURN rsrv_id;
END;
FUNCTION find(v_guest IN VARCHAR2
,rsrv_date IN DATE) RETURN NUMBER IS
rsrv_id NUMBER;
BEGIN
SELECT rsrv_id
INTO rsrv_id
FROM reservations
WHERE rsrv_date = find_rsrv_date
and guest = v_guest; -- don't use "guest = guest" because this will select all rows.
RETURN rsrv_id;
END;
END hotelmanagement;
My test function is this
CREATE OR REPLACE FUNCTION MULTI_VAL
(MYNAME OUT EMP2017.ENAME%TYPE)
RETURN NUMBER AS
MYSAL EMP2017.SAL%TYPE;
BEGIN
SELECT SAL, ENAME INTO MYSAL, MYNAME FROM EMP2017 ;
RETURN MYSAL;
END;
/
When I run it like
variable mynm varchar2(20)
SELECT MULTI_VAL(:mynm) FROM dual;
it gives this error
ERROR at line 1:
ORA-06553: PLS-561: character set mismatch on value for parameter 'MYNAME'
The error you get now indicates a datatype mismatch.
However there is a fundamental problem with your code. We cannot use functions which have OUT parameters in SQL. So once you have fixed the datatype issue you will get this error: ORA-06572: Function MULTI_VAL has out arguments.
You can run it like this:
declare
n varchar2(20);
x number;
begin
x := multi_val(n);
end;
/
Generally, functions with OUT parameters are considered bad practice. The syntax allows them, but the usage is hard to understand. It's better to use a procedure with two OUT parameters (because we can only call the program in PL/SQL anyway) or else have the function return a user-defined type.
CREATE TABLE EMP2017(ENAME VARCHAR2(10),SAL NUMBER);
INSERT INTO EMP2017 VALUES ('SMITH',5000);
INSERT INTO EMP2017 VALUES ('JOHNS',1000);
COMMIT;
CREATE TYPE RET_MULT AS OBJECT
(ENAME VARCHAR2(10),SAL NUMBER);
CREATE TYPE T_RET_MULT AS TABLE OF RET_MULT;
CREATE OR REPLACE FUNCTION MULTI_VAL RETURN T_RET_MULT PIPELINED IS
MYSAL RET_MULT;
BEGIN
FOR I IN(SELECT SAL, ENAME FROM EMP2017) LOOP
MYSAL := RET_MULT(I.ENAME,I.SAL);
PIPE ROW(MYSAL);
END LOOP ;
RETURN ;
END;
SELECT * FROM TABLE(MULTI_VAL());
I think this question can be solved without using pipeline functions. Just like this. All pre required data as described #Sedat.Turan except function. Sorry for copy/past.
CREATE TABLE EMP2017(ENAME VARCHAR2(10),SAL NUMBER);
INSERT INTO EMP2017 VALUES ('SMITH',5000);
INSERT INTO EMP2017 VALUES ('JOHNS',1000);
COMMIT;
CREATE TYPE RET_MULT AS OBJECT
(ENAME VARCHAR2(10),SAL NUMBER);
CREATE TYPE T_RET_MULT AS TABLE OF RET_MULT;
create or replace function MULTI_VAL return T_RET_MULT is
RET_SET T_RET_MULT;
begin
select RET_MULT(ENAME, SAL) bulk collect into RET_SET from EMP2017;
return RET_SET;
end;
I want to create a procedure in PL/SQL that has 5 steps. Step 1 and 2 execute first and return an ID. In step 3, we have a SELECT statement that has a condition with that returned ID. I want then to take all of the results of that SELECT statement and use them in a JOIN in another SELECT statement and use THOSE results in a 3rd SELECT statement again using JOIN. From what I've seen, I can't use CURSOR in JOIN statements. Some of my co-workers have suggested that I save the results in a CURSOR and then use a loop to iterate through each row and use that data for the next SELECT. However since I'm going to do 2 selects this will create a huge fork of inside loops and that's exactly what I'm trying to avoid.
Another suggestion was to use Temprary Tables to store the data. However this procedure could be executed at the same time by many users and the table's data would conflict with each other. Right now I'm looking at LOCAL Temporary tables that supposedly filter the data according the the session but I'm not really sure I want to create dummy tables for my procedures since I want to avoid leaving trash in the schema (this procedure is for a custom part of the application). Is there a standard way of doing this? Any ideas?
Sample:
DECLARE
USERID INT := 1000000;
TEXT1 VARCHAR(100);
TEXT_INDEX INT;
CURSOR NODES IS SELECT * FROM NODE_TABLE WHERE DESCRIPTION LIKE TEXT || '%';
CURSOR USERS IS SELECT * FROM USERGROUPS JOIN NODES ON NODES.ID = USERGROUPS.ID;
BEGIN
SELECT TEXT INTO TEXT1 FROM TABLE_1 WHERE ID = USERID;
TEXT_INDEX = INSTR(TEXT, '-');
TEXT = SUBSTR(TEXT, 0, TEXT_INDEX);
OPEN NODES;
OPEN USERS;
END;
NOTE: This does NOT work. Oracle doesn't support joins between cursors.
NOTE2: This CAN be done in a single query but for the sake of argument (and in my real use case) I want to break those steps down in a procedure. The sample code is a depiction of what I'm trying to achieve IF joins between cursors worked. But they don't and I'm looking for an alternative.
I ended up using a function (although a procedure could be used as well) along with tables. Things I've learned and one should pay attention to:
PL/SQL functions can only return types that have been declared in the schema in advance and are clear. You can't create a function that returns something like MY_TABLE%ROWTYPE, even though it seems the type information is available it is not acceptable. You have to instead create a custom type of MY_TABLE%ROWTYPE is you want to return it.
Oracle treats tables of declared types differently from tables of %ROWTYPE. This confused the hell out of me at first but from what I've gathered this is how it works.
DECLARE TYPE MY_CUSTOM_TABLE IS TABLE OF MY_TABLE%ROWTYPE;
Declares a collection of types of MY_TABLE row. In order to add to this we must use BULK COLLECT INTO from an SQL statement that queries MY_TABLE. The resulting collection CANNOT be used in JOIN statements is not queryable and CANNOT be returned by a function.
DECLARE
CREATE TYPE MY_CUSTOM_TYPE AS OBJECT (COL_A NUMBER, COL_B NUMBER);
CREATE TYPE MY_CUSTOM_TABLE AS TABLE OF MY_CUSTOM_TYPE;
my_custom_tab MY_CUSTOM_TABLE;
This create my_custom_tab which is a table (not a collection) and if populated can be queried at using TABLE(my_custmo_tab) in the FROM statement. As a table which is declared in advance in the schema this CAN be returned from a function. However it CANNOT be populated using BULK COLLECT INTO since it is not a collection. We must instead use the normal SELECT INTO statement. However, if we want to populate it with data from an existing table that has 2 number columns we cannot simply do SELECT * INTO my_custom_tab FROM DOUBLE_NUMBER_TABLE since my_custom_tab hasn't been initialized and doesn't contain enough rows to receive the data. And if we don't know how many rows a query returns we can't initialize it. The trick into populating the table is to use the CAST command and cast our select result set as a MY_CUSTOM_TABLE and THEN add it.
SELECT CAST(MULTISET(SELECT COL_A, COL_B FROM DOUBLE_NUMBER_TABLE) AS MY_CUSTOM_TABLE) INTO my_custom_tab FROM DUAL
Now we can easily use my_custom_tab in queries etc through the use of the TABLE() function.
SELECT * FROM TABLE(my_custom_tab)
is valid.
You can do such decomposition in many ways, but all of them have a significant performance penalty in comaration with single SQL statement.
Maintainability improvement are also questionable and depends on specific situation.
To review all possibilities please look through documentation.
Below is some possible variants based on simple logic:
calculate Oracle user name prefix based on given Id;
get all users whose name starts with this prefix;
find all tables owned by users from step 2;
count a total number of found tables.
1. pipelined
Prepare types to be used by functions:
create or replace type TUserRow as object (
username varchar2(30),
user_id number,
created date
)
/
create or replace type TTableRow as object (
owner varchar2(30),
table_name varchar2(30),
status varchar2(8),
logging varchar2(3)
-- some other useful fields here
)
/
create or replace type TUserList as table of TUserRow
/
create or replace type TTableList as table of TTableRow
/
Simple function to find prefix by user id:
create or replace function GetUserPrefix(piUserId in number) return varchar2
is
vUserPrefix varchar2(30);
begin
select substr(username,1,3) into vUserPrefix
from all_users
where user_id = piUserId;
return vUserPrefix;
end;
/
Function searching for users:
create or replace function GetUsersPipe(
piNameStart in varchar2
)
return TUserList pipelined
as
vUserList TUserList;
begin
for cUsers in (
select *
from
all_users
where
username like piNameStart||'%'
)
loop
pipe row( TUserRow(cUsers.username, cUsers.user_id, cUsers.created) ) ;
end loop;
return;
end;
Function searching for tables:
create or replace function GetUserTablesPipe(
piUserNameStart in varchar2
)
return TTableList pipelined
as
vTableList TTableList;
begin
for cTables in (
select *
from
all_tables tab_list,
table(GetUsersPipe(piUserNameStart)) user_list
where
tab_list.owner = user_list.username
)
loop
pipe row ( TTableRow(cTables.owner, cTables.table_name, cTables.status, cTables.logging) );
end loop;
return;
end;
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from table(GetUserTablesPipe(GetUserPrefix(vUserId)));
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
2. Simple table functions
This solution use same types as a variant with pipelined functions above.
Function searching for users:
create or replace function GetUsers(piNameStart in varchar2) return TUserList
as
vUserList TUserList;
begin
select TUserRow(username, user_id, created)
bulk collect into vUserList
from
all_users
where
username like piNameStart||'%'
;
return vUserList;
end;
/
Function searching for tables:
create or replace function GetUserTables(piUserNameStart in varchar2) return TTableList
as
vTableList TTableList;
begin
select TTableRow(owner, table_name, status, logging)
bulk collect into vTableList
from
all_tables tab_list,
table(GetUsers(piUserNameStart)) user_list
where
tab_list.owner = user_list.username
;
return vTableList;
end;
/
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from table(GetUserTables(GetUserPrefix(vUserId)));
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
3. cursor - xml - cursor
It's is a specific case, which may be implemented without user-defined types but have a big performance penalty, involves unneeded type conversion and have a low maintainability.
Function searching for users:
create or replace function GetUsersRef(
piNameStart in varchar2
)
return sys_refcursor
as
cUserList sys_refcursor;
begin
open cUserList for
select * from all_users
where username like piNameStart||'%'
;
return cUserList;
end;
Function searching for tables:
create or replace function GetUserTablesRef(
piUserNameStart in varchar2
)
return sys_refcursor
as
cTableList sys_refcursor;
begin
open cTableList for
select
tab_list.*
from
(
XMLTable('/ROWSET/ROW'
passing xmltype(GetUsersRef(piUserNameStart))
columns
username varchar2(30) path '/ROW/USERNAME'
)
) user_list,
all_tables tab_list
where
tab_list.owner = user_list.username
;
return cTableList;
end;
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from
XMLTable('/ROWSET/ROW'
passing xmltype(GetUserTablesRef(GetUserPrefix(vUserId)))
columns
table_name varchar2(30) path '/ROW/TABLE_NAME'
)
;
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
Of course, all variants may be mixed, but SQL looks better at least for simple cases:
declare
vUserId number := 5;
vUserPrefix varchar2(100);
vTableCount number;
begin
-- Construct prefix from Id
select max(substr(user_list.username,1,3))
into vUserPrefix
from
all_users user_list
where
user_list.user_id = vUserId
;
-- Count number of tables owned by users with name started with vUserPrefix string
select
count(1) into vTableCount
from
all_users user_list,
all_tables table_list
where
user_list.username like vUserPrefix||'%'
and
table_list.owner = user_list.username
;
dbms_output.put_line('Users with name started with "'||vUserPrefix||'" owns '||vTableCount||' tables');
end;
P.S. All code only for demonstration purposes: no optimizations and so on.
I am new to PL/SQL. I have created a pipelined function inside a package which takes as its parameter input an array of numbers (nested table).
But I am having trouble trying to run it via an sql query. Please see below
my input array
CREATE OR REPLACE TYPE num_array is TABLE of number;
my function declaration
CREATE OR REPLACE PACKAGE "my_pack" as
TYPE myRecord is RECORD(column_a NUMBER);
TYPE myTable IS TABLE of myRecord;
FUNCTION My_Function(inp_param num_array) return myTable PIPELINED;
end my_pack;
my function definition
CREATE OR REPLACE PACKAGE BODY "my_pack" as
FUNCTION My_Function(inp_param num_array) return myTable PIPELINED as
rec myRecord;
BEGIN
FOR i in 1..inp_param.count LOOP
FOR e IN
(
SELECT column_a FROM table_a where id=inp_param(i)
)
LOOP
rec.column_a := e.column_a;
PIPE ROW (rec);
END LOOP;
END LOOP;
RETURN;
END;
end my_pack;
Here is the latest code I've tried running from toad. But it doesn't work
declare
myarray num_array;
qrySQL varchar2(4000);
begin
myarray := num_array(6341,6468);
qrySQL := 'select * from TABLE(my_pack.My_Function(:myarray))';
execute immediate qrySQL;
end;
So my question is how can I feed an array into this pipelined function from either TOAD or SQL Developer. An example would be really handy.
Thanks
The error is fairly clear, you have a bind variable that you haven't assigned anything to. You need to pass your actual array with:
qrySQL := 'select * from TABLE(my_pack.My_Function(:myarray))';
execute immediate qrySQL using myarray;
It's maybe more useful, if you want to call it from PL/SQL, to use static SQL as a cursor:
set serveroutput on
declare
myarray num_array;
begin
myarray := num_array(6341,6468);
for r in (select * from TABLE(my_pack.My_Function(myarray))) loop
dbms_output.put_line(r.column_a);
end loop;
end;
/
Or just query it statically as a test, for fixed values:
select * from TABLE(my_pack.My_Function(num_array(6341,6468)));
SQL Fiddle with some minor tweaks to the function to remove errors I think came from editing to post.
Thought I had followed creation pattern, but the body will not compile. What I am trying to accomplish is to develop a package to run a procedrure periodically to determine at what time and date more than 15 are in use.. Oracle 11g.
The only other data that needs to go into the table beingg inserted into the sysdate.
CREATE OR REPLACE
PACKAGE TAPES_USED AS
function TAPESCOUNT(count number) return number;
procedure INSERT_TAPES_COUNT(sysdate date, count NUMBER);
END TAPES_USED;
/
-----------------------------------------
CREATE OR REPLACE
PACKAGE body TAPES_USED AS
function TAPESCOUNT(count number) return number as count number;
begin
select count(*)
into
count
from DEV.TAPES_IN USE where count(*) > 15;
procedure INSERT_TAPES_COUNT(sysdate date, count NUMBER)as
begin
INSERT INTO DEV.TAPES_USED VALUES
(sysdate, count);
end INSERT_TAPES_COUNT;
END TAPES_USED;
/
Any help or suggestion anyone can offer will be appreciated.
CREATE OR REPLACE
PACKAGE BODY tapes_used AS
FUNCTION tapescount(in_ct NUMBER) RETURN NUMBER IS
ct NUMBER;
BEGIN
SELECT COUNT(*)
INTO ct
FROM dev.tapes_in_use;
IF ct > in_ct THEN
RETURN ct;
ELSE
RETURN NULL;
END IF;
END tapescount;
PROCEDURE insert_tapes_count(sysdt date, ct NUMBER) IS
BEGIN
INSERT INTO dev.tapes_used VALUES (sysdt, ct);
END insert_tapes_count;
END tapes_used;
/
You should refrain from using reserved words such as COUNT and SYSDATE for variable names (I don't know but that could be some of your compilation issues), so I've renamed them. Also, you forgot to END your function. I think you were missing an underscore in your table name in the FROM clause of the SELECT in your function, and you didn't have a RETURN statement in your function, which you must have.
Generally speaking, a function should accept one or more input parameters and return a single value. You're not making use of the input parameter in your function. I've implemented a suggested parameter.
As Egor notes, this isn't a realistic function, and I'm not certain about your intent here. What is the function supposed to do?
Maybe you want your function to return the Date/Time your count was exceeded? You could also combine everything into a single procedure:
PROCEDURE ck_tape_ct(min_tape_ct NUMBER) IS
ct NUMBER;
BEGIN
SELECT COUNT(*)
INTO ct
FROM dev.tapes_in_use;
IF ct > min_tape_ct THEN
INSERT INTO dev.tapes_used VALUES(SYSDATE, ct);
END IF;
END;