9

I'm writing sort of a travel-'dating' app.

  • Users register themselves
  • Users tell the app if they are male or female
  • Users tell the app which countries they would like to visit
  • Users tell the app if they want to travel with males (pref_m=1) or females (pref_f=1)

My tables

table 1: users

id (key) | gender | pref_m | pref_f
------------------------------------
 1          male     1        0
 2          male     1        1


table 2: countryselection

id (key) | userid | countryid
------------------------------------
 1          1        123
 2          1        111
 3          1        100
 4          1        110
 5          2        123
 6          2        111
 7          2        202
 8          2        210

So what the select statement has to do

Input: the userid of the current user
Output (in logic): SELECT the userids AND matching countries OF ALL people that want to travel to the same countries as I do, and want to travel with someone that has my gender
(join) Of that selection I obviously only need the people that are of the gender that I am looking for.
ORDERED by people that have the most matching countries with me DESC.

What I have so far (warning: not much)


$sql = "SELECT userid,count(*) AS matches from countryselection";
 $sql .= " WHERE countryid IN (SELECT countryid FROM countryselection WHERE userid = :userid) GROUP BY userid ORDER BY matches DESC;";
This gives me a list of all people that want to travel to the same countries as me (and how many countries we have in common)

final note

I'm obviously struggling with the gender-selection part.
Not sure if I have done the right thing to store the user selections in the way that I have.
I might need some guidance there too.


Obviously - thanks all.

Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
Wayfarer
  • 620
  • 6
  • 16
  • 2
    I like part with users. User with `pref_m=0 | pref_f=0` doesn't look for anybody, just hanging around :)) – Peter Jan 27 '13 at 22:53
  • Please clarify a few thing for us like: A user can prefer to travel with males and females? Are you sure that is how you want to design your table? I expected to see a single column for travel parter gender preference. – Paul Sasik Jan 27 '13 at 22:53
  • Are you only looking for pairs (simple) or also groups of more than two persons (hard). – wildplasser Jan 27 '13 at 22:58
  • 3
    Just a thought that has little to do with your question: Have you considered making a single column called preference where 0 = f, 1 = m, and 2 = both? – Tebc Jan 27 '13 at 23:13
  • LOL@ brain too small =)) can't stop laughing – Aniket Inge Jan 27 '13 at 23:23
  • 2
    tip: provide an sqlfiddle with your data populated, so people can easily fork it. – goat Jan 27 '13 at 23:24
  • 1
    @PeterSzymkowski : true, they are just sad ppl. No - 0 is default value of that column. – Wayfarer Jan 27 '13 at 23:39
  • @PaulSasik a user can travel with only males or only females, or both - yes – Wayfarer Jan 27 '13 at 23:40
  • @wildplasser (nice nick :p) I'm just looking to get an array back of (single) people that match my preferences as a user so I can walk through them in my resulting array – Wayfarer Jan 27 '13 at 23:41
  • Why isn't your primary key a combination of `userid` and `countryid`? Seems like you're storing more than necessary. Doesn't help answer you but just an observation. – Dagrooms Jul 14 '15 at 20:16

6 Answers6

8
SELECT        
    us2.id, -- etc. 
    COUNT(cs2.countryid) as countries_in_common               
FROM
    countryselection cs1 -- let's gather user countries he want to visit
LEFT JOIN -- now let's find other users!
    countryselection cs2 ON
    (
        cs2.userid <> :userid AND -- which are not him
        cs2.countryid = cs1.countryid -- and want to visit same countries
    )
INNER JOIN -- let's grab our user_data
    users us1 ON 
    (
        us1.id = cs1.userid
    )
INNER JOIN -- and let's grab other user data!
    users us2 ON
    (
        us2.id = cs2.userid
    )
WHERE
    cs1.userid = :userid AND -- finding our user countries he want to visit
    -- final checks
    (
        (us1.pref_m = 1 AND us2.gender = 'male') 
        -- he is looking for male and second user is male
        OR
        (us1.pref_f = 1 AND us2.gender = 'female') 
        -- he is looking for female and second user is female
    ) AND
    (
        (us2.pref_m = 1 AND us1.gender = 'male')
        OR
        (us2.pref_f = 1 AND us1.gender = 'female') 
    ) 
GROUP BY
    cs2.userid -- finally group by user_id

Best thing is there are no sub-queries, and you can easily use this query in many ways. (changing order, group by, and using aggregate functions)

Dagrooms
  • 1,507
  • 2
  • 16
  • 42
Peter
  • 16,453
  • 8
  • 51
  • 77
  • why not standard sql comments with `--`? – Janus Troelsen Jan 27 '13 at 23:24
  • @JanusTroelsen didn't know i have to use `` – Peter Jan 28 '13 at 00:03
  • Some of your where conditions should be used as join conditions and join type should be checked. –  Jan 28 '13 at 00:07
  • sorry this is taking me so long :) I'm going through all your answers one by one - at least I owe you guys that. Yours was the first I did Peter. Implemented it in my code and seems to work perfectly so far. (haven't tested all the use cases, but who am I to doubt here) Thanks for the nice commenting too. very helpfull. – Wayfarer Jan 28 '13 at 00:20
  • @PeterSzymkowski not saying it doesn't work but really take a look at your where conditions vs join conditions. –  Jan 28 '13 at 00:27
  • @bmewsing Changed left joins to inner joins for you, but i'm pretty sure it doesn't matter as long i use primarykey. And about join/where conditions I already answered below your post – Peter Jan 28 '13 at 01:05
2

It's pretty easy if you don't do the sorting by most countries in common (you could do it in code later if the result sets won't be too large):

SELECT
    o.id userid, u_cs.countryid
FROM users u
JOIN countryselection u_cs ON (u.id = u_cs.userid)
JOIN countryselection o_cs ON (u_cs.countryid = o_cs.countryid)
JOIN users o ON (o_cs.userid = o.id)
WHERE
    u.id = :userid AND   -- The user we want
    u.id <> o.id AND     -- Exclude ourselves
    (                    -- Check whether the other person is
                         -- compatible with us
        (u.pref_m = 1 AND o.gender = 'male') OR
        (u.pref_f = 1 AND o.gender = 'female')
    ) AND
    (                    -- Check whether we're compatible with the
                         -- other person
        (o.pref_m = 1 AND u.gender = 'male') OR
        (o.pref_f = 1 AND u.gender = 'female')
    )

SQL Fiddle


If you do want the sorting, I think the best option is to use GROUP_CONCAT (because MySQL sucks and doesn't support windowing/analytic functions).

SELECT
    o.id userid, GROUP_CONCAT(u_cs.countryid) countries
FROM users u
JOIN countryselection u_cs ON (u.id = u_cs.userid)
JOIN countryselection o_cs ON (u_cs.countryid = o_cs.countryid)
JOIN users o ON (o_cs.userid = o.id)
WHERE
    u.id = :userid AND   -- The user we want
    u.id <> o.id AND     -- Exclude ourselves
    (                    -- Check whether the other person is
                         -- compatible with us
        (u.pref_m = 1 AND o.gender = 'male') OR
        (u.pref_f = 1 AND o.gender = 'female')
    ) AND
    (                    -- Check whether we're compatible with the
                         -- other person
        (o.pref_m = 1 AND u.gender = 'male') OR
        (o.pref_f = 1 AND u.gender = 'female')
    )
GROUP BY o.id
ORDER BY COUNT(u_cs.countryid) DESC

You could probably pull this off with some nasty subqueries too, but I get the feeling it'll kill performance.

SQL Fiddle

impl
  • 783
  • 5
  • 14
2
SELECT t4.id, COUNT(t4.id) AS frequency
FROM users t1
LEFT JOIN countryselection t2
ON t1.id = t2.userid
INNER JOIN countryselection t3 
  ON t2.userid != t3.userid AND t2.countryid = t3.countryid
INNER JOIN users t4 
  ON t3.userid = t4.id 
   AND ((t4.pref_m = 1 AND t1.gender = 'male' OR t4.pref_f = 1 AND t1.gender = 'female')
     AND (t1.pref_m = 1 AND t4.gender = 'male' OR t1.pref_f = 1 AND t4.gender = 'female'))
WHERE t1.id = ?
GROUP BY t4.id
ORDER BY frequency DESC

Similar to others here but using appropriate join types and join conditions instead of where conditions.

From MySQL docs:

The conditional_expr used with ON is any conditional expression of the form that can be used in a WHERE clause. Generally, you should use the ON clause for conditions that specify how to join tables, and the WHERE clause to restrict which rows you want in the result set.

  • +1 as it working now, but there is no such thing as difference between "join conditions" and "where conditions" :) SQL handle both in same way. – Peter Jan 28 '13 at 00:33
  • @PeterSzymkowski there is a difference an important one if the rdbms optimiser doesn't get it right. –  Jan 28 '13 at 00:43
  • yes read docs from your answer `Generally, you should use the ON clause for conditions that specify how to join tables, and the WHERE clause to restrict which rows you want in the result set.`. And you are pushing where condition to join? – Peter Jan 28 '13 at 00:46
  • Please read http://stackoverflow.com/questions/1018952/condition-within-join-or-where and http://stackoverflow.com/questions/354070/sql-join-where-clause-vs-on-clause – Peter Jan 28 '13 at 00:48
  • @PeterSzymkowski nice links, I still stand by my answer :-) –  Jan 28 '13 at 00:52
1

inferred DDL :

create table users (id int, gender text, pref_m bool, pref_f bool);
create table countryselection (id int, userid int, countryid int);

Here is CSV that you can .import (using sqlite3, after doing .separator ,) into the tables in the question:

users.csv:

1,male,1,0
2,male,1,1

countryselection.csv

1,1,123
2,1,111
3,1,100
4,1,110
5,2,123
6,2,111
7,2,202
8,2,210

peter's sql, edited to use field names from question:

SELECT        
    us2.id,
    COUNT(cs2.*) as countries_in_common
FROM
    countryselection cs1 
LEFT JOIN 
    countryselection cs2 ON
    (
        cs2.userid <> $userid AND 
        cs2.countryid = cs1.countryid 
    )
LEFT JOIN 
    users us1 ON 
    (
        us1.id = cs1.userid
    )
LEFT JOIN 
    users us2 ON
    (
        us2.id = cs2.userid
    )
WHERE
    cs1.userid = $userid AND 
    cs2.userid IS NOT NULL AND
    (
        (us1.pref_m = 1 AND us2.gender = 'male') 
        OR
        (us1.pref_f = 1 AND us2.gender = 'female') 
    ) AND
    (
        (us2.pref_m = 1 AND us1.gender = 'male')
         OR
        (us2.pref_f = 1 AND us1.gender = 'female') 
    )          
GROUP BY
    cs2.userid 
;

you can execute it like this:

sqlite3 myDBname < peters_sql.sql

setting $userid to 1, i get 2 as output.

Peter
  • 16,453
  • 8
  • 51
  • 77
Janus Troelsen
  • 20,267
  • 14
  • 135
  • 196
1

I think this works

select me.id meid
     , them.id themid
     , me.gender mygender
     , them.gender themgender
     , me.pref_m mepref_m
     , me.pref_f mepref_f
     , them.pref_m thempref_m
     , them.pref_f thempref_f
     , csme.countryid

  from users me

 cross
  join users them

 inner
  join countryselection csme
    on me.id = csme.userid

 inner
  join countryselection csthem
    on them.id = csthem.userid

 where csme.countryid = csthem.countryid
   and ((me.gender = 'male' and them.pref_m) or (me.gender = 'female' and them.pref_f))
   and ((them.gender = 'male' and me.pref_m) or (them.gender = 'female' and me.pref_f))
   and me.id != them.id
   and me.id = 2

http://sqlfiddle.com/#!2/06351/25/0

I intentionally left out the group by so that the results are more easily verified.

goat
  • 31,486
  • 7
  • 73
  • 96
1

UPDATE: (added the countries)

SELECT u1.id AS uid1
        , u2.id AS uid2
        , cs.countryid
FROM users u1
, users u2
JOIN countryselection cs ON cs.userid = u2.id
-- WHERE u1.id < u2.id -- tiebreaker
WHERE u1.id = 12345
AND EXISTS (
        SELECT * FROM countryselection cs1
        JOIN countryselection cs2 ON cs1.countryid = cs2.countryid
        WHERE cs1.userid = u1.id
        AND cs2.userid = u2.id
        )
AND ((u1.pref_m = True AND u2.gender = 'male')
        OR (u1.pref_f = True AND u2.gender = 'female') )
        -- the love must be mutual ...
AND ((u2.pref_m = True AND u1.gender = 'male')
        OR (u2.pref_f = True AND u1.gender = 'female') )
        ;
wildplasser
  • 43,142
  • 8
  • 66
  • 109
  • - subquery.. - no countries count – Peter Jan 27 '13 at 23:49
  • I don't inderstand. Both persons could have the same preference *of no countries at all* ? – wildplasser Jan 27 '13 at 23:51
  • @wildplasser - no. that is not possible. all users have at least 4 chosen countries in the database. It's obligatory. (should have mentioned that in my desc) – Wayfarer Jan 27 '13 at 23:54
  • i mean 1. your solution comes with subquery which is nasty thing as mysql is forced to create tmp table for your subquery. 2. there is no option to get count of matched countries for every user – Peter Jan 27 '13 at 23:55
  • 1) I don't care about mysql being too stupid to handle EXISTS (which existed **before** mysql), on normal platforms EXISTS generates equivalent or superior plans. 2) you are right. The OP wanted a COUNT() of matched countries, too. – wildplasser Jan 28 '13 at 00:00