I am doing a project using MySQL 5. The requirement is the following:
Give the user names, device types, OS version and fruit involved in picks, where users had the
same device type, were running iOS 4 or 4.1, and picked the same fruit
The relevant tables are as follows:
User: {uID: INT, name: VARCHAR(45), deviceOS: VARCHAR(45), deviceType: VARCHAR(45)}
Pick: {uID: INT, ts: TIMESTAMP, fruit: VARCHAR(45)}
(Primary keys in bold. uID in Pick is a foreign key of uID in User.)
I am doing the following query:
SELECT DISTINCT NAME1, OS1, DEV1, NAME2, OS2, DEV2, P1.fruit FROM Pick AS P1, Pick AS P2,
(SELECT U1.uID AS User1, U1.name AS NAME1, U1.deviceOS AS OS1, U1.deviceType AS DEV1,
U2.uID AS User2, U2.name AS NAME2, U2.deviceOS AS OS2, U2.deviceType AS DEV2
FROM User AS U1, User AS U2
WHERE (U1.uID != U2.uID) AND
(U1.deviceType = U2.deviceType) AND
(U1.deviceOS = "4" OR U1.deviceOS = "4.1") AND
(U2.deviceOS = "4" OR U2.deviceOS = "4.1")) AS PartialResult
WHERE (P1.uID = PartialResult.User1) AND
(P2.uID = PartialResult.User2) AND
(P1.fruit = P2.fruit)
This returns the following result, but as you see, it is in some way "duplicated":
I have tried solving this using GROUP BY fruit but it will not return the correct result on the general case. Limit 1 also would not work on the general case. So after numerous hours trying to figure this out, I must ask:
Is there a way to prevent this kind of duplication on the general case?
Thank you!
Instead of U1.uID != U2.uID, write U1.uID > U2.uID.
The problem you're encountering is that every single row is going to be duplicated, a--b and b--a. You need some way of specifying that you only want one or the other, but the question is, which one? Do you have a preference whether Priscilla is listed before Marcia, or vice versa?
If there is no preference, then you can just make up some arbitrary rule that will only allow one or the other to go through. For example, you can compare names and only grab rows where the first name is lexicographically before the second (see last line):
SELECT DISTINCT NAME1, OS1, DEV1, NAME2, OS2, DEV2, P1.fruit FROM Pick AS P1, Pick AS P2,
(SELECT U1.uID AS User1, U1.name AS NAME1, U1.deviceOS AS OS1, U1.deviceType AS DEV1,
U2.uID AS User2, U2.name AS NAME2, U2.deviceOS AS OS2, U2.deviceType AS DEV2
FROM User AS U1, User AS U2
WHERE (U1.uID != U2.uID) AND
(U1.deviceType = U2.deviceType) AND
(U1.deviceOS = "4" OR U1.deviceOS = "4.1") AND
(U2.deviceOS = "4" OR U2.deviceOS = "4.1")) AS PartialResult
WHERE (P1.uID = PartialResult.User1) AND
(P2.uID = PartialResult.User2) AND
(P1.fruit = P2.fruit) AND
(STRCMP(NAME1, NAME2) < 0)
Of course, you can implement any rule you want that picks one or the other. #igelkott's answer solves the problem the same way by enforcing person 1's uID to be higher than person 2's uID, which is very reasonable (and faster than doing string compares).
Related
I have 3 tables in my DB; Transactions, transaction_details, and accounts - basically as below.
transactions :
id
details
by_user
created_at
trans_details :
id
trans_id (foreign key)
account_id
account_type (Enum -[c,d])
amount
Accounts :
id
sub_name
In each transaction each account may be creditor or debtor. What I'm trying to get is an account statement (ex : bank account movements) so I need to query each movement when the account is type = c (creditor) or the account type is = d (debtor)
trans_id, amount, created_at, creditor_account, debtor_account
Update : I tried the following query but i get the debtor column values all Null!
SELECT transactions.created_at,trans_details.amount,(case WHEN trans_details.type = 'c' THEN sub_account.sub_name END) as creditor,
(case WHEN trans_details.type = 'd' THEN sub_account.sub_name END) as debtor from transactions
JOIN trans_details on transactions.id = trans_details.trans_id
JOIN sub_account on trans_details.account_id = sub_account.id
GROUP by transactions.id
After the help of #Jalos I had to convert the query to Laravel which also toke me 2 more hours to convert and get the correct result :) below is the Laravel code in case some one needs to perform such query
I also added between 2 dates functionality
public function accountStatement($from_date,$to_date)
{
$statemnt = DB::table('transactions')
->Join('trans_details as credit_d',function($join) {
$join->on('credit_d.trans_id','=','transactions.id');
$join->where('credit_d.type','c');
})
->Join('sub_account as credit_a','credit_a.id','=','credit_d.account_id')
->Join('trans_details as debt_d',function($join) {
$join->on('debt_d.trans_id','=','transactions.id');
$join->where('debt_d.type','d');
})
->Join('sub_account as debt_a','debt_a.id','=','debt_d.account_id')
->whereBetween('transactions.created_at',[$from_date,$to_date])
->select('transactions.id','credit_d.amount','transactions.created_at','credit_a.sub_name as creditor','debt_a.sub_name as debtor')
->get();
return response()->json(['status_code'=>2000,'data'=>$statemnt , 'message'=>''],200);
}
Your transactions table denotes transaction records, while your accounts table denotes account records. Your trans_details table denotes links between transactions and accounts. So, since in a transaction there is a creditor and a debtor, I assume that trans_details has exactly two records for each transaction:
select transactions.id, creditor_details.amount, transactions.created_at, creditor.sub_name, debtor.sub_name
from transactions
join trans_details creditor_details
on transactions.id = creditor_details.trans_id and creditor_details.account_type = 'c'
join accounts creditor
on creditor_details.account_id = creditor.id
join trans_details debtor_details
on transactions.id = debtor_details.trans_id and debtor_details.account_type = 'd'
join accounts debtor
on debtor_details.account_id = debtor.id;
EDIT
As promised, I am looking into the query you have written. It looks like this:
SELECT transactions.id,trans_details.amount,(case WHEN trans_details.type = 'c' THEN account.name END) as creditor,
(case WHEN trans_details.type = 'd' THEN account.name END) as debtor from transactions
JOIN trans_details on transactions.id = trans_details.trans_id
JOIN account on trans_details.account_id = account.id
GROUP by transactions.id
and it is almost correct. The problem is that due to the group-by MySQL can only show a single value for each record for creditor and debtor. However, we know that there are exactly two values for both: there is a null value for creditor when you match with debtor and a proper creditor value when you match with creditor. The case for debtor is similar. My expectation for this query would have been that MySQL would throw an error because you did not group by these computed case-when fields, yet, there are several values, but it seems MySQL can surprise me after so many years :)
From the result we see that MySQL probably found the first value and used that both for creditor and debtor. Since it met with a creditor match as a first match, it had a proper creditor value and a null debtor value. However, if you write bullet-proof code, you will never meet these strange behavior. In our case, doing some minimalistic improvements on your code transforms it into a bullet-proof version of it and provides correct results:
SELECT transactions.id,trans_details.amount,max((case WHEN trans_details.type = 'c' THEN account.name END)) as creditor,
max((case WHEN trans_details.type = 'd' THEN account.name END)) as debtor from transactions
JOIN trans_details on transactions.id = trans_details.trans_id
JOIN account on trans_details.account_id = account.id
group by transactions.id
Note, that the only change I did with your code is to wrap a max() function call around the case-when definitions, so we avoid the null values, so your approach was VERY close to a bullet-proof solution.
Fiddle: http://sqlfiddle.com/#!9/d468dc/10/0
However, even though your thought process was theoretically correct (theoretically there is no difference between theory and practice, but in practice they are usually different) and some slight changes are transforming it into a well-working code, I still prefer my query, because it avoids group by clauses, which can be useful, if necessary, but here it's unnecessary to do group by, which is probably better in terms of performance, memory usage, it's easier to read and keeps more options open for you for your future customisations. Yet, your try was very close to a solution.
As about my query, the trick I used was to do several joins with the same tables, aliasing them and from that point differentiating them as if they were different tables. This is a very useful trick that you will need a lot in the future.
I am trying to check if the current user is already following the selected user, and I am doing this like so:
(I know it's not the best way, but as I am new to MYSQL this is as much as I have been able to come up with)
SELECT EXISTS (SELECT 1 FROM Activity WHERE IdOtherUser = 86 AND id = 145)
I am '145' and the user I selected is '86'.
Now that return '0' If I am not following and '1' If I am following that person.
Seems to be working already but it definetly needs improving!
Now what I would like to do is count the followers in the same query.
So count the people I am following and the people following me.
'Activity' is the table where I store the followers and I save them like this:
'id' = me
'idOtherUser' = other user I followed
'type' = type of action "follow"
I have done count's before when calculating the like counts, but I just cannot get my head around this!!
If anyone could spare some time to help me it is much appreciated!
I am sorry if the question is not the best, but I am still learning and trying my best to format them as clear as possible to understand.
Thanks in advance!!
If you trying to count the followers from specific id from table Activity you might do this way:
SELECT COUNT(idOtherUser) AS "I Follow",
(SELECT COUNT(idOtherUser) FROM Activity WHERE idOtherUser = 145 AND type = "follow"
) AS "FOLLOW ME",
(SELECT COALESCE(id,0) FROM Activity WHERE IdOtherUser = 86 AND id = 145 AND type = "follow")
FROM Activity WHERE id = 145 AND type = "follow"
you can use a "correlated subquery" to simplify the query and you might want distinct in the count also (depends on you data). I would avoid using spaces in column aliases too.
SELECT
COUNT(DISTINCT A1.idOtherUser) as i_follow
, (
SELECT
COUNT(DISTINCT A2.id)
FROM Activity A2
WHERE A2.idOtherUser = A1.id
AND A2.type = 'follow'
) as following_me
FROM Activity A1
WHERE A1.id = 145
AND A1.idOtherUser = 86
AND A1.type = 'follow'
Try it with distinct then without, if the result is the same leave distinct out of the query.
I have a table of user with differents fields : id, firstname, name.
I have a table called friend with differents fields : invite_id, friend_sender (id of a user), friend_receiver (id of a user), validity (boolean).
I'm filling the friend table with
1, 1, 2, 0;
2, 3, 1, 1;
3, 1, 5, 1;
Let's imagine I'm user 1, and I want to find all my friends. I can be the one who sent the friend invitation (sender), or the one who received it (receiver). When the receiver accept the invitation, the validity of the relation is set to 1. So for example, I'm not friend with user 2 because he didn't accepted.
The result I should get from doing the query with user 1 should be :
3, firstnameofuser3, nameofuser3
5, firstnameofuser3, nameofuser3
I tried some SQL things, with double JOIN, renaming table to avoid the "double same table" problems etc ... but I couldn't figure it out.
I've found some post about it, but for more complex things, like here :
Finding mutual friend sql
Thank you in advance for you help.
I know there are already answers, but mine is unique AND I have a fiddle! ;)
SELECT
id,
firstname,
name
FROM
user
WHERE id IN
(
SELECT
CASE WHEN friend_sender = 1 THEN friend_receiver ELSE friend_sender END
FROM friend
WHERE
(friend_sender = 1 OR friend_receiver = 1)
AND
validity = 1
)
Fiddle: http://sqlfiddle.com/#!9/d8f55a/1
Try this:
SELECT u.*
FROM user u
WHERE u.id IN (
SELECT f.friend_sender
FROM friend f
WHERE f.friend_receiver = 2 -- My fixed ID about Jin Jey
UNION
SELECT f.friend_receiver
FROM friend f
WHERE f.friend_sender = 2 AND f.validity = 1)
I used UNION because you can query two sets of data and merge it.
I fixed ID (2) because in your request you want to know all friends about Jin Jey
You can try the below query
select
id,
firstname,
name
from
user inner join friend
on
(friend_sender=1 or friend_receiver=1 )and validity=1
and
user.id=
(case
when friend_sender=1 then friend_receiver
else friend_sender
end)
demo link here
I have two tables in MySQL: One called gtfsws_users which contains users for a system I'm developing and another called gtfsws_repository_users which contains roles for these users.
gtfsws_users has these fields: email, password, name, is_admin and enabled.
gtfsws_repository_users has: user_email, repository_id and role.
The role is an integer that defines privileges over a GTFS repository (public transportation data, not relevant for my problem).
One important thing is that every administrator accont (that is, every user that has the is_admin flag set as 1 in gtfsws_users) has full access to all repositories.
Now, only users registered in gtfsws_repository_users will have access to a specific repository defined there (unless they are administrators, of course). One user can have multiple repositories which him/her can access.
What I'm trying to do is to get all users with access to a specific repository (it doesn't matter which type of role the user has, I just want to know if they can access the repository or not). So I'm writing this SQL statement for getting them:
(
SELECT DISTINCT
gtfsws_users.email AS email,
gtfsws_users.name AS name,
gtfsws_users.is_admin AS is_admin,
gtfsws_users.enabled AS enabled,
gtfsws_repository_users.role AS role
FROM
gtfsws_users
INNER JOIN
gtfsws_repository_users
ON
gtfsws_users.email = gtfsws_repository_users.user_email
WHERE
gtfsws_repository_users.repository_id = '2'
)
UNION
(
SELECT
email,
name,
is_admin,
enabled,
null AS role
FROM
gtfsws_users
WHERE
is_admin = 1
)
Now, this works fine for users with access to different repositories. It also gets all administrators too.
The problem is when I have an administrator that is also registered in gtfsws_repository_users, because I get it duplicated.
So for example, it I have this in gtfsws_users:
('test#test.com', '*****', 'Real name', 1, 1)
And also the user is registered in gtfsws_repository_users as this:
('test#test.com', 2, 10)
When I do the SELECT in MySQL (using the UNION to add all administrators) I get:
('test#test.com', 'Real name', 1, 1, 10)
('test#test.com', 'Real name', 1, 1, NULL)
What I need to do is to filter that table to remove duplicates, that is getting only:
('test#test.com', 'Real name', 1, 1, NULL)
Yes, getting NULL as the role (since it will be ignored as the user is an administrator).
Does anybody have a clue on how to achieve that?
Thanks a lot.
EDIT: Ok, thanks to Katrin's suggestion, I'm getting some progress. I do get one row, but it's the one with the role number defined. Any way to preserve the one with the NULL role instead of the defined one?
Since aggregate functions ignore null values, you can convert null to a number that can be extracted as min or max.
Assuming all your roles are greater than 0:
SELECT email, name, is_admin, enabled, nullif(min(coalesce(role, 0)), 0) as role
from
((
SELECT DISTINCT
gtfsws_users.email AS email,
gtfsws_users.name AS name,
gtfsws_users.is_admin AS is_admin,
gtfsws_users.enabled AS enabled,
gtfsws_repository_users.role AS role
FROM
gtfsws_users
INNER JOIN
gtfsws_repository_users
ON
gtfsws_users.email = gtfsws_repository_users.user_email
WHERE
gtfsws_repository_users.repository_id = '2'
)
UNION
(
SELECT
email,
name,
is_admin,
enabled,
null AS role
FROM
gtfsws_users
WHERE
is_admin = 1
)) as Q
GROUP BY email, name, is_admin, enabled
Here i posted a question about doing JOIN depending on the value of the column in the row.
You will need that is you make an Add Friend feature, where you write the ID of the 2 users who are Adding each other into user_1_id (my id) and user_2_id (friend id).
When you want to see who you are friends with, select DEPENDING on whether user_1_id or user_2_id has the ID of the surrent User (the one who is browsing)
I figured it out so down below is the query you want to use in case you need to do it.
Here is the query
$sql_inp = 'SELECT DISTINCT
users.id, users.first_name, users.last_name,
CASE
WHEN friends.user_2_id="'.$_SESSION[USER][id].'" //equal to current user id
THEN (SELECT friends.user_1_id FROM friends WHERE friends.user_2_id="'.$_SESSION[USER][id].'") // if user_2_id is My id, then fetch the other row
ELSE friends.user_2_id // obviously the opposite
END
FROM users
LEFT JOIN friends ON users.id= // this case is completely the same as one above
CASE
WHEN friends.user_2_id="'.$_SESSION[USER][id].'" THEN (SELECT friends.user_1_id FROM friends WHERE friends.user_2_id="'.$_SESSION[USER][id].'")
ELSE friends.user_2_id
END
WHERE friends.user_1_id="'.$_SESSION[USER][id].'" OR friends.user_2_id="'.$_SESSION[USER][id].'" // fetch the row where the either one of the values is equal to My id
';
Hope this helps if anyone had trouble
Personally, I hate using CASE statement. It makes queries look cluttered. Try using the IF function
$sql_inp = 'SELECT
table1.val1,table1.val2,
table2.val1,table2.val2,
IF(table3.val1="'.$user_id.'",table3.val1,
IF(table3.val2 ="'.$user_id.'",table3.val2,
IFNULL(table3.val2,-1))) users_fetch
FROM table1
INNER JOIN table2 ON table2.val1=table1.val1
LEFT JOIN table3 ON table2.val1=users_fetch';
In this query, if table3.val1 and table3.val2 <> $user_id, then users_fetch is -1.
Give it a Try !!!