In my table "Group" I have not a name=John,so I want to get the NULL value in "new_name".
SELECT name INTO new_name FROM Group WHERE name="John";
in pl/sql:
if(new_name IS NULL) then ---here is the problem,I can't enter in "if",instead of having name=NULL, I have name=" ",and when I try to use if(new_name=" "), there's an error....---
some code .....
end if;
so how to check in "if" satement, is it NULL or not(no need to use EXISTS)?
A simple method is to use aggregation:
SELECT MAX(name) INTO new_name
FROM Group
WHERE name = 'John';
An aggregation query with no GROUP BY is guaranteed to return one row. If all rows are filtered out, then the results of the aggregations are NULL.
It looks like you don't have NULL but a space; if that's what you are saying, then you should enclose it into single quotes:
if new_name = ' ' then ...
However: back to beginning. If there's no row whose name = 'John', then select won't return null but raise the no_data_found exception which you should handle, somehow.
One option is to actually "handle" it:
begin
begin
select name into new_name from group where name = 'John';
exception
when no_data_found then new_name := null;
end;
if new_name is null then
some code ...
end if;
end;
Or, if you use an aggregate, you'll avoid the exception handling section (which means "less typing", but it kind of hides what's going on):
begin
select max(name) into new_name from group where name = 'John';
if new_name is null then
some code ...
end if;
end;
I suggest you handle it properly.
your IF condition should be fine but like Littlefoot mentioned, your logic will never satisfy your If condition since the NAME will always be not NULL.
You can use NVL() too if you want to assign values in your new_name variable other than NULL then use that in your IF condition.
Related
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!!
I have an Oracle function that has 3 parameters and uses parameters to set where clause values in multiple select statements that are union'd together. Here is the pseudo code:
create or replace function fn_newfunction
(IN_1_id in VARCHAR2, IN_2 in VARCHAR2, IN_3 in VARCHAR2)
RETURN T_varchar_table AS
v_tab T_varchar_table;
begin
select
cast(multiset (
--add users
select * from table1 opt where opt.col2 = IN_2 and opt.col3 = IN_3 and opt.col1 = IN_1_id
union
...
<insert 10+ select statements here with same values>
) as T_varchar_table)
END
into v_tab
from dual;
return v_tab;
end;
A use case has come up to pass blank values into the function for any of the IN parameters and have it select ANY value in the where clause where the parameter is blank. An example is if IN_1_id is passed a blank value, the where clause in the first select statement would show where ANY value (even null) is in the opt.col1. How can I make this happen? Thank you!
The logic I most frequently use, albeit not in Oracle, is the following. I've written it pseudo, simply because as I mentioned, I believe this more methodological question, as opposed to syntax.
The Logic
Function (#Parameter1, #Parameter2)
SELECT * FROM MyTable
WHERE
--Parameter1
(#Parameter1 IS NULL OR MyTable.Parameter1 = #Parameter1)
AND
--Parameter2
(#Parameter2 IS NULL OR MyTable.Parameter2 = #Parameter2)
Why It works
If you don't pass #Parameter1:
--This evaluates to TRUE for every single row, because the first condition has been met
(#Parameter1 IS NULL OR MyTable.Parameter1 = #Parameter1)
If you do pass #Parameter1:
--The first condition will never be met, because #Parameter1 is NOT NULL.
--The second condition will only be met for rows that match the parameter.
(#Parameter1 IS NULL OR MyTable.Parameter1 = #Parameter1)
Using this method, you can conditionally add fields to your WHERE clause.
You can use dynamic sql to get your issue addressed
if IN_1_id is not null then
lv_where := ' and opt.col1 =IN_1_id1 ';
else
lv_where :=' ';
end if;
EXECUTE IMMEDIATE 'select * from table1 opt where opt.col2 = IN_2 and opt.col3 = IN_3 ' ||lv_where ;
I need to write such a trigger that will check name of the person and will print out his/her id if those people have any digits in theirs names.
What I have by now:
set SERVEROUTPUT ON
create or replace trigger BeforeUpdate
Before insert on customer
for each row
declare
n varchar2(10);
counter number;
nextR number:=0;
begin
select count(id_customer) into counter from customer;
LOOP
nextR:= nextR +1;
select cname into n from customer where id_customer = nextR;
if n not like '%[0-9]%' then
DBMS_OUTPUT.PUT_LINE(nextR || ' has incorrect name');
end if;
exit when nextR = counter;
end loop;
end;
/
It compiles and when I am trying to fire this trigger it do nothing.
I will be grateful for any help.
There are a couple of problems in your code:
using dbms_output in a trigger doesn't really make sense; usually, the INSERT will be performed by client code that doesn't handle the console output.
The sensible thing is to raise an exception instead.
You don't need to perform a SELECT in your trigger code. In fact, doing so will usually either be superfluous or raise a mutating table error. Instead, use :new and :old to refer to the values of the row that was inserted
(minor) naming a before insert trigger BeforeUpdate is somewhat confusing
use a regular expression for testing this business rule (seriously; regexes rule for this kind of thing)
Altogether, here's the fixed version (untested, I don't have an Oracle instance available for testing right now):
create or replace trigger TR_BI_CUSTOMER
Before insert on customer
for each row
begin
if regexp_like(:new.name, '.*[0-9].*') then
raise_application_error(-20001, 'Incorrect name: ' || :new.name);
end if;
end;
Use regular expression to get your result.
In your case, if you get a digit in n, your if clause should be executed.
So,
if regexp_replace(n,'[^[:digit:]]') IS NOT NULL then
DBMS_OUTPUT.PUT_LINE(nextR || ' has incorrect name');
end if
It seems you are also attempting to use a regular expression for digit. However, what your code is searching is for a string that has [0-9] in it. Like Bat[0-9]Man, which is not your desired result.
In my code, whatever expression is not digit in the given name is being replaced. If the name does not contain any digits, the regular expression would return null. If there is any digit at any place,the expression would return those digits.
You could analyse the following query for better grasping of what is happening here:
select regexp_replace(cname,'[^[:digit:]]') OUTP, cname from customer;
EDIT :
This is not how you write a trigger !
The trigger will be fired each time an insert is going to take place. You don't need the counter. You need to use :NEW reference
set SERVEROUTPUT ON
create or replace trigger update or
Insert on customer
for each row
begin
if regexp_replace(:NEW.cname,'[^[:digit:]]') IS NOT NULL then
DBMS_OUTPUT.PUT_LINE(nextR || ' has incorrect name');
end if;
end;
/
This is a job for REGEXP_LIKE()! The regex of '\d' matches a number.
SQL> with tbl(id, name) as (
select 1, 'Batman' from dual union
select 2, 'Robin1' from dual union
select 3, 'Supe4rman' from dual union
select 4, '3Joker' from dual
)
select id, name bad_name
from tbl
where regexp_like(name, '\d');
ID BAD_NAME
---------- ---------
2 Robin1
3 Supe4rman
4 3Joker
SQL>
If your goal is to strip out the digits on the way in (but be careful, a company really could have a number in the name like Level3 Communications or 3Com, if it's a person its less likely but these days who knows!) This is untested:
CREATE OR REPLACE TRIGGER customer_bu
BEFORE INSERT OR UPDATE
ON customer
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
-- If the new name contains a digit, strip it.
if regexp_like(:new.name, '\d') then
:new.name := regexp_replace(:new.name, '\d', NULL);
end if;
END customer_bu;
/
I have the following table with the following tables and values and types.
create table example (
fname text,
lname text,
value int);
insert into example values
('doge','coin',123),
('bit','coin',434),
('lite','coin',565),
('doge','meme',183),
('bit','meme',453),
('lite','meme',433);
create type resultrow as (
nam text,
amount int);
I would like to write a function, that groups by a parameter I give to the function.
This example works:
do $$
declare
my_parameter text;
results resultrow[];
begin
my_parameter = 'last';
results := array(select row( case when my_parameter = 'first' then fname
when my_parameter = 'last' then lname
end,
sum(salary))::resultrow
from example
group by case when my_parameter = 'first' then fname
when my_parameter = 'last' then lname
end);
raise notice '%', results;
end;
$$ language plpgsql;
I have been told, that CASE WHEN decisions are really expensive. One obvious solution would be to create the select statements twice:
if my_parameter = 'first' then
results := array(select row(fname,sum(salary))::resultrow
from example
group by fname);
end if;
if my_parameter = 'last' then
results := array(select row(lname,sum(salary))::resultrow
from example
group by lname);
end if;
But this leads to a lot of ugly duplicated code.
Is there another solution to make the group by parameterisable?
If you don't want to use case, you can use this:
with cte(name, salary) as (
select fname, salary from example where my_parameter = 'first'
union all
select lname, salary from example where my_parameter = 'last'
)
select name, sum(salary)
from cte
group by name
But, actually, it's better to test, I've not heard that case is expensive.
If you'll find that case is not expensive, I still suggest use subquery or cte to avoid code duplication, like:
with cte(name, salary) as (
select
case
when my_parameter = 'first' then fname
when my_parameter = 'last' then lname
end as name,
salary
from example
)
select name, sum(salary)
from cte
group by name
Simplify what you have:
DO
$do$
DECLARE
_param text := 'last'; -- one can assign at declaration time
results resultrow[];
BEGIN
results := ARRAY(
SELECT t::resultrow -- refer to table alias to get whole row
FROM (
SELECT CASE _param -- simple "switched" CASE
WHEN 'first' THEN fname
WHEN 'last' THEN lname
END
,sum(salary)
FROM example
GROUP BY 1 -- simpler with positional reference
) t
);
RAISE NOTICE '%', results;
END
$do$ LANGUAGE plpgsql;
Using simple CASE syntax variant. This way the expression is only evaluated once and the syntax is simpler. Since your question refers to CASE - even if that's hardly relevant.
Also using a positional reference in the GROUP BY clause. This seems relevant to the title of your question. More explanation in these related answers:
Select first row in each GROUP BY group?
GROUP BY + CASE statement
This kind of query can be very inefficient. It's not a problem of the (very cheap!) CASE statement per se. It's because the planner has to provide for varying input in the first column and may be forced to use a generic, less optimized plan.
Dynamic SQL
I assume the actual goal is to write a function that takes my_parameter. Use dynamic SQL with EXECUTE, which will likely result in a superior query plan, i.e. superior performance. There are lots of code example here, try a search.
Also, I return a set of resultrow instead of the awkward ARRAY you had in your example (since you cannot return from a DO statement):
CREATE FUNCTION f_salaray_for_param(_param text)
RETURNS SETOF resultrow AS
$func$
DECLARE
_fld text :=
CASE _param
WHEN 'first' THEN 'fname' -- SQL injection not possible
WHEN 'last' THEN 'lname'
END;
BEGIN
IF _fld IS NULL THEN -- exception for invalid params
RAISE EXCEPTION 'Unexpected value for _param: %', _param;
END IF;
RETURN QUERY EXECUTE '
SELECT ' || _fld || ', sum(salary)
FROM example
GROUP BY 1'; -- query is very simple now
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM f_salaray_for_param('first');
BTW, the plpgsql assignment operator is := (not =).
I am trying to enter a student id from the students table to get the decision of whether they passed or failed from the module_grades table but i keep getting that error when i enter the student id can anyone help, thanks
Here is my code:
set serverouput on
DECLARE
Student Students.Sid%type:='&sid';
decision MODULE_GRADES.MDECISION%type;
BEGIN
SELECT MDECISION INTO decision FROM MODULE_GRADES
WHERE Sid = Student;
IF
(decision = 'Pass')
Then
DBMS_OUTPUT.PUT_LINE('You got'||Decision||' congratulations');
END IF;
END;
You're getting the error because the SELECT INTO returns at least two rows. In PL/SQL a SELECT INTO must return exactly one row, no more, no less.
When using SELECT INTO, you should query the table by a combination of conditions that guarantees uniqueness -- usually a primary key. In your case you are probably missing a filter or you have an anomaly in the data.
If you're expecting cases where there could be more than one row, you should either:
use a loop:
FOR cc IN (SELECT DECISION FROM MODULE_GRADES WHERE Sid = Student) LOOP
-- do something
END LOOP;
or catch the exception
BEGIN
SELECT MDECISION INTO decision
FROM MODULE_GRADES
WHERE Sid = Student;
-- do something
EXCEPTION
WHEN TOO_MANY_ROWS THEN
-- do something else
END;