Are these snippets equivalents? (NVL vs Exception) - exception

I have a doubt managing the situation of asigning values to variables based in sql statements, when no data is found. First, are these blocks equivalents? and if that is true, is better one of the two?
declare
nonsense number;
begin
select nvl((select 1 from dual where 1 <> 1),2) into nonsense from dual;
dbms_output.put_line(nonsense);
end;
declare
nonsense number;
begin
begin
select 1 into nonsense from dual where 1<>1;
exception when no_data_found then
nonsense := 2;
end;
dbms_output.put_line(nonsense);
end;

Short answer YES, long answer, the nvl is faster in this case, the result value if not found is right inside de select, in the exception it first execute the select and AFTER the process it calls the exception. In this case nvl is better because you have a fixed value.
Always opt for nvl if you have an "else".

Both blocks output 2 so both are "equivalent". I would argue that the second form is more standard and easier to read.
Compare the two functions:
FUNCTION getSal (p_id NUMBER) RETURN NUMBER
IS
l_return NUMER;
BEGIN
SELECT sal INTO l_return FROM emp WHERE id = p_id;
RETURN l_return;
EXCEPTION
WHEN NO_DATA_FOUND
THEN RETURN 0;
END;
FUNCTION getSal (p_id NUMBER) RETURN NUMBER
IS
l_return NUMER;
BEGIN
SELECT NVL((SELECT sal INTO l_return FROM emp WHERE id = p_id),
0)
INTO l_return
FROM DUAL;
RETURN l_return;
END;
While the first one has a few more lines, it is easier to understand that 0 will be returned if the employee is not found. The second one would takes more time to understand and would therefore be more confusing.
I would definitely go for the first one since they will perform the same and readability equals maintainability and is therefore important.
Also note that my two blocks are not fully equivalent. If an employee has a NULL salary, my first query will return NULL whereas my second query will return 0.

Related

PL/SQL Function, how to use SELECT INTO clause to declare variables from an existing table?

I would like to create a PL/SQL Function that calculates the age of any person from an existing table "Family tree" (Familienbaum), based on their Name. The Table has the needed Values Name, BirthYear (Geburtsjahr), YearOfDeath (Sterbejahr), etc.
Now I want to calculate the age of the person in two ways:
If the Person has a YearOfDeath, it should subtract the BirthYear from the YearofDeath to calculate
If the Person has no YearOfDeath, it should subtract the BirthYear from the Current System Year of the Oracle SQL Server
So far I have tried using the SELECT INTO clause to declare the variables needed for the calculation from the table Family Tree (Familienbaum):
CREATE OR REPLACE FUNCTION BerechneAlter(Person VARCHAR2)
RETURN INTEGER IS
BEGIN
SELECT Name, Sterbejahr, Geburtsjahr FROM Familienbaum
WHERE Person = Name;
RETURN (CASE
WHEN Sterbejahr IS NULL THEN (year(curDate()) - Geburtsjahr)
WHEN Sterbejahr IS NOT NULL THEN (Sterbejahr - Geburtsjahr)
END);
END BerechneAlter;
The SELECT INTO clause is giving me a lot of problems, does anyone know what needs to be changed in the syntax?
I also tried using cursors, but it seems more complicated than needed:
create or replace FUNCTION BerechneAlter(Person VARCHAR2)
RETURN INTEGER IS
Sterbejahr INTEGER;
Geburtsjahr INTEGER;
CURSOR SJ IS SELECT familienbaum.sterbejahr FROM familienbaum WHERE familienbaum.name=Person;
CURSOR GJ IS SELECT familienbaum.geburtsjahr FROM familienbaum WHERE familienbaum.name=Person;
BEGIN
OPEN SJ;
FETCH SJ INTO Sterbejahr;
CLOSE SJ;
OPEN GJ;
FETCH GJ INTO Geburtsjahr;
CLOSE GJ;
RETURN (CASE
WHEN Sterbejahr IS NULL THEN (2022 - Geburtsjahr)
WHEN Sterbejahr IS NOT NULL THEN (Sterbejahr - Geburtsjahr)
END);
END BerechneAlter;
If you are using a SQL SELECT statement within an anonymous block or function or procedure, etc (in PL/SQL - between the BEGIN and the END keywords) you must select INTO something so that PL/SQL can utilize a variable to hold your result from the query. It is important to note here that if you are selecting multiple columns, (which you are by "SELECT Name, Sterbejahr, Geburtsjahr"), you must specify multiple variables or a record to insert the results of your query into.
for example:
SELECT 1
INTO v_dummy
FROM dual;
SELECT 1, 2
INTO v_dummy, v_dummy2
FROM dual;
It is also worth pointing out that if your SELECT , ... FROM.... will return multiple rows, PL/SQL will throw an error. You should only expect to retrieve 1 row of data from a SELECT INTO.
In your case, it would look something like this (note - I haven't confirmed that your logic is correct, also note that I don't know your datatypes so you'll have to work on that bit too):
CREATE OR REPLACE FUNCTION BerechneAlter(p_person VARCHAR2)
RETURN INTEGER IS
v_name VARCHAR2(100);
v_sterbejahr VARCHAR2(100);
v_geburtsjahr VARCHAR2(100)
BEGIN
SELECT Name, Sterbejahr, Geburtsjahr
INTO v_name, v_sterbejahr, v_geburtsjahr
FROM Familienbaum
WHERE Name = p_person;
RETURN (CASE
WHEN v_sterbejahr IS NULL THEN (year(curDate()) - v_geburtsjahr)
WHEN v_sterbejahr IS NOT NULL THEN (v_sterbejahr - v_geburtsjahr)
END);
END BerechneAlter;
I think the function's logic is overcomplicated. You can get data and calculate age in an SQL statement, so only one var is needed.
create or replace function get_age(p_name varchar2) return number
is
l_aelter number;
begin
select nvl(sterbejahr, to_char(sysdate, 'YYYY')) - geburtsjahr aelter
into l_aelter
from familienbaum
where name = p_name;
return l_aelter;
end;
If using plsql is not required, you might wanted to use a standalone SQL statement to perform all the calculations you need:
select nvl(sterbejahr, to_char(sysdate, 'YYYY')) - geburtsjahr aelter
from familienbaum
where name = p_name;

mysql trigger with select from database and update a column

I have this trigger. If the incoming log agrees with input filter, than is not saved into database. But, I want to keep number of "hits" of each Primitive_filter. I have a column named hit_rate, which is int(30). Is there some way how to do that? Maybe specific error? Or sth else? Thx for help.
UPDATE Primitive_filters SET hit_rate = hit_rate + 1 where Primitive_filters.id = ???;
trigger
delimiter //
CREATE TRIGGER inputFilter
before insert
on Logs
for each row
begin
declare msg varchar(255);
IF (SELECT COUNT(*) FROM Primitive_filters, Primitive_in_filter, Filters WHERE
Filters.name = "input" AND Filters.id = Primitive_in_filter.id_filter AND Primitive_in_filter.id_primitive = Primitive_filters.id AND
(Primitive_filters.id_host LIKE CONCAT('%',(SELECT host FROM Hosts WHERE id = new.id_host),'%') OR Primitive_filters.id_host IS NULL) AND
(Primitive_filters.facility LIKE CONCAT('%',new.facility,'%') OR Primitive_filters.facility IS NULL) AND
(Primitive_filters.priority LIKE CONCAT('%',new.priority,'%') OR Primitive_filters.priority IS NULL) AND
(Primitive_filters.program LIKE CONCAT('%',new.program,'%') OR Primitive_filters.program IS NULL) AND
(new.msg REGEXP Primitive_filters.msg OR Primitive_filters.msg IS NULL)) > 0 THEN CALL raise_error; END IF;
END //
delimiter ;
This is NOT the answer to your question.
It's only a hint how to fix a potentially serious performance problem in your code.
Don't use this:
IF (SELECT COUNT(*) FROM ... giant query ...) > 0
THEN CALL raise_error;
END IF;
Use this instead:
IF EXISTS (SELECT 1 FROM ... giant query ...)
THEN CALL raise_error;
END IF;
The former condition calculates a count ... it must read all rows returned by the query
If the query returns billion rows, it must reads them all --> because you asked give me a count of rows.
Then, after the query return the count, there is a check: if the query returns at least one row, then do something.
The latter condition stops executing the query when the query returns first row, saving time and resources.

Get max id of all sequences in PostgreSQL

We have a monitor on our databases to check for ids approaching max-int or max-bigint. We just moved from MySQL, and I'm struggling to get a similar check working on PostgreSQL. I'm hoping someone can help.
Here's the query in MySQL
SELECT table_name, auto_increment FROM information_schema.tables WHERE table_schema = DATABASE();
I'm trying to get the same results from PostgreSQL. We found a way to do this with a bunch of calls to the database, checking each table individually.
I'd like to make just 1 call to the database. Here's what I have so far:
CREATE OR REPLACE FUNCTION getAllSeqId() RETURNS SETOF record AS
$body$
DECLARE
sequence_name varchar(255);
BEGIN
FOR sequence_name in SELECT relname FROM pg_class WHERE (relkind = 'S')
LOOP
RETURN QUERY EXECUTE 'SELECT last_value FROM ' || sequence_name;
END LOOP;
RETURN;
END
$body$
LANGUAGE 'plpgsql';
SELECT last_value from getAllSeqId() as(last_value bigint);
However, I need to somehow add the sequence_name to each record so that I get output in records of [table_name, last_value] or [sequence_name, last_value].
So I'd like to call my function something like this:
SELECT sequence_name, last_value from getAllSeqId() as(sequence_name varchar(255), last_value bigint);
How can I do this?
EDIT: In ruby, this creates the output we're looking for. As you can see, we're doing 1 call to get all the indexes, then 1 call per index to get the last value. Gotta be a better way.
def perform
find_auto_inc_tables.each do |auto_inc_table|
check_limit(auto_inc_table, find_curr_auto_inc_id(auto_inc_table))
end
end
def find_curr_auto_inc_id(table_name)
ActiveRecord::Base.connection.execute("SELECT last_value FROM #{table_name}").first["last_value"].to_i
end
def find_auto_inc_tables
ActiveRecord::Base.connection.execute(
"SELECT c.relname " +
"FROM pg_class c " +
"WHERE c.relkind = 'S'").map { |i| i["relname"] }
end
Your function seems quite close already. You'd want to modify it a bit to:
include the sequences names as literals
returns a TABLE(...) with typed columns instead of SET OF RECORD because it's easier for the caller
Here's a revised version:
CREATE OR REPLACE FUNCTION getAllSeqId() RETURNS TABLE(seqname text,val bigint) AS
$body$
DECLARE
sequence_name varchar(255);
BEGIN
FOR sequence_name in SELECT relname FROM pg_class WHERE (relkind = 'S')
LOOP
RETURN QUERY EXECUTE 'SELECT ' || quote_literal(sequence_name) || '::text,last_value FROM ' || quote_ident(sequence_name);
END LOOP;
RETURN;
END
$body$
LANGUAGE 'plpgsql';
Note that currval() is not an option since it errors out when the sequence has not been set in the same session (by calling nextval(), not sure if there's any other way).
Would something as simple as this work?
SELECT currval(sequence_name) from information_schema.sequences;
If you have sequences that aren't keys, I guess you could use PG's sequence name generation pattern to try to restrict it.
SELECT currval(sequence_name) from information_schema.sequences
WHERE sequence_name LIKE '%_seq';
If that is still too many false positives, you can get table names from the information_schema (or the pg_* schemata that I don't know very well) and refine the LIKE parameter.

MySQL - creating a user-defined function for a custom sort

I'm working with a large set of legacy data (converted from a flat-file db), where a field is formatted as the last 2 digits of the year the record was entered, followed by a 4 digit increment...
e.g., the third record created in 1998 would be "980003", and the eleventh record created in 2004 would be "040011".
i can not change these values - they exist through their company, are registered with the state, clients, etc. I know it'd be great to separate out the year and the rest of it into separate columns, but that's not possible. i can't even really do it "internally" since each row has about 300 fields that are all sortable, and they're very used to working with this field as a record identifier.
so i'm trying to implement a MySQL UDF (for the first time) to sort. The query executes successfully, and it allows me to "select whatever from table order by custom_sort(whatever)", but the order is not what i'd expect.
Here's what I'm using:
DELIMITER //
CREATE FUNCTION custom_sort(id VARCHAR(8))
RETURNS INT
READS SQL DATA
DETERMINISTIC
BEGIN
DECLARE year VARCHAR(2);
DECLARE balance VARCHAR(6);
DECLARE stringValue VARCHAR(8);
SET year = SUBSTRING(0, 2, id);
SET balance = SUBSTRING(2, 6, id);
IF(year <= 96) THEN
SET stringValue = CONCAT('20', year, balance);
ELSE
SET stringValue = CONCAT('19', year, balance);
END IF;
RETURN CAST(stringValue as UNSIGNED);
END//
The records only go back to 96 (thus the arbitrary "if first 2 characters are less than 96, prepend '20' otherwise prepend '19'). I'm not thrilled with this bit, but don't believe that's where the core problem is.
To throw another wrench in the works, it turns out that 1996 and 1997 are both 5 digits, following the same pattern described above but instead of a 4 digit increment, it's a 3 digit increment. Again, I suspect this will be a problem, but is not the core problem.
An example of the returns I'm getting with this custom_sort:
001471
051047
080628
040285
110877
020867
090744
001537
051111
080692
040349
110941
020931
090808
001603
051175
I really have no idea what I'm doing here and have never used MySQL for a UDF like this - any help would be appreciated.
TYIA
/EDIT typo
/EDIT 2 concat needed "year" value added - still getting same results
You have some problems with your substrings, and the cast to int at the end makes it sort values with more digits at the end, not by year. This should work better;
DELIMITER //
CREATE FUNCTION custom_sort(id VARCHAR(8))
RETURNS VARCHAR(10)
READS SQL DATA
DETERMINISTIC
BEGIN
DECLARE year VARCHAR(2);
DECLARE balance VARCHAR(6);
DECLARE stringValue VARCHAR(10);
SET year = SUBSTRING(id, 1, 2);
SET balance = SUBSTRING(id, 3, 6);
IF(year <= 96) THEN
SET stringValue = CONCAT('20', year, balance);
ELSE
SET stringValue = CONCAT('19', year, balance);
END IF;
RETURN stringValue;
END//
DELIMITER ;
This can be simplified a bit to;
DELIMITER //
CREATE FUNCTION custom_sort(id VARCHAR(8))
RETURNS varchar(10)
DETERMINISTIC
BEGIN
IF(SUBSTRING(id, 1, 2) <= '96') THEN
RETURN CONCAT('20', id);
ELSE
RETURN CONCAT('19', id);
END IF;
END//
DELIMITER ;

how to calculate within a procedure by the result of two queries

I have a Table called TblOrders.the fields are FldSlNo, FldStrategyID, FldTradeServerName, FldBaseDir, FldBinaryStartTime, FldInstrumentID, FldOrderNumber, FldBuySell, FldDisplayQuantity, FldRemainingQuantity, FldTotalTradeQuantity, FldLastTradePrice, FldLastTradeQuantity, FldPrice, FldOrderTime, FldReferenceText and FldOrderStatusID. Now I have a procedure called ProfitCalculation.
the procedure is given below:
delimiter //
CREATE PROCEDURE ProfitCalculation
(
IN instrument INT(20) ,
OUT profit float(10,2)
)
BEGIN
DECLARE buy DECIMAL(10,2);
DECLARE sell DECIMAL(10,2);
DECLARE oprofit DECIMAL(8,2);
SELECT SUM(FldLastTradeQuantity*FldPrice)
FROM TblOrders
WHERE FldInstrumentID = instrument AND FldBuySell = 'b' AND FldLastTradePrice != 0 AND FldLastTradeQuantity != 0 group by FldInstrumentID INTO buy;
SELECT SUM(FldLastTradeQuantity*FldPrice)
FROM TblOrders
WHERE FldInstrumentID = instrument AND FldBuySell = 's' AND FldLastTradePrice != 0 AND FldLastTradeQuantity != 0 group by FldInstrumentID INTO sell;
SELECT (sell-buy) INTO oprofit;
SELECT oprofit INTO profit;
END
//
delimiter ;
It always return null.
Is there have any solution for this problem.
Please help me out..
Thanks in advance
First, on both of the queries, you're specifying a GROUP BY, but expecting a single result. Since you're already including the grouped field in the where clause, this is not necessary.
Secondly, try running the individual statements (without the 'INTO' statement) - my guess is that at least one of them is returning NULL, causing the output to be null. If you would prefer zero to NULL as an output with an empty recordset, wrap the SUM() statement in COALESCE, as in:
SELECT COALESCE(SUM(FldLastTradeQuantity*FldPrice), 0)
This way, if one of the two statements return NULL, the other one will still work.
Specifying the conditions FldLastTradePrice != 0 AND FldLastTradeQuantity != 0 are unnecessary, as they'd be handled within the SUM statement.