Turn SQL statement in a function - sql

How to replace a concrete value with a variable in Oracle SQL? I have
select 5, min(id) from my_table where id > 5 --AND ..
UNION ALL
select 6, min(id) from my_table where id > 6 --AND ..
UNION ALL
....
| 5 | 6 |
| 6 | 8 |
...
How to wrap it in a function executing pseudocode below?
for ( $i in ( select id from my_table)){
UNION ALL
select $i, min(id) from my_table where id > $i
}
Edit: To make it clear, I am looking for a general method to turn a select with hard wired values into a function that accepts variables. Note the --AND part.
Edit2:
Let me translate it in Java.
Qu estion: I have code
System.out.println(1+2+" = 1+2"+ " ");
that returns a sum of two numbers. How to replace concrete 1+2 with any a and b?
Answer:
You need to define
int getSum(int a, int b){ return a+b;}
Now you can write
for(int a : setA){
for(int b : setB){
System.out.println(" " +a+"+"+b+" = "+ getSum(a,b)+" ");
}}
This way you can iterate over all elements of setA and all elements of setB instead of providing concrete values 1 and 2.
I have exactly the same question for SQL. If I have a query that returns a result for a concrete value (Java example: 1,2; SQL example: where id > 6 ) - how to modify it so that SQL iterates over all possible values (obtained by select id from t group by id)?

It looks like you want to get the next ID for each id. That can be done simply by using the lead function:
select
t.id,
lead(t.id) over (order by t.id) as next_id
from
my_table t
If you do want to return it from a function, you would probably need a table function returning a custom table type, because the rowtype to return doesn't match your table structure, so you can't use my_table%rowtype.
The snippet below would create such a row type and matching table type, and a function that would return the same as above query. The last lines contain the same query, now using the function instead of the table:
-- Define a row type (an object).
create or replace type my_row_type as object (
id int,
next_id int
);
-- Define a table type of that row type
create type my_table_type as table of my_row_type;
-- Create a function that returns a table.
create or replace function my_function
return my_table_type
is
result my_table_type;
begin
select
-- Use the rowtype constructor to put the id and next_id into a row object
my_row_type(
t.id,
lead(t.id) over (order by t.id))
-- use bulk collect into to query all rows into the table variable 'result'
bulk collect into
result
from
my_table t;
-- Don't forget to actually return it.
return result;
end;
/
-- Query it by 'casting' the function result to a table.
select
id,
next_id
from
table(my_function);
For more info on table functions, I think this tutorial/article is a good start:
http://stevenfeuersteinonplsql.blogspot.com/2015/04/table-functions-introduction-and.html

Related

Convert a piece of SQL into an Oracle function

My table "COMMA_SEPERATED" looks something like this
ID NAME CITY
--- ---- -----------------------------
1 RAJ CHENNAI, HYDERABAD, JABALPUR
2 SAM BHOPAL,PUNE
I want to separate each City as a new record so my SQL is: (working fine)
SELECT id, TRIM(CITY_NEW)
FROM COMMA_SEPERATED, xmltable
(
'if (contains($X,",")) then ora:tokenize($X,"\,") else $X'
passing city AS X
columns CITY_NEW varchar2(4000) path '.'
);
I want to convert this piece of SQL into a function so that i can simply call the function like this
SELECT id, split_function(city) FROM COMMA_SEPERATED
Output:
1 CHENNAI
1 HYDERABAD
1 JABALPUR
2 BHOPAL
2 PUNE
Can anyone help on how to do that? I am very new to PL/SQL.
The query you're trying to get to:
SELECT id, split_function(city) FROM COMMA_SEPERATED
won't work, because you're trying to return multiple rows for each source row. You have to make it a bit more complicated than that unfortunately.
If the goal is to hide the splitting mechanism then the closest I can think of is to create a function which returns a collection of strings, which could be pipelined:
create or replace function split_function (p_string varchar2)
return sys.odcivarchar2list pipelined as
begin
for r in (
select result
from xmltable (
'if (contains($X,",")) then ora:tokenize($X,"\,") else $X'
passing p_string as x
columns result varchar2(4000) path '.'
)
)
loop
pipe row (trim(r.result));
end loop;
end split_function;
/
Your proposed call would then give you one row per ID with a collection:
select id, split_function(city) from comma_seperated;
ID SPLIT_FUNCTION(CITY)
---------- -----------------------------------------------------------------
1 ODCIVARCHAR2LIST('CHENNAI', 'HYDERABAD', 'JABALPUR')
2 ODCIVARCHAR2LIST('BHOPAL', 'PUNE')
which isn't quite what you want; but you can use a table collection expression and cross-join to convert into multiple rows instead:
select cs.id, t.column_value as city
from comma_seperated cs
cross join table(split_function(cs.city)) t;
ID CITY
---------- ------------------------------
1 CHENNAI
1 HYDERABAD
1 JABALPUR
2 BHOPAL
2 PUNE
db<>fiddle demo.
That isn't as simple as you hoped for, but is arguably still better than cross-joining to the xmltable(), particularly if you want to reuse that splitting logic/function in multiple places, as well as hide the details of how the split is done - which would let you change the mechanism easily if you wanted to, e.g. to use a more common regular expression to do the splitting.
Apart what #Alex showed, you can also create an object and get the object returned via function. See below:
--Created an object to hold your result columns
create or replace type Obj IS OBJECT (id number, city varchar2(20));
/
--Table of object
create or replace type var_obj is table of Obj;
/
--Function with return as with Object type.
create or replace function splt_fnct
return var_obj
as
var var_obj:=var_obj();
begin
Select obj(col,col1)
bulk collect into var
from (
Select distinct col , regexp_substr(col1,'[^,]+',1,level) col1
from tbl
connect by regexp_substr(col1,'[^,]+',1,level) is not null
order by 1);
return var;
end;
/
--Selecting result
select * from table(splt_fnct);
Edit: I was trying with #Alex solution and got some error as shown below:
create or replace function splt_fnct(input_strng varchar2)
return var_obj
as
var var_obj:=var_obj();
begin
Select obj(col,col1)
bulk collect into var
from (
select tbl.col, t.rslt --<--This column name should the same as used in colmns clause in the below query. Its giving error "invalid column". How to handle this case.
FROM tbl, xmltable
(
'if (contains($X,",")) then ora:tokenize($X,"\,") else $X'
passing col1 AS X
columns input_strng varchar2(4000) path '.'
) t
);
return var;
end;
/
Correction as per #Alex suggestion:
create or replace function splt_fnct(input_strng varchar2)
return var_obj
as
var var_obj:=var_obj();
begin
select obj(tbl.col, t.rslt)
bulk collect into var
FROM tbl, xmltable
(
'if (contains($X,",")) then ora:tokenize($X,"\,") else $X'
passing input_strng AS X
columns rslt varchar2(4000) path '.'
) t;
return var;
end;
/

Function to select existing value or insert new row

I'm new to working with PL/pgSQL, and I'm attempting to create a function that will either find the ID of an existing row, or will insert a new row if it is not found, and return the new ID.
The query contained in the function below works fine on its own, and the function gets created fine. However, when I try to run it, I get an error stating "ERROR: column reference "id" is ambiguous". Can anybody identify my problem, or suggest a more appropriate way to do this?
create or replace function sp_get_insert_company(
in company_name varchar(100)
)
returns table (id int)
as $$
begin
with s as (
select
id
from
companies
where name = company_name
),
i as (
insert into companies (name)
select company_name
where not exists (select 1 from s)
returning id
)
select id
from i
union all
select id
from s;
end;
$$ language plpgsql;
This is how I call the function:
select sp_get_insert_company('TEST')
And this is the error that I get:
SQL Error [42702]: ERROR: column reference "id" is ambiguous
Detail: It could refer to either a PL/pgSQL variable or a table column.
Where: PL/pgSQL function sp_get_insert_company(character varying) line 3 at SQL statement
As the messages says, id is in there twice. Once in the queries, once in the table definition of the return type. Somehow this clashes.
Try qualifying the column expressions, everywhere.
...
with s as (
select
companies.id
from
companies
where name = company_name
),
i as (
insert into companies (name)
select company_name
where not exists (select 1 from s)
returning companies.id
)
select i.id
from i
union all
select s.id
from s;
...
By qualifying the column expression the DBMS does no longer confuse id of a table with the id in the return type definition.
The next problem will be, that your SELECT has no target. It will tell you to do a PERFORM instead. But I assume you want to return the results. Change the body to
...
RETURN QUERY (
with s as (
select
companies.id
from
companies
where name = company_name
),
i as (
insert into companies (name)
select company_name
where not exists (select 1 from s)
returning companies.id
)
select i.id
from i
union all
select s.id
from s);
...
to do so.
In the function you display there is no need for returns table (id int). It's supposed to always return exactly one integer ID. Simplify to RETURNS int. This also makes ERROR: column reference "id" is ambiguous go away, since we implicitly removed the OUT parameter id (visible in the whole function block).
How to return result of a SELECT inside a function in PostgreSQL?
There is also no need for LANGUAGE plpgsql. Could simply be LANGUAGE sql, then you wouldn't need to add RETURNS QUERY, either. So:
CREATE OR REPLACE FUNCTION sp_get_insert_company(_company_name text)
RETURNS int AS
$func$
WITH s as (
select c.id -- still good style to table-qualify all columns
from companies c
where c.name = _company_name
),
i as (
insert into companies (name)
select _company_name
where not exists (select 1 from s)
returning id
)
select s.id from s
union all
select i.id from i
LIMIT 1; -- to optimize performance
$func$ LANGUAGE sql;
Except that it still suffers from concurrency issues. Find a proper solution for your undisclosed version of Postgres in this closely related answer:
Is SELECT or INSERT in a function prone to race conditions?

Selecting and passing a record as a function argument

It may look like a duplicate of existing questions (e.g. This one) but they only deal with passing "new" arguments, not selecting rows from the database.
I have a table, for example:
CREATE TABLE my_table (
id bigserial NOT NULL,
name text,
CONSTRAINT my_table_pkey PRIMARY KEY (id)
);
And a function:
CREATE FUNCTION do_something(row_in my_table) RETURNS void AS
$$
BEGIN
-- does something
END;
$$
LANGUAGE plpgsql;
I would like to run it on data already existing in the database. It's no problem if I would like to use it from another PL/pgSQL stored procedure, for example:
-- ...
SELECT * INTO row_var FROM my_table WHERE id = 123; -- row_var is of type my_table%rowtype
PERFORM do_something(row_var);
-- ...
However, I have no idea how to do it using an "ordinary" query, e.g.
SELECT do_something(SELECT * FROM my_table WHERE id = 123);
ERROR: syntax error at or near "SELECT"
LINE 1: SELECT FROM do_something(SELECT * FROM my_table ...
Is there a way to execute such query?
You need to pass a scalar record to that function, this requires to enclose the actual select in another pair of parentheses:
SELECT do_something( (SELECT * FROM my_table WHERE id = 123) );
However the above will NOT work, because the function only expects a single column (a record of type my_table) whereas select * returns multiple columns (which is something different than a single record with multiple fields).
In order to return a record from the select you need to use the following syntax:
SELECT do_something( (SELECT my_table FROM my_table WHERE id = 123) );
Note that this might still fail if you don't make sure the select returns exactly one row.
If you want to apply the function to more than one row, you can do that like this:
select do_something(my_table)
from my_table;

SQL: Turning Rows into Columns for Variable Number of Rows

I have two tables whose simplified structure looks like this:
RESPONSES
id
created
ACCESSORY VALUES
id
response_id
sensor_id
value
I want to create a view that flattens all accessory values for a given response into one row over a time period (filtering on response.created). I think I want a Pivot table or Crosstab, but I'm unfamiliar with both and the examples I've found mainly deal with a known number of columns. To complicate things, a given sensor could only appear for part of the time period if a user started or stopped tracking it during the time in question. Ideally I'd hold a NULL in that column for the sensor in any rows when it was not present. Is this possible? If so, am I on the right track or looking in the wrong place?
The SQL to get the data as individual rows looks like
SELECT r.id, a.sensor_id, a.value from results_response r
INNER JOIN results_accessoryvalue a ON r.id = a.response_id
WHERE r.installation_id = 40
AND r.created BETWEEN '2013-04-01' AND '2013-05-01'
ORDER BY r.created
but I'm not having any luck trying to use it in a crosstab because I don't know how to specify dynamic columns.
you should use crosstab with some additons. i had this problem too and solved it for my proposal like this.
first install crosstab extension
the trick i used is to create 2 additional functions. one to get the type information, needed as resultset for crosstab function. look at this:
CREATE OR REPLACE FUNCTION "TEMPORARY.TYPE.FROM.COLUMN"(text, text)
RETURNS text AS
$BODY$
DECLARE
typestring TEXT;
returnrec RECORD;
BEGIN
typestring := '';
FOR returnrec IN EXECUTE $1 LOOP
typestring := typestring||', "'||returnrec."Column"||'" '||$2;
END LOOP;
RETURN typestring;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Now we can write our second function:
CREATE OR REPLACE FUNCTION "DYNAMIC.CROSSTAB"(text, text, text)
RETURNS text AS
$BODY$
DECLARE
typestring TEXT;
executestring TEXT;
BEGIN
DROP TABLE IF EXISTS "TBL.Crosstab";
SELECT "REPORTING"."TEMPORARY.TYPE.FROM.COLUMN"($2,$3) INTO typestring;
executestring := 'CREATE TEMPORARY TABLE "TBL.Crosstab" AS (SELECT * FROM crosstab('''||$1||''','''||$2||''') AS (row_name DATE'||typestring||'));';
EXECUTE executestring;
RETURN '"TBL.Crosstab"';
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
To create your crosstab simply call:
SELECT * FROM "DYNAMIC.CROSSTAB"(text, text, text);
It returns the name of an temporary table for your session filled with your result. I needed it this way.
Parameter explanation:
First = The query to get your data (must have 3 columns: row_name, cat and value)
Second = The query to get your columns wich returns all your categories (cat)
Third = The columntype for our temporary type
it's not perfect but fit's our needs. we have statements, fetching more than 450 coloumns and some thounds rows at this way. hope it helps
This is an alternative approach using PostgreSQL arrays
create table responses (
id serial not null unique,
x integer,
created timestamp default now()
);
create table accessory (
id serial not null,
responses_id integer references responses (id),
sensor_id integer,
value text,
created timestamp default now()
);
insert into responses (x) values ( 1), (2),(3),(4);
insert into accessory (responses_id , sensor_id, value ) values
( 1, 1, 'A' ), ( 1, 2, 'Ab' ), ( 1, 3, 'Ac' ), ( 1, 4, 'Ad' ),
( 2, 4, 'Ab' ), ( 1, 2, 'bAb' ), ( 3, 3, 'bAc' ), ( 4, 4, 'bAd' );
select *, array(
select value
from accessory
where accessory.responses_id = responses.id
order by accessory.created
) as accessory_values
from responses;
Query result includes an array column with all the accessory values that match response.id

Oracle and SQLServer function evaluation in queries

Let's say I have a function call on a select or where clause in Oracle like this:
select a, b, c, dbms_crypto.hash(utl_raw.cast_to_raw('HELLO'),3)
from my_table
A similar example can be constructed for MS SQLServer.
What's the expected behavior in each case?
Is the HASH function going to be called once for each row in the table, or DBMS will be smart enough to call the function just once, since it's a function with constant parameters and no side-effects?
Thanks a lot.
The answer for Oracle is it depends. The function will be called for every row selected UNLESS the Function is marked 'Deterministic' in which case it will only be called once.
CREATE OR REPLACE PACKAGE TestCallCount AS
FUNCTION StringLen(SrcStr VARCHAR) RETURN INTEGER;
FUNCTION StringLen2(SrcStr VARCHAR) RETURN INTEGER DETERMINISTIC;
FUNCTION GetCallCount RETURN INTEGER;
FUNCTION GetCallCount2 RETURN INTEGER;
END TestCallCount;
CREATE OR REPLACE PACKAGE BODY TestCallCount AS
TotalFunctionCalls INTEGER := 0;
TotalFunctionCalls2 INTEGER := 0;
FUNCTION StringLen(SrcStr VARCHAR) RETURN INTEGER AS
BEGIN
TotalFunctionCalls := TotalFunctionCalls + 1;
RETURN Length(SrcStr);
END;
FUNCTION GetCallCount RETURN INTEGER AS
BEGIN
RETURN TotalFunctionCalls;
END;
FUNCTION StringLen2(SrcStr VARCHAR) RETURN INTEGER DETERMINISTIC AS
BEGIN
TotalFunctionCalls2 := TotalFunctionCalls2 + 1;
RETURN Length(SrcStr);
END;
FUNCTION GetCallCount2 RETURN INTEGER AS
BEGIN
RETURN TotalFunctionCalls2;
END;
END TestCallCount;
SELECT a,TestCallCount.StringLen('foo') FROM(
SELECT 0 as a FROM dual
UNION
SELECT 1 as a FROM dual
UNION
SELECT 2 as a FROM dual
);
SELECT TestCallCount.GetCallCount() AS TotalFunctionCalls FROM dual;
Output:
A TESTCALLCOUNT.STRINGLEN('FOO')
---------------------- ------------------------------
0 3
1 3
2 3
3 rows selected
TOTALFUNCTIONCALLS
----------------------
3
1 rows selected
So the StringLen() function was called three times in the first case. Now when executing with StringLen2() which is denoted deterministic:
SELECT a,TestCallCount.StringLen2('foo') from(
select 0 as a from dual
union
select 1 as a from dual
union
select 2 as a from dual
);
SELECT TestCallCount.GetCallCount2() AS TotalFunctionCalls FROM dual;
Results:
A TESTCALLCOUNT.STRINGLEN2('FOO')
---------------------- -------------------------------
0 3
1 3
2 3
3 rows selected
TOTALFUNCTIONCALLS
----------------------
1
1 rows selected
So the StringLen2() function was only called once since it was marked deterministic.
For a function not marked deterministic, you can get around this by modifying your query as such:
select a, b, c, hashed
from my_table
cross join (
select dbms_crypto.hash(utl_raw.cast_to_raw('HELLO'),3) as hashed from dual
);
For SQL server, it will be evaluated for every single row.
You will be MUCH better off by running the function once and assigning to a variable and using the variable in the query.
short answer....it depends.
If the function is accessing data ORACLE does not know if it is going to be the same for each row, therefore, it needs to query for each. If, for example, your function is just a formatter that always returns the same value then you can turn on caching (marking it as Deterministic) which may allow for you to only do the function call once.
Something you may want to look into is ORACLE WITH subquery:
The WITH query_name clause lets you
assign a name to a subquery block. You
can then reference the subquery block
multiple places in the query by
specifying the query name. Oracle
optimizes the query by treating the
query name as either an inline view or
as a temporary table
I got the quoted text from here, which has plenty of examples.