How to use pl/pgSQL to handle 'comma separated list' returns? - sql

I'am trying UNION ALL many tables into a new table.The columns of the old tables are the same, but the order of the columns is different, so the below SQL statement will get wrong result:
CREATE TABLE sum_7_2018_xia_weijian
AS
(
SELECT * FROM huiwen
UNION
SELECT * FROM penglai
UNION
SELECT * FROM baoluo
UNION
SELECT * FROM dongge
UNION
SELECT * FROM resultdonglu
UNION
SELECT * FROM resultwencheng
UNION
SELECT * FROM tan_illeg
);
I finally corrected it, but the SQL statements is too redundant:
step 1. get column names of one of the old tables named huiwen
SELECT string_agg(column_name, ',')
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'huiwen';
results:
> string_agg
> ----------------------------------------------------------------------
>
> gid,id,geom,sxm,sxdm,sxxzqdm,xzqhdm,xzmc,sfzgjsyd,sfkfbj,sfjbnt,sfld,sflyhx,sfhyhx
step 2. union tables as a new table. I copy the string_agg of table huiwen to each SELECT-UNION to keep the order of columns, this is clumsy.
CREATE TABLE sum_2018_xia_weijian
AS
(
SELECT gid,id,geom,sxm,sxdm,sxxzqdm,xzqhdm,xzmc,sfzgjsyd,sfkfbj,sfjbnt,sfld,sflyhx,sfhyhx
FROM huiwen
UNION ALL
SELECT gid,id,geom,sxm,sxdm,sxxzqdm,xzqhdm,xzmc,sfzgjsyd,sfkfbj,sfjbnt,sfld,sflyhx,sfhyhx
FROM penglai
UNION ALL
SELECT gid,id,geom,sxm,sxdm,sxxzqdm,xzqhdm,xzmc,sfzgjsyd,sfkfbj,sfjbnt,sfld,sflyhx,sfhyhx
FROM baoluo
);
results:
> Query returned successfully: 2206 rows affected, 133 msec execution time.
I tried to do some optimization by pl/pgSQL using Declarations of variable to handle column names, but failed to find any SQL data type can handle this. Using of RECORD result Pseudo-Types ERROR:
CREATE or replace FUNCTION ct() RETURNS RECORD AS $$
DECLARE
clms RECORD;
BEGIN
SELECT column_name INTO clms
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'huiwen';
RETURN clms;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE sum_2018_xia_weijian
AS
(
SELECT ct() FROM huiwen
UNION ALL
SELECT ct() FROM penglai
UNION ALL
SELECT ct() FROM baoluo
UNION ALL
SELECT ct() FROM dongge
UNION ALL
SELECT ct() FROM resultdonglu
UNION ALL
SELECT ct() FROM resultwencheng
UNION ALL
SELECT ct() FROM tan_illeg
);

You may use STRING_AGG twice for getting the UNION ALL. You can get all the columns in specific order by explicitly ordering it by column_name in the string_agg.
Here's a generic function which takes an array of tables and a final table name.
CREATE or replace FUNCTION fn_create_tab(tname_arr TEXT[], p_tab_name TEXT)
RETURNS VOID AS $$
DECLARE
l_select TEXT;
BEGIN
select STRING_AGG(query,' UNION ALL ' ) INTO l_select
FROM
(
SELECT 'select ' || string_agg( column_name,','
ORDER BY column_name ) || ' from ' || table_name as query
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = ANY (tname_arr)
GROUP BY table_name
) s;
IF l_select IS NOT NULL
THEN
EXECUTE format ('DROP TABLE IF EXISTS %I',p_tab_name);
EXECUTE format ('create table %I AS %s',p_tab_name,l_select);
END IF;
END;
$$ LANGUAGE plpgsql;
Now, run the function like this:
select fn_create_tab(ARRAY['huiwen','penglai'],'sum_2018_xia_weijian');

Instead of making the programming block complex you can follow some below concepts from the documentation of Union or Union All as it says :
The number of columns in all queries must be the same.
The corresponding columns must have the compatible data type.
The column names of the first query determine the column names of the combined result set.
The GROUP BY and HAVING clauses are applied to each individual query, not the final result set.
The ORDER BY clause is applied to the combined result set, not within the individual result set.
By following the 3rd point make your Union query adjusted to refer to the table whose column order is expected in the result.

Related

Trying to query a redshift within SELECT statement

Current table1:
col1
-------------
schema.table1
schema.table2
schema.table3
Desired table1:
col1 col2
------------------------------------------------------------
schema.table1 value of (select count(*) from schema.table1)
schema.table2 value of (select count(*) from schema.table1)
schema.table3 value of (select count(*) from schema.table1)
It is not working, I tried using function too, but function doesn't allow to use 'FROM'
select col1, (select count(*) from col1)
from table1
I am trying to create this query in redshift. Can anyone please help me out?
To perform this task you will need a stored procedure AND a defined cursor. The stored procedure allows for looping and the cursor provides the ability to execute a newly created statement (dynamic querying).
For example:
Create the starting materials, 3 tables and a table that references these tables.
create table foo as (select 1 as A);
create table goo as (select 2 as A);
create table hoo as (select 3 as A);
create table tabs as (select 'foo' as tab union all select 'goo' union all select 'hoo');
Next define the stored procedure the will create the dynamic SQL
CREATE OR REPLACE procedure count_tabs(curs1 INOUT refcursor)
AS
$$
DECLARE
row record;
statement varchar := '';
union_needed BOOL := false;
BEGIN
for row in select tab from tabs LOOP
IF union_needed THEN
statement := statement || ' UNION ALL ';
END IF;
statement := statement || 'select \'' || row.tab || '\' as table_name, count(*) as table_count from ' || row.tab ;
union_needed := true;
END LOOP;
RAISE NOTICE 'sql to execute: %',statement;
open curs1 for execute statement;
END;
$$ LANGUAGE plpgsql;
Lastly we need to call the procedure and execute the cursor
call count_tabs('mycursor');
fetch 1000 from mycursor;
A few notes on this:
This assumes you want the results as output on your bench. If you want to create a table with the results this is doable in the same structure
Since the FROM clause value(s) is unknown at compile time this needs to be done in 2 steps - create the query and then execute the query.
I believe you can have the procedure walk this same cursor itself but doing this is exceptionally slow

How to select values from a table, whose name is derived from another table?

I have a table called folder that stores the name of others tables (named fileXXX, where X is a digit), having the same structure, in the same Postgres DB.
I want to build up a SQL statement that retrieve the name of all the fileXXX tables in the DB from the folder table and create a single SQL Statement with this structure
SELECT * FROM _file001_
UNION
SELECT * FROM _file002_
UNION
SELECT * FROM _file003_
...
I've found a lot of example on how to use SELECT statements in the WHERE clause, but none for using one in the FROM clause, in such this way.
It is possible to write a function for that (see here)
demo:db<>fiddle
Query all table names from information schema:
SELECT table_name
FROM information_schema.tables
WHERE table_name LIKE 'file%'
Instead of SELECT table_name write include table name into query string
SELECT
'SELECT * FROM ' || table_name
...
Group every result row with string_agg, use UNION ALL as delimiter:
SELECT
string_agg(/*see (2)*/, ' UNION ALL ')
...
This results in your query you mentioned in the question.
Last this string can be interpreted as real query and can be executed within this function:
CREATE OR REPLACE function union_all() returns table (ids int) AS $$
declare
_t text := '';
begin
SELECT
string_agg('SELECT * FROM ' || table_name, ' UNION ALL ')
into _t
FROM information_schema.tables
WHERE table_name LIKE 'file%';
return query execute _t;
end;$$ language plpgsql;
call this function:
SELECT * FROM union_all()

Postgres array value must starts

I want to make an array and put into it two id's, but I got a mistake:
array value must start with “{” or dimension information
ids_list character varying[] := ' || (SELECT COALESCE(quote_literal((array_agg(DISTINCT house_guid)) || ''',''' || quote_literal(array_agg(DISTINCT guid))), 'NULL') FROM tb) || ';
use array_agg function
with t1 as
(
select * from
(
select 'test_SQL_01' as ID
union
select 'test_SQL_02_PQR_01'
union
select 'test_SQL_03_055'
union
select 'test_SQL_04_ABC_99'
) as t
) select array_agg(ID) from t1
You seem to be using this inside a PL/pgSQL function. You should be using SELECT ... INTO variable FROM... instead:
declare
ids_list character varying[];
begin
.....
select array_agg(id)
into ids_list
from (
select house_guid
from tab
union
select guid
from tab
) t;
.... work with the ids_list variable
end;
The UNION will automatically remove all duplicates (as you tried to do with DISTINCT.

Dynamic UNION ALL query in Postgres

We are using a Postgres / PostGis connection to get data that is published via a geoserver.
The Query looks like this at the moment:
SELECT
row_number() over (ORDER BY a.ogc_fid) AS qid, a.wkb_geometry AS geometry
FROM
(
SELECT * FROM test
UNION ALL
SELECT * FROM test1
UNION ALL
SELECT * FROM test2
)a
In our db only valid shapefiles will be imported each in a single table so it would make sense to make the UNION ALL part dynamic (loop over each table and make the UNION ALL statement). Is there a way to do this in a standard Postgres way or do I need to write a function and how would the syntax look like? I am pretty new to SQL.
The shapefiles have a different data structure and only the ogc_fid column and the wkb_geometry column are always available and we would like to union all tables from the DB.
This is just general guidelines you need work in the details specially syntaxis.
You need create a store procedure
Create a loop checking information_schema.tables filter for the tablenames you want
DECLARE
rec record;
strSQL text;
BEGIN
Then create a strSQL with each table
FOR rec IN SELECT table_schema, table_name
FROM information_schema.tables
LOOP
strSQL := strSQL || 'SELECT ogc_fid, wkb_geometry FROM ' ||
rec.table_schema || '.' || rec.table_name || ' UNION ';
END LOOP;
-- have to remove the last ' UNION ' from strSQL
strSQL := 'SELECT row_number() over (ORDER BY a.ogc_fid) AS qid,
a.wkb_geometry AS geometry FROM (' || strSQL || ')';
EXECUTE strSQL;
One solution is to serialize the rest of the columns to json with row_to_json(). (available since PostgreSQL9.2).
For PG9.1 (and earlier) you can use hstore, but note that all values are cast to text.
Why serialize? It is not possible to union rows where the number of colums vary, or the datatypes do not match between the union queries.
I created a quick example to illustrate:
--DROP SCHEMA testschema CASCADE;
CREATE SCHEMA testschema;
CREATE TABLE testschema.test1 (
id integer,
fid integer,
metadata text
);
CREATE TABLE testschema.test2 (
id integer,
fid integer,
city text,
count integer
);
CREATE TABLE testschema.test3 (
id integer,
fid integer
);
INSERT INTO testschema.test1 VALUES (1, 4450, 'lala');
INSERT INTO testschema.test2 VALUES (33, 6682, 'London', 12345);
INSERT INTO testschema.test3 VALUES (185, 8991);
SELECT
row_number() OVER (ORDER BY a.fid) AS qid, a.*
FROM
(
SELECT id, fid, row_to_json(t.*) AS jsondoc FROM testschema.test1 t
UNION ALL
SELECT id, fid, row_to_json(t.*) AS jsondoc FROM testschema.test2 t
UNION ALL
SELECT id, fid, row_to_json(t.*) AS jsondoc FROM testschema.test3 t
) a
SELECT output:
qid id fid jsondoc
1; 1; 4450; "{"id":1,"fid":4450,"metadata":"lala"}"
2; 33; 6682; "{"id":33,"fid":6682,"city":"London","count":12345}"
3; 185; 8991; "{"id":185,"fid":8991}"

How to select a column from all tables in which it resides?

I have many tables that have the same column 'customer_number'.
I can get a list of all these table by query:
SELECT table_name FROM ALL_TAB_COLUMNS
WHERE COLUMN_NAME = 'customer_number';
The question is how do I get all the records that have a specific customer number from all these tables without running the same query against each of them.
To get record from a table, you have write a query against that table. So, you can't get ALL the records from tables with specified field without a query against each one of these tables.
If there is a subset of columns that you are interested in and this subset is shared among all tables, you may use UNION/UNION ALL operation like this:
select * from (
select customer_number, phone, address from table1
union all
select customer_number, phone, address from table2
union all
select customer_number, phone, address from table3
)
where customer_number = 'my number'
Or, in simple case where you just want to know what tables have records about particular client
select * from (
select 'table1' src_tbl, customer_number from table1
union all
select 'table2', customer_number from table2
union all
select 'table3', customer_number from table3
)
where customer_number = 'my number'
Otherwise you have to query each table separatelly.
DBMS_XMLGEN enables you to run dynamic SQL statements without custom PL/SQL.
Sample Schema
create table table1(customer_number number, a number, b number);
insert into table1 values(1,1,1);
create table table2(customer_number number, a number, c number);
insert into table2 values(2,2,2);
create table table3(a number, b number, c number);
insert into table3 values(3,3,3);
Query
--Get CUSTOMER_NUMBER and A from all tables with the column CUSTOMER_NUMBER.
--
--Convert XML to columns.
select
table_name,
to_number(extractvalue(xml, '/ROWSET/ROW/CUSTOMER_NUMBER')) customer_number,
to_number(extractvalue(xml, '/ROWSET/ROW/A')) a
from
(
--Get results as XML.
select table_name,
xmltype(dbms_xmlgen.getxml(
'select customer_number, a from '||table_name
)) xml
from user_tab_columns
where column_name = 'CUSTOMER_NUMBER'
);
TABLE_NAME CUSTOMER_NUMBER A
---------- --------------- -
TABLE1 1 1
TABLE2 2 2
Warnings
These overly generic solutions often have issues. They won't perform as well as a plain old SQL statements and they are more likely to run into bugs. In general, these types of solutions should be avoided for production code. But they are still very useful for ad hoc queries.
Also, this solution assumes that you want the same columns from each row. If each row is different then things get much more complicated and you may need to look into technologies like ANYDATASET.
I assume you want to automate this. Two approaches.
SQL to generate SQL scripts
.
spool run_rep.sql
set head off pages 0 lines 200 trimspool on feedback off
SELECT 'prompt ' || table_name || chr(10) ||
'select ''' || table_name ||
''' tname, CUSTOMER_NUMBER from ' || table_name || ';' cmd
FROM all_tab_columns
WHERE column_name = 'CUSTOMER_NUMBER';
spool off
# run_rep.sql
PLSQL
Similar idea to use dynamic sql:
DECLARE
TYPE rcType IS REF CURSOR;
rc rcType;
CURSOR c1 IS SELECT table_name FROM all_table_columns WHERE column_name = 'CUST_NUM';
cmd VARCHAR2(4000);
cNum NUMBER;
BEGIN
FOR r1 IN c1 LOOP
cmd := 'SELECT cust_num FROM ' || r1.table_name ;
OPEN rc FOR cmd;
LOOP
FETCH rc INTO cNum;
EXIT WHEN rc%NOTFOUND;
-- Prob best to INSERT this into a temp table and then
-- select * that to avoind DBMS_OUTPUT buffer full issues
DBMS_OUTPUT.PUT_LINE ( 'T:' || r1.table_name || ' C: ' || rc.cust_num );
END LOOP;
CLOSE rc;
END LOOP;
END;