Proper way of checking if row exists in table in PL/SQL block - sql

I was writing some tasks yesterday and it struck me that I don't really know THE PROPER and ACCEPTED way of checking if row exists in table when I'm using PL/SQL.
For examples sake let's use table:
PERSON (ID, Name);
Obviously I can't do (unless there's some secret method) something like:
BEGIN
IF EXISTS SELECT id FROM person WHERE ID = 10;
-- do things when exists
ELSE
-- do things when doesn't exist
END IF;
END;
So my standard way of solving it was:
DECLARE
tmp NUMBER;
BEGIN
SELECT id INTO tmp FROM person WHERE id = 10;
--do things when record exists
EXCEPTION
WHEN no_data_found THEN
--do things when record doesn't exist
END;
However I don't know if it's accepted way of doing it, or if there's any better way of checking, I would really apprieciate if someone could share their wisdom with me.

I wouldn't push regular code into an exception block. Just check whether any rows exist that meet your condition, and proceed from there:
declare
any_rows_found number;
begin
select count(*)
into any_rows_found
from my_table
where rownum = 1 and
... other conditions ...
if any_rows_found = 1 then
...
else
...
end if;

IMO code with a stand-alone SELECT used to check to see if a row exists in a table is not taking proper advantage of the database. In your example you've got a hard-coded ID value but that's not how apps work in "the real world" (at least not in my world - yours may be different :-). In a typical app you're going to use a cursor to find data - so let's say you've got an app that's looking at invoice data, and needs to know if the customer exists. The main body of the app might be something like
FOR aRow IN (SELECT * FROM INVOICES WHERE DUE_DATE < TRUNC(SYSDATE)-60)
LOOP
-- do something here
END LOOP;
and in the -- do something here you want to find if the customer exists, and if not print an error message.
One way to do this would be to put in some kind of singleton SELECT, as in
-- Check to see if the customer exists in PERSON
BEGIN
SELECT 'TRUE'
INTO strCustomer_exists
FROM PERSON
WHERE PERSON_ID = aRow.CUSTOMER_ID;
EXCEPTION
WHEN NO_DATA_FOUND THEN
strCustomer_exists := 'FALSE';
END;
IF strCustomer_exists = 'FALSE' THEN
DBMS_OUTPUT.PUT_LINE('Customer does not exist!');
END IF;
but IMO this is relatively slow and error-prone. IMO a Better Way (tm) to do this is to incorporate it in the main cursor:
FOR aRow IN (SELECT i.*, p.ID AS PERSON_ID
FROM INVOICES i
LEFT OUTER JOIN PERSON p
ON (p.ID = i.CUSTOMER_PERSON_ID)
WHERE DUE_DATA < TRUNC(SYSDATE)-60)
LOOP
-- Check to see if the customer exists in PERSON
IF aRow.PERSON_ID IS NULL THEN
DBMS_OUTPUT.PUT_LINE('Customer does not exist!');
END IF;
END LOOP;
This code counts on PERSON.ID being declared as the PRIMARY KEY on PERSON (or at least as being NOT NULL); the logic is that if the PERSON table is outer-joined to the query, and the PERSON_ID comes up as NULL, it means no row was found in PERSON for the given CUSTOMER_ID because PERSON.ID must have a value (i.e. is at least NOT NULL).
Share and enjoy.

Many ways to skin this cat. I put a simple function in each table's package...
function exists( id_in in yourTable.id%type ) return boolean is
res boolean := false;
begin
for c1 in ( select 1 from yourTable where id = id_in and rownum = 1 ) loop
res := true;
exit; -- only care about one record, so exit.
end loop;
return( res );
end exists;
Makes your checks really clean...
IF pkg.exists(someId) THEN
...
ELSE
...
END IF;

select nvl(max(1), 0) from mytable;
This statement yields 0 if there are no rows, 1 if you have at least one row in that table. It's way faster than doing a select count(*). The optimizer "sees" that only a single row needs to be fetched to answer the question.
Here's a (verbose) little example:
declare
YES constant signtype := 1;
NO constant signtype := 0;
v_table_has_rows signtype;
begin
select nvl(max(YES), NO)
into v_table_has_rows
from mytable -- where ...
;
if v_table_has_rows = YES then
DBMS_OUTPUT.PUT_LINE ('mytable has at least one row');
end if;
end;

If you are using an explicit cursor, It should be as follows.
DECLARE
CURSOR get_id IS
SELECT id
FROM person
WHERE id = 10;
id_value_ person.id%ROWTYPE;
BEGIN
OPEN get_id;
FETCH get_id INTO id_value_;
IF (get_id%FOUND) THEN
DBMS_OUTPUT.PUT_LINE('Record Found.');
ELSE
DBMS_OUTPUT.PUT_LINE('Record Not Found.');
END IF;
CLOSE get_id;
EXCEPTION
WHEN no_data_found THEN
--do things when record doesn't exist
END;

You can do EXISTS in Oracle PL/SQL.
You can do the following:
DECLARE
n_rowExist NUMBER := 0;
BEGIN
SELECT CASE WHEN EXISTS (
SELECT 1
FROM person
WHERE ID = 10
) THEN 1 ELSE 0 INTO n_rowExist END FROM DUAL;
IF n_rowExist = 1 THEN
-- do things when it exists
ELSE
-- do things when it doesn't exist
END IF;
END;
/
Explanation:
In the query nested where it starts with SELECT CASE WHEN EXISTS and after the parenthesis (SELECT 1 FROM person WHERE ID = 10) it will return a result if it finds a person of ID of 10. If the there's a result on the query then it will assign the value of 1 otherwise it will assign the value of 0 to n_rowExist variable. Afterwards, the if statement checks if the value returned equals to 1 then is true otherwise it will be 0 = 1 and that is false.

Select 'YOU WILL SEE ME' as ANSWER from dual
where exists (select 1 from dual where 1 = 1);
Select 'YOU CAN NOT SEE ME' as ANSWER from dual
where exists (select 1 from dual where 1 = 0);
Select 'YOU WILL SEE ME, TOO' as ANSWER from dual
where not exists (select 1 from dual where 1 = 0);

select max( 1 )
into my_if_has_data
from MY_TABLE X
where X.my_field = my_condition
and rownum = 1;
Not iterating through all records.
If MY_TABLE has no data, then my_if_has_data sets to null.

Related

in the below code, if last query is not returning reject_count(as there are no rejects in the table), if statement below this query is not executing

1)In the below code, if the last query is not returning "reject_count"(as there are no rejects in the table) if the statement below this query is not executing.
Any alternative for this situation
2)if d_count value is empty, how do i check in the if condition, d_count=r_id
create or replace procedure editor30 (p_title in paper.title%type,round_num in integer)
is
cursor c2 is
select pv.decision,pv.rcomment from paper_review pv,paper p where p.pid=pv.pid and p.title=p_title and pv.round=round_num;
r_decision integer;
p_title1 paper.title%type;
c integer;
r_comment paper_review.rcomment%type;
r_id integer;
d_count integer;
r_num integer;
reject_count integer;
begin
select count(*) into c from paper where title=p_title;
if c=0 then
dbms_output.put_line('No such paper');
else
dbms_output.put_line(p_title||':');
select count(distinct rid) into r_id from paper p,paper_review pv where p.pid=pv.pid and p.title=p_title and pv.round=round_num;
dbms_output.put_line('total no of reviews in round '||round_num||' is :'||r_id);
open c2;
loop
fetch c2 into r_decision,r_comment;
exit when c2%notfound;
dbms_output.put_line(r_decision ||' ' ||r_comment);
end loop;
close c2;
select count(decision) into d_count from paper p,paper_review pv where p.pid=pv.pid and pv.round=round_num and p.title=p_title and pv.decision=1 group by p.title,pv.round;
dbms_output.put_line('total no of accepts :'||d_count);
select count(decision) into reject_count from paper p,paper_review pv where p.pid=pv.pid and pv.round=round_num and p.title=p_title and pv.decision=4 group by p.title,pv.round;
dbms_output.put_line('total no of rejects :'||reject_count);
if d_count=r_id then
dbms_output.put_line('suggestion or the paper is accept');
elsif reject_count>=2 then
dbms_output.put_line('suggestion for the paper is reject');
else
dbms_output.put_line('suggestion to be decided by editor');
end if;
end if;
exception
when no_data_found then
dbms_output.put_line('');
end;
From my point of view, it looks as if you're misinterpreting what's going on.
First of all, remove EXCEPTION handler as it doesn't handle anything; it displays an empty string so you have no idea whether NO_DATA_FOUND happened or not. Or, at least, display something meaningful.
Then, count function will return 0 even there are no rows to be returned:
SQL> select count(*) from emp, dept where 1 = 2;
COUNT(*)
----------
0
SQL>
Therefore, saying that "query is not returning reject_count" or "d_count value is empty" is most probably wrong.
However, those queries have group by clauses which might raise TOO_MANY_ROWS as you're fetching count into scalar variables, and that just won't work:
SQL> select count(*) from emp e, dept d group by e.deptno;
COUNT(*)
----------
24
20
12
There's no way to put 3 values (rows) into reject_count which is declared as integer.
how do i check in the if condition, d_count=r_id
One option is to use NVL function, e.g.
if nvl(d_count, -1) = nvl(r_id, -1) then ...
Finally, do you see anything on the screen? Any output at all? If not, did you set serveroutput on?
For us, it is difficult to test code you wrote as we don't have your tables nor data. Consider creating test case for us (CREATE TABLE and INSERT INTO statements), explaining what result you expect out of it.

How to use an explicit cursor to sort through table information

I'm trying to create a procedure which returns the names of people who have had a test result of 1, but currently have a status of 0. I've broken up how I would accomplish this into essentially three steps: check the people table for everyone whose current status is 0, use an explicit cursor with the pids of those people to go through the people_testresult table which stores the test result information, and then see if any of the pids has taken a test and gotten a result of 1 before.
I've done (or at least tried to) the first two steps, but I'm getting an error for the select statement of my cursor (screenshot at the bottom). I'm also confused about how exactly I would use my explicit cursor to go through all the test result data, especially because every person takes multiple tests. How would you get it to print a name only once, and to only count when the test result is 1 if the test was taken before they had a status of 0?
Here's my work so far:
set serveroutput on;
create or replace procedure get_Recovered
is
current_zero int;
cursor c1 is
select pid from people p, people_testresult ptr where p.pid = ptr.pid and p.status = 0;
v_pid people.pid%type;
begin
-- check people table for current 0 status
select count(*) into current_zero from people p where status = 0;
if current_zero = 0 then
dbms_output.put_line('No person with a current status of 0 found');
-- save pids to explicit cursor to check with test results
else
open c1;
loop
fetch c1 into current_zero;
exit when c1%notfound;
dbms_output.put_line('ID: ' || v_pid);
dbms_output.put_line(current_zero);
-- see if pid has positive test result in past (date before)
--- of the multiple tests a person/pid takes, one with past date before status = 0 has tresult = 1
dbms_output.put_line(pname ' has tested 1 before but has current status 0');
end loop;
close c1;
end if;
end;
Without any sample data, I have difficulties in understanding what exactly you want. However, see if such a modified code helps.
Error you got (column ambiguously defined) is related to cursor's SELECT statement. As both tables contain the pid column, Oracle doesn't know which one to use - prepend its name with a table name (or its alias).
I also defined a cursor variable (c1r c1%rowtype) which is capable of holding the whole row returned by a cursor (so that you wouldn't have to declare all variables separately). My cursor returns some more info than yours: pname (you used in dbms_output.put_line) and result (so that you could check whether its value is 1 or not).
This code probably won't do what you wanted, but - it should compile, at least (if tables contain names I used). Feel free to improve it.
CREATE OR REPLACE PROCEDURE get_Recovered
IS
current_zero INT;
CURSOR c1 IS
SELECT p.pid, p.pname, ptr.result
FROM people p, people_testresult ptr
WHERE p.pid = ptr.pid
AND p.status = 0;
c1r c1%ROWTYPE;
BEGIN
-- check people table for current 0 status
SELECT COUNT (*)
INTO current_zero
FROM people p
WHERE status = 0;
IF current_zero = 0
THEN
DBMS_OUTPUT.put_line ('No person with a current status of 0 found');
ELSE
-- save pids to explicit cursor to check with test results
OPEN c1;
LOOP
FETCH c1 INTO c1r;
EXIT WHEN c1%NOTFOUND;
DBMS_OUTPUT.put_line ('ID: ' || c1r.pid);
-- see if pid has positive test result in past (date before)
-- of the multiple tests a person/pid takes, one with past date before status = 0 has tresult = 1
IF c1r.result = 1
THEN
DBMS_OUTPUT.put_line (
c1r.pname || ' has tested 1 before but has current status 0');
END IF;
END LOOP;
CLOSE c1;
END IF;
END;

How can i do a count(*) during a loop?

So I have a cursor full of info, and some of them have the same attribute(lets say matricula) but i need to insert the content of the cursor in a table where matricula is a PK. So no same attribute are allowed.
To solve this i am doing a two select count(*) to verify that
1st there is no license plate in the table, and i can do the insert statement.
2nd if there is a license plate i will check if there is some null attribute and update them.
I do all of this inside the fetch loop statement.
c1 := funcObterInfoSemanalVeiculos(data_GuardarInfo);
LOOP
FETCH c1 INTO data_inicio, data_fim, matricula, nr_viagens, soma_duracao, soma_km;
EXIT WHEN c1%NOTFOUND;
-- Verify if registry exists in the table
SELECT count(*) into verificacao
FROM resumosveiculos rv
WHERE rv.matricula = matricula and
rv.soma_km = soma_km;
-- Verify if resgitry as some null values
SELECT count(*) into verificacao_2
FROM resumosveiculos rv
WHERE rv.matricula = matricula and
rv.soma_km = 0;
IF (verificacao = 0) THEN
INSERT INTO resumosveiculos (
instante,
data_inicio,
data_fim,
matricula,
nr_viagens,
soma_km,
soma_duracao)
VALUES((SELECT CURRENT_TIMESTAMP FROM DUAL),
l_data_inicio,
l_data_fim,
matricula,
nr_viagens,
soma_duracao,
soma_km
);
ELSIF (verificacao_2 > 0 and nr_viagens != 0 and soma_km != 0 and soma_duracao != 0 )
THEN
Update resumosveiculos rv
SET rv.nr_viagens = nr_viagens,
rv.soma_km = soma_km,
rv.soma_duracao = soma_duracao
Where rv.matricula = matricula;
ELSE
DBMS_OUTPUT.PUT_LINE ('Not inserted-> ' || matricula);
DBMS_OUTPUT.PUT_LINE ('--------------------------------------------');
END IF;
END LOOP;
When a row is inserted a the verification always show that the current value exist altough if was never inserted in the table.
There is no rows in the table before the procedure is executed.
In your query, you are using variable name which is same as column name of the table.
rv.matricula = matricula
Here, matricula is the name of the column as well as the name of the declared parameter. You are confusing PL/SQL compiler. Haha!!
But PL/SQL compiler will consider it as the column name. This is how it behaves. Hence, condition will always be satisfied(true) which is causing problem in your case.
As I commented, please change the name of the variable to something like v_matricula and enjoy coding.
Cheers!!

How to fix the code for trigger that prevent the insert based on some conditions

I am trying to create a trigger that I have problems with.
The triggers work is to stop the item from inserting.
Here there are 2 tables of student and subjects.
I have to check and prevent inserting when the student has not enrolled in that subject and if he has he will start it from the next semester so currently he is not enrolled there.
Please help me with this.
CREATE OR REPLACE TRIGGER check_student
BEFORE INSERT ON subject
FOR EACH ROW
BEGIN
IF :new.student_no
AND :new.student_name NOT IN (
SELECT DISTINCT student_no,student_name
FROM student
)
OR :new.student_enrollment_date < (
select min(enrolment_date)
from student
where
student.student_name = :new.student_name
and student.student_no = :new.guest_no
group by student.student_name , student.student_no
)
AND :new.student_name = (
select distinct student_name
from student
)
AND :new.student_no = (
select distinct student_no
from student
)
THEN
raise_application_error(-20000, 'Person must have lived there');
END IF;
END;
/
I have to check and prevent inserting when the student has not enrolled in that subject and if he has he will start it from the next semester so currently he is not enrolled there.
Please help me with this.
You probably have a logical prescedence issues in your conditions, since it contains ANDs and ORs without any parentheses. Also, you are checking scalar values against subqueries that return more than one row or more than one column, this will generate runtime errors.
But overall, I think that your code can be simplified by using a unique NOT EXISTS condition with a subquery that checks if all functional conditions are satisfied at once. This should be pretty close to what you want:
create or replace trigger check_student
before insert on subject
for each row
begin
if not exists (
select 1
from student
where
name = :new.student_name
and student_no = :new.student_no
and enrolment_date <= :new.student_enrollment_date
) then
raise_application_error(-20000, 'Person must have livd there');
end if;
end;
/
Note: it is unclear what is the purpose of colum :new.guest_no, so I left it apart from the time being (I assumed that you meant :new.student_no instead).
Your code is quite unclear so I am just using the same conditions as used by you in your question to let you know how to proceed.
CREATE OR REPLACE TRIGGER CHECK_STUDENT BEFORE
INSERT ON SUBJECT
FOR EACH ROW
DECLARE
LV_STU_COUNT NUMBER := 0; --declared the variables
LV_STU_ENROLLED_FUTURE NUMBER := 0;
BEGIN
SELECT
COUNT(1)
INTO LV_STU_COUNT -- assigning value to the variable
FROM
STUDENT
WHERE
STUDENT_NO = :NEW.STUDENT_NO
AND STUDENT_NAME = :NEW.STUDENT_NAME;
-- ADDED BEGIN EXCEPTION BLOCK HERE
BEGIN
SELECT
COUNT(1)
INTO LV_STU_ENROLLED_FUTURE -- assigning value to the variable
FROM
STUDENT
WHERE
STUDENT.STUDENT_NAME = :NEW.STUDENT_NAME
AND STUDENT.STUDENT_NO = :NEW.GUEST_NO
GROUP BY
STUDENT.STUDENT_NAME,
STUDENT.STUDENT_NO
HAVING
MIN(ENROLMENT_DATE) > :NEW.STUDENT_ENROLLMENT_DATE;
EXCEPTION WHEN NO_DATA_FOUND THEN
LV_STU_ENROLLED_FUTURE := 0;
END;
-- OTHER TWO CONDITIONS SAME LIKE ABOVE, IF NEEDED
-- using variables to raise the error
IF LV_STU_COUNT = 0 OR ( LV_STU_ENROLLED_FUTURE >= 1 /* AND OTHER CONDITIONS*/ ) THEN
RAISE_APPLICATION_ERROR(-20000, 'Person must have livd there');
END IF;
END;
/
Cheers!!

How can I perform an AND on an unknown number of booleans in postgresql?

I have a table with a foreign key and a boolean value (and a bunch of other columns that aren't relevant here), as such:
CREATE TABLE myTable
(
someKey integer,
someBool boolean
);
insert into myTable values (1, 't'),(1, 't'),(2, 'f'),(2, 't');
Each someKey could have 0 or more entries. For any given someKey, I need to know if a) all the entries are true, or b) any of the entries are false (basically an AND).
I've come up with the following function:
CREATE FUNCTION do_and(int4) RETURNS boolean AS
$func$
declare
rec record;
retVal boolean = 't'; -- necessary, or true is returned as null (it's weird)
begin
if not exists (select someKey from myTable where someKey = $1) then
return null; -- and because we had to initialise retVal, if no rows are found true would be returned
end if;
for rec in select someBool from myTable where someKey = $1 loop
retVal := rec.someBool AND retVal;
end loop;
return retVal;
end;
$func$ LANGUAGE 'plpgsql' VOLATILE;
... which gives the correct results:
select do_and(1) => t
select do_and(2) => f
select do_and(3) => null
I'm wondering if there's a nicer way to do this. It doesn't look too bad in this simple scenario, but once you include all the supporting code it gets lengthier than I'd like. I had a look at casting the someBool column to an array and using the ALL construct, but I couldn't get it working... any ideas?
No need to redefine functions PostgreSQL already provides: bool_and() will do the job:
select bool_and(someBool)
from myTable
where someKey = $1
group by someKey;
(Sorry, can't test it now)
Similar to the previous one, but in one query, this will do the trick, however, it is not clean nor easily-understandable code:
SELECT someKey,
CASE WHEN sum(CASE WHEN someBool THEN 1 ELSE 0 END) = count(*)
THEN true
ELSE false END as boolResult
FROM table
GROUP BY someKey
This will get all the responses at once, if you only want one key just add a WHERE clause
I just installed PostgreSQL for the first time this week, so you'll need to clean up the syntax, but the general idea here should work:
return_value = NULL
IF EXISTS
(
SELECT
*
FROM
My_Table
WHERE
some_key = $1
)
BEGIN
IF EXISTS
(
SELECT
*
FROM
My_Table
WHERE
some_key = $1 AND
some_bool = 'f'
)
SELECT return_value = 'f'
ELSE
SELECT return_value = 't'
END
The idea is that you only need to look at one row to see if any exist and if at least one row exists you then only need to look until you find a false value to determine that the final value is false (or you get to the end and it's true). Assuming that you have an index on some_key, performance should be good I would think.
(Very minor side-point: I think your function should be declared STABLE rather than VOLATILE, since it just uses data from the database to determine its result.)
As someone mentioned, you can stop scanning as soon as you encounter a "false" value. If that's a common case, you can use a cursor to actually provoke a "fast finish":
CREATE FUNCTION do_and(key int) RETURNS boolean
STABLE LANGUAGE 'plpgsql' AS $$
DECLARE
v_selector CURSOR(cv_key int) FOR
SELECT someBool FROM myTable WHERE someKey = cv_key;
v_result boolean;
v_next boolean;
BEGIN
OPEN v_selector(key);
LOOP
FETCH v_selector INTO v_next;
IF not FOUND THEN
EXIT;
END IF;
IF v_next = false THEN
v_result := false;
EXIT;
END IF;
v_result := true;
END LOOP;
CLOSE v_selector;
RETURN v_result;
END
$$;
This approach also means that you are only doing a single scan on myTable. Mind you, I suspect you need loads and loads of rows in order for the difference to be appreciable.
You can also use every, which is just an alias to bool_and:
select every(someBool)
from myTable
where someKey = $1
group by someKey;
Using every makes your query more readable. An example, show all persons who just eat apple every day:
select personId
from personDailyDiet
group by personId
having every(fruit = 'apple');
every is semantically the same as bool_and, but it's certainly clear that every is more readable than bool_and:
select personId
from personDailyDiet
group by personId
having bool_and(fruit = 'apple');
Maybe count 'all' items with somekey=somevalue and use it in a boolean comparison with the count of all 'True' occurences for somekey?
Some non-tested pseudo-sql to show what i mean...
select foo1.count_key_items = foo2.count_key_true_items
from
(select count(someBool) as count_all_items from myTable where someKey = '1') as foo1,
(select count(someBool) as count_key_true_items from myTable where someKey = '1' and someBool) as foo2
CREATE FUNCTION do_and(int4)
RETURNS boolean AS
$BODY$
SELECT
MAX(bar)::bool
FROM (
SELECT
someKey,
MIN(someBool::int) AS bar
FROM
myTable
WHERE
someKey=$1
GROUP BY
someKey
UNION
SELECT
$1,
NULL
) AS foo;
$BODY$
LANGUAGE 'sql' STABLE;
In case you don't need the NULL value (when there aren't any rows), simply use the query below:
SELECT
someKey,
MIN(someBool::int)::bool AS bar
FROM
myTable
WHERE
someKey=$1
GROUP BY
someKey
SELECT DISTINCT ON (someKey) someKey, someBool
FROM myTable m
ORDER BY
someKey, someBool NULLS FIRST
This will select the first ordered boolean value for each someKey.
If there is a single FALSE or a NULL, it will be returned first, meaning that the AND failed.
If the first boolean is a TRUE, then all other booleans are also TRUE for this key.
Unlike the aggregate, this will use the index on (someKey, someBool).
To return an OR, just reverse the ordering:
SELECT DISTINCT ON (someKey) someKey, someBool
FROM myTable m
ORDER BY
someKey, someBool DESC NULLS FIRST