Binding variables in dynamic PL/SQL - oracle

I have a dynamic PL/SQL that will construct the SELECT statement based on what the searching criteria input from the users,likes:
l_sql := 'SELECT * INTO FROM TABLEA WHERE 1=1 ';
IF in_param1 IS NOT NULL THEN
l_sql := l_sql || 'AND column1 = in_param1 ';
END IF;
IF in_param2 IS NOT NULL THEN
l_sql := l_sql || 'AND column2 = in_param2 ';
END IF;
...................................
IF in_paramXX IS NOT NULL THEN
l_sql := l_sql || 'AND columnXX = in_paramXX ';
END IF;
To reduce the hard parse overhead , I consider to use the binding variables. However , it is difficult to manage when supplying the actual values to the binding variables as there are so many binding variables and combination of the generated SELECT statement . I cannot use the method of DBMS_SESSION.set_context() introduced at http://www.dba-oracle.com/plsql/t_plsql_dynamic_binds.htm because my account has no right to use this package. Besides , I want the generated SQL only contains the conditions on the fields that the user did not leave empty. So I cannot change the dynamic SQL to something likes
SELECT * INTO FROM TABLEA WHERE 1=1
and ( in_param1 is NULL or column1 = in_param1)
and ( in_param2 is NULL or column2 = in_param2)
...............................................
and ( in_paramXX is NULL or columnXX = in_paramXX)
So , I want to try to use the DBMS_SQL method .Can anyone give an example about how to use DBMS_SQL to call dynamic SQL with binding variables? Especially , how can I get the result executed from DBMS_SQL.execute() to the SYS_REFCURSOR , something like :
open refcursor for select .... from
The oracle version that I use is 10g and it seems that the oracle 10g does not have DBMS_Sql.To_Refcursor()

In your Oracle version you can apply some tricks to your query in order to do this. The idea is to use a query in the following form:
select *
from
(select
:possibleParam1 as param1
-- do the same for every possible param in your query
:possibleParamN as paramN
from dual
where rownum > 0) params
inner join
-- join your tables here
on
-- concatenate your filters here
where
-- fixed conditions
then execute it with:
open c for query using param1, ..., paramN;
It works by using DUAL to generate a fake row with every single param, then inner joining this fake row to your real query (without any filters) using only the filters you want to apply. This way, you have a fixed list of bind variables in the SELECT list of the params subquery, but can control which filters are applied by modifying the join condition between params and your real query.
So, if you have something like, say:
create table people (
first_name varchar2(20)
last_name varchar2(20)
);
you can construct the following query if you just want to filter on first name
select *
from
(select
:first_name as first_name,
:last_name as last_name
from dual
where rownum > 0) params
inner join
people
on
people.first_name = params.first_name;
and this if you want to filter on both first_name and last_name
select *
from
(select
:first_name as first_name,
:last_name as last_name
from dual
where rownum > 0) params
inner join
people
on
people.first_name = params.first_name and
people.last_name = params.last_name;
and in every case you would execute with
open c for query using filterFirstName, filterLastName;
It is important for performance to use the where rownum > 0 with DUAL as it forces Oracle to "materialize" the subquery. This usually makes DUAL stop interfering with the rest of the query. Anyway, you should check the execution plans to be sure Oracle is not doing anything wrong.

In 10g a DBMS_SQL cursor can't be changed into a Ref Cursor. Going through a result set through DBMS_SQL is tortuous since, as well as looping through the rows, you also have to loop through the columns in a row.
I want the generated SQL only contains
the conditions on the fields that the
user did not leave empty
Is that purely for performance reasons ? If so, I suggest you work out what the practical execution plans are and use separate queries for them.
For example, say I'm searching on people and the parameters are first_name, last_name. gender, date_of_birth. The table has indexes on (last_name,first_name) and (date_of_birth), so I only want to allow a query if it specifies either last_name or date_of_birth.
IF :p_firstname IS NOT NULL and :p_lastname IS NOT NULL THEN
OPEN cur FOR
'SELECT * FROM PEOPLE WHERE last_name=:a AND first_name=:b AND
(date_of_birth = :c or :c is NULL) AND (gender = :d or :d IS NULL)' USING ....
ELSIF :p_lastname IS NOT NULL THEN
OPEN cur FOR
'SELECT * FROM PEOPLE WHERE last_name=:a AND
(date_of_birth = :c or :c is NULL) AND (gender = :d or :d IS NULL)' USING ....
ELSIF :p_dateofbirth IS NOT NULL THEN
OPEN cur FOR
'SELECT * FROM PEOPLE WHERE date_of_birth=:a AND
(first_name=:b OR :b IS NULL) AND (gender = :d or :d IS NULL)' USING ....
ELSE
RAISE_APPLICATION_ERROR(-20001,'Last Name or Date of Birth MUST be supplied);
END IF;

Related

ORA-00947 not enough values with function returning table of records

So I'm trying to build a function that returns the records of items that are included in some client subscription.
So I've been building up the following:
2 types:
CREATE OR REPLACE TYPE PGM_ROW AS OBJECT
(
pID NUMBER(10),
pName VARCHAR2(300)
);
CREATE OR REPLACE TYPE PGM_TAB AS TABLE OF PGM_ROW;
1 function:
CREATE OR REPLACE FUNCTION FLOGIN (USER_ID NUMBER) RETURN PGM_TAB
AS
SELECTED_PGM PGM_TAB;
BEGIN
FOR RESTRICTION
IN ( SELECT (SELECT LISTAGG (ID_CHANNEL, ',')
WITHIN GROUP (ORDER BY ID_CHANNEL)
FROM (SELECT DISTINCT CHA2.ID_CHANNEL
FROM CHANNELS_ACCESSES CHA2
JOIN CHANNELS CH2
ON CH2.ID = CHA2.ID_CHANNEL
WHERE CHA2.ID_ACCESS = CMPA.ID_ACCESS
AND CH2.ID_CHANNELS_GROUP = CG.ID))
AS channels,
(SELECT LISTAGG (ID_SUBGENRE, ',')
WITHIN GROUP (ORDER BY ID_SUBGENRE)
FROM (SELECT DISTINCT SGA2.ID_SUBGENRE
FROM SUBGENRES_ACCESSES SGA2
JOIN CHANNELS_ACCESSES CHA2
ON CHA2.ID_ACCESS = SGA2.ID_ACCESS
JOIN CHANNELS CH2
ON CH2.ID = CHA2.ID_CHANNEL
WHERE SGA2.ID_ACCESS = CMPA.ID_ACCESS
AND CH2.ID_CHANNELS_GROUP = CG.ID))
AS subgenres,
CG.NAME,
A.BEGIN_DATE,
A.END_DATE,
CMP.PREVIEW_ACCESS
FROM USERS U
JOIN COMPANIES_ACCESSES CMPA
ON U.ID_COMPANY = CMPA.ID_COMPANY
JOIN COMPANIES CMP ON CMP.ID = CMPA.ID_COMPANY
JOIN ACCESSES A ON A.ID = CMPA.ID_ACCESS
JOIN CHANNELS_ACCESSES CHA
ON CHA.ID_ACCESS = CMPA.ID_ACCESS
JOIN SUBGENRES_ACCESSES SGA
ON SGA.ID_ACCESS = CMPA.ID_ACCESS
JOIN CHANNELS CH ON CH.ID = CHA.ID_CHANNEL
JOIN CHANNELS_GROUPS CG ON CG.ID = CH.ID_CHANNELS_GROUP
WHERE U.ID = USER_ID
GROUP BY CG.NAME,
A.BEGIN_DATE,
A.END_DATE,
CMPA.ID_ACCESS,
CG.ID,
CMP.PREVIEW_ACCESS)
LOOP
SELECT PFT.ID_PROGRAM, PFT.LOCAL_TITLE
BULK COLLECT INTO SELECTED_PGM
FROM PROGRAMS_FT PFT
WHERE PFT.ID_CHANNEL IN
( SELECT TO_NUMBER (
REGEXP_SUBSTR (RESTRICTION.CHANNELS,
'[^,]+',
1,
ROWNUM))
FROM DUAL
CONNECT BY LEVEL <=
TO_NUMBER (
REGEXP_COUNT (RESTRICTION.CHANNELS,
'[^,]+')))
AND PFT.ID_SUBGENRE IN
( SELECT TO_NUMBER (
REGEXP_SUBSTR (RESTRICTION.SUBGENRES,
'[^,]+',
1,
ROWNUM))
FROM DUAL
CONNECT BY LEVEL <=
TO_NUMBER (
REGEXP_COUNT (RESTRICTION.SUBGENRES,
'[^,]+')))
AND (PFT.LAUNCH_DATE BETWEEN RESTRICTION.BEGIN_DATE
AND RESTRICTION.END_DATE);
END LOOP;
RETURN SELECTED_PGM;
END FLOGIN;
I expect the function tu return a table with 2 columns containing all the records from table PROGRAMS_FT that are included in the user access.
For some reason, I'm getting compilation warning ORA-000947.
My understanding of the error code is that it occurs when the values inserted does not match the type of the object receiving the values, and I can't see how this can be the case here.
You're selecting two scalar values and trying to put them into an object. That doesn't happen automatically, you need to convert them to an object:
...
LOOP
SELECT PGM_ROW(PFT.ID_PROGRAM, PFT.LOCAL_TITLE)
BULK COLLECT INTO SELECTED_PGM
FROM PROGRAMS_FT PFT
...
(It's an unhelpful quirk of PL/SQL that it says 'not enough values' rather than 'too many values', as you might expect when you try to put two things into one; I'm sure I came up with a fairly convincing explanation/excuse for that once but it escapes me at the moment...)
I'm not sure your loop makes sense though. Assuming your cursor query returns multiple rows, each time around the loop you're replacing the contents of the SELECTED_PGM collection - you might think you are appending to it, but that's not how it works. So you will end up returning a collection based only on the final iteration of the loop.
Aggregating and then splitting the data seems like a lot of work too. You could maybe use collections for those; but you can probably get rid of the cursor and loop and combine the cursor query with the inner query, which would be more efficient and would allow you to do a single bulk-collect for all the combined data.

Using IF statements in Oracle when trying to return data

How do I return data out of IF statements? I have a IF statement which is meant to return a different result dependent of the result of that statement.
IF :Value = 1 THEN
SELECT Name FROM TABLE_A
ELSEIF :Value = 2 THEN
SELECT Name FROM TABLE_B
ELSEIF :Value = 3 THEN
SELECT Name FROM TABLE_C
but this doesn't work. It expects an INTO statement in those selects. I suspect this is because Oracle can't return out of a block?
Is there a quicker way to return those select statements without creating table variables to store the data or messing around with functions?
You can do this by plain SQL:
SELECT
':Value' user_input,
CASE
WHEN ':Value' IN('a1','a2','a3')
THEN (select name from table_a)
WHEN ':Value' = 'i'
THEN (select name from table_b)
END AS output
FROM
dual
(good info about case)
If you want more than one result in your cases, then you may opt to an intelligent UNION:
SELECT t1_Col_1, t1_Col_2,'&VALUE' from TABLE_1
WHERE '&VALUE' = '1'
UNION ALL
SELECT t2_Col_1, t2_Col_2,'&VALUE' from TABLE_2
WHERE '&VALUE' = '2'
In this solution, types and number of tx_Coln must be the same.

Cursor giving NO_DATA_FOUND if column with empty value included

I have a cursor defined as
CURSOR all_movie_nominations IS SELECT a.title, a.award_year,n.people_id FROM award a JOIN
nominee n ON
a.award_id = n.award_id
WHERE n.movie_id = (SELECT movie_id FROM movie WHERE movie_name = movieName);
The cursor is being opened and closed right.
Using the cursor in following code as
FETCH all_movie_nominations INTO award_title, award_year,nominee_people_id;
WHILE all_movie_nominations%FOUND LOOP
SELECT given_name, family_name INTO first_name, last_name FROM people WHERE people_id=nominee_people_id;
IF first_name = '' THEN
DBMS_OUTPUT.PUT_LINE('The Movie was nominated for '||award_title||' for the year '||award_year);
ELSE
DBMS_OUTPUT.PUT_LINE(first_name||' '||last_name||' was nominated for '||award_title||' for the year '||award_year);
END IF;
FETCH all_movie_nominations INTO award_title, award_year, nominee_people_id;
END LOOP;
Now the attribute n.people_id in cursor definition. This column contains empty values for some rows.
On executing the above code, I get the correct output but a 'NO_DATA_FOUND` Exception at the end of the output.
If I exclude the n.people_id from the cursor, I do not get any exception.
Any suggestions on what is possibly going wrong?
Unless this is student code about learning to use cursors, you would never code this way in real life. SQL statements inside a loop construct are a red flag. Because you are hammering the database this way with numerous (number of awards) tiny select statements retrieving a single person. Which means the PL/SQL engine has to switch to the SQL engine numerous times, leading to relatively expensive context switches. If you use joins, or in your case outer joins, you can minimize the number of context switches to one.
An example of an alternative (untested of course):
for r in
( select a.title
, a.award_year
, p.given_name
, p.family_name
from award
inner join nominee n on a.award_id = n.award_id
left outer join people p on n.people_id = p.people_id
where n.movie_id = (select movie_id from movie where movie_name = movieName)
)
loop
dbms_output.put_line
( case
when r.given_name is null then
'The Movie'
else
r.given_name || ' ' || r.family_name
end ||
' was nominated for ' || r.title || ' for the year ' || r.award_year
);
end loop;
I am not sure what could be happen, I think that the not_found exception is thrown by the inner select sentence:
IF nominee_people_id is not null THEN
SELECT given_name, family_name INTO first_name, last_name FROM people WHERE
people_id=nominee_people_id;
END IF;
I would try to put a condition that checks nominee_people_id is not null for executing the senetence.
I hope it was useful for you
Redgards
Hi you can opt for exception block for that if you want to ignore IF--END IF BLOCK;
BEGIN
SELECT given_name, family_name INTO first_name, last_name FROM people WHERE people_id=nominee_people_id;
EXCEPTION WHEN NO_DATA_FOUND THEN
dbms_output.put_line('no data found for '||nominee_people_id);
END;

ORA-01791 Pl-Sql error

hi guy i have a query that give me the followin error:
ORA-01791: not a SELECTed expression
this is the select expresison , please can you tell me why ?
declare
freqLettura varchar2(64);
billingcy varchar2(64);
begin
freqLettura := null;
billingcy := null;
for rec in ( select distinct(fn_get_facilityid(z.uidfacility) ) as a, 1 as b
from facilityhistory z,
locality l ,
plant p ,
ztmp_sam_tb_sdv zsdv ,
ztmp_sam_tb_plantcode zplant ,
sam_tb_ca_pdr sam,
meterhistory mh,
meter m ,
meterclass mc
where
Z.UIDLOCALITY = L.UIDLOCALITY and
p.UIDPLANT = L.UIDPLANT and
z.uidaccount = zsdv.uidaccount and
p.plantcode = zplant.plantcode and
sam.uidfacility = z.uidfacility and
z.stoptime is null and
sam.status = 'U' and
mh.uidfacility = z.uidfacility and
mh.uidmeter = m.uidmeter and
m.uidmeterclass = mc.uidmeterclass and
(billingcy is null or p.UIDBILLINGCYCLE = billingcy )
AND
(
(
(freqLettura = 'G') AND ( mh.corrmeterid is not null and mh.stoptime is null and mc.maxflowmeter >= SAM_FN_GET_PARAMETER_FLOAT('MAXFLOWMET_DETT_GIORN'))
)
OR
(
nvl(freqLettura,'nullo') <> 'G' AND (freqLettura is null or sam.readfrequency = freqLettura)
)
) and ROWNUM = 1 order by sam.stoptime, sam.uidsamtbpdr desc ) loop
begin
insert into ztmp_sam_tb_elab_pdr (facilityid, uidbatchrequest) VALUES (rec.a, rec.b);
exception
when dup_val_on_index then
null;
end;
end loop;
end;
Whenever you get an Oracle error message you don't understand, the first thing to do is look up the meaning. One way is simply to Google it. In this case the full description found in
Oracle9i Database Error Messages is:
ORA-01791 not a SELECTed expression
Cause: There is an incorrect ORDER
BY item. The query is a SELECT DISTINCT query with an ORDER BY clause.
In this context, all ORDER BY items must be constants, SELECT list
expressions, or expressions whose operands are constants or SELECT
list expressions.
Action: Remove the inappropriate ORDER BY item from the SELECT list
and retry the statement.
(Oddly this error message isn't documented in the 10G or 11G manuals, despite still being raised!)
This matches the statement you have written, which is a SELECT DISTINCT query where you are trying to order the results by a column that you did not select.
If you think about it, what you are asking for doesn't make sense: by selecting DISTINCT values that do not include sam.stoptime (for example) you may be consolidating many rows with different values for sam.stoptime, so which one would govern the ordering?
Also, as Noel's answer points out, there is no reason to have an ORDER BY clause in this code anyway, so the solution is simply to remove it.
If you are using DISTINCT in your SELECT query, then your ORDER BY clause should contain only those columns that your selecting. In this case sam.stoptime, sam.uidsamtbpdr are not there in SELECT statement. You can remove the ORDER BY clause, as it is not doing anything useful in your example.

Oracle cursor with variables help needed

I am trying to do a cursor which does something like below, struggling with different approaches with no results. Seems, I won't be able to do it by myself, and decided to ask you for help.
Below code shows what I want to achieve rather than ready approach. Please help.
I dont know it it matters but note, that I need to update CUSTOMERS in loop. I also need to select some data from another table referencing customer in this loop, then insert something to third table and update customer table.
DECLARE
CURSOR MY_CURSOR
IS
SELECT CUSTOMERID FROM CUSTOMERS WHERE ACTIVE = 1 ;
MY_RECORD MY_CURSOR%ROWTYPE;
BEGIN
FOR MY_RECORD IN MY_CURSOR
LOOP
DECLARE TEMPORARY_TABLE TABLE (A DATE, B NUMBER, C VARCHAR)
INSERT INTO #TEMPORARY_TABLE(A,B,C) (SELECT CREATEDDATE, ID, NAME FROM ACCOUNT WHERE CUSTOMER = MY_RECORD.CUSTOMERID)
INSERT INTO SOME_EVENT_TABLE(ID, NAME, DATE, ACCOUNT_ID) VALUE (some_seq.NEXTVAL, #TEMPORARY_TABLE[C], #TEMPORARY_TABLE[A], #TEMPORARY_TABLE[B])
UPDATE CUSTOMERS SET LAST_ACCOUNT_CHECK_NAME=#TEMPORARY_TABLE(C), LAST_INSERTED_EVENT_ID = some_seq.CURRVAL WHERE ID = MY_RECORD.CUSTOMERID
END LOOP;
COMMIT;
END;
First, you can't declare a temporary table in Oracle like you do in SQL Server. However, you really don't need it here anyway.
Something like this should work:
FOR MY_RECORD IN MY_CURSOR LOOP
FOR R IN (SELECT CREATEDDATE, ID, NAME
FROM ACCOUNT WHERE CUSTOMER = MY_RECORD.CUSTOMERID) LOOP
INSERT INTO some_event_table(ID, NAME, DATE, ACCOUNT_ID)
VALUES (some_seq.NEXTVAL, R.NAME, R.CREATEDATE, R.ID);
UPDATE customers
SET last_account_check_name = R.name
, last_inserted_event_id = some_seq.CURRVAL
WHERE id = MY_RECORD.CUSTOMER_ID;
END LOOP;
END LOOP;
COMMIT;
Row by row actions in SQL are terribly inefficient. You will get vastly better performance if you do this in a set-based way.
INSERT INTO some_event_table(ID, NAME, DATE, ACCOUNT_ID)
SELECT some_seq.NEXTVAL, a.name, a.createdate, a.id
FROM ACCOUNT a
INNER JOIN customers c ON c.customerid = a.customerid
WHERE c.active = 1;
UPDATE customers
SET last_account_check_name =
( SELECT a.name FROM account a WHERE a.customerid = c.customerid ),
last_inserted_event_id = some_seq.CURRVAL
WHERE c.active = 1;
There may be concurrency issues with that (what happens if customers is updated between the two statements?), but that might be good enough for your needs.

Resources