In PostgreSQL exists some ways to make a statement using bulk collect into like in Oracle?
Example in Oracle:
create or replace procedure prc_tst_bulk_test is
type typ_person is table of tb_person%rowtype;
v_tb_person typ_person;
begin
select *
bulk collect into v_tb_person
from tb_person;
-- make a selection in v_tb_person, for instance
select name, count(*) from v_tb_person where age > 50
union
select name, count(*) from v_tb_person where gender = 1
end;
In PostgreSQL 10 you can use array_agg:
declare
v_ids int[];
begin
select array_agg(id) INTO v_ids
from mytable1
where host = p_host;
--use v_ids...
end;
You'll have array and it can be used to make select from it using unnest:
select * from unnest(v_ids) where ...
There is no such syntax in PostgreSQL, nor a close functional equivalent.
You can create a temporary table in your PL/PgSQL code and use that for the desired purpose. Temp tables in PL/PgSQL are a little bit annoying because the names are global within the session, but they work correctly in PostgreSQL 8.4 and up.
A better alternative for when you're doing all the work within a single SQL statement is to use a common table expression (CTE, or WITH query). This won't be suitable for all situations.
The example above would be much better solved by a simple RETURN QUERY in PL/PgSQL, but I presume your real examples are more complex.
Assuming that tb_person is some kind of expensive-to-generate view that you don't just want to scan in each branch of the union, you could do something like:
CREATE OR REPLACE FUNCTION prc_tst_bulk()
RETURNS TABLE (name text, rowcount integer) AS
$$
BEGIN
RETURN QUERY
WITH v_tb_person AS (SELECT * FROM tb_person)
select name, count(*) from v_tb_person where age > 50
union
select name, count(*) from v_tb_person where gender = 1;
END;
$$ LANGUAGE plpgsql;
This particular case can be further simplified into a plain SQL function:
CREATE OR REPLACE FUNCTION prc_tst_bulk()
RETURNS TABLE (name text, rowcount integer) AS
$$
WITH v_tb_person AS (SELECT * FROM tb_person)
select name, count(*) from v_tb_person where age > 50
union
select name, count(*) from v_tb_person where gender = 1;
$$ LANGUAGE sql;
You can use a PostgreSQL arrays too - it is similar to Oracle's collections:
postgres=# create table _foo(a int, b int);
CREATE TABLE
postgres=# insert into _foo values(10,20);
INSERT 0 1
postgres=# create or replace function multiply()
returns setof _foo as $$
/*
* two tricks are here
* table name can be used as type name
* table name can be used as fictive column that packs all fields
*/
declare a _foo[] = (select array(select _foo from _foo));
begin
return query select * from unnest(a)
union
all select * from unnest(a);
end;
$$ language plpgsql;
CREATE FUNCTION
postgres=# select * from multiply();
a | b
----+----
10 | 20
10 | 20
(2 rows)
But in your case Craig Ringer's proposal is perfect and should be preferable.
-- Fetch the next 5 rows in the cursor_01:
FETCH FORWARD 5 FROM cursor_01;
PostgreSQL 10+ works.
https://www.postgresql.org/docs/10/sql-fetch.html
Related
i'm trying to find the total number of contacts who ARE in some lists, but who are NOT in others. basically i want to get the set difference between these two groups of contacts.
here's my function. i'm using supabase. this function works perfectly via the supabase browser sql editor
CREATE or replace FUNCTION get_net_num_contacts(to_list_ids bigint[], not_to_list_ids bigint[])
returns bigint
AS $$
declare
net_num_contacts bigint;
BEGIN
CREATE TEMP TABLE IF NOT EXISTS t1 AS
select distinct list_members.contact_id from list_members where list_members.list_id = ANY(to_list_ids);
CREATE TEMP TABLE IF NOT EXISTS t2 AS
select distinct list_members.contact_id from list_members where list_members.list_id = ANY(not_to_list_ids);
CREATE TEMP TABLE IF NOT EXISTS t3 AS
select t1.contact_id from t1 except select t2.contact_id from t2;
SELECT count(*) from t3 into net_num_contacts;
return net_num_contacts;
END;
$$ LANGUAGE plpgsql;
but when i save this to my db and try running it in my web app with an .rpc call, it always returns 0.
i did some tinkering and found that the function, loaded into supabase via my filesystem (supabase start), started returning empty sets any time i tried to create 2 or more temp tables.
this works great:
CREATE or replace FUNCTION demo_works(to_list_ids bigint[], not_to_list_ids bigint[])
returns setof bigint
AS $$
declare
net_num_contacts bigint;
BEGIN
CREATE TEMP TABLE IF NOT EXISTS t1 AS
select distinct list_members.contact_id from list_members where list_members.list_id = ANY(to_list_ids);
return query select * from t1;
END;
$$ LANGUAGE plpgsql;
doesn't work, always returns an empty array:
CREATE or replace FUNCTION demo_doesnt_work(to_list_ids bigint[], not_to_list_ids bigint[])
returns setof bigint
AS $$
declare
net_num_contacts bigint;
BEGIN
CREATE TEMP TABLE IF NOT EXISTS t1 AS
select distinct list_members.contact_id from list_members where list_members.list_id = ANY(to_list_ids);
CREATE TEMP TABLE IF NOT EXISTS t2 AS
select distinct list_members.contact_id from list_members where list_members.list_id = ANY(not_to_list_ids);
return query select * from t1;
END;
$$ LANGUAGE plpgsql;
i've seen hints online that maybe it's understood that you can't have multiple temp tables in postgres (i lost the link), and indications that some systems dont allow temp tables in functions at all.
so i'm confused as to why this works in the supabase sql editor but not after being saved into my db. and hoping to gain clarity on the limitations here, or maybe i'm doing something irrelevant wrong.
and if anyone has insight on how to properly accomplish what i'm trying to do, that would be great too
update
here's my successful alternative function for getting the set difference between these two groups of contacts, if useful for anyone. and open to feedback. but i'm still very curious about why my initial attempt didn't usually work.
CREATE or replace FUNCTION get_net_num_contacts(to_list_ids bigint[], not_to_list_ids bigint[])
returns bigint
AS $$
declare
net_num_contacts bigint;
BEGIN
WITH r0 as
(
WITH r1 AS
(
select distinct contact_id from list_members where list_id = ANY(to_list_ids)
)
select contact_id from r1 except
select contact_id from list_members where list_id = ANY(not_to_list_ids)
)
select count(*) from r0 into net_num_contacts;
return net_num_contacts;
END;
$$ LANGUAGE plpgsql;
imagine there are 5 schemas in my database and in every schema there is a common name table (ex:- table1) after every 5mins records get inserted in table1, how I can iterate in all schemas n calculate the count of table1[i have to automate the process so i am going to write the code in function and call that function after every 5mins using crontab].
Basically 2 options: Hard code schema.table and union the results. So something like:
create or replace function count_rows_in_each_table1()
returns table (schema_name text, number_or_rows integer)
language sql
as $$
select 'schema1', count(*) from schema1.table1 union all
select 'schema2', count(*) from schema2.table1 union all
select 'schema3', count(*) from schema3.table1 union all
...
select 'scheman', count(*) from scheman.table1;
$$;
The alternative being building the query dynamically from information_scheme.
create or replace function count_rows_in_each_table1()
returns table (schema_name text, number_of_rows bigint)
language plpgsql
as $$
declare
c_rows_count cursor is
select table_schema::text
from information_schema.tables
where table_name = 'table1';
l_tbl record;
l_sql_statement text = '';
l_connector text = '';
l_base_select text = 'select ''%s'', count(*) from %I.table1';
begin
for l_tbl in c_rows_count
loop
l_sql_statement = l_sql_statement ||
l_connector ||
format (l_base_select, l_tbl.table_schema, l_tbl.table_schema);
l_connector = ' union all ';
end loop;
raise notice E'Running Query: \n%', l_sql_statement;
return query execute l_sql_statement;
end;
$$;
Which is better. With few schema and few schema add/drop, opt for the first. It is direct and easily shows what you are doing. If you add/drop schema often then opt for the second. If you have many schema, but seldom add/drop them then modify the second to generate the first, save and schedule execution of the generated query.
NOTE: Not tested
I'm new to Postgres. I tried to run the following in pgAdmin/DBeaver but getting "ERROR: query has no destination for result data" error
do $$
declare customerid integer := 151;
begin
SELECT * FROM get_orders(customerid);
end $$
My guts tell me that it is something simple. What do I need to change so it will display the results in DBeaver or pgAdmin?
I don't want something like this:
SELECT * FROM get_orders(151);
I do want to use something like a variable to separate from the actual select statement.
Thanks.
script segments to prepare the table/function
------------------
CREATE TABLE orders
(
id integer,
customerid INTEGER,
description varchar(100)
)
------------------
INSERT INTO Orders VALUES
(1,101, 'Test Order 1'),
(2,151, 'Random Order')
------------------
CREATE OR REPLACE FUNCTION get_orders (p_customerid int)
RETURNS TABLE (
id integer,
customerid INTEGER,
description varchar(100)
)
AS $$
BEGIN
RETURN QUERY SELECT
*
FROM
orders ord
WHERE
ord.customerid = p_customerid;
END; $$
LANGUAGE 'plpgsql';
SQL is different from other programming languages:
It has no concept of execution order, so the idea of first setting a variable and then using it is alien to SQL.
SQL has no concept of “variables” in the first place.
In SQL, a “program” is a single statement.
So, SQL is lacking when it comes to variables and procedural programming. The remedy is to use a function in a procedural language like PL/pgSQL. Then you can use variables, and you can return the output as function result.
The DO statement has no way of returning results.
with the help of this: How to declare a variable in a PostgreSQL query.
I guess DO statement can't return results. Here are alternatives:
DO $$
declare customerid integer := 151;
BEGIN
CREATE TEMP TABLE tmp_orders ON COMMIT DROP AS
SELECT * FROM get_orders(customerid);
END $$;
SELECT * FROM tmp_orders;
-----------------------------
PREPARE temp(int) AS
SELECT * FROM get_orders($1);
EXECUTE temp(151)
-----------------------------
WITH
custid AS (VALUES (151))
SELECT * FROM get_orders((table custid))
-- DBeaver Only
#set custid = 151
SELECT * FROM get_orders(${custid});
I want to select a column from a table, with the column name being the result of a query like the following:
-- This query returns a single value
with x as (
select a from table1 where <condition>
)
-- my_function() yields a table
select x from my_function()
How do I do that?
Thank you very much.
You could write it in SQL with a temporary function:
CREATE FUNCTION pg_temp.tablefunc()
RETURNS SETOF my_function_result_type
LANGUAGE plpgsql AS
$$DECLARE
v_colname text;
BEGIN
SELECT a INTO v_colname
FROM table1
LIMIT 1;
RETURN QUERY EXECUTE
format(E'SELECT %I\n'
'FROM my_function()',
v_colname);
END;$$;
SELECT * FROM pg_temp.tablefunc();
Can we able to use SELECT statement within CASE conditional expression in function?
I have tried the following function to accomplish the above task.
Example: I have a function which is used to display table containing some rows.
create or replace function test(n integer)
returns table (name text,city text) as
$body$
begin
case n when 1 then
select * from table1
when 2 then
select * from table2
when 3 then
select * from view1
end;
end;
$body$
language plpgsql;
--Calling function
select * from test(1);
/*have to show details of table1*/
select * from test(2);
/*have to show details of table2*/
select * from test(3);
/*have to display details of view1*/
Actually, there is a plpgsql CASE statement, not to be confused with the SQL CASE expression:
CREATE OR REPLACE function test(n integer)
RETURNS TABLE (name text, city text) AS
$func$
BEGIN
CASE n
WHEN 1 THEN
RETURN QUERY SELECT t.name, t.city FROM table1 t;
WHEN 2 THEN
RETURN QUERY SELECT t.foo, t.bar FROM table2 t;
WHEN 3 THEN
RETURN QUERY SELECT t.bar, t.bamm FROM view1 t;
END CASE;
END
$func$ LANGUAGE plpgsql;
If you declare your function as RETURNS TABLE (name text, city text), then your SELECT statements should have a column list with matching types.
If on the other hand you want to SELECT *, declare the function as RETURNS SETOF table1 accordingly.
When naming columns in the return type, those variable are visible in the function body. Be sure to table-qualify column names that would conflict with same names. So t.name instead of just name.
Column names from queries inside the function are not visible outside. Only the declared return type. So names don't have to match, just data types.
Either way, I suggest to simplify things:
CREATE OR REPLACE function test(n integer)
RETURNS SETOF table1 AS
$func$
SELECT * FROM table1 t WHERE n = 1
UNION ALL
SELECT * FROM table2 t WHERE n = 2
UNION ALL
SELECT * FROM view1 t WHERE n = 3;
$func$ LANGUAGE sql;
Same result. Just as fast. SQL or PL/pgSQL function is a matter of taste and some other details. PL/pgSQL is probably faster for stand-alone calls. SQL can more easily be nested.
For this you need an IF statement and RETURN QUERY, e.g.
create or replace function test(n integer)
returns table (name text,city text) as
$body$
begin
IF n = 1 THEN
RETURN QUERY select * from table1;
ELIF n = 2 THEN
RETURN QUERY select * from table2;
ELIF n = 3 THEN
RETURN QUERY select * from view1;
END IF;
end;
$body$
language plpgsql;
It's a very weird thing to want to do, though.