Left join with dynamic table name derived from column - sql

I am new in PostgreSQL and I wonder if it's possible to use number from table tbc as part of the table name in left join 'pa' || number. So for example if number is 456887 I want left join with table pa456887. Something like this:
SELECT tdc.cpa, substring(tdc.ku,'[0-9]+') AS number, paTab.vym
FROM public."table_data_C" AS tdc
LEFT JOIN concat('pa' || number) AS paTab ON (paTab.cpa = tdc.cpa)
And I want to use only PostgreSQL, not additional code in PHP for example.

Either way, you need dynamic SQL.
Table name as given parameter
CREATE OR REPLACE FUNCTION foo(_number int)
RETURNS TABLE (cpa int, nr text, vym text) AS -- adapt to actual data types!
$func$
BEGIN
RETURN QUERY EXECUTE format(
'SELECT t.cpa, substring(t.ku,'[0-9]+'), p.vym
FROM public."table_data_C" t
LEFT JOIN %s p USING (cpa)'
, 'pa' || _number
);
END
$func$ LANGUAGE plpgsql;
Call:
SELECT * FROM foo(456887)
Generally, you would sanitize table names with format ( %I ) to avoid SQL injection. With just an integer as dynamic input that's not necessary. More details and links in this related answer:
INSERT with dynamic table name in trigger function
Data model
There may be good reasons for the data model. Like partitioning / sharding or separate privileges ...
If you don't have such a good reason, consider consolidating multiple tables with identical schema into one and add the number as column. Then you don't need dynamic SQL.
Consider inheritance. Then you can add a condition on tableoid to only retrieve rows from a given child table:
SELECT * FROM parent_table
WHERE tableoid = 'pa456887'::regclass
Be aware of limitations for inheritance, though. Related answers:
Get the name of a row's source table when querying the parent it inherits from
Select (retrieve) all records from multiple schemas using Postgres
Name of 2nd table depending on value in 1st table
Deriving the name of the join table from values in the first table dynamically complicates things.
For only a few tables
LEFT JOIN each on tableoid. There is only one match per row, so use COALESCE.
SELECT t.*, t.tbl, COALESCE(p1.vym, p2.vym, p3.vym) AS vym
FROM (
SELECT cpa, ('pa' || substring(ku,'[0-9]+'))::regclass AS tbl
FROM public."table_data_C"
-- WHERE <some condition>
) t
LEFT JOIN pa456887 p1 ON p1.cpa = t.cpa AND p1.tableoid = t.tbl
LEFT JOIN pa456888 p2 ON p2.cpa = t.cpa AND p2.tableoid = t.tbl
LEFT JOIN pa456889 p3 ON p3.cpa = t.cpa AND p3.tableoid = t.tbl
For many tables
Combine a loop with dynamic queries:
CREATE OR REPLACE FUNCTION foo(_number int)
RETURNS TABLE (cpa int, nr text, vym text) AS
$func$
DECLARE
_nr text;
BEGIN
FOR _nr IN
SELECT DISTINCT substring(ku,'[0-9]+')
FROM public."table_data_C"
LOOP
RETURN QUERY EXECUTE format(
'SELECT t.cpa, _nr, p.vym
FROM public."table_data_C" t
LEFT JOIN %I p USING (cpa)
WHERE t.ku LIKE (_nr || '%')'
, 'pa' || _nr
);
END LOOP;
END
$func$ LANGUAGE plpgsql;

Related

Query table within variable schemanames

I want to run a query of a table within variable schemas.
I have a postgresql db that I've written the following query for:
SELECT
project_id, project_name, schema_p
FROM public.projects
JOIN public.schema_ps
ON public.schema_ps.root_id = public.projects.project_id
ORDER BY project_id ASC
The schema_p column contains the names of schemas that all have a table named com_set that I need to query as follows:
SELECT dev_id, dev_name FROM [variable_schama_name].com_set
What I'd like to do is write a query to generate a single combined result, where my resulting dataset would contain the columns of project_id, project_name, schema_p, dev_id, and dev_name. This would mean I'd have repeats in the first three columns and unique entries in the second last two columns. One way to think of it is that I'd like to run the first query to get the names of the schemas, then run the second query on each of the schemas.
Hopefully that all makes sense. Thanks for the help!
A possible solution relies on a dynamic query without guarantee about the performance :
CREATE OR REPLACE FUNCTION my_query(OUT project_id int, OUT project_name text, OUT schema_p text, OUT dev_id int, OUT dev_name text)
RETURNS setof record LANGUAGE plpgsql AS $$
DECLARE
txt text ;
BEGIN
SELECT string_agg(
'SELECT p.project_id, p.project_name, s.schema_p, c.dev_id, c.dev_name
FROM public.projects AS p
INNER JOIN public.schema_ps AS s
ON s.root_id = p.project_id
CROSS JOIN ' || ps.schema_p || '.com_set AS c', ' UNION ALL ')
|| ' ORDER BY project_id ASC'
FROM public.schema_ps AS ps
INTO txt ;
RETURN QUERY EXECUTE txt ;
END ; $$ ;
and then you just call my_query :
SELECT my_query() ;

Table variable join equivalent in Oracle

I'm given quite a huge table My_Table and a user-defined collection Picture_Arr as an input usually having 5-10 rows declared as:
TYPE Picture_Rec IS RECORD (
seq_no NUMBER,
task_seq NUMBER);
TYPE Picture_Arr IS TABLE OF Picture_Rec;
In MS SQL I would normally write something like:
DECLARE #Picture_Arr TABLE (seq_no INT, task_seq INT)
SELECT M.*
FROM My_Table M
INNER JOIN #Picture_Arr A
ON M.seq_no = A.seq_no AND M.task_seq = A.task_seq
But I can't get my head around how to re-write the same code in Oracle as Picture_Arr is not a table. As some tutorials state that I could've looped through My_Table and compare keys, but is it efficient in Oracle or is there another way of doing that?
Perhaps this is what you are looking for. It is a bit complicated to understand what is the desired output, and whether the data of the record is stored somewhere or not
create type Picture_Rec as object(
seq_no NUMBER,
task_seq NUMBER);
)
/
create type Picture_Tab as table of Picture_Rec
/
create or replace function get_picture_list
return Picture_Tab
is
l_pic Picture_Tab;
begin
select Picture_Rec ( seqno, taskseq )
bulk collect into l_pic
from your_table; -- the table you have these records
return l_pic;
end;
/
Then you run
SELECT M.*
FROM My_Table M
JOIN TABLE ( get_picture_list() ) p
ON M.seq_no = p.seq_no AND M.task_seq = p.task_seq

Oracle function with select all from tables

SELECT DISTINCT L.* FROM LABALES L , MATCHES M
WHERE M.LIST LIKE '%ENG'
ORDER BY L.ID
I need to create function with this select, I tried this but it doesn't work.
CREATE OR REPLACE FUNCTION getSoccerLists
RETURN varchar2 IS
list varchar2(2000);
BEGIN
SELECT DISTINCT L.* FROM LABALES L , MATCHES M
WHERE M.LIST LIKE '%ENG'
ORDER BY L.ID
return list;
END;
How will I create function that returns all from table L.
Thanks
You may use implicit result using DBMS_SQL.RETURN_RESULT(Oracle12c and above) in a procedure using a cursor to your query.
CREATE OR REPLACE PROCEDURE getSoccerLists
AS
x SYS_REFCURSOR;
BEGIN
OPEN x FOR SELECT DISTINCT L.* FROM LABALES L
JOIN MATCHES M ON ( 1=1 ) -- join condition
WHERE M.LIST LIKE '%ENG'
ORDER BY L.ID;
DBMS_SQL.RETURN_RESULT(x);
END;
/
then simply call the procedure
EXEC getSoccerLists;
For lower versions(Oracle 11g) , you may use a print command to display the cursor's o/p passing ref cursor as out parameter.
CREATE OR REPLACE PROCEDURE getSoccerLists (x OUT SYS_REFCURSOR)
AS
BEGIN
OPEN x FOR SELECT DISTINCT L.* FROM LABALES L
JOIN MATCHES M ON ( 1=1 ) -- join condition
WHERE M.LIST LIKE '%ENG'
ORDER BY L.ID;
END;
/
Then, in SQL* Plus or running as script in SQL developer and Toad, you may get the results using this.
VARIABLE r REFCURSOR;
EXEC getSoccerLists (:r);
PRINT r;
Another option is to use TABLE function by defining a collection of the record type of the result within a package.
Refer Create an Oracle function that returns a table
I guess this questions is a repetition of the your previously asked question, where you wanted to get all the columns of tables but into separate column. I already answered in stating this you cannot do if you call your function via a SELECT statement. If you call your function in a Anoymous block you can display it in separate columns.
Here Oracle function returning all columns from tables
Alternatively, you can get the results separated by a comma(,) or pipe (|) as below:
CREATE OR REPLACE
FUNCTION getSoccerLists
RETURN VARCHAR2
IS
list VARCHAR2(2000);
BEGIN
SELECT col1
||','
||col2
||','
||col2
INTO LIST
FROM SOCCER_PREMATCH_LISTS L ,
SOCCER_PREMATCH_MATCHES M
WHERE M.LIST LIKE '%' || (L.SUB_LIST) || '%'
AND (TO_TIMESTAMP((M.M_DATE || ' ' || M.M_TIME), 'DD.MM.YYYY HH24:MI') >
(SELECT SYSTIMESTAMP AT TIME ZONE 'CET' FROM DUAL
))
ORDER BY L.ID");
Return list;
End;
Note here if the column size increased 2000 chars then again you will lose the data.
Edit:
From your comments
I want it to return a table set of results.
You then need to create a table of varchar and then return it from the function. See below:
CREATE TYPE var IS TABLE OF VARCHAR2(2000);
/
CREATE OR REPLACE
FUNCTION getSoccerLists
RETURN var
IS
--Initialization
list VAR :=var();
BEGIN
SELECT NSO ||',' ||NAME BULK COLLECT INTO LIST FROM TEST;
RETURN list;
END;
Execution:
select * from table(getSoccerLists);
Note: Here in the function i have used a table called test and its column. You replace your table with its columnname.
Edit 2:
--Create a object with columns same as your select statement
CREATE TYPE v_var IS OBJECT
(
col1 NUMBER,
col2 VARCHAR2(10)
)
/
--Create a table of your object
CREATE OR REPLACE TYPE var IS TABLE OF v_var;
/
CREATE OR REPLACE FUNCTION getSoccerLists
RETURN var
IS
--Initialization
list VAR :=var();
BEGIN
--You above object should have same columns with same data type as you are selecting here
SELECT v_var( NSO ,NAME) BULK COLLECT INTO LIST FROM TEST;
RETURN list;
END;
Execution:
select * from table(getSoccerLists);
This is not an answer on how to build a function for this, as I'd recommend to make this a view instead:
CREATE OR REPLACE VIEW view_soccer_list AS
SELECT *
FROM soccer_prematch_lists l
WHERE EXISTS
(
SELECT *
FROM soccer_prematch_matches m
WHERE m.list LIKE '%' || (l.sub_list) || '%'
AND TO_TIMESTAMP((m.m_date || ' ' || m.m_time), 'DD.MM.YYYY HH24:MI') >
(SELECT SYSTIMESTAMP AT TIME ZONE 'CET' FROM DUAL)
);
Then call it in a query:
SELECT * FROM view_soccer_list ORDER BY id;
(It makes no sense to put an ORDER BY clause in a view, because you access the view like a table, and table data is considered unordered, so you could not rely on that order. The same is true for a pipelined function youd access with FROM TABLE (getSoccerLists). Always put the ORDER BY clause in your final queries instead.)

Find out which schema based on table values

My database is separated into schemas based on clients (i.e.: each client has their own schema, with same data structure).
I also happen to have an external action that does not know which schema it should target. It comes from another part of the system that has no concepts of clients and does not know in which client's set it is operating. Before I process it, I have to find out which schema that request needs to target
To find the right schema, I have to find out which holds the record R with a particular unique ID (string)
From my understanding, the following
SET search_path TO schema1,schema2,schema3,...
will only look through the tables in schema1 (or the first schema that matches the table) and will not do a global search.
Is there a way for me to do a global search across all schemas, or am I just going to have to use a for loop and iterate through all of them, one at a time?
You could use inheritance for this. (Be sure to consider the limitations.)
Consider this little demo:
CREATE SCHEMA master; -- no access of others ..
CREATE SEQUENCE master.myseq; -- global sequence for globally unique ids
CREATE table master.tbl (
id int primary key DEFAULT nextval('master.myseq')
, foo text);
CREATE SCHEMA x;
CREATE table x.tbl() INHERITS (master.tbl);
INSERT INTO x.tbl(foo) VALUES ('x');
CREATE SCHEMA y;
CREATE table y.tbl() INHERITS (master.tbl);
INSERT INTO y.tbl(foo) VALUES ('y');
SELECT * FROM x.tbl; -- returns 'x'
SELECT * FROM y.tbl; -- returns 'y'
SELECT * FROM master.tbl; -- returns 'x' and 'y' <-- !!
Now, to actually identify the table a particular row lives in, use the tableoid:
SELECT *, tableoid::regclass AS table_name
FROM master.tbl
WHERE id = 2;
Result:
id | foo | table_name
---+-----+-----------
2 | y | y.tbl
You can derive the source schema from the tableoid, best by querying the system catalogs with the tableoid directly. (The displayed name depends on the setting of search_path.)
SELECT n.nspname
FROM master.tbl t
JOIN pg_class c ON c.oid = t.tableoid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE t.id = 2;
This is also much faster than looping through many separate tables.
You will have to iterate over all namespaces. You can get a lot of this information from the pg_* system catalogs. In theory, you should be able to resolve the client -> schema mapping at request time without talking to the database so that the first SQL call you make is:
SET search_path = client1,global_schema;
While I think Erwin's solution is probably preferable if you can re-structure your tables, an alternative that doesn't require any schema changes is to write a PL/PgSQL function that scans the tables using dynamic SQL based on the system catalog information.
Given:
CREATE SCHEMA a;
CREATE SCHEMA b;
CREATE TABLE a.testtab ( searchval text );
CREATE TABLE b.testtab (LIKE a.testtab);
INSERT INTO a.testtab(searchval) VALUES ('ham');
INSERT INTO b.testtab(searchval) VALUES ('eggs');
The following PL/PgSQL function searches all schemas containing tables named _tabname for values in _colname equal to _value and returns the first matching schema.
CREATE OR REPLACE FUNCTION find_schema_for_value(_tabname text, _colname text, _value text) RETURNS text AS $$
DECLARE
cur_schema text;
foundval integer;
BEGIN
FOR cur_schema IN
SELECT nspname
FROM pg_class c
INNER JOIN pg_namespace n ON (c.relnamespace = n.oid)
WHERE c.relname = _tabname AND c.relkind = 'r'
LOOP
EXECUTE
format('SELECT 1 FROM %I.%I WHERE %I = $1',
cur_schema, _tabname, _colname
) INTO foundval USING _value;
IF foundval = 1 THEN
RETURN cur_schema;
END IF;
END LOOP;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
If there are are no matches then null is returned. If there are multiple matches the result will be one of them, but no guarantee is made about which one. Add an ORDER BY clause to the schema query if you want to return (say) the first in alphabetical order or something. The function is also trivially modified to return setof text and RETURN NEXT cur_schema if you want to return all the matches.
regress=# SELECT find_schema_for_value('testtab','searchval','ham');
find_schema_for_value
-----------------------
a
(1 row)
regress=# SELECT find_schema_for_value('testtab','searchval','eggs');
find_schema_for_value
-----------------------
b
(1 row)
regress=# SELECT find_schema_for_value('testtab','searchval','bones');
find_schema_for_value
-----------------------
(1 row)
By the way, you can re-use the table definitions without inheritance if you want, and you really should. Either use a common composite data type:
CREATE TYPE public.testtab AS ( searchval text );
CREATE TABLE a.testtab OF public.testtab;
CREATE TABLE b.testtab OF public.testtab;
in which case they share the same data type but not any data; or or via LIKE:
CREATE TABLE public.testtab ( searchval text );
CREATE TABLE a.testtab (LIKE public.testtab);
CREATE TABLE b.testtab (LIKE public.testtab);
in which case they're completely unconnected to each other after creation.

Concatenate multiple fields in query string in plpgsql

I am using plpgsql and hibernate and want to create a function which contains the query string given below. In the select clause I want to concatenate 3 fields but while running this query I am getting error message like:
ERROR: syntax error at or near "' '"
SQL state: 42601
Context: PL/pgSQL function "est_fn_dept_wise_emp_report" line 30 at open
I am new using stored functions, it might be a basic question but somehow I was unable to find a solution.
query1 = 'SELECT est_emp_empmaster.emp_no AS est_emp_empmaster_emp_no,
adm_m_department.dept_name AS adm_m_department_dept_name,
adm_m_subdepartment.sub_dept_id AS adm_m_subdepartment_sub_dept_id,
adm_m_subdepartment.sub_dept_name AS adm_m_subdepartment_sub_dept_name,
est_m_designation.desig_name AS est_m_designation_desig_name,
est_emp_empmaster.first_name'|| ' ' ||'est_emp_empmaster.middle_name'|| ' ' ||'est_emp_empmaster.surname AS empname
FROM public.adm_m_department adm_m_department
INNER JOIN public.adm_m_subdepartment adm_m_subdepartment
ON adm_m_department.dept_id = adm_m_subdepartment.dept_id
INNER JOIN public.est_emp_empmaster est_emp_empmaster
ON adm_m_department.dept_id = est_emp_empmaster.dept_id
AND adm_m_subdepartment.sub_dept_id = est_emp_empmaster.sub_dept_id
INNER JOIN public.est_emp_salary est_emp_salary
ON est_emp_empmaster.emp_no = est_emp_salary.emp_no
INNER JOIN public.est_m_designation est_m_designation
ON est_emp_salary.pre_desig_code = est_m_designation.desig_code
AND est_emp_salary.retired_flag ='|| quote_literal('N') ||'
WHERE est_emp_empmaster.corp_coun_id=0 or est_emp_empmaster.corp_coun_id is null or est_emp_empmaster.corp_coun_id = '|| quote_literal($1) ||'
ORDER BY adm_m_department.dept_id,adm_m_subdepartment.sub_dept_id,est_emp_empmaster.emp_no ASC';
OPEN refcur FOR
EXECUTE query1;
LOOP
FETCH refcur INTO return_record;
EXIT WHEN NOT FOUND;
RETURN NEXT return_record;
END LOOP;
CLOSE refcur;**
The above query runs fine if I execute it without executing through query string. But as I want to use this query for multiple conditions and in those condition I want to modify this query to get different results.
This can be much simpler, safer and faster (assuming at least Postgres 8.4):
CREATE OR REPLACE FUNCTION foo(_corp_coun_id int) -- guessing type
RETURNS TABLE (
emp_no int -- guessing data types ..
,dept_name text -- .. replace with actual types
,sub_dept_id int
,sub_dept_name text
,desig_name text
,empname text) AS
$func$
BEGIN
RETURN QUERY
SELECT em.emp_no
,dp.dept_name
,sb.sub_dept_id
,sb.sub_dept_name
,ds.desig_name
,concat_ws(' ', em.first_name, em.middle_name, em.surname) -- AS empname
FROM adm_m_department dp
JOIN adm_m_subdepartment su ON sb.dept_id = dp.dept_id
JOIN est_emp_empmaster em ON em.dept_id = sb.dept_id
AND em.sub_dept_id = sb.sub_dept_id
JOIN est_emp_salary sl ON sl.emp_no = em.emp_no
AND sl.retired_flag = 'N' -- untangled join cond.
JOIN est_m_designation ds ON ds.desig_code = sl.pre_desig_code
WHERE em.corp_coun_id = 0 OR
em.corp_coun_id IS NULL OR
em.corp_coun_id = $1
ORDER BY dp.dept_id, sb.sub_dept_id, em.emp_no;
END
$func$
LANGUAGE plpgsql SET search_path=public;
To address your primary question: use concat_ws() for simple and secure concatenation of multiple columns (doesn't fail with NULL).
You do not need dynamic SQL here since the variables are only values (not identifiers).
You do not need a CURSOR here.
You do not need a LOOP. RETURN QUERY does the same, simpler and faster.
You do not need column aliases, only the names of the OUT parameters (implicitly the column names in RETURNS TABLE (...)) are relevant.
Replace the multiple schema qualification public. in your query with a single SET search_path = public.
I also untangled your query, used short table aliases and reformatted to make it easier to read.
You don't even need plpgsql at all here. Can be a simpler SQL function:
CREATE OR REPLACE FUNCTION foo(_corp_coun_id int)
RETURNS TABLE (
emp_no int -- guessing data types ..
,dept_name text -- .. replace with actual types!
,sub_dept_id int
,sub_dept_name text
,desig_name text
,empname text) AS
$func$
SELECT em.emp_no
,dp.dept_name
,sb.sub_dept_id
,sb.sub_dept_name
,ds.desig_name
,concat_ws(' ', em.first_name, em.middle_name, em.surname) -- AS empname
FROM adm_m_department dp
JOIN adm_m_subdepartment sb ON sb.dept_id = dp.dept_id
JOIN est_emp_empmaster em ON em.dept_id = sb.dept_id
AND em.sub_dept_id = sb.sub_dept_id
JOIN est_emp_salary sl ON sl.emp_no = em.emp_no
AND sl.retired_flag = 'N' -- untangled join cond.
JOIN est_m_designation ds ON ds.desig_code = sl.pre_desig_code
WHERE em.corp_coun_id = 0 OR
em.corp_coun_id IS NULL OR
em.corp_coun_id = $1
ORDER BY dp.dept_id, sb.sub_dept_id, em.emp_no;
$func$
LANGUAGE sql SET search_path=public;
I found a solution to the above problem,
actually it was well working in normal query but i got problem while running it in dynamic query.
The solution to the above problem is as follows.Thanks again.. :)
est_emp_empmaster.first_name||'' ''||est_emp_empmaster.middle_name||'' ''||est_emp_empmaster.surname AS empname