value limitation in an IN clause Oracle - sql

I work for a company that has a DW - ETL setup. I need to write a query that looks for over 2500+ values in an WHEN - IN clause and also over 1000+ values in a WHERE - IN clause. Basically it would look like the following:
SELECT
,user_id
,CASE WHEN user_id IN ('user_n', +2500 user_[n+1] ) THEN 1
ELSE 0
,item_id
FROM user_table
WHERE item_id IN ('item_n', +1000 item_[n+1] );
As you probably already know PL/SQL allows a maximum of 1000 values in an IN clause, so I tried adding OR - IN clauses (as suggested in other stackoverflow threads):
SELECT
,user_id
,CASE WHEN user_id IN ('user_n', +999 user_[n+1] )
OR user_id IN ('user_n', +999 user_[n+1] )
OR user_id IN ('user_n', +999 user_[n+1] ) THEN 1
ELSE 0 END AS user_group
,item_id
FROM user_table
WHERE item_id IN ('item_n', +999 item_[n+1] )
OR item_id IN ('item_n', +999 item_[n+1] );
NOTE: i know the math is erroneous in the examples above, but you get the point
The problem is that queries have a maximum executing time of 120 minutes and the job is being automatically killed. So I googled what solutions I could find and it seems Temporary Tables could be the solution I'm looking for, but with all honesty none of the examples I found is clear enough on how to include the values I want in the table and also how to use this table in my original query. Not even the ORACLE documentation was of much help.
Another potential problem is that I have limited rights and I've seen other people mention that in their companies they don't have the rights to create temporary tables.
Some of the info I found in my research:
ORACLE documentation
StackOverflow thread
[StackOverflow thread 2]
Another solution I found was using tuples instead, as mentioned in THIS thread (which I haven't tried) because as another user mentions performance seems greatly affected.
Any guidance on how to use a Temporary Table or if anyone has another way of dealing with this limitation would be greatly appreciated.

Create a global temporary table so no undo logs are created
CREATE GLOBAL TEMPORARY TABLE <table_name> (
<column_name> <column_data_type>,
<column_name> <column_data_type>,
<column_name> <column_data_type>)
ON COMMIT DELETE ROWS;
then depending on how the user list arrives import the data into a holding table and then run
select 'INSERT INTO global_temporary_table <column> values '
|| holding_table.column
||';'
FROM holding_table.column;
This gives you insert statements as output which you run to insert the data.
then
SELECT <some_column>
FROM <some_table>
WHERE <some_value> IN
(SELECT <some_column> from <global_temporary_table>

Use a collection:
CREATE TYPE Ints_Table AS TABLE OF INT;
CREATE TYPE IDs_Table AS TABLE OF CHAR(5);
Something like this:
SELECT user_id,
CASE WHEN user_id MEMBER OF Ints_Table( 1, 2, 3, /* ... */ 2500 )
THEN 1
ELSE 0
END
,item_id
FROM user_table
WHERE item_id MEMBER OF IDs_table( 'ABSC2', 'DITO9', 'KMKM9', /* ... */ 'QD3R5' );
Or you can use PL/SQL to populate a collection:
VARIABLE cur REFCURSOR;
DECLARE
t_users Ints_Table;
t_items IDs_Table;
f UTL_FILE.FILE_TYPE;
line VARCHAR2(4000);
BEGIN
t_users.EXTEND( 2500 );
FOR i = 1 .. 2500 LOOP
t_users( t_users.COUNT ) := i;
END LOOP;
// load data from a file
f := UTL_FILE.FOPEN('DIRECTORY_HANDLE','datafile.txt','R');
IF UTL_FILE.IS_OPEN(f) THEN
LOOP
UTL_FILE.GET_LINE(f,line);
IF line IS NULL THEN EXIT; END IF;
t_items.EXTEND;
t_items( t_items.COUNT ) := line;
END LOOP;
OPEN :cur FOR
SELECT user_id,
CASE WHEN user_id MEMBER OF t_users
THEN 1
ELSE 0
END
,item_id
FROM user_table
WHERE item_id MEMBER OF t_items;
END;
/
PRINT cur;
Or if you are using another language to call the query then you could pass the collections as a bind value (as shown here).

In PL/SQL you could use a collection type. You could create your own like this:
create type string_table is table of varchar2(100);
Or use an existing type such as SYS.DBMS_DEBUG_VC2COLL which is a table of VARCHAR2(1000).
Now you can declare a collection of this type for each of your lists, populate it, and use it in the query - something like this:
declare
strings1 SYS.DBMS_DEBUG_VC2COLL := SYS.DBMS_DEBUG_VC2COLL();
strings2 SYS.DBMS_DEBUG_VC2COLL := SYS.DBMS_DEBUG_VC2COLL();
procedure add_string1 (p_string varchar2) is
begin
strings1.extend();
strings1(strings.count) := p_string;
end;
procedure add_string2 (p_string varchar2) is
begin
strings2.extend();
strings2(strings2.count) := p_string;
end;
begin
add_string1('1');
add_string1('2');
add_string1('3');
-- and so on...
add_string1('2500');
add_string2('1');
add_string2('2');
add_string2('3');
-- and so on...
add_string2('1400');
for r in (
select user_id
, case when user_id in table(strings2) then 1 else 0 end as indicator
, item_id
from user_table
where item_id in table(strings1)
)
loop
dbms_output.put_Line(r.user_id||' '||r.indicator);
end loop;
end;
/

You can use below example to understand Global temporary tables and the type of GTT.
CREATE GLOBAL TEMPORARY TABLE GTT_PRESERVE_ROWS (ID NUMBER) ON COMMIT PRESERVE ROWS;
INSERT INTO GTT_PRESERVE_ROWS VALUES (1);
COMMIT;
SELECT * FROM GTT_PRESERVE_ROWS;
DELETE FROM GTT_PRESERVE_ROWS;
COMMIT;
TRUNCATE TABLE GTT_PRESERVE_ROWS;
DROP TABLE GTT_PRESERVE_ROWS;--WONT WORK IF YOU DIDNOT TRUNCATE THE TABLE OR THE TABLE IS BEING USED IN SOME OTHER SESSION
CREATE GLOBAL TEMPORARY TABLE GTT_DELETE_ROWS (ID NUMBER) ON COMMIT DELETE ROWS;
INSERT INTO GTT_DELETE_ROWS VALUES (1);
SELECT * FROM GTT_DELETE_ROWS;
COMMIT;
SELECT * FROM GTT_DELETE_ROWS;
DROP TABLE GTT_DELETE_ROWS;
However as you mentioned you receive the input in an excel file so you can simply create a table and load data in that table. Once the data is loaded you can use the data in IN clause of your query.
select * from employee where empid in (select empid from temptable);

create temporary table userids (userid int);
insert into userids(...)
then a join or in subquery
select ...
where user_id in (select userid from userids);
drop temporary table userids;

Related

SQL - Call a proc or function where user enters two table names

I cannot seem to find an answer for my question or even if its possible, I want to create a function or procedure that asks the user for two table names something like Table A and Table B
call myfunction(table_a, table_b)
or
call myprocedure(table_a, table_b)
inside each table contains addresses and i've created a script to tear the address to parts and try to match them together, but i only want to produce the call function above.
each table structure would have the same structure
SELECT * FROM (SELECT KEY_A
,ADDRESS_LINE
,POSTCODE
,ROW_NUMBER() OVER(PARTITION BY KEY_A ORDER BY ADDRESS_LINE) ADDRESS_LINE_RN
FROM TABLE_NAME) A
WHERE ADDRESS_LINE_RN = 1 AND LENGTH(TRIM(ADDRESS_LINE)) > 0 AND LENGTH(TRIM(POSTCODE)) > 0
Is this even possible? I just want to keep the end user experience easy and fast.
Many Thanks
create or replace procedure select_table (name1 varchar2) is
begin
execute immediate 'select * from '||name1;
end;
Here is the code but u wont see any output u need to put the result in a variable or in a collection and then loop through collection to output the result, or u can create a pipelined table function like this:
CREATE OR REPLACE FUNCTION select_table (name1 VARCHAR2)
RETURN YOUR_OBJECT%ROWTYPE PIPELINED IS
TYPE t_sql_result IS
TABLE OF your_object%rowtype INDEX BY PLS_INTEGER;
sql_result t_sql_result;
BEGIN
EXECUTE IMMEDIATE
'SELECT * FROM (SELECT KEY_A ,ADDRESS_LINE ,POSTCODE ,ROW_NUMBER()
OVER(PARTITION BY KEY_A ORDER BY ADDRESS_LINE) ADDRESS_LINE_RN FROM '||name1||')
A WHERE ADDRESS_LINE_RN = 1 AND LENGTH(TRIM(ADDRESS_LINE)) > 0 AND LENGTH(TRIM(POSTCODE)) > 0'
BULK COLLECT INTO sql_result;
FOR I IN sql_result.FIRST..sql_result.LAST LOOP
PIPE ROW (T_sql_result(I.FIRST_COLUMN,I.SECOND_COLUMN...));
END LOOP;
RETURN;
END;
SELECT * FROM TABLE(select_table('table_name'));

How to write a procedure to display the contents of a table in sql

I have a created a procedure as
create or replace procedure availability(num in number) as
begin
delete from vehicle_count;
insert into vehicle_count from select engine_no,count(engine_no)
from vehicle
where engine_no = num
group by engine_no;
end;
/
The procedure was created successfully but now i have to write a separate query to view the contents of vehicle_count as
select * from vehicle_count;
I tried inserting the select statement into the procedure after insertion but it showed a error stating "an INTO clause is expected in the select statement".
How can I create procedure to select the required contents and display it in a single execute statement?
Table schema
vehicle(vehicle_no,engine_no,offence_count,license_status,owner_id);
vehicle_count(engine_no,engine_count);
Check this (MS SQL SERVER)-
create or alter procedure availability(#num as int) as
begin
delete from vehicle_count;
insert into vehicle_count
output inserted.engine_no,inserted.count_engine_no
select engine_no,count(engine_no) as count_engine_no
from vehicle
where engine_no=#num
group by engine_no;
end;
If you want to use a SELECT into a PL/SQL block you should use either a SELECT INTO or a loop (if you want to print more rows).
You could use something like this:
BEGIN
SELECT engine_no, engine_count
INTO v_engine, v_count
FROM vehicle_count
WHERE engine_no = num;
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_engine := NULL;
v_count := NULL;
END;
v_engine and v_count are two variables. You can declare them in your procedure, and they will contain the values you want to print.
You said that the procedure you wrote (actually, you posted here) compiled successfully. Well, sorry to inform you - that's not true. This is not a valid syntax:
insert into vehicle_count from select engine_no,count(engine_no)
----
from? Here?
Consider posting true information.
As of your question (if we suppose that that INSERT actually inserted something into a table):
at the beginning, you delete everything from the table
as SELECT counts number of rows that share the same ENGINE_NO (which is equal to the parameter NUM value), INSERT inserts none (if there's no such NUM value in the table) or maximum 1 row (because of aggregation)
therefore, if you want to display what's in the table, all you need is a single SELECT ... INTO statement whose result is displayed with a simple DBMS_OUTPUT.PUT_LINE which will be OK if you're doing it interactively (in SQL*Plus, SQL Developer, TOAD and smilar tools). Regarding table description, I'd say that ENGINE_NO should be a primary key (i.e. that not more than a single row with that ENGINE_NO value can exist in a table).
create or replace procedure availability (num in number) as
l_engine_no vehicle_count.engine_no%type;
l_engine_count vehicle_count.engine_count%type;
begin
delete from vehicle_count;
insert into vehicle_count (engine_no, engine_count)
select engine_no, count(engine_no)
from vehicle
where engine_no = num
group by engine_no;
-- This query shouldn't return TOO-MANY-ROWS if ENGINE_NO is a primary key.
-- However, it might return NO-DATA-FOUND if there's no such NUM there, so you should handle it
select engine_no, engine_count
into l_engine_no, l_engine_count
from vehicle_count
where engine_no = num;
dbms_output.put_line(l_engine_no ||': '|| l_engine_count);
exception
when no_data_found then
dbms_output.put_line('Nothing found for ENGINE_NO = ' || num);
end;
/
There are numerous alternatives to that (people who posted their answers/comments before this one mentioned some of those), and the final result you'd be satisfied with depends on where you want to display that information.

Working with lists or someting similar to DataSet

I have two tables, let's call them Table1 and Table2.
I have a variable named: role
Now I need to do a select statement from Table1 (which can return several rows):
SELECT roles INTO role FROM TABLE1
And then I need to insert the role in Table2
INSERT INTO Table2(RELATION_ID, ROLES) VALUES (maxID+1, role);
Now my problem is that, the first select statement returns more than 1 row, and so I cannot put the value in the role variable. So I need something like an ArrayList or DataSet to put the values in, since I need to do a loop over these values, and insert them in another table. How do I use a list and iterate over it in SQL (Oracle).
Below you will see my code, simplified a bit, so that it is only my issue that is in focus. Of course the code will not compile, just trying to make clear what I need. Thanks.
CREATE OR REPLACE PROCEDURE "myProcedure"(eventID IN NUMBER)
IS
roleNumber NUMBER;
maxID NUMBER;
BEGIN
SELECT roles FROM TABLE1;
SELECT MAX(RELATION_ID) INTO maxID FROM SOMEOTHERTABLE;
IF maxID IS NULL
THEN
maxID := 0;
END IF;
FOR counter IN theListThatINeed
INSERT INTO Table2(RELATION_ID, ROLES) VALUES (maxID+1, roleAtCounter);
END IF;
END "myProcedure"
In this situation I would use just a single SQL Statement. No need to context switch back and forth between the SQL engine and another language, and definitely no need to process something like this 1 row at a time!
INSERT INTO Table2(RELATION_ID, ROLES)
SELECT (SELECT NVL(MAX(RELATION_ID),0) FROM SOMEOTHERTABLE)+rownum, roles
FROM table1;
Use a collection and get your job done. See below:
CREATE OR REPLACE PROCEDURE myProcedure (eventID IN NUMBER)
IS
ID_max NUMBER;
CURSOR cur
IS
SELECT roles FROM TABLE1;
TYPE x IS TABLE OF cur%ROWTYPE
INDEX BY PLS_INTEGER;
var x;
BEGIN
OPEN CUR;
FETCH cur BULK COLLECT INTO var;
CLOSE cur;
SELECT MAX (RELATION_ID) INTO ID_max FROM SOMEOTHERTABLE;
IF ID_max IS NULL
THEN
ID_max := 0;
END IF;
FOR i IN 1 .. var.COUNT
LOOP
INSERT INTO Table2 (RELATION_ID, ROLES)
VALUES ( (ID_max + i), var(i).roles);
END LOOP;
COMMIT;
END myProcedure;
I know this is not exactly you're asking about but solution for your problem is:
create sequence seqid start with 1 increment by 1;
INSERT INTO Table2(RELATION_ID, ROLES)
select seqid.nextval, role from table1;
If some other process can also insert to table2 and you really need max(id) you can go with:
with maxid as (select max(id) mid from table2)
INSERT INTO Table2(RELATION_ID, ROLES)
select mid+rownum, role from table1, maxid;

Postgres: Dynamic creation of a temporat table

I would like to create a report based on a few variables, one of them being months in a year. I would like to create a temporary table containing only the desired periods. Here's what I came up with:
The parts I'm having trouble with are placed between << and >>
DO $$
DECLARE myPeriodFrom INTEGER := 7;
DECLARE myPeriodTo INTEGER := 9;
DECLARE myColumn VARCHAR(255) := 'Period';
BEGIN
DROP TABLE IF EXISTS tmpTable;
CREATE TABLE tmp_table (tmpId INT, tmpDesc varchar(255));
FOR i IN myPeriodFrom..myPeriodTo LOOP
-- Create dynamic column name and fill with data
<< code here >> -- myColumn := (myColumn || CAST(i AS VARCHAR));
ALTER TABLE tmpTable ADD COLUMN << myColumn >> INT;
INSERT INTO tmpTable (myId, myColumn)
SELECT "Id", "YearAdded" FROM "Item" WHERE "Item"."MonthAdded" = i;
END LOOP;
END $$;
SELECT * FROM tmpTable
With a little regret I'm afraid to conclude that what I want is not possible. There is no real workable solution to dynamicly create columns.
Second, the application I'm using the query in turns out not to support queries with dynamic output. For these reasons I had to rethink my whole approach.
For those who read it and gave a suggestion, thanks.

Oracle SQL Creating Trigger to Increment a Sequence Number For Each Row

I'm trying to create a trigger but I have learned I can not design it as in my first attempt, which I'm showing below. This will cause a 'mutating table' error due to selecting from the table as it is being modified. It actually didn't cause this error when inserting only one record at a time, but when I insert multiple records at once it does.
The purpose of the trigger is to count the number of records in the table where the customer is equal to the customer about to be inserted, and to set the new order_num value as count+1. I also have a public key value set by the trigger which draws from a sequence. This part works ok once I remove the order_num part of the trigger and the associated SELECT. How can I achieve what I am trying to do here? Thanks in advance.
CREATE OR REPLACE TRIGGER t_trg
BEFORE INSERT ON t
FOR EACH ROW
DECLARE
rec_count NUMBER(2,0);
BEGIN
SELECT COUNT(*) INTO rec_count
FROM t
WHERE customer_id = :NEW.customer_id;
:NEW.order_num:= rec_count+1;
:NEW.order_pk_id:= table_seq.NEXTVAL;
END;
Two triggers and temp table approach can provide solution to you seek, preventing mutating table error. However performance will most likely suffer.
create global temporary table cust_temp(customer_id number, cust_cnt number);
create or replace trigger t_trig1
before insert on t
declare
begin
insert into cust_temp select customer_id, count(*) from t group by customer_id;
end;
/
CREATE OR REPLACE TRIGGER t_trg2
BEFORE INSERT ON t
FOR EACH ROW
DECLARE
rec_count number;
BEGIN
BEGIN
SELECT cust_cnt INTO rec_count
FROM cust_temp
WHERE customer_id = :NEW.customer_id;
EXCEPTION when no_data_found then rec_count := 0;
END;
:NEW.order_num:= rec_count+1;
:NEW.order_pk_id:= table_seq.NEXTVAL;
update cust_temp set cust_cnt = rec_count + 1
where customer_id = :NEW.customer_id;
END;
/