I have two tables as follows:
create table first (id int, name varchar(20));
create table second (id int, value char(1),fid int);
These tables have the following records:
insert into first values (1,'Ahmad'),(2,'Sami'),(3,'Khan');
insert into second values
(1,'a',1)
, (2,'b',1)
, (3,'c',2)
, (4,'d',1)
, (5,'e',2)
, (6,'f',3);
I want to select those records from table second
which have matching records in table first
in a single row.
The desired output would be:
id | name | value | value | value ---+-------+-------+-------+------- 1 | Ahmad | a | b | d 2 | Sami | c | e | 3 | Khan | f | |
The number of values for a single record of table first
is not limited and there can be n
number of values for a single record of table first
.
n
is unknown, but I need to repeat the column value
for each value in table second
which has matching record in table first
. I need the whole table like above The desired result
. I need only those rows referenced by table second
.
The version of Postgres will be the latest one.
2 Answers 2
Actually, since
there can be
n
number of values for a single record of tablefirst
.
the number of columns to return is not exactly arbitrary. There is a maximum of columns, and this has a clean solution - unless you have more columns than Postgres allows for a table:
250 - 1600 depending on column types
BTW, typically, you would also have a third
table listing all possible values of value
, the whole of it implementing a classical many-to-many relationship.
You can use CASE
statements, or more elegantly, the crosstab()
function of the additional module tablefunc
. If you are unfamiliar with it, read basic instructions here first:
Columns to the right of actual values are filled with NULL. Assuming a maximum of 5 possible values and building on this setup:
CREATE TABLE first (id int, name text);
CREATE TABLE second (id int, value "char", fid int);
INSERT INTO first VALUES
(1,'Ahmad')
, (2,'Sami')
, (3,'Khan')
, (4,'Nobody'); -- Added to demonstrate difference
INSERT INTO second VALUES
(1,'a',1)
, (2,'b',1)
, (3,'c',2)
, (4,'d',1)
, (5,'e',2)
, (6,'f',3);
Either use crosstab(text)
(1 parameter form) and join to table first
another time:
SELECT id, f.name, value1, value2, value3, value4, value5
FROM crosstab(
'SELECT f.id, 1 AS dummy_category, s.value
FROM first f
JOIN second s ON s.fid = f.id
ORDER BY f.id, s.value'
) ct (id int
, value1 "char", value2 "char", value3 "char", value4 "char", value5 "char")
JOIN first f USING (id);
Or use crosstab(text, text)
(2 parameter form) and generate dummy categories for your values:
SELECT *
FROM crosstab(
'SELECT f.id, f.name
, row_number() OVER (PARTITION BY f.id ORDER BY s.value) AS dummy_category
, s.value
FROM first f
JOIN second s ON s.fid = f.id
ORDER BY f.id, s.value'
, ('SELECT generate_series(1,5)')
) ct (id int, name text
, value1 "char", value2 "char", value3 "char", value4 "char", value5 "char");
Result is the same either way:
id | name | value1 | value2 | value3 | value4 | value5
----+-------+--------+--------+--------+--------+--------
1 | Ahmad | a | b | d | |
2 | Sami | c | e | | |
3 | Khan | f | | | |
If you want to include all rows from table first, make it a LEFT [OUTER] JOIN
:
...
LEFT JOIN second s ON s.fid = f.id
...
Then we get one additional result row for the above example:
4 | Nobody | | | | |
The thing is this: you can't get an arbitrary number of columns based on values in rows using regular sql.
You can get similar results to what you want by:
Select f.id, f.name, array_agg(s.value)
From first f inner join second s on f.id = s.fid
Group by f.id, f.name
Which will return three columns, the third one will be an array.