PL/SQL rewrite concatenated query with 'IN' clause - oracle

Currently I have in my pl/sql code following statements:
-- vList looks like '1,2,3,4'
vStatement := 'SELECT NAME FROM T_USER WHERE ID IN ( ' || vList || ' ) ';
Execute Immediate vStatement BULK COLLECT INTO tNames;
I think that concatenating of query if bad practice, so I want to make this query without using stings. What is the way to rewrite this ?
P.S. maybe people here can point out why concatenation of queries is bad, because i don't have enough reasons to prove that this style is bad.

my guess is that you took some steps previously to get vList id's into a delimited string (you don't say how vList was populated ). Why not keep as one query?
begin
...
select name
bulk collect into tNames
from t_user
where id in (select id from some_table where ...);
...
Context switching when run many times can be painful, but to me the worst part is that you are blindly accepting parameter input to be a list of numbers, when it could be anything really. It could (innocently) be '1,2,X', and you'll get a runtime error "invalid number". Or worse, it could be a SQL injection attack. Its bad practice in general (dynamic sql does have its place), but definitely NOT how you're using it.
Try something like this:
create or replace type t_num_tab as table of number;
create or replace procedure test_proc(i_list in t_num_tab) as
type t_name_tab is table of varchar2(100);
l_names t_name_tab;
begin
-- get names
select name
bulk collect into l_names
from user_table
where id in (select * from table(i_list));
-- do something with l_names
dbms_output.put_line('Name count: ' || l_names.count);
end;
You can create an object type if you need something more complicated than a list of numbers.

It's not just that concatenation is slow. It's that dynamic queries in plsql are REALLY slow. Here's a good writeup of both the how and why to do this:
Ask Tom: How can I do a variable "in list"

Related

How to print a field type on oracle without a `SELECT`

Simple question:
How to print a field's type?
DESC TABLE_FOO.FIELD_FOO;
Results printing the whole table description.
How to print a specific field details with a command, i.e, without a SELECT?
There is no built-in way to do this, at least in any of the clients I've used. As mentioned in comments, describe is a client wrapper around a data dictionary query, and each client can implement it differently - SQL*Plus and SQL Developer seem to be slightly different, and no client is actually required to support this command at all.
Just for fun, if you really wanted to you could create a procedure to figure out and format the data type the same as desc, something like:
create or replace procedure col_data_type (p_table_name varchar2,
p_column_name varchar2)
as
l_data_type varchar2(30);
begin
select data_type
|| case when data_type = 'VARCHAR2' then '(' || data_length || ')' end
|| case when data_type = 'NUMBER' and data_precision is not null then
'(' || data_precision
|| case when data_scale is not null and data_scale > 0 then
',' || data_scale end
|| ')' end
into l_data_type
from user_tab_columns
where table_name = p_table_name
and column_name = p_column_name;
dbms_output.put_line(l_data_type);
end col_data_type;
/
Perhaps with more special formatting for other data types, but those are the obvious ones. You can then call that with execute. With a dummy table:
create table t42(i integer, n1 number, n2 number(10), n3 number(10,5),
v varchar2(10), c clob)
Then:
set serveroutput on
exec col_data_type('T42','I');
NUMBER
exec col_data_type('T42','N1');
NUMBER
exec col_data_type('T42','N2');
NUMBER(10)
exec col_data_type('T42','N3');
NUMBER(10,5)
exec col_data_type('T42','V');
VARCHAR2(10)
exec col_data_type('T42','C');
CLOB
Not entirely sure how useful that might be, or why you want to be able to do this at all. Also notice that it requires the client to be retrieving and displaying dbms_output buffer. You could make it a function instead, which puts you back to using a select, albeit a shorter one...
I don't believe it is possible to accomplish this without using a SELECT statement.
DESC OWNER.TABLE_NAME; is basically just running a query like this anyways (although not exactly the same, that depends on your client):
SELECT *
FROM ALL_TAB_COLS
WHERE OWNER = &theOwner
AND TABLE_NAME = &theTable;
If you want to only return a single column, you can do this:
SELECT *
FROM ALL_TAB_COLS
WHERE OWNER = &theOwner
AND TABLE_NAME = &theTable
AND COLUMN_NAME = &theColumn;
As #AlexPoole suggests, you could work around this by writing your own custom PROCEDURE or FUNCTION to return exactly what you need, but I believe the answer to the question "is there a built in command other than SELECT that does exactly what you need" is no, there is not.
I don't think you can do this without using a SELECT. You may be able to come up with some way to route the output of DESC to a file and then parse the file out to get what you want, but honestly - SELECT is going to be much easier.
Relational databases store the description of what they store in the database, where said description can be obtained in the same manner as any other information in the database, i.e. by using a SELECT to read it. The actual tables which store this are somewhat difficult to interpret, but happily Oracle has taken pity on us poor users and provided views which present this info in an easy-to-read manner. To get the type of a field you want to do a select one of the *_TAB_COLS views, where * is either USER, ALL, or DBA. The particular columns of interest are likely going to be COLUMN_NAME, DATA_TYPE, and DATA_LENGTH.
Share and enjoy.

100 strings in IN operator, oracle pl/sql

I am passing 100 table_names in the IN operator as strings, but I am getting numeric overflow error due to too many operands.
Is there a way where I can use something else besides IN ?
set serveroutput on
DECLARE
...
BEGIN
FOR r IN
(
SELECT table_name, column_name
FROM all_tab_columns
WHERE table_name IN (...100 strings)
)
AND data_type = 'NUMBER'
ORDER BY table_name, column_id
)
LOOP
execute immediate 'SELECT COUNT("' || r.column_name || '")
,COUNT(nvl2("' || r.column_name || '", NULL, 1))
FROM "' || r.table_name || '"'
INTO not_null_count, null_count;
DBMS_OUTPUT.PUT_LINE(..)
Note: For variables I am using PLS_Integer.
The suggested action for ORA-01426 is "reduce the operands". This doesn't mean reduce the number of operands. It means you're trying to put too large a number into a variable. So shrink the number, or enlarge the variable.
You say:
"for variables I am using PLS_Integer"
So, if you have a large table, and by large I mean more than 2,147,483,647 rows, you will get a numeric overflow. Because PLS_INTEGER is a 32-bit data type.
If this is your scenario then you need to declare your variables of data type INTEGER instead (or NUMBER(38,0)).
As #BobJarvis points out, PLS_INTEGER is optimized for hardware arithmetic. So the general advice would be to use it for counting type operations. However, your case simply requires a variable to hold the output of a SQL count() operation, so I don't think there will be any difference in efficiency.
I believe the limit on 'IN' clause is 1000 strings and not 100 strings. To debug:
a.) Try running your implicit cursor query in SQL.
b.) If it works fine then run the query in execute immediate after substituting the column name.
Also , try increasing the size of your not_null_count and null_count variables.
Hope it Helps
Vishad
some other possible solutions
use a temp table - populate it with the table names to filter join to it.
create a global array type
create type table_of_varchar2 is table of varchar2(30)
populate the array and filter using table_name member of arr_tables_list
Is there a way where I can use something else besides IN ?
Consider using a cursor instead.

Oracle PL/SQL - Bulk Collection usage into WHERE and FROM clause inside conditions and inner views

I have a strange problem using bulk collection as element of FROM clause.
When I execute this code example, I get, just at run-time, the error "invalid table name".
If I replace the collection with a table everything works well.
Is there any restriction about bulk collection that I'm missing?
Maybe I cannot use anonymous block in FROM clause?
In the sql debugger I see that l_vol(i) has values but l_vol(i).FIELD doesn't exists.
Thanks.
TYPE t_bulk_vol is table of vol%ROWTYPE;
l_vol t_bulk_vol;
...
cursor cur is SELECT * FROM vol where ... ;
OPEN CUR;
LOOP
FETCH CUR BULK COLLECT INTO l_vol;
....
insert into dest
select col1, col2, ... from
(inner view with some pivot, unpivot and l_vol(i).FIELD ...) src where l_vol(i).FIELD = src.FIELD;
PS: I cannot paste original code.
TYPE t_bulk_vol is a PL/SQL type. That means you can only use it in PL/SQL constructs. You cannot use it in SQL, even if it's SQL in a PL/SQL program.
If you want to use a nested table in the FROM clause of a SELECT you will need to define a SQL TYPE. This is a pain, because it means you can't use the %ROWTYPE definition (that's a PL/SQL only keyword). So you'll have to create an object whose signature matches the projection of the table, and then create nested table of that type. Find out more.
Your cursor is defined wrongly. It should just be a SELECT statement.
cursor cur is SELECT * FROM vol where ... ;
Save the BULK COLLECT INTO l_vol for the actual fetch.
Although presumably this is just a artefact of you faking some PL/SQL because you "cannot paste original code."
I have created the type as you said, but I get the same error at the same point (ORA-00903 - invalid table name).
This is an example of what I've done:
CREATE TYPE REC_VOL AS OBJECT (
FIELD1 VARCHAR2(25),
...
);
create TYPE T_BULK IS TABLE OF REC_VOL;
....
l_vol t_bulk;
...
This is the way I collect the records (I don't use the cursor anymore):
SELECT REC_VOL(FIELD1, ...) BULK COLLECT INTO l_vol
FROM vol where ...;
The exception is still raised at the insert-select statement.

Can a table variable be used in a select statement where clause?

I have a stored procedure that is doing a two-step query. The first step is to gather a list of VARCHAR2 type characters from a table and collect them into a table variable, defined like this:
TYPE t_cids IS TABLE OF VARCHAR2(50) INDEX BY PLS_INTEGER;
v_cids t_cids;
So basically I have:
SELECT item BULK COLLECT INTO v_cids FROM table_one;
This works fine up until the next bit.
Now I want to use that collection in the where clause of another query within the same procedure, like so:
SELECT * FROM table_two WHERE cid IN v_cids;
Is there a way to do this? I am able to select an individual element, but I would like to use the table variable like a would use a regular table. I've tried variations using nested selects, but that doesn't seem to work either.
Thanks a lot,
Zach
You have several choices as to how you achieve this.
If you want to use a collection, then you can use the TABLE function to select from it but the type of collection you use becomes important.
for a brief example, this creates a database type that is a table of numbers:
CREATE TYPE number_tab AS TABLE OF NUMBER
/
Type created.
The next block then populates the collection and performs a rudimentary select from it using it as a table and joining it to the EMP table (with some output so you can see what's happening):
DECLARE
-- Create a variable and initialise it
v_num_tab number_tab := number_tab();
--
-- This is a collection for showing the output
TYPE v_emp_tabtype IS TABLE OF emp%ROWTYPE
INDEX BY PLS_INTEGER;
v_emp_tab v_emp_tabtype;
BEGIN
-- Populate the number_tab collection
v_num_tab.extend(2);
v_num_tab(1) := 7788;
v_num_tab(2) := 7902;
--
-- Show output to prove it is populated
FOR i IN 1 .. v_num_tab.COUNT
LOOP
dbms_output.put_line(v_num_tab(i));
END LOOP;
--
-- Perform a select using the collection as a table
SELECT e.*
BULK COLLECT INTO v_emp_tab
FROM emp e
INNER JOIN TABLE(v_num_tab) nt
ON (e.empno = nt.column_value);
--
-- Display the select output
FOR i IN 1 .. v_emp_tab.COUNT
LOOP
dbms_output.put_line(v_emp_tab(i).empno||' is a '||v_emp_tab(i).job);
END LOOP;
END;
You can see from this that the database TYPE collection (number_tab) was treated as a table and could be used as such.
Another option would be to simply join your two tables you are selecting from in your example:
SELECT tt.*
FROM table_two tt
INNER JOIN table_one to
ON (to.item = tt.cid);
There are other ways of doing this but the first might suit your needs best.
Hope this helps.
--Doesn't work.
--SELECT item BULK COLLECT AS 'mySelectedItems' INTO v_cids FROM table_one;
SELECT table_two.*
FROM table_two INNER JOIN v_cids
ON table_two.paramname = v_cids.mySelectedItems;
Unless I'm misunderstanding the question, this should only return results that are in the table variable.
Note: I've never used Oracle, but I imagine this case would be the same.

plsql cursor via straight sql

Im looking at the following two ways of retrieving a value and storing it later via an insert statement. i.e. via Pl/SQL cursors or via direct SQL.
Is there any advantage to either approach? Or is there a more efficient approach?
Approach 1
Cursor system_date
Is
select sysdate from dual;
system_date_rec system_date%type;
Open system_Date;
Fetch system_date into system_date_rec;
Insert into table(dateValue)
values(system_date_rec.date);
Approach 2
dateString varchar(20);
Select sysdate into dateString from dual;
Insert into table(dateValue)
values(dateString);
How about approach 3:
Insert into table(dateValue)
values(sysdate);
or assuming you did actually need to do a select to get the data:
Insert into table(dateValue)
select dateValue from other_table where ...;
Regarding whether an explicit cursor or a SELECT INTO is preferable when one or the other is needed, I would go for the SELECT INTO because it is neater and safer if you expect the query to return exactly one row:
select some_value
into l_var
from other_table
where ...;
if l_var = 'A' then
do_something;
end if;
Now you will get an exception (NO_DATA_FOUND or TOO_MANY_ROWS) if the number of rows returned is not as expected. With the cursor you will just end up with l_var unchanged, or set to the value from the first matching row - which probably means yu've got a bug but don't know it.
Each approach has it's merit but if it's one and only one value you are getting then I'd go with select ... into ... as this is much simpler and will check that you have one and only one value.
Although Tony's approach is possibly preferable to both in the right circumstances.
If you also want to get the value back there is always the RETURNING clause of the insert statement.
my_date_value date;
...
INSERT into table(datevalue)
values (sysdate)
returning sysdate into my_date_value;
I'd agree with #Tony and #MikeyByCrikey that select ... into is generally preferable, not least - in my personal, subjective opinion - because it keeps the select and into together instead of having the select out of sight up in the declare section. Not really an issue if it's simple but you've suggested you're doing several big queries and manipulations, which implies a longish procedure.
Slightly off-topic, but if all the manipulations are to gather data for a single insert at the end, then rather than having lots of separate variables I'd consider declaring a single variable as a row type and updating the columns as appropriate:
declare
l_row my_table%ROWTYPE;
begin
select ... into l_row.column1;
select ... into l_row.column2;
if l_row.column2 = 'A' then
/* do something */
end if;
l_row.column3 := 'somevalue';
fetch ... into l_row.column4;
/* etc */
insert into my_table values l_row;
end;

Resources