"friends of friends" like sql query - mysql

I am building an application where users can connect with each other (something like friends in any social network).
I am saving that connections in a table with the following structure:
id_user1 | id_user2 | is_accepted | is_blocked | created_at
The connections between users are bi-directional, so when two users are connected there is only one record in the table. Doesnt matter if the user_id is in id_user1 or id_user2 collumn.
Now i need to write a sql query to get "friends of friends" of a certain user that are not already friends of the user.
Also the user must be accepted and not blocked.
In resume, here are the steps i need to execute.
Find all the users id associated with the user I want (id_user1 = current_user or id_user2 = current_user and is_accepted and !blocked)
foreach of the returned user_ids --> get all the associated users( ignore associations with current user) (make sure it is accepted and !blocked also).
How can I do such query?.
Thanks for your help.

For the reasons others have mentioned, and because I've seen it work better in other systems, I'd go with a row for each direction.
primary_user_id | related_user_id | is_accepted | is_blocked | created_at
You can also then create a clustered index on the user_id which should more than offset the overhead of doubling the number of rows.
Your first query would then translate into something like this:
SELECT f1.related_user_id
FROM friends f1
WHERE f1.primary_user_id = #current_user
AND f1.is_accepted = 1 AND f1.is_blocked = 0
AND EXISTS (
SELECT *
FROM friends f2
WHERE f1.related_user_id = f2.primary_user_id
AND f2.related_user_id = #current_user
AND f2.is_accepted = 1 AND f2.is_blocked = 0
Not sure if you can do table functions in MySql. If so then wrap this up into a function to make your second query simpler.

SELECT CASE f2.id_user1 WHEN CASE f1.id_user1 WHEN $user THEN f1.id_user2 ELSE f1.id_user1 END THEN f2.id_user2 ELSE f2.id_user1 END
FROM friends f1
JOIN friends f2
ON f2.id_user1 = CASE f1.id_user1 WHEN $user THEN f1.id_user2 ELSE f1.id_user1 END
OR f2.id_user2 = CASE f1.id_user1 WHEN $user THEN f1.id_user2 ELSE f1.id_user1 END
WHERE (f1.id_user1 = $user OR f1.id_user = $user)
AND f1.is_accepted = 1
AND f2.is_accepted = 1
AND f1.is_blocked = 0
AND f2.is_blocked = 0
AND NOT (f1.id_user1, f1.id_user2) = (f2.id_user1, f2.id_user2)
Note that it is better to store the users least first, greatest second. In this case the query would be more simple.

When working with the one-record-per-friendship table directly, all queries will be bloated and error-prone, because you will have to write 'id_user1 = ... or id_user2 = ...' often. I'd create a view
CREATE VIEW bidifreinds (id_user1, id_user2, is_accepted, is_blocked, created_at) AS
SELECT id_user1, id_user2, is_accepted, is_blocked, created_at FROM friends
UNION
SELECT id_user2, id_user1, is_accepted, is_blocked, created_at FROM friends
This will make life much easier.
Then you can write
SELECT f1.id_user1, f2.id_user2
FROM friends f1, friends f2
WHERE f2.id_user1 = f1.id_user2
AND f1.is_accepted
AND NOT f1.is_blocked
AND f2.is_accepted
AND NOT f2.is_blocked
And I hope you're not usind MySQL, because MySQL is very slow at querying over views.

select id_user1 from friends where is_accepted = 1 and is_blocked = 0 and id_user2 in
(select id_user1 from friends where is_accepted = 1 and is_blocked = 0 and id_user2 = :a_user:
union
select id_user2 from friends where is_accepted = 1 and is_blocked = 0 and id_user1 = :a_user:)
union
select id_user2 from friends where is_accepted = 1 and is_blocked = 0 and id_user1 in
(select id_user1 from friends where is_accepted = 1 and is_blocked = 0 and id_user2 = :a_user:
union
select id_user2 from friends where is_accepted = 1 and is_blocked = 0 and id_user1 = :a_user:)
You can add a whereClause to weed out :a_user: from the resultset.

Related

MySQL "AND ONLY HAS" type operator/function?

MySQL here. I have the following data model:
[applications]
===
id : PK
status : VARCHAR
...lots of other fields
[invoices]
===
id : PK
application_id : FK to applications.id
status : VARCHAR
... lot of other fields
It is possible for the same application to have 0+ invoices associated with it, each with a different status. I am trying to write a query that looks for applications that:
have a status of "Pending"; and
have only invoices whose status is "Accepted"
My best attempt at such a query is:
SELECT a.id,
i.id,
a.status,
i.status
FROM application a
INNER JOIN invoice i ON a.id = i.application_id
WHERE a.status = "Pending"
AND i.status = "Accepted"
The problem here is that this query does not exclude applications that are associated with non-Accepted invoices. Hence it might return a row of, say:
+--------+--------+-----------+-----------+
| id | id | status | status |
+--------+--------+-----------+-----------+
| 123 | 456 | Pending | Accepted |
+--------+--------+-----------+-----------+
However, when I query the invoice table for any invoices tied to application id = 123, there are many non-Accepted invoices that come back in the results. Its almost as if I wished SQL support some type of "AND ONLY HAS" so I could make my clause: "AND ONLY HAS i.status = 'Accepted'"
So I'm missing the clause that excludes results for applications with 1+ non-Accepted invoices. Any ideas where I'm going awry?
You can use the following logic:
SELECT *
FROM application
WHERE status = 'pending'
AND EXISTS (
SELECT 1
FROM invoice
WHERE invoice.application_id = application.id
HAVING SUM(CASE WHEN invoice.status = 'accepted' THEN 1 ELSE 0 END) > 0 -- count of accepted invoices > 0
AND SUM(CASE WHEN invoice.status = 'accepted' THEN 0 ELSE 1 END) = 0 -- count of anyother invoices = 0
)

Getting Follower and FollwedBy Users from Table in one list

I have a Table that tracks followers
FollowerUserId, FollowingUserId
1 2
2 1
3 1
4 1
1 5
I want to get all user that given Id follows and is followed by or Both.
for example for UserId 1,I want result to be: (FG: Following, FD: Followed, B: Both ways)
2,B
5,FG
3,FD
4,FD
i can easily get FG and FD by doing union
Select FollowerUserId, 'FD' From Table Where FollowingUserId =1
Union
Select FollowingUserId, 'FG' From Table Where FollowerUserId =1;
with above i get user 2 as
2,FG
2,FD
from above but I really need 2,B without UserId 2 duplicated.
How can this be done efficiently?
You can use aggregation on your basic query:
SELECT UserId,
(CASE WHEN COUNT(DISTINCT which) = 1 THEN MIN(which)
ELSE 'B'
END)
FROM (Select FollowerUserId as UserId, 'FD' as which From Table Where FollowingUserId = 1
Union ALL
Select FollowingUserId, 'FG' From Table Where FollowerUserId = 1
) f
GROUP BY UserId;

Return boolen from two counts on same table

From the same table, what would be a good way to do two different selects and generate a Boolean based on the results.
testable
pKey | prodStatus
-----------------
1 | 0
2 | 0
I'm trying to do this
select count(pKey) from testable where pKey = 1 and prodStatus = 0
along with
select count(pKey) from testable where pKey = 2 and prodStatus = 0
If both results where 1 then `true` else `false`
Right I do this using php and a lot of code because I've no idea how its done purely in sql and something like this is complete beyond me. How can I do something like this in sql itself?
Will this work for you?
SELECT (
select count(pKey) = 1 from testable where pKey = 1 and prodStatus = 0
) AND (
select count(pKey) = 1 from testable where pKey = 2 and prodStatus = 0
)
Check Demo
SELECT SUM(pKey = 1) = 1 and SUM(pKey = 2) = 1
FROM testable
WHERE prodStatus = 0
SELECT SUM(CASE WHEN pKey = 1 THEN 1 ELSE 0 END)
= SUM(CASE WHEN pKey = 2 THEN 1 ELSE 0 END)
FROM testable
WHERE prodStatus = 0
Based on Barmar's answer, except (1) he has AND where I think you want an = and (2) not all DBs allow the implicit conversion of boolean to 1/0. This is more portable, and will work if someone using a different DB lands here with Google.
The query planner may be smart enough to optimize two queries with subselects into just one pass over the table, but I wouldn't bet on it.
[edit] left out the END in first version.
select Sum(case When (pKey = 1 And prodStatus = 0) Or
(pKey = 2 and prodStatus = 0)
Then 1 Else 0 End)
from testable

where clause matching 2 rows in the same table to get results

i have a search query that i need to modify and adapt into a custom profile system we have, that system use the following table:
profile_key | profile_value | user_id
1 | test1 | 10
2 | test2 | 10
3 | ["test3","test4"] | 10
i need to add to the where clause something that would match all the rows (depending of what the user defined in the search form) to get the user_id, something like:
select user_id from table where (profile_key = 1 && profile_value regexp 'test1') && (profile_key = 3 && profile_value regexp 'test4')
i need to get all the user_id IF it matched all the defined profile_key and the regexp.
any idea how i can accomplish this?
Regards.
The simplest way would be to use EXISTS:
SELECT user_id
FROM users
WHERE EXISTS (SELECT 1 FROM profiles WHERE profile_key = 1
AND profile_value regexp 'test1' AND profiles.user_id = users.user_id)
AND EXISTS (SELECT 1 FROM profiles WHERE profile_key = 3
AND profile_value regexp 'test4' AND profiles.user_id = users.user_id)
You could also accomplish this with an INNER JOIN, once for each row you want to match:
SELECT user_id
FROM users
INNER JOIN profiles p1 ON users.user_id = p1.user_id
INNER JOIN profiles p2 ON users.user_id = p2.user_id
WHERE p1.profile_key = 1 AND p1.profile_value regexp 'test1'
AND p2.profile_key = 3 AND p2.profile_value regexp 'test4'
You're saying that profile_key should be 1 and 3. This is impossible.
You need to use an OR, not AND.
SELECT user_id
FROM table
WHERE ( profile_key = 1 && profile_value REGEXP 'test1' ) OR (
profile_key = 3 && profile_value REGEXP 'test4' )
What about using an 'IN', something like this
select user_id
from table
where (profile_key = 1 && 'test1' IN profile_value)
&& (profile_key = 3 && 'test4' IN profile_value )

Simplify MySQL query

I have a MySQL query that I used to use to return mutual friends between two users, but now that I am recoding my website, I am trying to simplify this code, or at least make it better.
So here's my code below to check mutual friends:
SELECT a.friendID
FROM
(SELECT CASE WHEN userID = $id
THEN userID2
ELSE userID
END AS friendID
FROM friends
WHERE (userID = $id OR userID2 = $id)
AND state='1'
) a
JOIN
(SELECT CASE WHEN userID = $session
THEN userID2
ELSE userID
END AS friendID
FROM friends
WHERE (userID = $session OR userID2 = $session)
AND state='1'
) b
ON b.friendID = a.friendID
My table is set up like this:
userID -- userID2 -- state
1 ------- 2 ------- 1
2 ------- 3 ------- 1
1 ------- 3 ------- 0
(sorry, I don't know how to do the pretty database structure design, so if someone could edit that for me...)
but for the above, when user 1 is on user 3's profile, since user 1 and user 2 are friends, and user 2 and user 3 are friends, but user 1 and user 3 are not, it should return user 2 as a mutual friend. (state 1 means friendship accepted, state 0 means friendship pending, so only if state is 1 should it be counted as a friend)
Also note that userID and userID2 can be in any order, it depends on who requests who as a friend, so like the above query does, I need to also have the "friendID" returned, as the above query does right.
SELECT t.f FROM friends JOIN (
SELECT IF(userID = $id, userID2, userID) f
FROM friends
WHERE state AND $id IN (userID, userID2)
) t ON t.f IN (userID, userID2)
WHERE state AND $session IN (userID, userID2)
See it on sqlfiddle.
Not necessarily the fastest but simple to read:
Create View ActiveFriends As
Select
UserId, UserID2
From
friends
Where
State = '1'
Union All
Select
UserId2, UserID
From
friends
Where
State = '1'
Select
f1.userID2 as MutualFriendId
From
ActiveFriends f1
Inner Join
ActiveFriends f2
On f1.UserID2 = f2.UserID
Where
f1.UserID = $session And
f2.UserID2 = $id
http://sqlfiddle.com/#!2/5748f/1/0
You could do :
SELECT userID2
FROM friends_table
WHERE userID IN ( $id, $session )
AND state = 1
GROUP BY userID2
HAVING COUNT(userID2) >= 2
friends_table :
user_id | friend_id | state
-----------------------------
1 | 7 | 1
2 | 3 | 0
7 | 1 | 1
User 1 and 7 are friends, user 2 wants to be friends with user 3, but user 3 hasn't responded yet.
at the first glance I would say that using intersect is a good solution
so this query should do the trick for you
SELECT userID2
FROM friends
WHERE userID = $fromID
INTERSECT
SELECT userID2
FROM friends
WHERE userID = $theOtherID
EDIT:
another less clear solution would be
SELECT userID2
FROM friends INNER JOIN friends f2
USING (userID2)
WHERE friends.UserID = $fromID AND f2.userID = $theOtherID