views:

458

answers:

10

I have a table where each row has a few fields that have ID's that relate to some other data from some other tables.

Let's say it's called people, and each person has the ID of a city, state and country.

So there will be three more tables, cities, states and countries where each has an ID and a name.

When I'm selecting a person, what's the easiest way to get the names of the city, state and country in a single query?

Note: I know this is possible with joins, however as there are more related tables, the nested joins makes the query hard to read, and I'm wondering if there is a cleaner way. It should also be possible for the person to have those fields empty.

+2  A: 

JOINS are the only way to really do this.

You might be able to change your schema, but the problem will be the same regardless.

(A City is always in a State, which is always in a Country - so the Person could just have a reference to the city_id rather than all three. You still need to join the 3 tables though).

Toby Hede
the city/state etc are just examples. but thanks, i guess i'll have to nest some left joins.
Murat Ayfer
A city name may not necessarily be unique though. For example, there is Miami, Ohio and Miami, Florida to give a U.S. example though one could just as easily look at Ontario, Canada which has cities London and Paris which are capitals in other countries.
JB King
That's OK - you don't necessarily need the name to be unique as you can use a different guaranteed unique key (probably an auto-incremented id value).
Toby Hede
A: 

Joins are the answer. With practise they will become more readable to you.
There may be special cases where creating a function would help you, for example you could do the following (in Oracle, I don't know any mysql):
You could create a function to return a formatted address given the city state and country codes, then your query becomes

SELECT first_name, last_name, formated_address(city_id, state_id, country_id) 
FROM people
WHERE some_where_clause;

where formated_address does individual lookups on the city state and country tables and puts separators between the decoded values, or returns "no address" if they are all empty, etc

hamishmcn
OOO. DON'T. There is no limit to the inefficiency of these constructs
erikkallen
A: 

If the tables involved are reference tables (i.e. they hold lookup data that isn't going to change during the life time of a session), depending on the nature of your application, you could pre-load the reference data during you application start up. Then your query doesn't need to do the joins, instead it returns the id values, and in your application you do a decode of the ids when you need to display the data.

hamishmcn
A: 

The easiest solution is to use the names as the primary keys in city, state, and country. Then your person table can reference them by the name instead of the pseudokey "id". That way, you don't need to do joins, since your person table already has the needed values.

It does take more space to store a string instead of a 4-byte pseudokey. But you may find the tradeoff worthwhile, if you are threatened by joins as much as you seem to be (which, by the way, is like a PHP programmer being reluctant to use foreach -- joins are fundamental to SQL in the same way).

Also there are many city names that appear in more than one state. So your city table should reference the state table and use these two columns as the primary key.

CREATE TABLE cities (
  city_name VARCHAR(30),
  state     CHAR(2),
  PRIMARY KEY (city_name, state),
  FOREIGN KEY (state) REFERENCES states(state)
);

CREATE TABLE persons (
  person_id    SERIAL PRIMARY KEY,
  ...other columns...
  city_name    VARCHAR(30),
  state        CHAR(2),
  country_name VARCHAR(30),
  FOREIGN KEY (city_name, state) REFERENCES cities(city_name, state),
  FOREIGN KEY (country_name) REFERENCES countries(country_name)
);

This just an example of the technique. Of course it's more complex than this, because you may have city names in more than one country, you may have countries with no states, and so on. The point is SQL doesn't force you to use integer pseudokeys, so use CHAR and VARCHAR keys where appropriate.

Bill Karwin
+1  A: 

There is no cleaner way than joins. If the fields are allowed to be empty, use outer joins

SELECT c.*, s.name AS state_name
  FROM customer c
  LEFT OUTER JOIN state s ON s.id = c.state
 WHERE c.id = 10
erikkallen
A: 

A disadvantage of standard SQL is the the return data needs to be in tabular format. However some database vendors have added features that makes it possible to select data in non-tabular format. I don't know whether MySQL knows such features.

tuinstoel
+2  A: 

Assuming the following tables:

create table People
(
     ID        int          not null primary key auto_increment
    ,FullName  varchar(255) not null
    ,StateID   int 
    ,CountryID int 
    ,CityID    int 
)
;
create table States
(
     ID   int          not null primary key auto_increment
    ,Name varchar(255) not null
)
;
create table Countries
(
     ID   int          not null primary key auto_increment
    ,Name varchar(255) not null
)
;
create table Cities
(
     ID   int          not null primary key auto_increment
    ,Name varchar(255) not null
)
;

With the Following Data:

insert into Cities(Name) values ('City 1'),('City 2'),('City 3');
insert into States(Name) values ('State 1'),('State 2'),('State 3');
insert into Countries(Name) values ('Country 1'),('Country 2'),('Country 3');
insert into People(FullName,CityID,StateID,CountryID) values ('Has Nothing'   ,null,null,null);
insert into People(FullName,CityID,StateID,CountryID) values ('Has City'      ,   1,null,null);
insert into People(FullName,CityID,StateID,CountryID) values ('Has State'     ,null,   2,null);
insert into People(FullName,CityID,StateID,CountryID) values ('Has Country'   ,null,null,   3);
insert into People(FullName,CityID,StateID,CountryID) values ('Has Everything',   3,   2,   1);

Then this query should give you what you are after.

select 
 P.ID
,P.FullName
,Ci.Name as CityName
,St.Name as StateName
,Co.Name as CountryName
from People P
left Join Cities    Ci on Ci.ID = P.CityID
left Join States    St on St.ID = P.StateID
left Join Countries Co on Co.ID = P.CountryID
feihtthief
this is exactly what i ended up doing.cheers
Murat Ayfer
+1  A: 

According to the description of the schema that you have given you will have to use JOINS in a single query.

SELECT 
   p.first_name 
 , p.last_name
 , c.name as city
 , s.name as state
 , co.name as country 
FROM people p 
LEFT OUTER JOIN city c 
  ON p.city_id = c.id
LEFT OUTER JOIN state s 
  ON p.state_id = s.id
LEFT OUTER JOIN country co
  ON p.country_id = co.id;

The LEFT OUTER JOIN will allow you to fetch details of person even if some IDs are blank or empty.

Another way is to redesign your lookup tables. A city is always in a state and a state in a country. Hence your city table will have columns : Id, Name and state_id. Your state table will be : Id, Name and country_id. And country table will remain the same : Id and Name.

The person table will now have only 1 id : city_id

Now your query will be :

 SELECT 
   p.first_name 
 , p.last_name
 , c.name as city
 , s.name as state
 , co.name as country 
FROM people p 
LEFT OUTER JOIN city c 
  ON p.city_id = c.id
LEFT OUTER JOIN state s 
  ON c.state_id = s.id
LEFT OUTER JOIN country co
  ON s.country_id = co.id;

Notice the difference in the last two OUTER JOINS

Rishi Agarwal
A: 

All great answers but the questioner specified they didn't want to use joins. As one respondent demonstrated, assuming your Cities, States, and Countries tables have an Id and a Description field you might be able to do something like this:

SELECT
     p.Name, c.Description, s.Description, ct.Description
FROM
     People p, Cities c, States s, Countries ct
WHERE
     p.Id = value AND
     c.Id = value AND
     s.Id = value AND
     ct.Id = value;

That's using a join with obscure syntax.
toast
A: 

Create a view that does the Person, City, State, and Country joins for you. Then just reference the View in all other joins.

Something like:

CREATE VIEW FullPerson AS  
SELECT Person.*, City.Name, State.Name, Country.Name  
FROM  
  Person LEFT OUTER JOIN City ON Person.CityId = City.Id  
  LEFT OUTER JOIN State ON Person.StateId = State.Id  
  LEFT OUTER JOIN Country ON Person.CountryId = Country.Id

Then in other queries, you can

SELECT FullPerson.*, Other.Value  
FROM FullPerson LEFT OUTER JOIN Other ON FullPerson.OtherId = Other.Id
toast