Getting related many-to-one objects with a single query - sql

I have three related tables in PostgreSQL:
objects - top-level table, that has an ID and a name.
object_events - a related table, that has a foreign key to objects
object_aux_data - a table that uses many-to-one relationship with object_event
Some example data:
objects:
id: 1, name: test_object
object_events
id: 1, object_id: 1
id: 2, object_id: 1
object_aux_data
object_event: 1, name: foo, value: foo_val
object_event: 1, name: bar, value: bar_val
object_event: 2, name: foo, value: foo_val2
object_event: 2, name: baz, value: baz_val
It is easy to get a list of object_events that are related to the test_object, but I'd like to also attach to the results some of the objec_aux_data. So the output results will look like this:
object.name
object_event.id
foo
bar
test_object
1
foo_val
bar_val
test_object
2
foo_val2
Note, that the foo has value for all object_event but bar only for the first one. baz is not taken into account in this case.
Is it possible to get this data with a single query, or should I have a separate query for each object_event?
Example schema:
create table objects(
id int,
name varchar
);
create table object_events(
id int,
object_id int
);
create table object_aux_data(
object_event int,
name varchar,
value varchar
);
insert into objects values (1,'test_object');
insert into object_events values (1,1),(2,1);
insert into object_aux_data values (1,'foo','foo_val'),(1,'bar','bar_val'),(2,'foo','foo_val2'),(2,'baz','baz_val');

You can do this with a simple join (Result here)
with bar as (
select oe.id, oad.value
from object_events oe join object_aux_data oad on oad.object_event = oe.id
where oad.name = 'bar'
),
foo as (
select oe.id,oad.value
from object_events oe join object_aux_data oad on oad.object_event = oe.id
where oad.name = 'foo')
select
o.name,oe.id,coalesce(foo.value,''),coalesce(bar.value,'')
from
objects o join object_events oe on o.id = oe.object_id
left join foo on foo.id = oe.id
left join bar on bar.id = oe.id

Related

Insert records from two tables that match

I have the following tables:
CREATE TABLE forms
(
ID INT NOT NULL,
NAME TEXT NOT NULL,
TITLE TEXT NOT NULL
);
CREATE TABLE new_forms
(
ID INT NOT NULL,
NAME TEXT NULL,
TITLE TEXT NULL
);
INSERT INTO forms VALUES (0, 'test', 'test');
INSERT INTO new_forms VALUES (0, 'new_test', NULL);
And I'm using the following query:
INSERT INTO forms(id, name, title)
SELECT
1, COALESCE(nf.name, f.name), COALESCE(nf.title, f.title)
FROM
forms f
LEFT OUTER JOIN
new_forms nf ON nf.id = f.id;
SELECT * FROM forms;
The idea is to add both rows that match to the table.
In this example this two new records should be added:
1 test test
1 new_test test
But it's only adding the last one.
I have tried with all the join and none of them worked.
Fiddle
Thanks
You are using a join in the query which will give you only 1 row. If you need 2 rows. You have to use UNION ALL clause -
INSERT INTO forms(id, name, title)
SELECT
1, COALESCE(nf.name, f.name), COALESCE(nf.title, f.title)
FROM
forms f
LEFT OUTER JOIN
new_forms nf ON nf.id = f.id
UNION ALL
SELECT
1, COALESCE(f.name, nf.name), COALESCE(nf.title, f.title)
FROM
forms f
LEFT OUTER JOIN
new_forms nf ON nf.id = f.id;

How to make an OUTER JOIN return ZERO instead of NULL

I am trying to accomplish this on SQL Server. The simplest table structure with data is shown below.
Table:Blog
BlogID, Title
----------------
1, FirstBlog
23, Pizza
Table:User
UserID, Name
-------------------
123, james
444, John
Table:UserBlogMapping
UserBlogMappingID, BlogID,UserID
----------------------------------
1, 1, 123
I want to get FormID and UserBlogMappingID in one SQL query. If provided UserID is not in the mapping table, return ZERO otherwise return the valid userBlogMappingID. I am trying to run the below query but its not correct.
SELECT
B.BlogID,
BUM.BlogUserMappingID
FROM
Blog AS B
LEFT JOIN BlogUserMapping AS BUM ON B.BlogID = BUM.BlogID
WHERE
(B.BlogID = 23) -- it exists in the table
AND BUM.userID = 444 -- it is NOT in the mmaping table but i want a ZERO return in such case
Assumption:
We can assume that the UserID provided in the WHERE clause is always valid UserID and is present in the User table.
You could put the criteria for the userID=444 in the ON clause of the LEFT JOIN.
And an ISNULL or a COALESCE to change a NULL to a 0.
Example using table variables:
declare #Blog table (BlogID int, Title varchar(30));
insert into #Blog (BlogId, Title) values
(1, 'FirstBlog'),
(23, 'Pizza');
declare #User table (UserID int, Name varchar(30));
insert into #User (UserID, Name) values
(123,'james'),
(444,'John');
declare #BlogUserMapping table (BlogUserMappingID int, BlogID int, UserID int);
insert into #BlogUserMapping (BlogUserMappingID, BlogID, UserID) values
(1, 1, 123),
(2, 23, 123),
(3, 1, 444);
-- Using the criteria in ON clause of the LEFT JOIN
SELECT
B.BlogID,
ISNULL(BUM.BlogUserMappingID,0) as BlogUserMappingID
FROM #Blog B
LEFT JOIN #BlogUserMapping BUM ON (B.BlogID = BUM.BlogID AND BUM.userID = 444)
WHERE B.BlogID = 23;
-- If there are more BlogId=23 with userID=444.
-- But only 1 row needs to be returned then you could also GROUP BY and take the maximum BlogUserMappingID
SELECT
B.BlogID,
MAX(ISNULL(BUM.BlogUserMappingID,0)) as BlogUserMappingID
FROM #Blog B
LEFT JOIN #BlogUserMapping BUM ON (B.BlogID = BUM.BlogID AND BUM.userID = 444)
WHERE B.BlogID = 23
GROUP BY B.BlogID;
-- Using an OR in the WHERE clause would also return a 0.
-- But it would also return nothing if the mapping table has a BlogID=23 with a userID<>444.
-- So not usefull in this case.
SELECT
B.BlogID,
ISNULL(BUM.BlogUserMappingID,0) as BlogUserMappingID
FROM #Blog B
LEFT JOIN #BlogUserMapping BUM ON B.BlogID = BUM.BlogID
WHERE B.BlogID = 23
AND (BUM.userID IS NULL OR BUM.userID = 444);

Selecting table of properties as columns

I have two tables, things and properties:
CREATE TABLE things (
id SERIAL PRIMARY KEY
);
CREATE TABLE properties (
thing_id INT,
key TEXT,
value TEXT
);
I want to select from things and join rows from properties as columns. For example, say I have the following:
INSERT INTO things DEFAULT_VALUES; -- suppose id is 1
INSERT INTO properties (thing_id, key, value) VALUES
(1, 'height', '5'),
(1, 'width', '6'),
(1, 'length', '7');
How can I select from things with height, width, and length as columns?
Also, I don't want to specifically select height, width, and length, but any rows that may be inside properties.
For just three columns:
SELECT t.thing_id
,max(CASE WHEN p.key = 'height' THEN p.value END) AS height
,max(CASE WHEN p.key = 'width' THEN p.value END) AS width
,max(CASE WHEN p.key = 'length' THEN p.value END) AS length
FROM things t
LEFT JOIN properties p ON p.thing_id = t.id
WHERE t.id = 1
GROUP BY 1;
Or use crosstab() from the additional module tablefunc which is typically faster, and shorter for long lists of attributes:
SELECT * FROM crosstab(
'SELECT t.thing_id, p.key, p.value
FROM things t
LEFT JOIN properties p ON p.thing_id = t.id
WHERE t.id = 1
ORDER BY 1'
,$$VALUES ('height'::text), ('width'), ('length')$$) -- add more
AS ct (thing_id int, height int, width int, length int); -- add more
Types have to match. Detailed explanation:
PostgreSQL Crosstab Query
A completely dynamic list of columns cannot be achieved in a single query. I have tried many times. Here is what can be done:
Dynamic alternative to pivot with CASE and GROUP BY
May be you can try table aliases here:
SELECT p1.key, p2.key, p3.key
FROM properties as t1
JOIN properties AS p1 ON p1.thing_id= t1.thing_id
JOIN properties AS p2 ON p2.thing_id= t1.thing_id
JOIN properties AS p3 ON p3.thing_id= t1.thing_id
WHERE t1.thing_id = 1;

How do I efficiently search for records with conditions on a one-to-many relationship

Say I have the following schema:
create table foo (
foo_id int not null,
name text not null,
primary key (foo_id)
);
create table bar (
bar_id int not null,
foo_id int not null,
index int not null,
value text not null,
primary key (bar_id),
foreign key (foo_id) references foo (foo_id)
);
So a one-to-many relationship between foo and bar; foo is an ordered container of bar.
How do I efficiently search for records from foo that contain a certain set of bars? That is, if I have a list of bar.values, what foos have an associated bar for each of them?
Note that I need to store information on the order of the bars, but want to ignore the ordering while searching.
The only ways I can think of doing this are:
programatically constructing a query with exists sub-queries for each value I'm interested in
programatically constructing a query with inner joins on bar for each value I'm interested in
For example:
select
f.*
from
foo f
where
exists (select 1 from bar b where b.foo_id = f.foo_id and value = 'value 1') and
exists (select 1 from bar b where b.foo_id = f.foo_id and value = 'value 2') and
...
exists (select 1 from bar b where b.foo_id = f.foo_id and value = 'value n')
Constructed dynamically depending on how many values you need to match.
Alternatively:
select
f.*
from
foo f,
bar b1,
bar b2,
...
bar bn
where
b1.foo_id = f.foo_id and b1.value = 'value 1' and
b2.foo_id = f.foo_id and b2.value = 'value 2' and
...
bn.foo_id = f.foo_id and bn.value = 'value n'
Is there a better way to achieve this in SQL?
Could I use a different schema to make this easier?
I think the exists technique is the best. But if you want to do it a different way, maybe this:
with x as (
select f.id
from foo f
join bar b
on b.foo_id = f.id
where b.value in ('value 1', 'value 2', ..., 'value n')
group by f.id
having count(distinct b.value)) = n
)
select f.*
from foo f
join x.id = f.id
You will have to excuse me if I have the syntax wrong. I did not build a database to test my answer.
I know some people do not like to use CTE, so here is an alternative:
select f.*
from (
select f.id
from foo f
join bar b
on b.foo_id = f.id
where b.value in ('value 1', 'value 2', ..., 'value n')
group by f.id
having count(distinct b.value)) = n
) x
join foo f on f.id = x.id
Unless I'm missing something here, this looks like a simple Join between the two tables.
How do I efficiently search for records from foo that contain a certain set of bars? That is, if I have a list of bar.values, what foos have an associated bar for each of them?
You can find the foos that are associated to the bar values with the following:
Select Distinct f.foo_id, f.name
From bar b
Inner Join foo f On f.foo_id = b.foo_id
Where b.value In ('your', 'search', 'values')

SQL: join on best string match

I have a table with a string "identifier", that I want to match on a "group" table, finding the "best match" (that is: the match that contains the longer part of string).
For instance: assume that I have two groups: "19" and "19.10". What I want is:
item "19.10.1" is part of the group "19.10"
item "19.10.xxxx" is part of the group "19.10"
item "19.20" is part of the group "19"
What I got till now is something like this:
SELECT * FROM Items i
LEFT JOIN MyGroup g ON g.Prefix = SUBSTRING(i.ItemID,1,LEN(g.Prefix))
that matches all the string, but I don't know how can I filter the "best match" (i.e. the longer match) from my results.
By the way, I'm working on SQL Server 2005.
Example SQL Fiddle:
http://sqlfiddle.com/#!3/9a9d8/1
Try this one.
SELECT t.ItemID, g1.prefix, g1.GroupDesc
FROM Items i1
LEFT JOIN MyGroup g1 ON g1.Prefix = SUBSTRING(i1.ItemID,1,LEN(g1.Prefix))
RIGHT JOIN
(
SELECT i2.ItemID, max(len(g2.prefix)) AS ln
FROM Items i2
LEFT JOIN MyGroup g2 ON g2.Prefix = SUBSTRING(i2.ItemID,1,LEN(g2.Prefix))
GROUP BY i2.ItemID
) t ON i1.ItemID = t.ItemID AND len(g1.prefix) = t.ln
You can test it on this test data:
CREATE TABLE dbo.MyGroup
(GroupDesc VARCHAR(100),
Prefix VARCHAR(10) );
CREATE TABLE dbo.Items
(ItemDesc VARCHAR(100),
ItemID VARCHAR(10) );
INSERT INTO MyGroup (GroupDesc, Prefix)
VALUES ( 'Group A', '19' );
INSERT INTO MyGroup (GroupDesc, Prefix)
VALUES ( 'Group B', '19.10' );
INSERT INTO MyGroup (GroupDesc, Prefix)
VALUES ( 'Group C', '19.10.3' );
INSERT INTO Items (ItemDesc, ItemID)
VALUES ( 'Item 1', '19.10.4' );
INSERT INTO Items (ItemDesc, ItemID)
VALUES ( 'Item 2', '19.10.3' );
INSERT INTO Items (ItemDesc, ItemID)
VALUES ( 'Item 3', '19.20' );
INSERT INTO Items (ItemDesc, ItemID)
VALUES ( 'Item 4', '44.55' );
I came up with this:
with tmp as
(
SELECT * FROM Items i
LEFT JOIN MyGroup g ON g.Prefix = SUBSTRING(i.ItemID,1,LEN(g.Prefix))
)
SELECT a.* FROM tmp a WHERE LEN(a.prefix) = (SELECT MAX(LEN(b.prefix)) FROM tmp b WHERE a.itemid = b.itemid )
Seems to work...
SQLFiddle