In Oracle, it's possible to return a cursor inside a SQL query, using the cursor keyword, like this:
select owner, table_name,
cursor (select column_name
from all_tab_columns
where owner = allt.owner
and table_name = allt.table_name) as columns
from all_tables allt
The questions are:
Does anyone know where can I find documentation for this?
Does PortgreSQL (or any other open source DBMS) have a similar feature?
It's called a CURSOR EXPRESSION, and it is documented in the obvious place, the Oracle SQL Reference. Find it here.
As for your second question, the closest thing PostgreSQL offers to match this functionality is "scalar sub-queries". However, as #tbrugz points out, these only return one row and one column, so they aren't much like Cursor Expressions. Read about them in the documentation here. MySQL also has Scalar Sub-queries, again limited to one column and one row. Docs here. Likewise SQL Server and DB2 (not open source but for completeness).
That rules out all the obvious contenders. So, it seems unlikely any other DBMS offers the jagged result set we get from Oracle's cursor expression.
Postgres provides cursor expressions but the syntax is a bit less handy than Oracle's.
First you need to create function for array to refcursor conversion:
create or replace function arr2crs(arr anyarray) returns refcursor as $$
declare crs refcursor;
begin
open crs for select * from unnest(arr);
return crs;
end;
$$ language plpgsql volatile;
Now let's create some test data
create table dep as
select 1 depid, 'Sales' depname
union all
select 2 depid, 'IT' depname;
create table emp as
select 1 empid, 1 depid, 'John' empname union all
select 2 empid, 1 depid, 'James' empname union all
select 3 empid, 2 depid, 'Rob';
You can query it like this
select
dep.*,
arr2crs(array(
select row(emp.*)::emp from emp
where emp.depid = dep.depid
)) emps
from dep
And process in on client side like this (Java)
public static List Rs2List(ResultSet rs) throws SQLException{
List result = new ArrayList();
ResultSetMetaData meta = rs.getMetaData();
while(rs.next()){
Map row = new HashMap();
for (int i = 1; i <= meta.getColumnCount(); i++){
Object o = rs.getObject(i);
row.put(
meta.getColumnName(i),
(o instanceof ResultSet)?Rs2List((ResultSet)o):o);
}
result.add(row);
}
return result;
}
Note that you must explicitly cast row to particular type. You can use CREATE TYPE to create necessary types.
Related
Currently, I have some SQL queries which looks like this:
Drop Table X;
Create Table X(id INTEGER);
Insert Into X
select ..
from..
where a.name = GIVENNAME;
Select SUM(..)
from ..
..
order by date desc;
And I want to put all these into a SQL Function, where I can choose the Parameter "GIVENNAME" when I call the function.
Is there a way to make this possible?
I would know how to do it in JSON/Java, but I have really no clue how to make it as a Function in SQL (using Oracle).
Edit:
After pointing out some things, I want to add my current code:
DROP TABLE TEMPTABLE;
CREATE TABLE TEMPTABLE
(mitID INTEGER);
INSERT INTO TEMPTABLE
select m.mitid
from mitarbeiter m
inner join abteilungen a on m.abt = a.abtid
where a.abtname = #GIVENNAME;
select SUM(g.kosten)
from gehaelter g
left outer join gehaelter k
on g.mitarbeiter = k.mitarbeiter
and g.vondatum < k.vondatum
where k.mitarbeiter is null AND g.mitarbeiter in (select * from TEMPTABLE)
order by g.vondatum desc;
I'm currently more interested in a working solution than a nice & clean one
Fortunately you can have both:
create or replace function get_sum_kosten
( p_givenname in abteilungen.abtname%type )
return number
as
return_value number;
begin
select SUM(g.kosten)
into return_value
from gehaelter g
left outer join gehaelter k
on g.mitarbeiter = k.mitarbeiter
and g.vondatum < k.vondatum
where k.mitarbeiter is null
AND g.mitarbeiter in (select m.mitid
from mitarbeiter m
inner join abteilungen a on m.abt = a.abtid
where a.abtname = P_GIVENNAME
)
;
return return_value;
end;
Possible? Yes. Recommended? No.
For any DDL, you'd have to use dynamic SQL (EXECUTE IMMEDIATE). If queries are complex, those commands will be difficult to maintain.
INSERT is a DML, but you can't use it in a function, unless it is an autonomous transaction (and you'll have to commit (or rollback) within the function).
If it were a procedure, you'd - at least - avoid the last problem I mentioned. If you're returning something, use an OUT parameter.
Can't you use a (global) temporary table, instead? Create it once, use it many times. I understand that your code might be very complex and maybe it really can't fit into a single SELECT statement, but you should - at least - try to do that job in an Oracle spirit (i.e. it is not MS SQL Server).
example of procedure
https://www.sitepoint.com/stored-procedures-mysql-php/
like this
DELIMITER $$
CREATE PROCEDURE `avg_sal`(out avg_sal decimal)
BEGIN
select avg(sal) into avg_sal from salary;
END
I have a query:
SELECT
q1.table_name,
q1.column_name,
q1.data_type,
q1.nullable,
q2.comments
FROM
(
SELECT
table_name,
column_name,
data_type,
nullable
FROM
USER_TAB_COLUMNS
WHERE TABLE_NAME = 'EMPLOYEES'
) q1
JOIN
(
SELECT
column_name,
comments
FROM
USER_COL_Comments
WHERE TABLE_NAME = 'EMPLOYEES'
) q2 ON q1.column_name = q2.column_name;
It works fine, but I need to get my table name as a parameter. And I just got stucked. How can I do this? What is the difference between function and stored procedure in Oracle? What is better to use in this case? Will be very grateful for any help.
I don't think you need subqueries here: just join the two views on table name and column name. This makes it easy to parameterise the query, because you only need populate the one instance of table_name.
I have used an outer join in this query because in my experience developers aren't very disciplined about writing column comments ;)
SELECT
q1.table_name,
q1.column_name,
q1.data_type,
q1.nullable,
q2.comments
FROM USER_TAB_COLUMNS q1
left outer JOIN USER_COL_Comments q2
ON q1.table_name = q2.table_name
and ON q1.column_name = q2.column_name
WHERE q1.TABLE_NAME = 'EMPLOYEES'
;
If you need to put this in a function then it's quite straightforward: you just need to decide what return type you want. Will the function be called by other programs or used in queries? If just SQL queries, probably you should write a view instead (without the WHERE clause).
For use by programs the function should return a ref cursor, which can be mapped to a JDBC or ODBC result set.
create or replace function get_table_details
(p_table_name in user_tables.table_name%type)
return sys_refcursor
as
rc sys_refcursor;
begin
open rc for
SELECT
q1.table_name,
q1.column_name,
q1.data_type,
q1.nullable,
q2.comments
FROM USER_TAB_COLUMNS q1
left outer JOIN USER_COL_Comments q2
ON q1.table_name = q2.table_name
and ON q1.column_name = q2.column_name
WHERE q1.TABLE_NAME = p_table_name
;
return rc;
end;
"What is the difference between function and stored procedure in Oracle?"
A function returns something whereas a procedure doesn't. The convention is that a function is used for read-only features and procedures are used for changing database state. However, procedures can have OUT parameters so they can return values, and some people are ill-mannered enough to execute DML in functions. But if you stick to the convention you'll be fine.
I'm struggling with a variable argument stored procedure that has to perform a SELECT on a table using every argument passed to it in its WHERE clause.
Basically I have N account numbers as parameter and I want to return a table with the result of selecting three fields for each account number.
This is what I've done so far:
function sp_get_minutes_expiration_default(retval IN OUT char, gc IN OUT GenericCursor,
p_account_num IN CLIENT_ACCOUNTS.ACCOUNT_NUM%TYPE) return number
is
r_cod integer := 0;
begin
open gc for select account_num, concept_def, minutes_expiration_def from CLIENT_ACCOUNTS
where p_account_num = account_num; -- MAYBE A FOR LOOP HERE?
return r_cod;
exception
-- EXCEPTION HANDLING
end sp_get_minutes_expiration_default;
My brute force solution would be to maybe loop over a list of account numbers, select and maybe do a UNION or append to the result table?
If you cast your input parameter as a table, then you can join it to CLIENT_ACCOUNTS
select account_num, concept_def, minutes_expiration_def
from CLIENT_ACCOUNTS ca, table(p_account_num) a
where a.account_num = ca.account_num
But I would recommend you select the output into another collection that is the output of the function (or procedure). I would urge you to not use reference cursors.
ADDENDUM 1
A more complete example follows:
create or replace type id_type_array as table of number;
/
declare
ti id_type_array := id_type_array();
n number;
begin
ti.extend();
ti(1) := 42;
select column_value into n from table(ti) where rownum = 1;
end;
/
In your code, you would need to use the framework's API to:
create an instance of the collection (of type id_type_array)
populate the collection with the list of numbers
Execute the anonymous PL/SQL block, binding in the collection
But you should immediately see that you don't have to put the query into an anonymous PL/SQL block to execute it (even though many experienced Oracle developers advocate it). You can execute the query just like any other query so long as you bind the correct parameter:
select account_num, concept_def, minutes_expiration_def
from CLIENT_ACCOUNTS ca, table(:p_account_num) a
where a.column_value = ca.account_num
I have three functions which are all doing the same. I like to know whether SELECT ... FROM EMP WHERE DEPT_ID = v_dept returns any row.
Which one would be the fastest way?
CREATE OR REPLACE FUNCTION RecordsFound1(v_dept IN EMP.DEPT_ID%TYPE) RETURN BOOLEAN IS
n INTEGER;
BEGIN
SELECT COUNT(*) INTO res FROM EMP WHERE DEPT_ID = v_dept;
RETURN n > 0;
END;
/
CREATE OR REPLACE FUNCTION RecordsFound2(v_dept IN EMP.DEPT_ID%TYPE) RETURN BOOLEAN IS
CURSOR curEmp IS
SELECT DEPT_ID FROM EMP WHERE DEPT_ID = v_dept;
dept EMP.DEPT_ID%TYPE;
res BOOLEAN;
BEGIN
OPEN curEmp;
FETCH curEmp INTO dept;
res := curEmp%FOUND;
CLOSE curEmp;
RETURN res;
END;
/
CREATE OR REPLACE FUNCTION RecordsFound3(v_dept IN EMP.DEPT_ID%TYPE) RETURN BOOLEAN IS
dept EMP.DEPT_ID%TYPE;
BEGIN
SELECT DEPT_ID INTO dept FROM EMP WHERE DEPT_ID = v_dept;
RETURN TRUE;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN FALSE;
WHEN TOO_MANY_ROWS THEN
RETURN TRUE;
END;
/
Assume table EMP is very big and condition WHERE DEPT_ID = v_dept could match on thousands of rows.
Usually I would expect RecordsFound2 to be the fastest, because it has to fetch (maximum) only one single row. So in terms of I/O it should be the best.
For the non-believers: the exists() version:
CREATE OR REPLACE FUNCTION RecordsFound0(v_dept IN EMP.DEPT_ID%TYPE) RETURN BOOLEAN IS
BEGIN
RETURN EXISTS( SELECT 1 FROM EMP WHERE DEPT_ID = v_dept);
END;
The Postgresql version:
CREATE OR REPLACE FUNCTION RecordsFound0(v_dept IN EMP.DEPT_ID%TYPE) RETURNS BOOLEAN AS
$func$
BEGIN
RETURN EXISTS( SELECT 1 FROM EMP WHERE DEPT_ID = v_dept);
END
$func$ LANGUAGE plpgsql;
And in Postgres the function can be implemented in pure sql, without the need for plpgsql(in Postgres the select does not need a ... FROM DUAL
CREATE OR REPLACE FUNCTION RecordsFound0s(v_debt IN EMP.DEPT_ID%TYPE) RETURNS BOOLEAN AS
$func$
SELECT EXISTS( SELECT NULL FROM EMP WHERE DEPT_ID = v_debt);
$func$ LANGUAGE sql;
Note: the unary EXISTS(...) operator yields a Boolean, which is exactly what you want.
Note2: I hope I have the Oracle syntax correct. (keywords RETURN <-->RETURNS and AS <-->IS)
Your solution 1 Count all occurrences:
You have the DBMS do much more work than needed. why let it scan the table and count all occurences when you only want to know whether at least one exists or not? This is slow. (But on a small emp table with an index on dept_id this may still look fast :-)
Your solution 2 Open a Cursor and only fetch the first record
A good idea and probably rather fast, as you stop, once you found a record. However, the DBMS doesn't know that you only want to look for the mere existence and may decide for a slow execution plan, as it expects you to fetch all matches.
Your solution 3 Fetch the one record or get an exception
This may be a tad faster, as the DBMS expects to find one record only. However, it must test for further matches in order to raise TOO_MANY_ROWS in case. So in spite of having found a record already it must look on.
solution 4 Use COUNT and ROWNUM
By adding AND ROWNUM = 1 you show the DBMS that you want one record only. At a minimum the DBMS knows it can stop at some point, at best it even notices that it is only one record needed. So depending on the implementation the DBMS may find the optimal execution plan.
solution 5 Use EXISTS
EXISTS is made to check for mere existence, so the DBMS can find the optimal execution plan. EXISTS is an SQL word, not a PL/SQL word and the SQL engine doesn't know BOOLEAN, so the function gets a bit clumsy:
CREATE OR REPLACE FUNCTION RecordsFound1(v_dept IN EMP.DEPT_ID%TYPE) RETURN BOOLEAN IS
v_1_is_yes_0_is_no INTEGER;
BEGIN
SELECT COUNT(*) INTO v_1_is_yes_0_is_no
FROM DUAL
WHERE EXISTS (SELECT * FROM EMP WHERE DEPT_ID = v_dept);
RETURN n = 1;
END;
The absolute fastest ways is to not call the count function at all.
A typical pattern is
count the number of rows
if cnt = 0 then do something
else read chunk of data and process
Simple read the data and than perform the count test on it.
you can break a SQL by adding an extra condition to your where-clause:
where ...
and rownum = 1;
this stops immediatly if at least one record is found and it is as fast as the "exists" operator.
See the followint sample code:
create or replace function test_record_exists(pi_some_parameter in varchar2) return boolean is
l_dummy varchar2(10);
begin
select 'x'
into l_dummy
from <your table>
where <column where you want to filter for> = pi_some_parameter
and rownum = 1;
return (true);
exception
when no_data_found then
return (false);
end;
If you use:
select count(*)
from my_table
where ...
and rownum = 1;
... then the query will:
be executed in the most efficient fashion
always return a single row
return either 0 or 1
This three factors make it very fast and very easy to use in PL/SQL as you do not have to concern yourself with whether a row is returned or not.
The returned value is also amenable to use as a true/false boolean, of course.
If you wanted to list the departments that either do or do not have any records in the emp table then I would certainly use EXISTS, as the semi-(anti)join is the most efficient means of executing the query:
select *
from dept
where [NOT] exists (
select null
from emp
where emp.dept_id = dept.id);
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 =).