I have a configuration table and a users table.
users:
| id | name |
|----|----------|
| 0 | Bob |
| 1 | Ted |
| 2 | Sam |
config:
| user_id | name | value |
|---------|----------|-------|
| 0 | a | 11 |
| 0 | b | 2 |
| 0 | c | 54 |
| 1 | a | 5 |
| 1 | b | 3 |
| 1 | c | 0 |
| 2 | a | 1 |
| 2 | b | 74 |
| 2 | c | 54 |
I normalized the configuration this way since the config will be of unknown amount, but I will have to query users based on this config, so it couldn't be stored in a serialized form.
My issue is how do I find users based on multiple rows? For instance:
Select all users with a > 4 and b < 5
This should return Bob and Ted.
Using groups:
SELECT u.name
FROM users u
JOIN config c
ON c.user_id = u.id
GROUP BY u.name
HAVING MAX(c.name = 'a' AND c.value > 4)
AND MAX(c.name = 'b' AND c.value < 5)
Using joins:
SELECT u.name
FROM users u
JOIN config a
ON a.user_id = u.id
AND a.name = 'a'
AND a.value > 4
JOIN config b
ON b.user_id = u.id
AND b.name = 'b'
AND b.value < 5
I prefer the JOIN method, as you can name each JOIN after the property and collect the conditions in the JOIN. You also don't have to worry about the GROUPs which makes it more flexible for aggregates.
A bonus over EXISTS is that you can easily access the properties of the config if you require further joins/calculations.
Try this:
SELECT us.name
FROM USERS us
WHERE EXISTS (SELECT name FROM CONFIG WHERE name='a' AND value>4 AND user_id=us.id)
AND EXISTS (SELECT name FROM CONFIG WHERE name='b' AND value<5 AND user_id=us.id)
Alternatively, you can use two joins:
SELECT us.name
FROM USERS us, CONFIG c1, CONFIG c2
WHERE us.id=c1.user_id
AND c1.name='a'
AND c1.value<4
AND us.id=c2.user_id
AND c2.name='b'
AND c2.value>5
select u.name from users as u inner join config as c on u.id = c.user_id where (c.name = "a" and c.value > 4) or (c.name = "b" and c.value < 5);
you will need two exists statements for this:
SELECT * FROM users u
WHERE EXISTS ( SELECT 1
FROM config
WHERE user_id = u.id
AND name = 'a' AND value > 4)
AND EXISTS ( SELECT 1
FROM config
WHERE user_id = u.id
AND name = 'b' AND value < 5)
the following query achieves the result, as does not rely joining between the tables in any way.
to include more of "name" you could add another OR clause inside the HAVING.
NOTE: This has been tested on the W3Schools WebSQL Interface
SELECT
value,
name,
user_id
FROM config
GROUP BY value, name, user_id
HAVING
(
(value > 4 AND name = 'a')
OR (value < 5 AND name = 'b')
)
After ditching the grouping (thanks Arth)
SELECT
value,
name,
user_id
FROM config
WHERE
(
(value > 4 AND name = 'a')
OR (value < 5 AND name = 'b')
)
Related
I have a problem to find the group membership for users. (yes it's not very clear like that)
For example:
I have 2 table:
- one contains a user list with their permission:
userId | permission
-------|-----------
1 | build
1 | play
1 | jump
2 | build
2 | jump
2 | run
3 | drink
3 | build
4 | run
-the second table contain the group and him permisson:
groupId | permission
--------|-----------
G1 | build
G1 | jump
G2 | play
G2 | jump
G3 | drink
G3 | run
G4 | drink
G5 | build
My goal is to find all the groups that the user can have:
userId | groupId
-------|-----------
1 | G1
1 | G2
1 | G5
2 | G1
2 | G5
3 | G4
3 | G5
I have created a request to find which users belong to the group but I can not do this for all my groups (I have more than 1000 group in my datasets):
SELECT DISTINCT userId
FROM (
SELECT userId, count(*) AS nbData
from table_a
WHERE permission in (
SELECT permission
from table_b
where groupId = 'g1'
)
group by userId
) as t
where nbData = (SELECT count(*) from table_b where groupId = 'g1');
An user belongs to a group if he has all the permission of the group. And the goal is to find every group of each user
If you don't have duplicates in either table, you can join the tables together on permissions and aggregate:
select up.userId, gp.groupId
from user_permissions up join
group_permissions gp
on up.permission = gp.permission
group by up.userId, gp.groupId
having count(*) = (select count(*)
from group_permissions gp2
where gp2.groupId = gp.groupId
);
The user is in the group if all the permissions for the group match.
Something like this might work:
SELECT DISTINCT
a.userId,
b.groupId
FROM
table_a a
JOIN table_b b
ON a.permission = b.permission
E.g.:
DROP TABLE IF EXISTS user_permissions;
CREATE TABLE user_permissions
(user_id INT NOT NULL
,permission VARCHAR(12) NOT NULL
,PRIMARY KEY(user_id,permission)
);
INSERT INTO user_permissions VALUES
(1,'build'),
(1,'play'),
(1,'jump'),
(2,'build'),
(2,'jump'),
(2,'run'),
(3,'drink'),
(3,'build'),
(4,'run');
DROP TABLE IF EXISTS group_permissions;
CREATE TABLE group_permissions
(group_id INT NOT NULL
,permission VARCHAR(12) NOT NULL
,PRIMARY KEY(group_id,permission)
);
INSERT INTO group_permissions VALUES
(101,'build'),
(101,'jump'),
(102,'play'),
(102,'jump'),
(103,'drink'),
(103,'run'),
(104,'drink'),
(105,'build');
SELECT DISTINCT u.user_id
, g.group_id
FROM user_permissions u
JOIN group_permissions g
ON g.permission = u.permission -- groups that users potentially belong to
LEFT
JOIN
( SELECT DISTINCT x.user_id
, y.group_id
FROM user_permissions x
LEFT
JOIN group_permissions y
ON y.permission = x.permission
LEFT
JOIN group_permissions z
ON z.group_id = y.group_id
LEFT
JOIN user_permissions a
ON a.user_id = x.user_id
AND a.permission = z.permission
WHERE a.user_id IS NULL
) n -- groups that users don't belong to (this could be much simpler ;-) )
ON n.user_id = u.user_id
AND n.group_id = g.group_id
WHERE n.user_id IS NULL;
+---------+----------+
| user_id | group_id |
+---------+----------+
| 1 | 101 |
| 1 | 105 |
| 1 | 102 |
| 2 | 101 |
| 2 | 105 |
| 3 | 105 |
| 3 | 104 |
+---------+----------+
i have a table which contains some data as an example:
+----------+-----+------+
| order_id | poi | povi |
+----------+-----+------+
| 1 | A | a |
| 1 | B | b |
| 1 | C | c |
| 2 | A | a |
| 2 | B | b |
| 2 | C | c |
| 3 | A | a |
| 3 | B | b |
| 4 | C | c |
| 5 | A | a |
| 5 | B | b |
| 6 | C | c |
| 7 | A | a |
| 8 | B | b |
| 9 | C | c |
+----------+-----+------+
i have 3 set of values of poi and povi like {A,a},{B,b},{C,c}
i want to get the order_id which contains all three of them, like in the above case the output should be.(order_id which have poi and povi as {A,a} and {B,b} and {C,c} but the problem is that they are diffrent rows)
+----------+
| order_id |
+----------+
| 1 |
| 2 |
+----------+
any idea?
So many times people just getting started ask similar questions to those already asked and answered, including this common scenario. However, not being able to apply know answers to your scenario doesn't help you wrap your head around what is asked, or how the query works in their own scenario... That said, lets look at yours.
You want all DISTINCT orders that have ALL of the following A/a, B/b, C/c entries. Multiple ways to resolve, but the most common is with a where / group by / having.
Start with something simple, looking for any order that has A/a
select
yt.Order_id
from
YourTable yt
where
( yt.poi = 'A' AND yt.poiv = 'a' )
and you would get order 1, 2, 3, 5 and 7. That is simple...
Now, add in your other criteria
select
yt.Order_id
from
YourTable yt
where
( yt.poi = 'A' AND yt.poiv = 'a' )
OR ( yt.poi = 'B' AND yt.poiv = 'b' )
OR ( yt.poi = 'C' AND yt.poiv = 'c' )
and this will give you all rows, but not what you want, but you should be able to see the where criteria is checking for both parts of POI / POIV with an OR between each possible combination. You obviously can not have one record that has a POI of both "A" and "B", that is why the "OR" between each paired ( AND ) criteria. But again, this gives ALL rows. But it is also qualifying only the pieces. So lets add one next step... a group by via the order, but HAVING clause expecting 3 records...
select
ytA.Order_id
from
YourTable ytA
where
( yt.poi = 'A' AND yt.poiv = 'a' )
OR ( yt.poi = 'B' AND yt.poiv = 'b' )
OR ( yt.poi = 'C' AND yt.poiv = 'c' )
group by
yt.Order_id
HAVING
count(*) = 3
The count(*) is to count how many records qualified the WHERE clause and will only return those records that had 3 entries.
Now, what if someone has multiple orders of A/a, A/a, B/b... This COULD give a false answer returned value, but please confirm these queries to meet your needs.
Although accepted, here is another way I would have written the query... somewhat similar to another post below. The premise of this version of the query is to utilize an index and qualify at least 1 record found before trying to find ALL. In this case, it first qualifies for those with an A/a. If an order does not have that, it does not care about looking for a B/b, C/c. If it DOES, then the join qualifies to the next levels too
select
ytA.Order_id
from
YourTable ytA
JOIN YourTable ytB
on ytA.Order_id = ytB.Order_id
AND ytB.poi = 'B'
AND ytB.poiv = 'b'
JOIN YourTable ytC
on ytB.Order_id = ytC.Order_id
AND ytC.poi = 'C'
AND ytC.poiv = 'c'
where
ytA.poi = 'A'
AND ytA.poiv = 'a'
find the "intersection" of lists, each of which contains one set
select id
from
(select id from mytable where poi = 'A' and povi= 'a') t1
inner join
(select id from mytable where poi = 'B' and povi= 'b') t2
using(id)
inner join
(select id from mytable where poi = 'C' and povi= 'c') t3
using(id)
demo
Using MySQL and I have two tables similar to the following. Both actually have more rows and more columns but for ease of reading, I've only put a few
wp_ahm_files
ID | Field_A | Field_B | Field_C
--------------------------------
69 | ABC | DEF | GHI
wp_ahm_filemeta
pID in this table refers to the ID of the table above
ID | pID | Name | Value
---------------------------------
25 | 69 | Version | 12345
26 | 69 | Expiry | 29/08/1981
How do I bring back a resultset such as
ID | Field_A | Field_B | Field_C | Version | Expiry
-------------------------------------------------------
69 | ABC | DEF | GHI | 12345 | 29/08/1981
You should be able to JOIN to your wp_ahm_filemeta twice to get the result:
select f.id,
f.field_a,
f.field_b,
f.field_c,
m1.value version,
m2.value expiry
from wp_ahm_files f
left join wp_ahm_filemeta m1
on f.id = m1.pid
and m1.name = 'Version'
left join wp_ahm_filemeta m2
on f.id = m2.pid
and m2.name = 'Expiry';
See SQL Fiddle with Demo. The key is to place a filter on the JOIN condition to return the rows with the specific name value you want.
You could also convert the rows of data into columns by using an aggregate function with a CASE expression:
select f.id,
f.field_a,
f.field_b,
f.field_c,
max(case when m.name = 'Version' then m.value end) version,
max(case when m.name = 'Expiry' then m.value end) expiry
from wp_ahm_files f
left join wp_ahm_filemeta m
on f.id = m.pid
group by f.id, f.field_a, f.field_b, f.field_c;
See SQL Fiddle with Demo
Something like
select a.*, version.value version, expiry.value expiry
from a join
( select * from b where name = 'Version' ) version
on version.table_a_id = a.id
join
( select * from b where name = 'Expiry' ) expiry
on expiry.table_a_id = a.id
In MySQL, I have a table like this:
+-----------------------+
| Assets |
+-----------------------+
| Id | Name | RootId |
+----+---------+--------+
| 1 | Asset A | 1 |
+----+---------+--------+
| 2 | Asset B | 2 |
+----+---------+--------+
| 3 | Asset C | 3 |
+----+---------+--------+
| 4 | Asset D | 2 |
+----+---------+--------+
| 5 | Asset E | 3 |
+----+---------+--------+
| 6 | Asset F | 3 |
+----+---------+--------+
Not the greatest table structure, I know...but I'm stuck with it for now.
I am trying to write a single query that, given an Id value, will return a RootId and RootName ONLY if there are exactly two (2) rows with the same RootId. Otherwise those columns should be NULL.
So, using the table above, if given an Id of 4 the query should return:
+----------------------------------+
| Assets |
+----------------------------------+
| Id | Name | RootId | RootName |
+----+---------+--------+----------+
| 4 | Asset D | 2 | AssetB |
+----+---------+--------+----------+
But if given any other Id value, such as 5, it should return:
+----------------------------------+
| Assets |
+----------------------------------+
| Id | Name | RootId | RootName |
+----+---------+--------+----------+
| 5 | Asset E | null | null |
+----+---------+--------+----------+
Any help on this would be greatly appreciated. I think it will require a subquery with a COUNT and possibly a GROUP BY, but I'm really not sure how to articulate it...
Thanks in advance!
The following should implement this logic:
select id, name,
(case when cnt = 2 then rootid end) as rootid,
(case when cnt = 2 then ari.name end) as rootname
from assets a join
(select rootid, count(*) as cnt
from assets a
group by rootid
) ri
on a.rootid = ri.rootid left join
assets ari
on a.rootid = ari.id
where id = 4;
You can also do this as:
select a.id, a.name,
(case when a.cnt = 2 then a.rootid end) as rootid,
(case when a.cnt = 2 then ari.name end) as rootname
from (select a.*,
(select count(*) from assets a2 where a2.rootid = a.rootid) as cnt
from assets a
where id = 4
) a left join
assets ari
on a.rootid = ari.id;
Without the full aggregation, this will perform better.
Here is a SQL Fiddle illustrating them.
Something like this will return the specified resultset:
SELECT a.Id
, a.Name
, IF(q.cnt=2,q.RootId,NULL) AS RootId
, IF(q.cnt=2,q.RootName,NULL) AS RootName
FROM Assets a
JOIN ( SELECT COUNT(1) AS cnt
, r.RootId
, r.RootName
FROM Assets r
JOIN Assets s
ON s.RootId = r.RootId
WHERE r.Id = 4
GROUP BY r.RootId, r.RootName
) q
ON q.Id = a.Id
If its possible for RootId to be NULL, then you'd want to use a LEFT [OUTER] JOIN, and include a . If you want to consider a NULL value for RootId as matching another NULL value, then replace the equality comparator with the null-safe equality comparison operator, <=>
Adding those two tweaks gives a more robust solution:
SELECT a.Id
, a.Name
, IF(q.cnt=2,q.RootId,NULL) AS RootId
, IF(q.cnt=2,q.RootName,NULL) AS RootName
FROM Assets a
JOIN ( SELECT COUNT(1) AS cnt
, r.RootId
, r.RootName
FROM Assets r
LEFT -- lef outer join
JOIN Assets s
ON s.RootId <=> r.RootId -- nullsafe equality
WHERE r.Id = 4
GROUP BY r.RootId, r.RootName
) q
ON q.Id = a.Id
I have a two select statements joined by UNION ALL. In the first statement a where clause gathers only rows that have been shown previously to the user. The second statement gathers all rows that haven't been shown to the user, therefore I end up with the viewed results first and non-viewed results after.
Of course this could simply be achieved with the same select statement using a simple ORDER BY, however the reason for two separate selects is simple after you realize what I hope to accomplish.
Consider the following structure and data.
+----+------+-----+--------+------+
| id | from | to | viewed | data |
+----+------+-----+--------+------+
| 1 | 1 | 10 | true | .... |
| 2 | 10 | 1 | true | .... |
| 3 | 1 | 10 | true | .... |
| 4 | 6 | 8 | true | .... |
| 5 | 1 | 10 | true | .... |
| 6 | 10 | 1 | true | .... |
| 7 | 8 | 6 | true | .... |
| 8 | 10 | 1 | true | .... |
| 9 | 6 | 8 | true | .... |
| 10 | 2 | 3 | true | .... |
| 11 | 1 | 10 | true | .... |
| 12 | 8 | 6 | true | .... |
| 13 | 10 | 1 | false | .... |
| 14 | 1 | 10 | false | .... |
| 15 | 6 | 8 | false | .... |
| 16 | 10 | 1 | false | .... |
| 17 | 8 | 6 | false | .... |
| 18 | 3 | 2 | false | .... |
+----+------+-----+--------+------+
Basically I wish all non viewed rows to be selected by the statement, that is accomplished by checking weather the viewed column is true or false, pretty simple and straightforward, nothing to worry here.
However when it comes to the rows already viewed, meaning the column viewed is TRUE, for those records I only want 3 rows to be returned for each group.
The appropriate result in this instance should be the 3 most recent rows of each group.
+----+------+-----+--------+------+
| id | from | to | viewed | data |
+----+------+-----+--------+------+
| 6 | 10 | 1 | true | .... |
| 7 | 8 | 6 | true | .... |
| 8 | 10 | 1 | true | .... |
| 9 | 6 | 8 | true | .... |
| 10 | 2 | 3 | true | .... |
| 11 | 1 | 10 | true | .... |
| 12 | 8 | 6 | true | .... |
+----+------+-----+--------+------+
As you see from the ideal result set we have three groups. Therefore the desired query for the viewed results should show a maximum of 3 rows for each grouping it finds. In this case these groupings were 10 with 1 and 8 with 6, both which had three rows to be shown, while the other group 2 with 3 only had one row to be shown.
Please note that where from = x and to = y, makes the same grouping as if it was from = y and to = x. Therefore considering the first grouping (10 with 1), from = 10 and to = 1 is the same group if it was from = 1 and to = 10.
However there are plenty of groups in the whole table that I only wish the 3 most recent of each to be returned in the select statement, and thats my problem, I not sure how that can be accomplished in the most efficient way possible considering the table will have hundreds if not thousands of records at some point.
Thanks for your help.
Note: The columns id, from, to and viewed are indexed, that should help with performance.
PS: I'm unsure on how to name this question exactly, if you have a better idea, be my guest and edit the title.
What a hairball! This gets progressively harder as you move from most recent, to second most recent, to third most recent.
Let's put this together by getting the list of IDs we need. Then we can pull the items from the table by ID.
This, relatively easy, query gets you the ids of your most recent items
SELECT id FROM
(SELECT max(id) id, fromitem, toitem
FROM stuff
WHERE viewed = 'true'
GROUP BY fromitem, toitem
)a
Fiddle: http://sqlfiddle.com/#!2/f7045/27/0
Next, we need to get the ids of the second most recent items. To do this, we need a self-join style query. We need to do the same summary but on a virtual table that omits the most recent items.
select id from (
select max(b.id) id, b.fromitem, b.toitem
from stuff a
join
(select id, fromitem, toitem
from stuff
where viewed = 'true'
) b on ( a.fromitem = b.fromitem
and a.toitem = b.toitem
and b.id < a.id)
where a.viewed = 'true'
group by fromitem, toitem
)c
Fiddle: http://sqlfiddle.com/#!2/f7045/44/0
Finally, we need to get the ids of the third most recent items. Mercy! We need to join that query we just had, to the table again.
select id from
(
select max(d.id) id, d.fromitem, d.toitem
from stuff d
join
(
select max(b.id) id, b.fromitem, b.toitem
from stuff a
join
(
select id, fromitem, toitem
from stuff
where viewed = 'true'
) b on ( a.fromitem = b.fromitem
and a.toitem = b.toitem
and b.id < a.id)
where a.viewed = 'true'
group by fromitem, toitem
) c on ( d.fromitem = c.fromitem
and d.toitem = c.toitem
and d.id < c.id)
where d.viewed='true'
group by d.fromitem, d.toitem
) e
Fiddle: http://sqlfiddle.com/#!2/f7045/45/0
So, now we take the union of all those ids, and use them to grab the right rows from the table, and we're done.
SELECT *
FROM STUFF
WHERE ID IN
(
SELECT id FROM
(SELECT max(id) id, fromitem, toitem
FROM stuff
WHERE viewed = 'true'
GROUP BY fromitem, toitem
)a
UNION
select id from (
select max(b.id) id, b.fromitem, b.toitem
from stuff a
join
(select id, fromitem, toitem
from stuff
where viewed = 'true'
) b on ( a.fromitem = b.fromitem
and a.toitem = b.toitem
and b.id < a.id)
where a.viewed = 'true'
group by fromitem, toitem
)c
UNION
select id from
(
select max(d.id) id, d.fromitem, d.toitem
from stuff d
join
(
select max(b.id) id, b.fromitem, b.toitem
from stuff a
join
(
select id, fromitem, toitem
from stuff
where viewed = 'true'
) b on ( a.fromitem = b.fromitem
and a.toitem = b.toitem
and b.id < a.id)
where a.viewed = 'true'
group by fromitem, toitem
) c on ( d.fromitem = c.fromitem
and d.toitem = c.toitem
and d.id < c.id)
where d.viewed='true'
group by d.fromitem, d.toitem
) e
UNION
select id from stuff where viewed='false'
)
order by viewed desc, fromitem, toitem, id desc
Tee hee. Too much SQL. Fiddle: http://sqlfiddle.com/#!2/f7045/47/0
And now, we need to cope with your last requirement, the requirement that your graph is unordered. That is, that from=n to=m is the same as from=m to=n.
To do this we need a virtual table instead of the physical table. This will do the trick.
SELECT id, least(fromitem, toitem) fromitem, greatest(fromitem,toitem) toitem, data
FROM stuff
Now we need to use this virtual table, this view, everywhere the physical table used to appear. Let's use a view to do this.
CREATE VIEW
AS
SELECT id,
LEAST(fromitem, toitem) fromitem,
GREATEST (fromitem, toitem) toitem,
viewed,
data;
So, our ultimate query is:
SELECT *
FROM stuff
WHERE ID IN
(
SELECT id FROM
(SELECT max(id) id, fromitem, toitem
FROM STUFF_UNORDERED
WHERE viewed = 'true'
GROUP BY fromitem, toitem
)a
UNION
SELECT id FROM (
SELECT max(b.id) id, b.fromitem, b.toitem
FROM STUFF_UNORDERED a
JOIN
(SELECT id, fromitem, toitem
FROM STUFF_UNORDERED
WHERE viewed = 'true'
) b ON ( a.fromitem = b.fromitem
AND a.toitem = b.toitem
AND b.id < a.id)
WHERE a.viewed = 'true'
GROUP BY fromitem, toitem
)c
UNION
SELECT id FROM
(
SELECT max(d.id) id, d.fromitem, d.toitem
FROM STUFF_UNORDERED d
JOIN
(
SELECT max(b.id) id, b.fromitem, b.toitem
FROM STUFF_UNORDERED a
JOIN
(
SELECT id, fromitem, toitem
FROM STUFF_UNORDERED
WHERE viewed = 'true'
) b ON ( a.fromitem = b.fromitem
AND a.toitem = b.toitem
AND b.id < a.id)
WHERE a.viewed = 'true'
GROUP BY fromitem, toitem
) c ON ( d.fromitem = c.fromitem
AND d.toitem = c.toitem
AND d.id < c.id)
WHERE d.viewed='true'
GROUP BY d.fromitem, d.toitem
) e
UNION
SELECT id FROM STUFF_UNORDERED WHERE viewed='false'
)
ORDER BY viewed DESC,
least(fromitem, toitem),
greatest(fromitem, toitem),
id DESC
Fiddle: http://sqlfiddle.com/#!2/8c154/4/0