extract different number of rows from multiple tables
Your JOIN is behaving perfectly normally.

If you have one airports row, and no matching contacts or runways, you would get something like this:

 Airport  | Contact | Runway
----------+---------+--------
 Schiphol |   NULL  |  NULL


If you had one airport, one contact, and one runway, you'd get:

 Airport  | Contact | Runway
----------+---------+--------
 Schiphol | Ruppert |  28R


However, when you've got multiple matching contacts and runways, and there's no connection between a contact and a runway, then each contact will get paired with each runway, as you've seen:

 Airport  | Contact | Runway
----------+---------+--------
 Schiphol | Ruppert |  28R
 Schiphol | Ruppert |  12L
 Schiphol | Ruppert |  14R
 Schiphol | Griffin |  28R
 Schiphol | Griffin |  12L
 Schiphol | Griffin |  14R


This is called a cross-product. You're getting double the rows you think you want because you're crossing 2 rows with 3 rows (2 * 3 = 6). If you had 7 contacts and 5 runways, you'd be getting 35 rows.

What your best solution is will depend on what you're DBMS is capable of.

Worst case scenario, run three queries: one with the airport results, one with the contacts results, and one with the runways results. Pull all three results sets into PHP. If you're bringing back results for more than one airport, you'd loop through the airports results, and (inside that loop) loop through the other two result sets to pull out the contacts and runways for the current airport.

Many DBMSes provide functions that would let you combine the rows for each airport's contacts or runways into XML or JSON. In that case, you'd have one main query, but you'd have two sub-queries within it to generate the two objects for the other tables. It's be something like (pseudo-code only!):

SELECT airport_name, airport_iata, airport_icao
      ,(SELECT &lt;XML/JSON func&gt;(first_name, last_name, email_address, phone_number)
          FROM contacts
         WHERE airport_id = airports.id
       ) as contacts_obj
      ,(SELECT &lt;XML/JSON func&gt;(designator, length, surface, landing_system)
          FROM runways
         WHERE airport_id = airports.id
       ) as runways_obj
  FROM airports
 WHERE airports.id = 1;


Again, pseudo-code only; you'll need to figure out the details of the function for your DBMS.

Barring an explicit XML or JSON function, most DBMSes have the ability to create string aggregates. This would work much like the function above, except more manually; you'd probably build a string for each row (first_name + '|' + last_name + '|' + ...), and use the aggregate function to combine the multiple contacts into a single string (with a different separator, of course).