1

I have four tables, such that:

CREATE TEMP TABLE country (
  countryid serial PRIMARY KEY,
  name text
);
CREATE TEMP TABLE city (
  cityid serial PRIMARY KEY,
  name text,
  countryid int REFERENCES country
);
CREATE TEMP TABLE user_country (
  userid int,
  countryid int REFERENCES country
);
CREATE TEMP TABLE user_city (
  userid int,
  cityid int REFERENCES city
);

I'm trying to write a query which will match users up based on their user_country and user_city entries, so that any one hit will produce a match if,

  1. a user has any of the same user_country or user_city entries as another user.
  2. given the relationship between city and country; if one user has an entry in the user_country table and another user has an entry in the user_city table with a city that is in the first users_country, this will also produce a match.

For example, given the following example data and the user id 1, the query will return the user ids 2, 3, 4: 2 is matched in the user_country table (both users have an entry containing the country_id 1) and user ids 3 and 4 are matched because their entries in the user_city table relate to London which has a country_id which matches user id 1's user_country entry. (In laymans terms, user 1 wants to go anywhere in England, so does user 2, producing a match; users 3 and 4 are more specific but because both London and Leeds are in England they both produce matches)

Given the same data and the user id 3, the query will return the user id 1 and 2, as the country_id in the city table relates to the country that both users 1 and 2 have selected, but not user 4. (in laymans terms, user 3 wants to go to London; users 1 and 2 want to go anywhere in England, so they match. However user 4 only wants to go to Leeds, not London, so doesn't match)

INSERT INTO country (name) VALUES ('England'),('Canada');
INSERT INTO city (name,countryid) VALUES ('London',1),('Leeds',1),('Vancouver',2);

INSERT INTO user_country (userid, countryid) VALUES (1,1),(2,1);
INSERT INTO user_city (userid, cityid) VALUES (3,1),(4,2);

Can anyone help me with a single performant SQL query, that given a user id, returns a list of user ids which match the above scenarios?

Note: I'm using a recent version of postgres

EDIT To clarify a few points: a user can have an entry in the user_city table without a corresponding entry in the user_country table for the country matching the city. This is because a user may have an interest in visiting a city, but not want to see the rest of the country. Being able to select a country is a shorthand way of selecting every single city in that country. In layman's terms: just because a user wants to visit London, they don't necessarily want to visit all of the UK. So a user can have a user_city entry for London, but not have a user_country entry for the UK; also worth noting that if a user has a user_country entry for the UK, they won't have any user_city entries for any cities in that country (user wants to visit all of the UK, so doesn't make sense to list London too).

Separately a city will always be in a country, so will always have a country_id

EDIT 2 A bit more example data to replace the data above (not in addition to!)

INSERT INTO country (name) VALUES ('Germany'),('Spain'),('Italy');
INSERT INTO city (name,countryid) VALUES ('Berlin',1),('Madrid',2),('Rome',3),('Venice',3);

INSERT INTO user_country (userid, countryid) VALUES (1,1),(2,1),(5,3)
INSERT INTO user_city (userid, cityid) VALUES (3,2),(4,2),(6,3),(7,4)

Testing a match only on matching user_country table entries:

  • Entering user id 1 returns user id 2
  • Entering user id 2 returns user id 1

Testing a match only on matching user_city table entries:

  • Entering user id 3 returns user id 4
  • Entering user id 4 returns user id 3

Testing that a user_country matches all entries with related cities:

  • Entering user id 5 returns user ids 6 & 7

Testing that a user_city entry picks up a user_country entry:

  • Entering user id 6 returns user id 5
  • Entering user id 7 returns user id 5
6
  • Does every city have a country? Jan 7, 2017 at 19:31
  • @evancarroll yes, every city has a country
    – Martyn
    Jan 7, 2017 at 19:43
  • user_id 3 shouldn't ppear on the user_country table?
    – joanolo
    Jan 7, 2017 at 19:49
  • I'm confused what does it mean that every city has a country, and not at the same time that every user_city does not have a user_country.. That's kind of why I asked. Anyway, it's only the diff between an inner join, and a left join in my query. Jan 7, 2017 at 20:04
  • 1
    @EvanCarroll in laymans terms: I meant that because a user wants to visit London, they don't necessarily want to visit all of the UK. So a user can have a user_city entry for London, but not have a user_country entry for the UK; also worth noting that if a user has a user_country entry for the UK, they won't have any user_city entries for any cities in that country (user wants to visit all of the UK, so doesn't make sense to list London too). Separately a city will always be in a country, so will always have a country_id
    – Martyn
    Jan 7, 2017 at 20:30

2 Answers 2

1

Sample Data

This is actually very easy, first let's create your schema. Next time around, it's better and easier for everyone if you provide data like this.

CREATE TEMP TABLE country (
  countryid serial PRIMARY KEY,
  name text
);
CREATE TEMP TABLE city (
  cityid serial PRIMARY KEY,
  name text,
  countryid int REFERENCES country
);
CREATE TEMP TABLE user_country (
  userid int,
  countryid int REFERENCES country
);
CREATE TEMP TABLE user_city (
  userid int,
  cityid int REFERENCES city
);
INSERT INTO country (name) VALUES ('England'),('Canada');
INSERT INTO city (name,countryid) VALUES ('London',1),('Leeds',1),('Vancouver',1);
INSERT INTO user_country (userid, countryid) VALUES (1,1),(2,1),(4,2);
INSERT INTO user_city (userid, cityid) VALUES (3,1),(4,2),(2,3);

Solution

SELECT DISTINCT
  user_city_country.userid AS user1,
  user_country.userid AS user2
FROM user_country
JOIN country USING (countryid)
JOIN (
  SELECT *
  FROM user_city
  JOIN city USING (cityid)
  JOIN country USING (countryid)
) AS user_city_country
  USING (countryid);

This breaks down easy... Path one is to resolve the user to the country..

SELECT DISTINCT user_country.userid
FROM user_country
JOIN country USING (countryid)

Path 2 is to resolve the user_city to the country.

SELECT *
FROM user_city
JOIN city USING (cityid)
JOIN country USING (countryid)

Then we just JOIN them. Note, we're not actually using the country at all. You have it in your schema. So I preserved it here.

This returns...

 user1 | user2 
-------+-------
     4 |     2
     3 |     1
     2 |     1
     3 |     2
     4 |     1
     2 |     2

Now, if you want to know all matches, just add WHERE user_country.userid = '1' OR user_city_country.userid = '1';

 user1 | user2 
-------+-------
     4 |     1
     3 |     1
     2 |     1
3
  • Yes! This seems to work in my test cases. I'm going to throw some more data at it and see if it breaks. I appreciate your constructive criticism in regards to the schema and will take that on board for next time (SQL isn't my forte, so always good to learn something)
    – Martyn
    Jan 7, 2017 at 20:42
  • Doesn't quite work as per my requirements. For example, if two users have matching entries in the user_country table, but nothing in the user_city table then no match is found. To replicate, remove all entries in the 'user_city' table, user id's 1 and 2 should match as they both have a matching entry in the user_country table
    – Martyn
    Jan 7, 2017 at 21:16
  • I've given some more example data - I hope that clears up where my description is falling short.
    – Martyn
    Jan 7, 2017 at 21:52
1

I am not sure I fully understant your request, but take a shot at the following query and comment if does what you mean:

-- For ease, we have the user_id of the first 'visitor'
WITH my_user (user_id) AS
(
    VALUES (1)
)
-- List the countries that (my_user.user_id) wants to visit
, my_countries (country_id) AS
(
SELECT 
    country_id 
FROM 
    user_country 
    JOIN my_user USING (user_id)
UNION
SELECT  -- Contryies of interest via city...
    country_id
FROM 
    user_city
    JOIN my_user USING(user_id)
    JOIN city ON city.id = user_city.city_id 
)
-- Users that want to go the same 'country'
SELECT DISTINCT
    uc.user_id
FROM
    my_countries
    JOIN user_country uc USING(country_id)

UNION
-- Users that want to go to some 'city' that is in my_countries
SELECT DISTINCT
    ui.user_id
FROM
    my_countries
    JOIN city USING (country_id)
    JOIN user_city ui ON city.id = ui.city_id

EXCEPT
-- Myself
SELECT
    user_id
FROM
    my_user ;
3
  • It's a good attempt, and satisfies use case 1 (user id 1 returns 2, 3 & 4) but not usecase 2 (user id 3 returns nothing). Thank you though, I like the way you structured your code!
    – Martyn
    Jan 7, 2017 at 20:40
  • @Martyn: Updated to take that into account too
    – joanolo
    Jan 7, 2017 at 20:58
  • Still not quite right - with the initial example data it's returning all user ids for each user id entered. I've given some more example data - hope that helps!
    – Martyn
    Jan 7, 2017 at 21:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.