Building SQL query on a one-to-many relationship - mysql

I have a search page where I am trying to build a complex search condition on two tables which look something like:
Users
ID NAME
1 Paul
2 Remy
...
Profiles
FK_USERS_ID TOPIC TOPIC ID
1 language 1
1 language 2
1 expertise 1
1 expertise 2
1 expertise 3
2 language 1
2 language 2
The second table Profiles, lists the "languages" or the "expertises" (among other stuff) of each user, and topic id is a foreign key to another table depending on the topic (if topic is "language", than topic ID is the ID of a language in the languages table, etc...).
The search needs to find something like where user name LIKE %PAU% and the user "has" language 1 and has language 2 and has expertise 1 and has expertise 2.
Any help would be really appreciated! I am performing a LEFT JOIN on the two tables although I am not sure that is the correct choice. My main problem lies on the "AND". The same user has to have both languages 1 and 2, and at the same time expertise 1 and 2.
I work in PHP and I usually try to avoid inner SELECTs and even joins, but I think an inner SELECT is imminent here?

You can accomplish this by building a set of users that matches the criterias from your profile tables, something like this:
SELECT FK_USERS_ID
FROM Profiles
WHERE topic='x'
AND TOPIC_ID IN (1,2)
GROUP BY FK_USERS_ID
HAVING COUNT(1) = 2
Here you list your users that matches the topics you need. By grouping by the user id and specifying the amount of rows that should be returned, you can effectively say "only those that has x and y in topic z. Just make sure that the COUNT(1) = x has the same number of different TOPIC_IDs to look for.
You can then query the user table
SELECT ID
FROM Users
WHERE name like '%PAU%'
AND ID IN (<insert above query here>)
You can also do it in a join and a derived table, but the essence should be explained above.
EDIT:
if you are looking for multiple combinations, you can use mysql's multi-column IN:
SELECT FK_USERS_ID
FROM Profiles
WHERE (topic,topic_id) IN (('x',3),('x',5),('y',3),('y',6))
GROUP BY FK_USERS_ID
HAVING COUNT(1) = 4
This will look for uses matching the pairs x-3, x-5, y-3 and y-6.
You should be able to build the topic-topic_id pairs easily in php and stuffing it into the SQL string, and also just counting the number of pairs you generate into a variable for using for the count(1) number. See http://www.mysqlperformanceblog.com/2008/04/04/multi-column-in-clause-unexpected-mysql-issue/ for performance talk using this approach.

Isn't it just a simple classical INNER JOIN?
SELECT
p.topic, p.topic_id
FROM
profiles p
INNER JOIN
users u
ON
u.id = p.fk_users_id
WHERE
u.name LIKE '%Paul%'
This query would return all the languages and expertise with their IDs for the users matching the pattern, in this case containing Paul in their name. Is this what you like? Or something else?

select *
from users u, profiles p
where u.id = p.fk_users_id
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'language'
and topic_id = 1)
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'language'
and topic_id = 22)
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'expertise'
and topic_id = 1)
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'expertise'
and topic_id = 1)
and u.name like '%PAU%'
EDIT:
Ok, a slight variation on #cairnz' answer:
SELECT ID
FROM Users
WHERE name like '%PAU%'
AND ID IN (SELECT FK_USERS_ID
FROM Profiles
WHERE topic='x'
AND ((TOPIC_ID = 1 AND TOPIC = 'language')
OR (TOPIC_ID = 2 AND TOPIC = 'language')
OR (TOPIC_ID = 1 AND TOPIC = 'expertise')
OR (TOPIC_ID = 2 AND TOPIC = 'expertise'))
GROUP BY FK_USERS_ID
HAVING COUNT(1) = 4)

I would do based on JOIN conditions multiple times against each condition that you are "requiring". I would also ensure an index on the Profiles table based on the each part of the key looking for... (FK_User_ID, Topic_ID, Topic)
SELECT STRAIGHT_JOIN
U.ID
FROM Users U
JOIN Profiles P1
on U.ID = P1.FK_User_ID
AND P1.Topic_Id = 1
AND P1.Topic = "language"
JOIN Profiles P2
on U.ID = P2.FK_User_ID
AND P2.Topic_Id = 2
AND P2.Topic = "language"
JOIN Profiles P3
on U.ID = P3.FK_User_ID
AND P3.Topic_Id = 1
AND P3.Topic = "expertise"
JOIN Profiles P4
on U.ID = P4.FK_User_ID
AND P4.Topic_Id = 2
AND P4.Topic = "expertise"
WHERE
u.name like '%PAU%'
This way, any additional criteria as expressed in other answer provided shouldn't be too much an impact. The tables are setup by the criteria as if simultaneous, and if any are missing, they will be excluded from the result immediately instead of trying to do a sub-select counting for every entry (which I think might be the lag you are encountering).
So, each of your "required" criteria would take the same "JOIN" construct, and as you can see, I'm just incrementing the "alias" of the join instance.

Related

Get mutual relationship from database table

I have table user_follow which has only two columns: who and whom. They represent links between users when WHO follows WHOM. If two users follow each other, they are considered friends. In such case the table would contain records:
WHO WHOM
1 2
2 1
Where 1 and 2 are simply user IDs.
In order to determine if two users are friends, I have to query the table and use simple condition
SELECT COUNT(*)
FROM user_follow
WHERE (who = 1 AND whom = 2) OR (who = 2 AND whom = 1)
If I get 2 then they are friends.
But if I want to load list of all user's friends, I cannot do it that way. So i came up with sql join into itself:
SELECT uf2.whom
FROM user_follow AS uf1
LEFT JOIN user_follow AS uf2 ON uf1.who = uf2.whom
WHERE uf1.whom = ? AND uf2.who = ? ORDER BY uf2.whom
I made a dummy table to test it and it works. But I would like for someone to confirm this is the correct solution.
I think your query is correct if you supply the same parameter value for ?. I find the query below simpler to understand.
The query below lists the friends (users who follow each other) of a user:
SELECT uf1.whom
FROM user_follow AS uf1
INNER JOIN user_follow AS uf2
-- filter down to followed users
ON uf1.whom = uf2.who
-- followed users follow their followers
AND uf1.who = uf2.whom
-- the user whose friends are listed
WHERE uf1.who = ?
ORDER BY 1
;

How to JOIN SELECT from multiple tables, where the SELECTS is based on different conditions?

I have three tables. One with notes Notes, one with users Users, and one a relational table between users and notes NotesUsers.
Users
user_id first_name last_name
1 John Smith
2 Jane Doe
Notes
note_id note_name owner_id
1 Math 1
2 Science 1
3 English 2
NoteUsers
user_id note_id
1 1
2 1
2 2
2 3
Hopefully, from the select statement you can tell what I'm trying to do. I am trying to select the notes that user_id = 2 has access to but doesn't necessarily own, but also along with this I'm trying to get the first and last name of the owner.
SELECT Notes.notes_id, note_name
FROM Notes, NotesUsers
WHERE NotesUsers.note_id = Notes.note_id AND NotesUsers.user_id = 2
JOIN SELECT first_name, last_name FROM Users, Notes WHERE Notes.owner_id = Users.user_id
My problem is that because the WHERE clause for first_name, and last_name versus that for notes are different, I don't know how to query the data. I understand that this is not how a JOIN works and
I don't necessarily want to use a JOIN, but I'm not sure how to structure the statement, so I left it in there so that you can understand what I'm trying to do.
You can join Notes with NoteUsers to check for access and with Users to add the user's details to the result:
SELECT n.noted_id, n.note_name, u.first_name, u.last_name
FROM Notes n
JOIN NoteUsers nu ON n.noted_id = nu.note_id AND nu.user_id = 2
JOIN Users u ON n.owner_id = u.user_id
you need here to use a query inside the main query. MySQL will return first all the note_id that the user with user_id = 2 has access to from NoteUser, then well build the outer query to return the first_name and the last_name of the owner.
SELECT u.first_name, u.last_name, n.note_name, n.note_id
FROM Notes AS n
LEFT JOIN Users AS u ON u.user_id = n.owner_id
WHERE n.note_id IN
(SELECT nu.note_id FROM NoteUser WHERE nu.user_id = 2)

Optimisation of subqueries

I have a relation between users and groups. Users can be in a group or not.
EDIT : Added some stuff to the model to make it more convenient.
Let's say I have a rule to add users in a group considering it has a specific town, and a custom metadata like age 18).
Curently, I do that to know which users I have to add in the group of the people living in Paris who are 18:
SELECT user.id AS 'id'
FROM user
LEFT JOIN
(
SELECT user_id
FROM user_has_role_group
WHERE role_group_id = 1 -- Group for Paris
)
AS T1
ON user.id = T1.user_id
WHERE
(
user.town = 'Paris' AND JSON_EXTRACT('custom_metadata', '$.age') = 18
)
AND T1.user_id IS NULL
It works & gives me the IDs of the users to insert in group.
But when I have 50 groups to proceed, like for 50 town or various ages, it forces me to do 50 requests, it's very slow and not efficient for my Database.
How could I generate a result for each group ?
Something like :
role_group_id user_to_add
1 1
1 2
2 1
2 3
The only way I know to do that for now is to do an UNION on several sub queries like the one above, but of course it's very slow.
Note that the custom_metadata field is a user defined field. I can't create specific columns or tables.
Thanks a lot for your help.
if I good understood you:
select user.id, grp.id
from user, role_group grp
where (user.id, grp.id) not in (select user_id, role_group_id from user_has_role_group) and user.town in ('Paris', 'Warsav')
that code give list of users and group which they not belong from one of towns..
To add the missing entries to user_has_role_group, you might want to have some mapping between those town names and their group_id's.
The example below is just using a subquery with unions for that.
But you could replace that with a select from a table.
Maybe even from role_group, if those names correlate with the user town names.
insert into user_has_role_group (user_id, group_id)
select u.user_id, g.group_id
from user u
join (
select 'Paris' as name, 1 as group_id union all
select 'Rome', 2
-- add more towns here
) g on (u.town = g.name)
left join user_has_role_group ug
on (ug.user_id = u.user_id and ug.role_group_id = g.group_id)
where u.town in ('Paris','Rome') -- add more towns here
and json_extract(u.custom_metadata, '$.age') = 18
and ug.id is null;

Finding mutual friend in one way relationship table

want mysql query for finding mutual friend between two friend but
I am maintain the friendship of user in one way relationship for ex.
first is users table
id name
1 abc
2 xyz
3 pqr
Now second table is friend
id user_id friend_id
1 1 2
2 1 3
3 2 3
Now here i can say that abc(id=1) is friend of xyz(id=2) now similar way the xyz is friend of abc but now i want to find mutual friend between abc(id=1) and xyz(id=2) that is pqr so I want mysql query for that.
REVISED
This query will consider the "one way" relationship of a row in the friend table to be a "two way" relationship. That is, it will consider a friend relationship: ('abc','xyz') to be equivalent to the inverse relationship: ('xyz','abc'). (NOTE: we don't have any guarantee that both rows won't appear in the table, so we need to be careful about that. The UNION operator conveniently eliminates duplicates for us.)
This query should satisfy the specification:
SELECT mf.id
, mf.name
FROM (
SELECT fr.user_id AS user_id
, fr.friend_id AS friend_id
FROM friend fr
JOIN users fru
ON fru.id = fr.user_id
WHERE fru.name IN ('abc','xyz')
UNION
SELECT fl.friend_id AS user_id
, fl.user_id AS friend_id
FROM friend fl
JOIN users flf
ON flf.id = fl.friend_id
WHERE flf.user IN ('abc','xyz')
) f
JOIN users mf
ON mf.id = f.friend_id
GROUP BY mf.id, mf.name
HAVING COUNT(1) = 2
ORDER BY mf.id, mf.name
SQL Fiddle here http://sqlfiddle.com/#!2/b23a5/2
A more detailed explanation of how we arrive at this is given below. The original queries below assumed that a row in the friend table represented a "one way" relationship, in that "'abc' ff 'xyz'" did not imply "'xyz' ff 'abc'". But additional comments from the OP hinted that this was not the case.
If there is a unique constraint on friend(user_id,friend_id), then one way to get the result would be to get all of the friends of each user, and get a count of rows for that friend. If the count is 2, then we know a particular friend_id appears for both user 'abc' and for 'xyz'
SELECT mf.id
, mf.name
FROM friend f
JOIN users uu
ON uu.id = f.user_id
JOIN users mf
ON mf.id = f.friend_id
WHERE uu.name IN ('abc','xyz')
GROUP BY mf.id, mf.name
HAVING COUNT(1) = 2
ORDER BY mf.id, mf.name
(This approach can also be extended to find a mutual friend of three or more users, by including more users in the IN list, and changing the value we compare the COUNT(1) to.
This isn't the only query that will return the specified resultset; there are other ways to get it as well.
Another way to get an equivalent result:
SELECT u.id
, u.name
FROM ( SELECT f1.friend_id
FROM friend f1
JOIN users u1
ON u1.id = f1.user_id
WHERE u1.name = 'abc'
) t1
JOIN ( SELECT f2.friend_id
FROM friend f2
JOIN users u2
ON u2.id = f2.user_id
WHERE u2.name = 'xyz'
) t2
ON t2.friend_id = t1.friend_id
JOIN users u
ON u.id = t1.friend_id
ORDER BY u.id, u.name
NOTES
These queries do not check whether user 'abc' is a friend of 'xyz' (the two user names specified in the WHERE clause). It is only finding the common friend of both 'abc' and 'xyz'.
FOLLOWUP
The queries above satisfy the specified requirements, and all the examples and test cases provided in the question.
Now it sounds as if you want a row in that relationship table to be considered a "two way" relationship rather than just a "one way" relationship. It sounds like you want to want to consider the friend relationship ('abc','xyz') equivalent to ('xyz','abc').
To get that, then all that needs to be done is to have the query create the inverse rows,, and that makes it easier to query. We just need to be careful that if both those rows ('abc','xyz') and ('xyz','abc') already exist, that we don't create duplicates of them when we invert them.
To create the inverse rows, we can use a query like this. (It's simpler to look at this when we don't have the JOIN to the users table, and we use just the id value:
SELECT fr.user_id
, fr.friend_id
FROM friend fr
WHERE fr.user_id IN (1,2)
UNION
SELECT fl.friend_id AS user_id
, fl.user_id AS friend_id
FROM friend fl
WHERE fl.friend_id IN (1,2)
It's simpler if we don't include the predicates on the user_id and friend_id table, but that could be a very large (and expensive) rowset to materialize.
try this:
given that you want to get the mutual friends of friends 1 & 2
select friend_id into #tbl1 from users where user_id = 1
select friend_id into #tbl2 from users where friend_id = 2
select id, name from users where id in(select friend_id from #tbl1 f1, #tbl2 f2 where f1.friend_id=f2.friend_id)

Mysql query in drupal database - groupwise maximum with duplicate data

I'm working on a mysql query in a Drupal database that pulls together users and two different cck content types. I know people ask for help with groupwise maximum queries all the time... I've done my best but I need help.
This is what I have so far:
# the artists
SELECT
users.uid,
users.name AS username,
n1.title AS artist_name
FROM users
LEFT JOIN users_roles ur
ON users.uid=ur.uid
INNER JOIN role r
ON ur.rid=r.rid
AND r.name='artist'
LEFT JOIN node n1
ON n1.uid = users.uid
AND n1.type = 'submission'
WHERE users.status = 1
ORDER BY users.name;
This gives me data that looks like:
uid username artist_name
1 foo Joe the Plumber
2 bar Jane Doe
3 baz The Tooth Fairy
Also, I've got this query:
# artwork
SELECT
n.nid,
n.uid,
a.field_order_value
FROM node n
LEFT JOIN content_type_artwork a
ON n.nid = a.nid
WHERE n.type = 'artwork'
ORDER BY n.uid, a.field_order_value;
Which gives me data like this:
nid uid field_order_value
1 1 1
2 1 3
3 1 2
4 2 NULL
5 3 1
6 3 1
Additional relevant info:
nid is the primary key for an Artwork
every Artist has one or more Artworks
valid data for field_order_value is NULL, 1, 2, 3, or 4
field_order_value is not necessarily unique per Artist - an Artist could have 4 Artworks all with field_order_value = 1.
What I want is the row with the minimum field_order_value from my second query joined with the artist information from the first query. In cases where the field_order_value is not valuable information (either because the Artist has used duplicate values among their Artworks or left that field NULL), I would like the row with the minimum nid from the second query.
The Solution
Using divide and conquer as a strategy and mysql views as a technique, and referencing this article about groupwise maximum queries, I solved my problem.
Create the View
# artists and artworks all in one table
CREATE VIEW artists_artwork AS
SELECT
users.uid,
users.name AS artist,
COALESCE(n1.title, 'Not Yet Entered') AS artist_name,
n2.nid,
a.field_image_fid,
COALESCE(a.field_order_value, 1) AS field_order_value
FROM users
LEFT JOIN users_roles ur
ON users.uid=ur.uid
INNER JOIN role r
ON ur.rid=r.rid
AND r.name='artist'
LEFT JOIN node n1
ON n1.uid = users.uid
AND n1.type = 'submission'
LEFT JOIN node n2
ON n2.uid = users.uid
AND n2.type = 'artwork'
LEFT JOIN content_type_artwork a ON n2.nid = a.nid
WHERE users.status = 1;
Query the View
SELECT
a2.uid,
a2.artist,
a2.artist_name,
a2.nid,
a2.field_image_fid,
a2.field_order_value
FROM (
SELECT
uid,
MIN(field_order_value) AS field_order_value
FROM artists_artwork
GROUP BY uid
) a1
JOIN artists_artwork a2
ON a2.nid = (
SELECT
nid
FROM artists_artwork a
WHERE a.uid = a1.uid
AND a.field_order_value = a1.field_order_value
ORDER BY
uid ASC, field_order_value ASC, nid ASC
LIMIT 1
)
ORDER BY artist;
A simple solution to this can be to create views in your database that can then be joined together. This is especially useful if you often want to see the intermediate data in the same way in some other place. While it is possible to mash together the one huge query, I just take the divide and conquer approach sometimes.