Get child table names in Postgres query [duplicate] - sql

This question already has answers here:
Get the name of a row's source table when querying the parent it inherits from
(2 answers)
Closed 5 years ago.
I have a base table
CREATE TABLE base_table (
id SERIAL PRIMARY KEY
);
And two child tables that inherit from it
CREATE TABLE child_a (
description TEXT
) INHERITS (base_table);
CREATE TABLE child_b (
age INT
) INHERITS (base_table);
I'm trying to write a query that will return all of the fields from the base table and all of its children but also append the name of each row's 'parent' table to the results.
So far, I can get all the other fields I need with
SELECT id, description, age
FROM ONLY base_table
NATURAL FULL JOIN child_a
NATURAL FULL JOIN child_b
I'm a little stumped on how to include the parent table name in the results. I'd like the results to be something like
Id, Description, Age, TableName
---------------------------------------
1 TestDescription null child_a
2 null 10 child_b
3 OtherDescription null child_a
Any help would be appreciated!

Use subqueries instead of child tables:
SELECT id, description, age, coalesce(t1, t2) as tablename
FROM base_table
NATURAL FULL JOIN (
SELECT *, 'child_a'::text AS t1
FROM child_a
) a
NATURAL FULL JOIN (
SELECT *, 'child_b'::text AS t2
FROM child_b
) b;
id | description | age | tablename
----+------------------+-----+-----------
1 | TestDescription | | child_a
2 | | 10 | child_b
3 | OtherDescription | | child_a
(3 rows)
Update. There is a nice solution described in ths answer. To use it in this case you have to explicitly specify a base_table for a system column tableoid:
SELECT *, base_table.tableoid::regclass::text AS table_name
FROM base_table
FULL JOIN child_a USING(id)
FULL JOIN child_b USING(id);

Related

SQL: Count references to item in same table

I have the following SQL table:
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(200),
parent int(11),
This stores some structured information in a tree:
"id" is the primary (uniq) key of the entries
"name" is some string
"parent" is the "id" of the parent entry (0: root element)
A sample table could be:
id name parent
--+-----------------+----------------
1 root_a 0
2 root_b 0
3 sub_b1 2
4 sub_sub_b1_1 3
5 sub_sub_b1_2 3
This could be a directory with folder ("root_"), sub-folder ("sub_"), sub-sub-folder ("sub_sub_*"), ...
Now I would like to have a SQL query, that returns for each entry how many child entries there are:
SELECT id,name,count(....) as child_count FROM table WHERE ...
For the example table this query shall return:
id name child_count
--+--------------+---------
1 root_a 0
2 root_b 1
3 sub_b1 2
4 sub_sub_b1_1 0
5 sub_sub_b1_2 0
How to perform such a count inside the same table?
Thanks
How about a correlated subquery?
select t.*,
(select count(*)
from t t2
where t2.parent = t.id
) as child_count
from t;
with qry1 as (
select parent,
count(*) as child_count
from table
group by parent
)
select table.id,
table.name,
isnull(qry1.child_count, 0) as child_count
from table
left join qry1
on table.id = qry1.parent
order by table.id
Note: Left Join. Inner join would exclude rows with no children.

SQL -- SELECT 3 TABLES WITH 2 IDs

I'm trying to join 3 tables where the two IDs from two tables are related but the third table is not related but the values are. It's very convoluted. I tried running a subquery without luck.
Here's the first table:
+---------------------------------+
| ListID | ListValue | ListLabel |
+--------------------------------+
| 1 | 1 | Male |
+--------------------------------+
Second table:
+--------------------+
| ListID | ListName |
+-------------------+
| 1 | Gender |
+-------------------+
Third table:
+---------------------------------+
| ClientID | Name | Gender |
+--------------------------------+
| 23422 | John West | 1 |
+--------------------------------+
So basically I'm looking for the Gender column from the third table to reference the first table where the ListValue column equals the Gender column. Yes, the Gender column value is stored as INT.
+---------------------------------+
| ClientID | Name | Gender |
+--------------------------------+
| 23422 | John West | Male |
+--------------------------------+
From a complete guess, the problem is your design, fix that, fix the problem. You should have 2 tables here, not 3. A Client table and a gender table, and then you can do a trivial JOIN:
CREATE TABLE dbo.Client (ClientID int NOT NULL,
[Name] nvarchar(50) NOT NULL,
Gender tinyint NULL); --As Gender is Sensitive data, and probably not mandatory as some don't wish to share
ALTER TABLE dbo.Client ADD CONSTRAINT PK_Client PRIMARY KEY CLUSTERED (ClientID);
GO
CREATE TABLE dbo.Gender (GenderID tinyint IDENTITY(1,1) NOT NULL,
GenderDescription varchar(15) NOT NULL);
ALTER TABLE dbo.Gender ADD CONSTRAINT PK_Gender PRIMARY KEY CLUSTERED (GenderID);
GO
ALTER TABLE dbo.Client ADD CONSTRAINT FK_ClientGender FOREIGN KEY (Gender) REFERENCES dbo.Gender (GenderID);
GO
--Assume existance of data
SELECT C.ClientID,
C.[Name],
G.GenderDescription AS Gender
FROM dbo.Client C
LEFT JOIN dbo.Gender G On C.Gender = G.GenderID; --LEFT JOIN as it's NULLable
GO
If you must stick with the design you have, you'll probably want to write it as subqueries; and it'll get messy pretty fast:
SELECT T3.ClientID,
T3.[Name],
(SELECT T1.ListLabel
FROM dbo.Table2 T2
JOIN dbo.Table1 T1 ON T2.ListID = T1.ListID
WHERE T2.ListName = N'Gender'
AND T1.ListValue = T3.Gender) AS Gender,
(SELECT T1.ListLabel
FROM dbo.Table2 T2
JOIN dbo.Table1 T1 ON T2.ListID = T1.ListID
WHERE T2.ListName = N'PrimaryLanguage'
AND T1.ListValue = T3.PrimaryLanguage) AS PrimaryLanguage
FROM dbo.Table3 T3;
You can put these (sub)queries in the FROM is you prefer using APPLY. I've used an OUTER rather than CROSS APPLY, as I assume that the value could be NULL and thus acts like a LEFT JOIN, rather than an INNER JOIN:
SELECT T3.ClientID,
T3.[Name],
G.Gender,
PL.PrimaryLanguage
FROM dbo.Table3 T3
OUTER APPLY (SELECT T1.ListLabel AS Gender
FROM dbo.Table2 T2
JOIN dbo.Table1 T1 ON T2.ListID = T1.ListID
WHERE T2.ListName = N'Gender'
AND T1.ListValue = T3.Gender) G
OUTER APPLY (SELECT T1.ListLabel AS PrimaryLanguage
FROM dbo.Table2 T2
JOIN dbo.Table1 T1 ON T2.ListID = T1.ListID
WHERE T2.ListName = N'PrimaryLanguage'
AND T1.ListValue = T3.Gender) PL;
To make it shorter, you could turn the "look up" part of the above into an inline table-value function, but I feel that entertaining that is validating the design, and I specifically don't want to do that.
This'll be a real pain to keep rewriting, but forced by the design choice. Though you could do this with Dynamic SQL, I doubt you'll be able to understand it or maintain it (as if you could understand dynamic SQL you'd have likely at least got the solution I give above), and I doubt you want to write all of your queries as a dynamic statement.
Fix the design, fix the problem (as I mentioned at the start).
This is a very bad data model. Change it if you can.
If there is a column gender in the client table, why muddle through with some generic list? Just add a table gender and link to its rows with client.gender_id:
table gender (gender_id, description)
table client (client_id, name, gender_id)
If you really must make this generic and are ready to live with the consequences (slower access, no guaranteed consistency, ...), then remove the gender column and add a table client_attribut instead, consisting of client_id, list_id and list_value.
Anyway, with your current design the query would be
select c.clientid, c.name, la.listlabel
from client c
join list_attribute la
on la.listvalue = c.gender
and la.listid = (select listid from list where listname = 'Gender')
order by c.clientid;
A simple join is of course possible, it's just unecessarily convoluted!
select t3.ClientId, t3.Name, t1.ListLabel as Gender
from t3 join t2 on t2.ListName='Gender'
join t1 on t1.ListId=t2.ListId and t2.ListValue=t3.Gender

Join table using column value as table name

Is it possible to join a table whereby the table name is a value in a column?
Here is a TABLE called food:
id food_name price_table pricing_reference_id
1 | 'apple' | 'daily_price' | 13
2 | 'banana' | 'monthly_price' | 13
3 | 'hotdog' | 'weekly_price' | 17
4 | 'sandwich' | 'monthly_price' | 9
There are three other tables (pricing tables): daily_price, weekly_price, and monthly_price tables.
Side note: Despite their names, the three pricing tables display vastly different kinds of information, which is why the three tables were not merged into one table
Each row in the food table can only be joined with one of the three pricing tables at most.
The following does not work -- it is just to illustrate what I am trying to get at:
SELECT *
FROM food
LEFT JOIN food.price_table ON food.pricing_reference_id = daily_price.id
WHERE id = 1;
Obviously the query does not work. Is there any way that the name of the table in the price_table column could be used as the table name in a join?
I would suggest left joins:
select f.*,
coalesce(dp.price, wp.price, mp.price) as price
from food f left join
daily_price dp
on f.pricing_reference_id = dp.id and
f.pricing_table = 'daily_price' left join
weekly_price wp
on f.pricing_reference_id = wp.id and
f.pricing_table = 'weekly_price' left join
monthly_price mp
on f.pricing_reference_id = mp.id and
f.pricing_table = 'monthly_price' ;
For the columns you reference, you need to use coalesce() to combine the results from the three tables. You say that the tables have different data, so you would need to list the columns separately.
The main reason I recommend this approach is performance. I think the left joins should be faster than any solution that uses union all.
Could you get your expected result using by a derived table with UNION SELECT which has a column of each table name?
SELECT *
FROM food
LEFT JOIN
(
SELECT 'daily_price' AS price_table, * FROM daily_price
UNION ALL SELECT 'monthly_price', * FROM monthly_price
UNION ALL SELECT 'weekly_price', * FROM weekly_price
) t
ON food.price_table = t.price_table AND
food.pricing_reference_id = t.id
ORDER BY food.id;
dbfiddle

UNION columns in one SELECT

Let's say :
SELECT Item.Id, Item.ParentId FROM Item ..."
Returns me this data:
Id | ParentId
----------------
1 | NULL
2 | 17
3 | 13
Is there is a way to get this data as one column by using some kind of UNION but on columns from only one SELECT ? Something like:
SELECT (Item.Id UNION Item.ParentId) AS Id FROM Item...
Result :
Id |
----
1 |
2 |
3 |
NULL
17 |
13 |
EDIT EXAMPLE:
I have Media Table:
Id | ParentId
----------------
1 | NULL
2 | 1
3 | 2
It have relations with itself, this is some kind of 3 level tree structure
(Series -> Seasons -> Episodes)
There is another Table Offer which contain information about availability:
Id | MediaId | Availability
------------------------------
1 | 3 | true
I need to get id's of all media that are available, but also all parent's id, of all levels.
I was thinking about:
SELECT Media.Id, MediaSeason.Id, MediaSeries.Id FROM Media
LEFT JOIN Media AS MediaSeason ON MediaSeason.Id = Media.ParentId
LEFT JOIN Media AS MediaSeries ON MediaSeries.Id = MediaSeason.ParentId
LEFT JOIN Offer ON Offer.MediaId = Media.Id
WHERE Offer.Availability = true
This gives me all id's i need but in three different columns and I'm trying to find a way to put it into one, without repeating join and where login in 3 different SELECTS.
I'm using MSSQL.
Try this:
SELECT * FROM (SELECT Item.Id FROM Item ...
UNION ALL
SELECT Item.ParentId FROM Item ...)
If your children and parents are in the same table (Item)
SELECT Id FROM Item
Will retrieve all Items, including Parents because parents are also Items.
But if what you want is to not repeat the where clause and have Ids of any matched Media and its associated parents (even if the parent media does not match the where clause) you can try this:
SELECT
m.Id
FROM
Media m INNER JOIN (
SELECT
m2.Id, m2.ParentId
FROM
Media m2
LEFT JOIN Offer ON Offer.MediaId = m2.Id
WHERE
Offer.Availability = true
) tmp ON (tmp.Id = m.Id OR tmp.ParentId = m.Id)
Finally, for three levels:
SELECT
m.Id
FROM
Media m INNER JOIN (
SELECT
m2.Id, m2.ParentId, m3.ParentId AS GrandParentId
FROM
Media m2
LEFT JOIN Media m3 ON m2.ParentId = m3.Id
LEFT JOIN Offer ON Offer.MediaId = m2.Id
WHERE
Offer.Availability = true
) tmp ON (tmp.Id = m.Id OR tmp.ParentId = m.Id OR tmp.GrandParentId = m.Id)
SELECT DISTINCT
pivot_hierarchy.media_id
FROM
offers o
LEFT JOIN
media m1
ON m1.id = o.media_id
LEFT JOIN
media m2
ON m2.id = m1.parent_id
OUTER APPLY
(
SELECT o.media_id
UNION ALL
SELECT m1.parent_id WHERE m1.parent_id IS NOT NULL
UNION ALL
SELECT m2.parent_id WHERE m2.parent_id IS NOT NULL
)
AS pivot_hierarchy
WHERE
o.availability = 'true'
Everything up to the APPLY should be self explanatory. Get the offers, get the parent of that media if it has one, and the parent of that media if it has one.
The APPLY then joins each row on to a function that can return more than one row each. In this case the function returns 1, 2 or 3 rows. Those being the media id, it parent if it has one, and its grand-parent if it has one. To do that, the function unions the three input columns, provided that they’re not null.
This avoids having to join back on to the media table again.
Also, you need a distinct in the select. Otherwise the same series or season id could return multiple times.
Nested selects can be avoided in UNION
create table tab (
Id int,
ParentId int
);
insert into tab
values
(1, NULL),
(2, 17),
(3, 13);
then do
select ID as ID
from tab
union all
select ParentId as ID
from tab
NOTE: DB queries can be conveniently tested live, e.g. http://sqlfiddle.com/#!17/7a3a8/2

Nested join on same table (tree structure)

My date is organized in tree structure.
The following applies (Oracle SQL syntax):
CREATE TABLE TREE
(
NAME VARCHAR2(20),
ID NUMBER(10, 0),
PARENT NUMBER(10, 0)
)
;
INSERT INTO "TREE" (NAME, ID) VALUES ('a', '1');
INSERT INTO "TREE" (NAME, ID, PARENT) VALUES ('a.1', '2', '1');
INSERT INTO "TREE" (NAME, ID, PARENT) VALUES ('a.2', '3', '1');
INSERT INTO "TREE" (NAME, ID, PARENT) VALUES ('a.2.1', '4', '3');
INSERT INTO "TREE" (NAME, ID, PARENT) VALUES ('a.2.2', '5', '3');
INSERT INTO "TREE" (NAME, ID) VALUES ('b', '6');
I would like to return full tree by id, so for query :
select name, id <<<TODO LOGIC>> where id = 1
I would get
| name | id |
| a | 1 |
| a.1 | 2 |
| a.2 | 3 |
| a.2.1 | 4 |
| a.2.2 | 5 |
for a sub tree I would get:
select name, id <<<TODO LOGIC>> where id = 3
I would get
| name | id |
| a.2 | 3 |
| a.2.1 | 4 |
| a.2.2 | 5 |
Where as, for flat entry b, it would get
select name, id <<<TODO LOGIC>> where id = 6
I would get
| name | id |
| b | 6 |
It seems that plain left out join queries fails to fulfill this purpose, or am I missing something?
The following query does return the full structure, but when starting to filter with where statements it fails.
select t1.id t1Id, t2.id t2Id, t1.name t1Name, t2.name t2Name from tree t1 left outer join tree t2 on t1.id = t2.parent
When you have a tree structure, you likely need a hierarchical query. Here it is:
select t.*
from tree t
connect by prior t.id = t.parent
start with t.id = :id
order siblings by t.id
See Hierarchical Queries for details.
You can use start with - connect by syntax on Oracle. If I'm not mistaken, it goes like this
select * from Tree t
start with t.ID = 1 connect by prior t.ID = t.Parent
But I have no Oracle to check it right away. Maybe its prior t.Parent = t.ID. Beware that it can be slow sometimes, use with caution.
Alternative is to create table to store all indirect relationship between nodes (not just a-a.1, but also a-a.2.1 and so on). You can fill it using PL/SQL recursive stored procedure. Two ways:
Simple way is to make a procedure that will do complete refill of indirect table. You can call it before running reports.
If you need instant effects, you should write refill procedure so that it will update indirect relationship just for one record in tree. Then you prohibit direct inserts and updates to Tree and force them to go via stored PL/SQL procedures (like InsertTree/UpdateTree) which in turn will call procedure to update table with indirect relationships.
You could use union for this, and you need to limit the depth of the tree to make it possible to select it in one query.
SELECT id, name
FROM TREE as node
WHERE
node.id = :id
UNION
SELECT child1.id, child1.name
FROM TREE as node
inner join TREE as child1 on node.id = child1.parent
WHERE
node.id = :id
UNION
SELECT child2.id, child2.name
FROM TREE as node
inner join TREE as child1 on node.id = child1.parent
inner join TREE as child2 on child1.id = child2.parent
WHERE
node.id = :id
The problem here is, SQL is very bad in recursion (while relational structures are actually great in this).
To make it fully dynamic, use a query for each level in the tree, or use a database engine specific SQL extension if there is anything usable.