Oracle: Declare multiple constants and use them in WHERE clause - oracle

I perform daily extractions from the same table. The only thing that ever changes is the range of a column called SAMPLE_ID, so I would like to assign a fixed value to two variables: id_min and id_max so that I can update their values before running the query.
I tried the following:
DECLARE
id_min CONSTANT INTEGER := 17778;
id_max CONSTANT INTEGER := 17803;
BEGIN
SELECT
*
FROM
MDB.SCORES
WHERE
SAMPLE_ID BETWEEN id_min AND id_max;
END;
I expected this code to use the numbers assigned to id_min and id_max as the lower and upper bounds of the BETWEEN function in the WHERE clause, but I'm getting this message:
ORA-06550: line 6, column 5:
PLS-00428: an INTO clause is expected in this SELECT statement
It seems someone ran into something similar in this thread, but I do not understand the solutions provided.

Since you are running a PL/SQL code, you cannot just run a SELECT query without doing something with it.
There are some ways on dealing with select queries in PL/SQL.
1) INTO clause, where you take the row returned from the query and assign them to variables. *Notice is said row, not rows. If your query returns more then one row you cannot use INTO clause since it will generate TO_MANY_ROWS exception.
DECLARE
id_min CONSTANT INTEGER := 17778;
id_max CONSTANT INTEGER := 17803;
variable1 your_variable_type;
variable2 your_variable_type;
variable3 your_variable_type;
BEGIN
SELECT
*
INTO
variable1,
variable2,
variable3
FROM
MDB.SCORES
WHERE
SAMPLE_ID BETWEEN id_min AND id_max;
---- DO SOMETHING WITH YOUR VARIABLES ----
END;
2) FOR loop if you have one or many rows.
DECLARE
id_min CONSTANT INTEGER := 17778;
id_max CONSTANT INTEGER := 17803;
BEGIN
FOR c IN (
SELECT
*
FROM
MDB.SCORES
WHERE
SAMPLE_ID BETWEEN id_min AND id_max
) LOOP
---- DO SOMETHING WITH YOUR VARIABLES ----
---- ACCESS A COLUMN ----
c.column_name;
END LOOP;
END;
3) CURSOR declaring.
DECLARE
id_min CONSTANT INTEGER := 17778;
id_max CONSTANT INTEGER := 17803;
v_cursor SYS_REFCURSOR;
your_variables
...
BEGIN
OPEN v_cursor FOR
SELECT
*
FROM
MDB.SCORES
WHERE
SAMPLE_ID BETWEEN id_min AND id_max;
LOOP
FETCH v_cursor INTO your_variables;
EXIT WHEN v_cursor%NOTFOUND;
---- DO SOMETHING WITH YOUR VARIABLES ----
END LOOP;
END;

Once you are between BEGIN and END you are no longer in SQL, but in PLSQL.
In PL/SQL there is no just select statement, there is a "select ... INTO ..." statement instead.
In other words, the database wants from you a variable to save the results to.
Or you might use a loop in order to operate with data.
Below is the loop example to print the data from some_column out
DECLARE
id_min CONSTANT INTEGER := 17778;
id_max CONSTANT INTEGER := 17803;
BEGIN
for score in (SELECT *
FROM MDB.SCORES
WHERE SAMPLE_ID BETWEEN id_min AND id_max) loop
dbms_output.put_line(sc.some_column_name);
end loop;
END;

You might find it simpler to use bind variables, and not run your query in PL/SQL at all.
var id_min NUMBER
var id_max NUMBER
begin
:id_min := 17778;
:id_max := 17803;
end;
/
SELECT
*
FROM
MDB.SCORES
WHERE
SAMPLE_ID BETWEEN :id_min AND :id_max;

I did not realize DECLARE would require PL/SQL. I found it easier to do the following using DEFINE.
DEFINE id_min = 17778
DEFINE id_max = 17803;
SELECT
*
FROM
MDB.SCORES
WHERE
SAMPLE_ID BETWEEN &id_min AND &id_max;

Related

06533. 00000 - "Subscript beyond count"

I have this plsql block that populates a table with information of two other tables and I have to use a variable array:
DECLARE nombre_grupo VARCHAR2(15);
direccion_tipo direccion;
persona_tipo persona;
personas_array personas := personas();
CURSOR departamento IS
SELECT * FROM departamentos;
CURSOR empleado IS
SELECT * FROM empleados, departamentos
WHERE empleados.dept_no = departamentos.dept_no;
i INTEGER;
BEGIN
FOR departamento IN (SELECT * FROM departamentos) LOOP
nombre_grupo := departamento.dnombre;
i := 1;
personas_array := personas();
FOR empleado IN (SELECT * FROM empleados WHERE dept_no = departamento.dept_no) LOOP
direccion_tipo := DIRECCION(departamento.loc, 'NULL', empleado.dir);
personas_array(i) := PERSONA(empleado.emp_no, empleado.apellido,
direccion_tipo, empleado.fecha_alt);
i := i + 1;
END LOOP;
INSERT INTO grupos VALUES (nombre_grupo, personas_array);
END LOOP;
END;
Here's the type personas:
CREATE OR REPLACE TYPE personas AS VARRAY(5) OF PERSONA
So when I execute that block and it reaches the personas_array(i) bit, it exits the execution with "subscript beyond count" error, no matter what value of i. What am I missing?
I've already deleted and created the type personas again, I've also tried creating the type inside the procedure, but it can't insert into the table
A few tips for a SQL beginner:
Don't learn 30 years old Oracle join syntax. Use modern ANSI join syntax, i.e.
SELECT *
FROM empleados
JOIN departamentos ON empleados.dept_no = departamentos.dept_no;
Your cursors are redundant. Either use
DECLARE
CURSOR cur_departamento IS
SELECT *
FROM departamentos;
BEGIN
FOR departamento IN cur_departamento LOOP
...
END LOOP;
END;
or
DECLARE
BEGIN
FOR departamento IN (SELECT * FROM departamentos) LOOP
...
END LOOP;
END;
You can also use this:
DECLARE
CURSOR cur_empleados(d IN EMPLEADOS.DEPT_NO%TYPE) IS
SELECT *
FROM EMPLEADOS
WHERE dept_no = d;
/*
-- Do not use this!
CURSOR cur_empleados(dept_no IN EMPLEADOS.DEPT_NO%TYPE) IS
SELECT *
FROM EMPLEADOS
WHERE EMPLEADOS.dept_no = dept_no; -> will return all rows
*/
BEGIN
FOR departamento IN (SELECT * FROM departamentos) LOOP
FOR empleado IN cur_empleados(departamento.dept_no) LOOP
...
END LOOP;
END LOOP;
END;
According to my feelings, VARRAYs are often part of student material but hardly used in real life.
Using string 'NULL' is most likely not want you want. Use literal NULL, i.e.
DIRECCION(departamento.loc, NULL, empleado.dir)
Type VARRAY(5) OF PERSONA defines a varray with maximum size of 5 elements. When you initialize it with personas_array := personas(); then the actual size is 0. You need to extend the varray.
You code may look like this:
DECLARE
nombre_grupo VARCHAR2(15);
direccion_tipo direccion;
persona_tipo persona;
personas_array personas;
i INTEGER;
BEGIN
FOR departamento IN (SELECT * FROM departamentos) LOOP
nombre_grupo := departamento.dnombre;
i := 1;
personas_array := personas();
FOR empleado IN (SELECT * FROM empleados WHERE dept_no = departamento.dept_no AND ROWNUM <= 5) LOOP
direccion_tipo := DIRECCION(departamento.loc, NULL, empleado.dir);
personas_array.extend();
personas_array(i) := PERSONA(empleado.emp_no, empleado.apellido, direccion_tipo, empleado.fecha_alt);
i := i + 1;
END LOOP;
INSERT INTO grupos VALUES (nombre_grupo, personas_array);
END LOOP;
END;
Just a note, such procedure would have rather low performance. The professional way of doing it would be a Nested Table and then insert the data with a single command:
CREATE OR REPLACE TYPE personas_NT AS TABLE OF PERSONA;
INSERT INTO grupos VALUES (nombre_grupo, personas_array)
SELECT dnombre,
CAST(MULTISET(
SELECT
emp_no,
apellido,
DIRECCION(dept.loc, NULL, dir),
fecha_alt
FROM EMPLEADOS
WHERE dept_no = dept.dept_no
) AS personas_NT) AS personas_array
FROM DEPARTAMENTOS dept;
But maybe, that would be a chapter in the "advanced" SQL course.

PLSQL Get Variable By Loop Index

I am trying to reach columns by loop index of a cursor record. Currently i am using multiple variables for each columns thus I think that I should refactor my codes. In java, I'd use reflection to do that but I stuck in PLSQL. Code will explain better
input is passed into from loop.
i_member_awd_rowtype in member_awd%rowtype
.
.
.
--many more inputs
l_step1_company := i_member_awd_rowtype.flt_company;
l_step1_subclass := i_member_awd_rowtype.class_code;
l_step2_company := i_member_awd_rowtype.ret_flt_company;
l_step2_subclass := i_member_awd_rowtype.ret_class_code;
l_step3_company := i_member_awd_rowtype.step3_company;
l_step3_subclass := i_member_awd_rowtype.step3_class_code;
l_step4_company := i_member_awd_rowtype.step4_company;
l_step4_subclass := i_member_awd_rowtype.step4_class_code;
l_step5_company := i_member_awd_rowtype.step5_company;
l_step5_subclass := i_member_awd_rowtype.step5_class_code;
l_step6_company := i_member_awd_rowtype.step6_company;
l_step6_subclass := i_member_awd_rowtype.step6_class_code;
l_step7_company := i_member_awd_rowtype.step7_company;
l_step7_subclass := i_member_awd_rowtype.step7_class_code;
l_step8_company := i_member_awd_rowtype.step8_company;
l_step8_subclass := i_member_awd_rowtype.step8_class_code;
I need to assign single class, company and subclass variables in a loop such as
/*for i in 1 .. 8 loop
l_step_subclass :=
end loop;*/
Is there any way to write as I wanted in PLSQL? Thanks.
The following code block containing a Dynamic SQL statement might be used, assuming you wanna gather the records for each identity column(id) of the table :
SQL> set serveroutput on
SQL> declare
v_tbl varchar2(31) := 'member_awd';
v_sql varchar2(250);
v_id member_awd.id%type := :p_id;
type typ is table of varchar2(32767) index by binary_integer;
l_step_subclass typ;
begin
for c in ( select column_id, column_name
from user_tab_columns
where table_name = upper(v_tbl) order by column_id )
loop
v_sql := 'select '||c.column_name||' from '||v_tbl||' where id = :i_id ';
execute immediate v_sql into l_step_subclass(c.column_id) using v_id;
dbms_output.put_line(l_step_subclass(c.column_id));
end loop;
end;
/
where the variable l_step_subclass is converted to the array type. This way, all values for each individual column are assigned to this column local variable.

Pass an array to bind variables

I have a table that contains queries, for example:
select text from queries;
TEXT
1 select item from items where item_no between :low_item_no and :high_item_no and description <> :irellevant
The queries already contains the place holders for the bind variables.
The values themselves exists in variables table:
select * from vars;
ID NAME VALUE
1 1 low_item_no 100
2 2 high_item_no 300
3 3 irellevant_desc old
I have a package that takes the query and execute it with
execute immediate statement
but how do I bind those variables?
I don't know how much variables I have in such query, it's not static.
I wish to have a way to do something like that:
Execute immedaite my_query_str using v_array_of_vars;
Until now I don't know of a way to do something like that, only with list of variables for example:
Execute immedaite my_query_str using v_1, v_2, v_3;
Thanks!
I don't think you can do this with execute immediate as too much is unknown at compile time, so you'll have to use the dbms_sql package instead.
Here's a quick demo that gets the query and variables based on a common query ID. This assumes that the values in vars.name actually match the bind variable names in queries.text, and I haven't included any checks or error handling for that or other potential issues, or dealt with multiple select-list items or data types - just the basics:
declare
my_query_str queries.text%type;
my_cursor pls_integer;
my_result pls_integer;
my_col_descs dbms_sql.desc_tab2;
my_num_cols pls_integer;
my_item items.item%type;
begin
select text into my_query_str from queries where query_id = 42;
dbms_output.put_line(my_query_str);
-- open cursor
my_cursor := dbms_sql.open_cursor;
-- parse this query
dbms_sql.parse(my_cursor, my_query_str, dbms_sql.native);
-- bind all variables by name; assumes bind variables match vars.name
for r in (select name, value from vars where query_id = 42) loop
dbms_output.put_line('Binding ' || r.name || ' || with <' || r.value ||'>');
dbms_sql.bind_variable(my_cursor, r.name, r.value);
end loop;
my_result := dbms_sql.execute(my_cursor);
dbms_output.put_line('execute got: ' || my_result);
dbms_sql.describe_columns2(my_cursor, my_num_cols, my_col_descs);
dbms_sql.define_column(my_cursor, 1, my_item, 30); -- whatever size matches 'item'
-- fetch and do something with the results
while true loop
my_result := dbms_sql.fetch_rows(my_cursor);
if my_result <= 0 then
exit;
end if;
dbms_sql.column_value(my_cursor, 1, my_item);
dbms_output.put_line('Got item: ' || my_item);
end loop;
dbms_sql.close_cursor(my_cursor);
end;
/
You don't seem to really need an array; but if you wanted to you could create and populate an associative array as name/value pairs and then use that fir the binds.
This is just a starting point; you may have to deal with an unknown number and/or types of columns being returned, though if that's the case processing them meaningfully will be a challenge. Perhaps you need to return the result of the query as a ref cursor, which is even simpler; demo using the SQL*Plus variable and print commands:
var rc refcursor;
declare
my_query_str queries.text%type;
my_cursor pls_integer;
my_result pls_integer;
begin
select text into my_query_str from queries where query_id = 42;
dbms_output.put_line(my_query_str);
-- open cursor
my_cursor := dbms_sql.open_cursor;
-- parse this query
dbms_sql.parse(my_cursor, my_query_str, dbms_sql.native);
-- bind all variables by name; assumes bind variables match vars.name
for r in (select name, value from vars where query_id = 42) loop
dbms_output.put_line('Binding ' || r.name || ' || with <' || r.value ||'>');
dbms_sql.bind_variable(my_cursor, r.name, r.value);
end loop;
my_result := dbms_sql.execute(my_cursor);
dbms_output.put_line('execute got: ' || my_result);
:rc := dbms_sql.to_refcursor(my_cursor);
end;
/
print rc
Notice you don't close the cursor inside the PL/SQL block in this scenario.
You could also convert to a ref cursor and then fetch from that within your procedure - there's a bulk-collect example in the docs - but again you'd need to know the number and types of the select-list items to do that.

Oracle PL/SQL dynamic if statement global vars

I'm having trouble with dynamic sql, Issue is (I think) reading and setting global variable. Here's what I have and any help at all is greatly appreciated. Please let me know if you need table data too although I have included the data in comments.
CREATE OR REPLACE PACKAGE data_load
IS
curr_rec NUMBER;
curr_rule VARCHAR2(200);
curr_sql VARCHAR2(4000);
curr_sql_two VARCHAR2(4000);
curr_data_element VARCHAR2 (200);
curr_rule_text VARCHAR2(200);
curr_error_code VARCHAR2(10);
curr_error_flag VARCHAR2(10);
curr_flag_val NUMBER;
v_check NUMBER;
v_ID NUMBER;
cur_hdl INT ;
rows_processed NUMBER;
PROCEDURE check_rules;
END data_load;
The package body:
create or replace PACKAGE BODY data_load IS
PROCEDURE check_rules IS
CURSOR c1
IS
SELECT * FROM STAGING_TABLE where rownum < 3;
CURSOR c2
IS
SELECT * FROM ERROR_CODES WHERE rule_text IS NOT NULL AND status =1;
BEGIN
FOR rec1 IN c1
LOOP
FOR rec2 IN c2
LOOP
curr_data_element := 'rec1.'||rec2.data_element; --- this results in value "rec1.SHIP_FROM_ACCOUNT_ORG_CODE" without quotes
curr_rule_text := rec2.rule_text; --- this value is "is not null" without quotes
curr_error_flag := rec2.error_flag; --this value is "FLAG_03" without quotes
curr_flag_val := to_number(rec2.error_code); --- this value is 31
curr_sql :='begin if :curr_data_element '||curr_rule_text||' then update table_with_column_FLAG_03 set '||curr_error_flag ||' = 0; else update table_with_column_FLAG_03 set '||curr_error_flag ||' = '||curr_flag_val||'; end if; end;';
dbms_output.put_line(curr_sql); -- results in "begin if :curr_data_element is null then update table_with_column_FLAG_03 set FLAG_03 = 0; else update table_with_column_FLAG_03 set FLAG_03 = 31; end if; end;"
EXECUTE IMMEDIATE curr_sql USING curr_data_element ; -- this always updates the column with 31 even when curr_data_element/ rec1.SHIP_FROM_ACCOUNT_ORG_CODE is null and that's the problem
COMMIT;
END LOOP;
curr_rec := curr_rec+1;
END LOOP;
dbms_output.put_line(curr_rec);
END check_rules;
END data_load;
You've already highlighted the problem really:
curr_data_element := 'rec1.'||rec2.data_element; --- this results in value "rec1.SHIP_FROM_ACCOUNT_ORG_CODE" without quotes
You can't refer to cursor columns dynamically. You are creating a string with value 'rec1.SHIP_FROM_ACCOUNT_ORG_CODE'; there is no mechanism to evaluate what that represents. You can't, for instance, try to dynamically select that from dual because the rec1 is not in scope for a SQL call, even dynamically.
When you bind that string value it is never going to be null. You are using that string, not the value in the outer cursor that it represents, and essentially you cannot do that.
The simplest way to deal with this, if you have a reasonably small number of columns in your staging table that might appear as the rec2.data_element value, is to use a case expression to assign the appropriate actual rec1 column value to the curr_data_element variable, based on the rec2.data_element value:
...
BEGIN
FOR rec1 IN c1
LOOP
FOR rec2 IN c2
LOOP
curr_data_element :=
case rec2.data_element
when 'SHIP_FROM_ACCOUNT_ORG_CODE' then rec1.SHIP_FROM_ACCOUNT_ORG_CODE
when 'ANOTHER_COLUMN' then rec1.ANOTHER_COLUMN
-- when ... -- repeat for all possible columns
end;
curr_rule_text := rec2.rule_text;
...
If you have a lot of columns you could potentially do that via a collection but it may not be worth the extra effort.
The curr_sql string stays the same, all that's changing is that you're binding the actual value from the relevant rec1 column, rather than never-null string you were forming.

Error when comparing values in Oracle

I write simple procedure.
DECLARE
connection_id LINE.CONNECTION_ID%TYPE := 11009;
tmp_integer INTEGER;
BEGIN
SELECT COUNT(*) INTO tmp_integer FROM LINE WHERE LINE.CONNECTION_ID = 11009;
DBMS_OUTPUT.PUT_LINE(connection_id);
DBMS_OUTPUT.PUT_LINE(tmp_integer);
END;
Result of the launch:
11009
3
It is good result. I have only 3 rows where CONNECTION_ID is 11009.
After modification:
DECLARE
connection_id LINE.CONNECTION_ID%TYPE := 11009;
tmp_integer INTEGER;
BEGIN
SELECT COUNT(*) INTO tmp_integer FROM LINE WHERE LINE.CONNECTION_ID = connection_id;
DBMS_OUTPUT.PUT_LINE(connection_id);
DBMS_OUTPUT.PUT_LINE(tmp_integer);
END;
But in this case I gain strange result:
11009
30997
Where is the mistake?
Try changing the name of your PL/SQL variable to be different from the column name in the table, e.g. v_connection_id.

Resources