How do I loop through a row while using a cursor - sql

create table ranks (
rank varchar(20)
);
create table people (
name varchar(20)
);
insert into people values('Sam', 'Bob', 'Tim');
declare cursor c1 is (select substr(name, -1) from people)
begin
for i in c1
loop
update ranks
set rank = 'S'
where i = 'S';
end loop;
end;
Hello, I am trying to use the last letter of the people table to decide who gets the S rank, but it isn't working. I keep getting - expression is of wrong type - error. Please help.

Data model looks wrong. That should be only one table with two columns.
SQL> CREATE TABLE people
2 (
3 name VARCHAR2 (20),
4 RANK VARCHAR2 (1)
5 );
Table created.
SQL> INSERT INTO people (name) VALUES ('Sam');
1 row created.
SQL> INSERT INTO people (name) VALUES ('Bob');
1 row created.
SQL> INSERT INTO people (name) VALUES ('Tim');
1 row created.
SQL> COMMIT;
Commit complete.
SQL> SELECT * FROM people;
NAME RANK
-------------------- -----
Sam
Bob
Tim
SQL>
Then, you don't need PL/SQL - a simple UPDATE will do. However, code you posted doesn't make much sense either - substr(name, -1) selects the last letter; nobody has a name that ends with an S so - no rows will ever be updated (at least, not for sample data). That's why I modified it to use the 1st letter.
SQL> UPDATE people
2 SET RANK = 'S'
3 WHERE SUBSTR (name, 1, 1) = 'S';
1 row updated.
SQL> SELECT * FROM people;
NAME R
-------------------- -
Sam S
Bob
Tim
SQL>
If it has to be PL/SQL (because you're learning it), then you'd
SQL> ROLLBACK;
Rollback complete.
SQL> BEGIN
2 FOR cur_r IN (SELECT name FROM people)
3 LOOP
4 UPDATE people
5 SET RANK = 'S'
6 WHERE name = cur_r.name
7 AND SUBSTR (name, 1, 1) = 'S';
8 END LOOP;
9 END;
10 /
PL/SQL procedure successfully completed.
SQL> SELECT * FROM people;
NAME RANK
-------------------- ----
Sam S
Bob
Tim
SQL>

insert into people values('Sam', 'Bob', 'Tim');
Will fail as you only have one column and not three. You want to either use multiple inserts:
insert into people (name) values('Sam');
insert into people (name) values('Bob');
insert into people (name) values('Tim');
Or, use an INSERT ... SELECT ...
insert into people (name)
SELECT 'Sam' FROM DUAL UNION ALL
SELECT 'Bob' FROM DUAL UNION ALL
SELECT 'Tim' FROM DUAL;
Then you want something like:
begin
for i in (select substr(name, -1) AS last_character from people)
loop
update ranks
set rank = 'S'
where i.last_character = 'S';
end loop;
end;
/
But that can be simplified to get rid of the cursor and use a single UPDATE statement:
UPDATE ranks
SET rank = 'S'
WHERE EXISTS(
SELECT 1
FROM people
WHERE name LIKE '%S'
);
But neither of those will do anything as:
The ranks table contains zero rows.
None of the people have a name ending in S.
If you fix both of those then you will just end up updating every row in the ranks table as there is no relationship between a person and a rank.
db<>fiddle here

A simplified version of using cursors is to declare the cursor and then use it
multiple times if required later. This way opening similar dataset in multiple
cursors can be avoided. A good practice to develop when working on code
intensive procedures.
SQL> Declare
cur_people is SELECT name FROM people;
BEGIN
FOR cur_r IN cur_people
LOOP
UPDATE people
SET RANK = 'S'
WHERE name = cur_r.name
AND SUBSTR (name, 1, 1) = 'S';
END LOOP;
END;
/

Related

Can I use a query by itself as a filter in pl/sql?

I know I didn't make it clear in the question title. Let me explain.
Say I have a Table SOURCE_TABLE with 1 column that looks like this:
Filter
------------------|
Name='John'
Surname = 'Smith'
Age = '25'
And I want to use this table as a filter. Like below:
SELECT * FROM TARGET_TABLE WHERE (SELECT FILTER FROM SOURCE_TABLE)
I heard maybe evaluate function could help me but to be honest I couldn't understand how.
Do you know any method that I can use a column as my filter source like above?
Edit1:
DECLARE
my_filter VARCHAR2(100);
my_query VARCHAR2(500);
BEGIN
my_query := 'SELECT FILTER FROM SOURCE_TABLE WHERE ROWNUM=1';
EXECUTE IMMEDIATE my_query INTO my_filter;
EXECUTE IMMEDIATE 'SELECT * FROM TARGET_TABLE WHERE '|| my_filter;
END;
#Sujitmohanty30 I have come up with the above after learning EXECUTE IMMEDIATE. But there is a problem I stumble upon. This needs to be dynamic regarding to the end result and i want to see the result of the select query at the end.
Let's say we have these 2 tables:
create table TARGET_TABLE(name varchar2(30), surname varchar2(30));
insert into TARGET_TABLE(name,surname) values ('John', 'Doe');
insert into TARGET_TABLE(name,surname) values ('Ann', 'Smith');
insert into TARGET_TABLE(name,surname) values ('Steven', 'Feuerstein');
insert into TARGET_TABLE(name,surname) values ('Steven', 'King');
commit;
create table SOURCE_TABLE(filter) as
select q'[name='John']' from dual union all
select q'[name='Ann']' from dual union all
select q'[name='Steven' and surname='King']' from dual union all
select q'[surname='Feuerstein']' from dual;
Then you can use xmltable and dbms_xmlgen.getXMLtype to get such rows dynamically:
https://dbfiddle.uk/?rdbms=oracle_18&fiddle=72abdf18b149cf30882cb4e1736c9c33
select *
from SOURCE_TABLE
, xmltable(
'/ROWSET/ROW'
passing dbms_xmlgen.getXMLtype('select * from TARGET_TABLE where '|| SOURCE_TABLE.filter)
columns
name varchar2(30) path 'NAME',
surname varchar2(30) path 'SURNAME'
) x;
Results:
FILTER NAME SURNAME
-------------------------------- ------------------------------ ------------------------------
name='John' John Doe
name='Ann' Ann Smith
name='Steven' and surname='King' Steven King
surname='Feuerstein' Steven Feuerstein

how to add trigger to count number of rows automatically after inserting in oracle sql developer

I want to add trigger to count number of movies after inserting!
This is the table to store the count value:
CREATE TABLE mov_count
(mcount NUMBER);
and the movie table:
create table movie
(mov_id number primary key,
mov_title varchar(20),
mov_lang varchar(20));
This is the trigger I have created:
create trigger count_movie_trg
after insert on movie
for each row
BEGIN
UPDATE mov_count
SET mcount = (SELECT COUNT(*) FROM movie);
END;
/
After creating this i tried to add movie but its showing mutating trigger/function may not see it error.
It is the FOR EACH ROW that bothers you. It is a table-level trigger, so:
Enter a dummy value for beginning (as you'll update it later):
SQL> insert into mov_count values (0);
1 row created.
Trigger:
SQL> create or replace trigger count_movie_trg
2 after insert on movie
3 begin
4 update mov_count c set
5 c.mcount = (select count(*) from movie m);
6 end;
7 /
Trigger created.
Testing:
SQL> insert into movie
2 select 1, 'Titanic' from dual union all
3 select 2, 'Superman' from dual;
2 rows created.
SQL> select count(*) from mov_count;
COUNT(*)
----------
1
SQL>
Why not just maintain the value without referring to the original table?
create trigger count_movie_trg after insert on movie for each row
begin
update mov_count set mcount = mcount + 1;
end;
To keep the count up-to-date, you'll need a delete trigger as well.
Don't use the table at all; use a view instead.
CREATE VIEW mov_count ( mcount ) AS
SELECT COUNT(*) FROM movie;
db<>fiddle

How to SQL Query Large List of Tables using variable for table names?

I have a question about running a Oracle DB query on multiple tables. Is there a way to make the table names variables to be iterated as opposed to having to state each table name?
Background Example
There are a large number of tables (ex. TABLE_1...TABLE_100).
Each of these tables are listed in the NAME column of another table (ex. TABLE_LIST) listing an even larger number of tables along with TYPE (ex. "Account")
Each of these tables has columnn VALUE a boolean column, ACTIVE.
Requirements
Query the TABLE_LIST by TYPE = 'Account'
For Each table found, query that table for all records where column ACTIVE = 'N'
Results show table NAME and VALUE from each table row where ACTIVE = 'N'.
Any tips would be appreciated.
There is a low tech and a high tech way. I'll put them in separate answers so that people can vote for them. This is the high tech version.
Set up: Same as in low tech version.
CREATE TYPE my_row AS OBJECT (name VARCHAR2(128), value NUMBER)
/
CREATE TYPE my_tab AS TABLE OF my_row
/
CREATE OR REPLACE FUNCTION my_fun RETURN my_tab PIPELINED IS
rec my_row := my_row(null, null);
cur SYS_REFCURSOR;
BEGIN
FOR t IN (SELECT name FROM table_list WHERE table_type='Account') LOOP
rec.name := dbms_assert.sql_object_name(t.name);
OPEN cur FOR 'SELECT value FROM '||t.name||' WHERE active=''N''';
LOOP
FETCH cur INTO rec.value;
EXIT WHEN cur%NOTFOUND;
PIPE ROW(rec);
END LOOP;
CLOSE cur;
END LOOP;
END my_fun;
/
SELECT * FROM TABLE(my_fun);
NAME VALUE
TABLE_1 1
TABLE_3 3
There is a low tech and a high tech way. I'll put them in separate answers so that people can vote for them. This is the low tech version.
Set up:
CREATE TABLE table_1 (value NUMBER, active VARCHAR2(1) CHECK(active IN ('Y','N')));
CREATE TABLE table_2 (value NUMBER, active VARCHAR2(1) CHECK(active IN ('Y','N')));
CREATE TABLE table_3 (value NUMBER, active VARCHAR2(1) CHECK(active IN ('Y','N')));
INSERT INTO table_1 VALUES (1, 'N');
INSERT INTO table_1 VALUES (2, 'Y');
INSERT INTO table_3 VALUES (3, 'N');
INSERT INTO table_3 VALUES (4, 'Y');
CREATE TABLE table_list (name VARCHAR2(128 BYTE) NOT NULL, table_type VARCHAR2(10));
INSERT INTO table_list (name, table_type) VALUES ('TABLE_1', 'Account');
INSERT INTO table_list (name, table_type) VALUES ('TABLE_2', 'Something');
INSERT INTO table_list (name, table_type) VALUES ('TABLE_3', 'Account');
The quick and easy way is to use a query to generate another query. I do that quite often, especially for one off jobs:
SELECT 'SELECT '''||name||''' as name, value FROM '||name||
' WHERE active=''N'' UNION ALL' as sql
FROM table_list
WHERE table_type='Account';
SELECT 'TABLE_1' as name, value FROM TABLE_1 WHERE active='N' UNION ALL
SELECT 'TABLE_3' as name, value FROM TABLE_3 WHERE active='N' UNION ALL
You'll have to remove the last UNION ALL and execute the rest of the query. The result is
NAME VALUE
TABLE_1 1
TABLE_3 3

Sorting query results

How can I properly sort the results to show the results in the following order
1st - Parent Name
2nd - Child Name
3rd - Teacher Name
4th - Other Name
Where the parent name and child name is on the same table Family
id | parent name | child name
-----------------------------
1 Denz Hanz
2 Denz Pog
3 Joann Mac
while the other names are on different tables teacher table
id | teacher name
-----------------
1 Miguel
2 Sean
and Other_guest table
id | guest name
-----------------
1 Mike
2 Mal
where in cases that the parent did not arrive, the child name will be showed. Result of the query should show something like this
Participant Name
----------------
1. Denz
2. Denz
3. Mac
4. Miguel
5. Sean
6. Mal
7. Mike
I tried using order by field(),order by field asc, field2 dec ... etc but it seems not the result we wanted.
Below query works in most of the databases, this query will first sort by type and then name:
select * from (
select parent_name, 0 sort_by from table1
union all
select chile_name, 1 from table1
union all
select teacher_name, 2 from table2
union all
select guest_name, 3 from table3) t
order by sort_by, parent_name;
Here is my solution for Oracle DB:
Note that the UNION does not allow ORDER BY clause in subsequent select statements, therefore a procedure is necessary that generates the list of guests in the GUESTS table.
The table ARRIVED contains the IDs of the arrived parents, all other tables are self explanatory.
create table Family (id number, parentname varchar2(100), childname varchar2(100));
create table Teacher (id number, teachername varchar2(100));
create table Other_guest (id number, guestname varchar2(100));
create table Arrived (id number);
create table Guests (guestname varchar2(100));
create sequence Family_seq start with 1 order;
create sequence Teacher_seq start with 1 order;
create sequence Other_seq start with 1 order;
create or replace trigger Family_id before insert on Family for each row
begin
select Family_seq.nextval into :new.id from dual;
end;
create or replace trigger Teacher_id before insert on Teacher for each row
begin
select Teacher_seq.nextval into :new.id from dual;
end;
create or replace trigger Other_id before insert on Other_guest for each row
begin
select Other_seq.nextval into :new.id from dual;
end;
insert into Family (parentname, childname) values ('Denz', 'Hanz');
insert into Family (parentname, childname) values ('Denz', 'Pog');
insert into Family (parentname, childname) values ('Joann', 'Mac');
insert into Teacher (teachername) values ('Miguel');
insert into Teacher (teachername) values ('Sean');
insert into Other_guest (guestname) values ('Mike');
insert into Other_guest (guestname) values ('Mal');
insert into Arrived (id) values (1);
insert into Arrived (id) values (2);
create or replace procedure update_guest_list as
pragma autonomous_transaction;
cursor c_parents is select parentname from family where id in (select id from arrived) order by family.parentname;
cursor c_children is select childname from family where id not in (select id from arrived) order by family.childname;
cursor c_teachers is select teachername from teacher order by teacher.teachername asc;
cursor c_others is select guestname from other_guest order by other_guest.guestname asc;
begin
delete from guests;
for parents_rec in c_parents
loop
insert into guests (guestname) values (parents_rec.parentname);
end loop;
for children_rec in c_children
loop
insert into guests (guestname) values (children_rec.childname);
end loop;
for teachers_rec in c_teachers
loop
insert into guests (guestname) values (teachers_rec.teachername);
end loop;
for others_rec in c_others
loop
insert into guests (guestname) values (others_rec.guestname);
end loop;
commit;
end;
/
execute update_guest_list;

PL/SQL MERGE Using Collection

I am having trouble merging a table with a collection.
Let's say I have a table emp.
Here is my PL/SQL code snippet.
TYPE empcol is table of emp%ROWTYPE INDEX BY BINARY_INTEGER;
tmpemp empcol;
-- Code here to load data from a CSV file into tmpemp collection
-- tmpemp(1).emp_id := parsedstring
-- etc.
MERGE INTO emp A using tmpemp B ON A.emp_id = B.emp_id
WHEN MATCHED THEN UPDATE SET A.fname = B.fname, A.lname = B.lname
WHEN NOT MATCHED THEN INSERT (emp_id, fname, lname) VALUES (b.emp_id, b.fname, b.lname);
Compiler doesn't like it. Its throwing ORA-0942 - Table or View doesn't exist.
What am I doing wrong? or How can I do this better.
Thanks a lot for any help you can provide.
PL/SQL types like emp%ROWTYPE or TABLE OF ... INDEX BY ... cannot be used in SQL queries.
The type must be declared as SQL type (not as PL/SQL type) to be used in SQL query.
Try this approach (example):
create table emp(
firstname varchar2(100),
salary number
);
insert into emp values( 'John', 100 );
commit;
create type my_emp_obj is object(
firstname varchar2(100),
salary number
);
/
create type my_emp_obj_table is table of my_emp_obj;
/
declare
my_emp_tab my_emp_obj_table;
begin
null;
my_emp_tab := my_emp_obj_table( my_emp_obj( 'John', 200 ), my_emp_obj( 'Tom', 300 ));
MERGE INTO emp
USING ( SELECT * FROM TABLE( my_emp_tab )) src
ON ( emp.firstname = src.firstname )
WHEN MATCHED THEN UPDATE SET salary = src.salary
WHEN NOT MATCHED THEN INSERT VALUES( src.firstname, src.salary );
end;
/
select * from emp;
FIRSTNAME SALARY
----------------------- ----------
John 200
Tom 300