Exclude results with join conditions - sql

I'm trying to make an sql request with join exclusion.
Explains:
Table element
id # name #
1 Sea
2 tree
Table colour
id # name #
1 green
2 blue
3 brown
Table relation
element_id # colour_id
1 2
2 1
2 3
I have my working request for "get elements for one of these colours".
Exemple with green and blue:
SELECT element.name, colour.name FROM element
LEFT JOIN relation
ON (element.id = relation.element_id)
LEFT JOIN colour
ON (colour.id = relation.colour_id)
WHERE (relation.colour_id = 1 OR relation.colour_id = 2)
I would like make request for "get elements where they have a relation with all listed colors". Where for green and brown it returns tree.
I've tried to change the 'OR' to 'AND' but request return 0 results :/

General way to solve this problem is to filter values and count how many times they appear in result. If equal, all elements are found.
select element_id
from relation
where colour_id in (1, 2)
group by element_id
having count (distinct colour_id) = 2
Having this table one might join it to original tables to produce full column set:
SELECT element.name, colour.name
FROM relation
INNER JOIN
(
select element_id
from relation
where colour_id in (1, 2)
group by element_id
having count (distinct colour_id) = 2
) matches
ON relation.element_id = matches.element_id
INNER JOIN element
ON element.id = relation.element_id
INNER JOIN colour
ON colour.id = relation.colour_id

This type of query can be handled with some of SQL's set-based operators:
Which elements have relations for all colours?
Using the ALL operator (syntax may vary slightly by database):
SELECT element.name
FROM element
WHERE ( SELECT colour.id FROM relation
INNER JOIN colour ON colour.id = relation.colour_id
WHERE relation.element_id = element.id )
= ALL ( SELECT colour.id from colour)
;
Using the EXCEPT operator:
SELECT element.name
FROM element
WHERE NOT EXISTS
( SELECT colour.id from colour
EXCEPT
SELECT colour.id FROM relation
INNER JOIN colour ON colour.id = relation.colour_id
WHERE relation.element_id = element.id
)
;

There is a typo in your WHERE clause - one of the ids is 2 ("blue") instead of 3 ("brown"). It should be
WHERE (relation.colour_id = 1 OR relation.colour_id = 3)
(or in shorter form:
WHERE relation.colour_id IN (1, 3)
).
Note however, that your current query - although after this fix it should work for your sample data - won't give you correct results in general. It will give you the elements associated with any of the specified colors. The correct solution to this is given in #Nikola's answer though.

without subselects, I would suggest:
SELECT
e.id AS id
FROM
element AS e
LEFT OUTER JOIN relation AS r ON r.element_id = e.id
GROUP BY e.id HAVING SUM(CASE WHEN r.colour_id = 1 THEN 1 ELSE 0 END ) = 0
ORDER BY e.id ASC;
after this you can select the elements by id.

Related

SQL Join / Union

I have two statements that I want to merge into one output.
Statement One:
select name from auxiliary_variable_inquiry
where inquiry_idbr_code = '063'
Returns the following list of names:
Name
------------
Affiliates
NetBookValue
Parents
Worldbase
Statement Two:
select name, value from auxiliary_variable_value
where inquiry_idbr_code = '063'
and ru_ref = 20120000008
and period = 200912
Returns the following:
Name Value
-------------------
Affiliates 112
NetBookValue 225.700
I would like to have an output like this:
Name Value
-------------------
Affiliates 112
NetBookValue 225.700
Parents 0
Worldbase 0
So basically, if the second query only returns 2 names and values, I'd still like to display the complete set of names from the first query, with no values. If all four values were returned by both queries, then all four would be displayed.
Sorry I must add, im using Ingres SQL so im unable to use the ISNULL function.
You can do a left join. This ensures that all records from the first table will stay included. Where value is null, no child record was found, and we use coalesce to display 0 in these cases.
select i.name, COALESCE(v.Value,0) from auxiliary_variable_inquiry i
left join auxiliary_variable_value v
on v.inquiry_idbr_code = i.inquiry_idbr_code
and v.ru_ref = 20120000008
and v.period = 200912
where i.inquiry_idbr_code = '063'
I'd recommend a self-JOIN using the LEFT OUTER JOIN syntax. Include your 'extra' conditions from the second query in the JOIN condition, while the first conditions stay in the WHERE, like this:
select a.name, CASE WHEN b.Value IS NULL THEN 0 ELSE b.Value END AS Value
from
auxiliary_variable_inquiry a
LEFT JOIN
auxiliary_variable_inquiry b ON
a.name = b.name and -- replace this with your real ID-based JOIN
a.inquiry_idbr_code = b.inquiry_idbr_code AND
b.ru_ref = 20120000008 AND
b.period = 200912
where a.inquiry_idbr_code = '063'
if i got right, you should use something like:
SELECT i.NAME,
v.NAME,
v.value
FROM auxiliary_variable_inquiry i
LEFT JOIN auxiliary_variable_value v
ON i.inquiry_idbr_code = v.inquiry_idbr_code
WHERE v.ru_ref = 20120000008
AND v.period = 200912

How to get first entry with a value from an hierarchical setting structure?

I have a couple of tables. One table with Groups:
[ID] - [ParentGroupID]
1 - NULL
2 1
3 1
4 2
And another with settings
[Setting] - [GroupId] - [Value]
Title 1 Hello
Title 2 World
Now I'd like to get "Hello" back if I'd query the Title for Group 3
And I'd like to get "World" back if I'd query the Title for Group 4 (And not "Hello" as well)
Is there any way to efficiently do this in MSSQL? At the moment I am resolving this recursively in code. But I was hoping that SQL could solve this problem for me.
Don't knoww the SQL Server syntax but something like the following?
SELECT settings.value
FROM settings
JOIN groups ON settings.groupid = groups.parentgroupid
WHERE settings.setting = 'Title'
AND groups.id = 3
This is a problem we've encountered multiple times in our company. This would work for any case, including when the settings can be set only at some levels and not others (see SQL Fiddle http://sqlfiddle.com/#!3/16af0/1/0 :
With GroupSettings(group_id, parent_group_id, value, current_level)
As
(
Select g.id as group_id, g.parent_id, s.value, 0 As current_Level
From Groups As g
Join Settings As s On s.group_id = g.id
Where g.parent_id Is Null
Union All
Select g.id, g.parent_id, Coalesce((Select value From Settings s Where s.group_id=g.id), gs.value), current_level+1
From GroupSettings as gs
Join Groups As g On g.parent_id = gs.group_id
)
Select *
From GroupSettings
Where group_id=4
I believe the following is what you are seeking. See the sqlfiddle
SELECT vALUE FROM
Groups g inner join Settings s
ON g.ParentGroupId = s.GroupID
WHERE g.ID = 3 -- will return Hello,], set ID = 4 will return World

How to create a subset query in sql?

I have two tables as follows:
CREATE List (
id INTEGER,
type INTEGER REFERENCES Types(id),
data TEXT,
PRIMARY_KEY(id, type)
);
CREATE Types (
id INTEGER PRIMARY KEY,
name TEXT
);
Now I want to create a query that determines all ids of List which has given type strings.
For example,
List:
1 0 "Some text"
1 1 "Moar text"
2 0 "Foo"
3 1 "Bar"
3 2 "BarBaz"
4 0 "Baz"
4 1 "FooBar"
4 2 "FooBarBaz"
Types:
0 "Key1"
1 "Key2"
2 "Key3"
Given the input "Key1", "Key2", the query should return 1, 4.
Given the input "Key2", "Key3", the query should return 3, 4.
Given the input "Key2", the query should return 1, 3, 4.
Thanks!
select distinct l.id
from list l
inner join types t on t.id = l.type
where t.name in ('key1', 'key2')
group by l.id
having count(distinct t.id) = 2
You have to adjust the having clause to the number of keys you are putting in your where clause. Example for just one key:
select distinct l.id
from list l
inner join types t on t.id = l.type
where t.name in ('key2')
group by l.id
having count(distinct t.id) = 1
SQlFiddle example
You can use the following trick to extend Jurgen's idea:
with keys as (
select distinct t.id
from types t
where t.name in ('key1', 'key2')
)
select l.id
from list l join
keys k
on l.type = keys.id cross join
(select count(*) as keycnt from keys) k
group by l.id
having count(t.id) = max(k.keycnt)
That is, calculate the matching keys in a subquery, and then use this for the counts. This way, you only have to change one line to put in key values, and you can have as many keys as you would like. (Just as a note, I haven't tested this SQL so I apologize for any syntax errors.)
If you can dynamically produce the SQL, this may be one of the most efficent ways, in many DBMS:
SELECT l.id
FROM List l
JOIN Types t1 ON t1.id = l.type
JOIN Types t2 ON t2.id = l.type
WHERE t1.name = 'Key1'
AND t2.name = 'Key2' ;
See this similar question, with more than 10 ways to get the same result, plus some benchmarks (for Postgres): How to filter SQL results in a has-many-through relation

SQL SELECT criteria in another table

I have 2 related tables:
messages
--------
mid subject
--- -----------------
1 Hello world
2 Bye world
3 The third message
4 Last one
properties
----------
pid mid name value
--- --- ---------------- -----------
1 1 read false
2 1 importance high
3 2 read false
4 2 importance low
5 3 read true
6 3 importance low
7 4 read false
8 4 importance high
And I need to get from messages using the criteria on the properties table.
Eg: if I have a criteria like return unread (read=false) high prio (importance=high) messages it should return
mid subject
--- -----------------
1 Hello world
4 Last one
How could I get this with a SELECT clause (MySQL dialect)?
In SQL, any expression in a WHERE clause can only reference one row at a time. So you need some way of getting multiple rows from your properties table onto one row of result. You do this with self-joins:
SELECT ...
FROM messages AS m
JOIN properties AS pRead
ON m.mid = pRead.mid AND pRead.name = 'read'
JOIN properties AS pImportance
ON m.mid = pImportance.mid AND pImportance.name = 'importance'
WHERE pRead.value = 'false' AND pImportance.value = 'high';
This shows how awkward it is to use the EAV antipattern. Compare with using conventional attributes, where one attribute belongs in one column:
SELECT ...
FROM messages AS m
WHERE m.read = 'false' AND m.importance = 'high';
By the way, both answers from #Abe Miessler and #Thomas match more mid's than you want. They match all mid's where read=false OR where importance=high. You need to combine these properties with the equivalent of AND.
I believe the query below will work.
UPDATE: #Gratzy is right, this query won't work, take a look at the structure changes I suggested.
SELECT DISTINCT m.id as mid, m.subject
FROM message as m
INNER JOIN properties as p
ON m.mid = p.mid
where (p.name = 'read' and p.value = 'false') or (p.name = 'importance' AND p.value = 'high')
The structure of your properties table seems a little off to me though...
Would it be possible to structure the table like this:
messages
--------
mid subject Read Importance
--- ----------------- --------- ------------
1 Hello world false 3
2 Bye world false 1
3 The third message true 1
4 Last one false 3
importance
----------
iid importanceName
--- --------------
1 low
2 medium
3 high
and use this query:
SELECT m.id as mid, m.subject
FROM message as m
where m.read = false AND m.importance = 3
Clearly, you are using an EAV (Entity-Attribute-Value) schema. One of the many reasons for avoiding such a structure is that it makes queries more difficult. However, for the example you gave, you could do something like:
Select ...
From messages As M
Where Exists (
Select 1
From Properties As P1
Where P1.mid = M.mid
And P1.name = 'unread' And P1.value = 'false'
)
And Exists (
Select 1
From Properties As P2
Where P2.mid = M.mid
And P2.name = 'importance' And P2.value = 'high'
)
A more succinct solution would be:
Select ...
From messages As M
Where Exists (
Select 1
From Properties As P1
Where P1.mid = M.mid
And ((P1.name = 'unread' And P1.value = 'false')
Or (P1.name = 'importance' And P1.value = 'high'))
Having Count(*) = 2
)
Select m.mid, m.subject
from properties p
inner join properties p1 on p.mid = p1.mid
inner join messages m on p.mid = m.mid
where
p.name = 'read'
and p.value = 'false'
and p1.name = 'importance'
and p2.value = 'high'
I prefer to put my filter criteria in the where clause and leave my join's to elements that are in both tables and are the actual criteria for the join.
Another way might be (untested) to use a derived table to hold the criteria that all messages must meet then use the standard relational division technique of double NOT EXISTS
SELECT mid,
subject
FROM messages m
WHERE NOT EXISTS
( SELECT *
FROM ( SELECT 'read' AS name,
'false' AS value
UNION ALL
SELECT 'importance' AS name,
'high' AS value
)
c
WHERE NOT EXISTS
(SELECT *
FROM properties P
WHERE p.mid = m.mid
AND p.name =c.name
AND p.value=c.value
)
)
If you want to keep your existing data model, then go with Bill Karwin's first suggestion. Run it with this select clause to understand what it's doing:
select m.*, r.value as read, i.value as importance
from message m
join properties r
on r.mid = m.mid and r.name = 'read'
join properties i
on i.mid = m.mid and i.name = 'importance'
where r.value = 'false' and i.value = 'high';
But if you go this way, there are a few constraints you should put in place to avoid storing and retrieving bad data:
A unique index on message(mid) and a unique index on properties(pid), both of which I'm sure you have already.
A unique index on properties(mid, name) so that each property can only be defined once for a message -- otherwise you may get duplicate results from your query. This will also help your query performance by allowing an index access for both joins.

Using (IN operator) OR condition in Where clause as AND condition

Please look at following image, I have explained my requirements in the image.
alt text http://img30.imageshack.us/img30/5668/shippment.png
I can't use here WHERE UsageTypeid IN(1,2,3,4) because this will behave as an OR condition and fetch all records.
I just want those records, of first table, which are attached with all 4 ShipmentToID .
All others which are attached with 3 or less ShipmentToIDs are not needed in result set.
Thanks.
if (EntityId, UsageTypeId) is unique:
select s.PrimaryKeyField, s.ShipmentId from shipment s, item a
where s.PrimaryKeyField = a.EntityId and a.UsageTypeId in (1,2,3,4)
group by s.PrimaryKeyField, s.ShipmentId having count(*) = 4
otherwise, 4-way join for the 4 fields,
select distinct s.* from shipment s, item a, item b, item c, item d where
s.PrimaryKeyField = a.EntityId = b.EntityId = c.EntityId = d.EntityId and
a.UsageTypeId = 1 and b.UsageTypeId = 2 and c.UsageTypeId = 3 and
d.UsageTypeId = 4
you'll want appropriate index on (EntityId, UsageTypeId) so it doesn't hang...
If there will never be duplicates of the UsageTypeId-EntityId combo in the 2nd table, so you'll never see:
EntityUsageTypeId | EntityId | UsageTypeId
22685 | 4477 | 1
22687 | 4477 | 1
You can count matching EntityIds in that table.
WHERE (count(*) in <tablename> WHERE EntityId = 4477) = 4
DECLARE #numShippingMethods int;
SELECT #numShippingMethods = COUNT(*)
FROM shippedToTable;
SELECT tbl1.shipmentID, COUNT(UsageTypeId) as Usages
FROM tbl2 JOIN tbl1 ON tbl2.EntityId = tbl1.EntityId
GROUP BY tbl1.EntityID
HAVING COUNT(UsageTypeId) = #numShippingMethods
This way is preferred to the multiple join against same table method, as you can simply modify the IN clause and the COUNT without needing to add or subtract more tables to the query when your list of IDs changes:
select EntityId, ShipmentId
from (
select EntityId
from (
select EntityId
from EntityUsage eu
where UsageTypeId in (1,2,3,4)
group by EntityId, UsageTypeId
) b
group by EntityId
having count(*) = 4
) a
inner join Shipment s on a.EntityId = s.EntityId