--- id: 'joins-and-nested-tables' title: 'Querying Joins and Nested tables' description: 'The Data APIs automatically detect relationships between Postgres tables.' --- The data APIs automatically detect relationships between Postgres tables. Since Postgres is a relational database, this is a very common scenario. ## One-to-many joins Let's use an example database that stores `countries` and `cities`: **Countries** | `id` | `name` | | ---- | -------------- | | 1 | United Kingdom | | 2 | United States | **Cities** | `id` | `name` | `country_id` | | ---- | ----------- | ------------ | | 1 | London | 1 | | 2 | Manchester | 1 | | 3 | Los Angeles | 2 | | 4 | New York | 2 | ```sql create table countries ( "id" serial primary key, "name" text ); insert into countries (id, name) values (1, 'United Kingdom'), (2, 'United States'); create table cities ( "id" serial primary key, "name" text, "country_id" int references "countries" ); insert into cities (name, country_id) values ('London', 1), ('Manchester', 1), ('Los Angeles', 2), ('New York', 2); ``` The APIs will automatically detect relationships based on the foreign keys: ```js const { data, error } = await supabase.from('countries').select(` id, name, cities ( id, name ) `) ``` ### TypeScript types for joins `supabase-js` always returns a `data` object (for success), and an `error` object (for unsuccessful requests). These helper types provide the result types from any query, including nested types for database joins. Given the following schema with a relation between cities and countries: ```sql create table countries ( "id" serial primary key, "name" text ); create table cities ( "id" serial primary key, "name" text, "country_id" int references "countries" ); ``` We can get the nested `CountriesWithCities` type like this: ```ts import { QueryResult, QueryData, QueryError } from '@supabase/supabase-js' const countriesWithCitiesQuery = supabase.from('countries').select(` id, name, cities ( id, name ) `) type CountriesWithCities = QueryData const { data, error } = await countriesWithCitiesQuery if (error) throw error const countriesWithCities: CountriesWithCities = data ``` ```dart final data = await supabase.from('countries').select('id, name, cities(id, name)'); ``` ```swift struct Country: Codable { let id: Int let name: String let cities: [City] struct City: Codable { let id: Int let name: String } } let countries: [Country] = try await supabase .from("countries") .select("id, name, cities(id, name)") .execute() .value ``` ```kotlin val data = supabase.from("countries").select(Columns.raw("id, name, cities(id, name)")) ``` ```python data = supabase.from_('countries').select('id, name, cities(id, name)').execute() ``` ```javascript const Query = ` query { countriesCollection { edges { node { id name cities { id, name } } } } } ` ``` ```bash GET https://[REF].supabase.co/rest/v1/countries?select=id,name,cities(id,name) ``` ## Many-to-many joins The data APIs will detect many-to-many joins. For example, if you have a database which stored teams of users (where each user could belong to many teams): ```sql create table users ( "id" serial primary key, "name" text ); create table teams ( "id" serial primary key, "team_name" text ); create table members ( "user_id" int references users, "team_id" int references teams, primary key (user_id, team_id) ); ``` In these cases you don't need to explicitly define the joining table (members). If we wanted to fetch all the teams and the members in each team: ```js const { data, error } = await supabase.from('teams').select(` id, team_name, users ( id, name ) `) ``` ```dart final data = await supabase.from('teams').select('id, team_name, users(id, name)'); ``` ```swift struct Team: Codable { let id: Int let name: String let users: [User] struct User: Codable { let id: Int let name: String } enum CodingKeys: String, CodingKey { case id, users case name = "team_name" } } let teams [Team] = try await supabase .from("teams") .select( """ id, team_name, users ( id, name ) """ ) .execute() .value ``` ```kotlin val data = supabase.from("teams").select(Columns.raw("id, team_name, users(id, name)")); ``` ```python data = supabase.from_('teams').select('id, team_name, users(id, name)').execute() ``` ````javascript const Query = ` query { ```javascript const Query = ` query { teamsCollection { edges { node { id team_name users { id, name } } } } } ` ```` ```bash GET https://[REF].supabase.co/rest/v1/teams?select=id,team_name,users(id,name) ``` ## Specifying the `ON` clause for joins with multiple foreign keys For example, if you have a project that tracks when employees check in and out of work shifts: ```sql -- Employees create table users ( "id" serial primary key, "name" text ); -- Badge scans create table scans ( "id" serial primary key, "user_id" int references users, "badge_scan_time" timestamp ); -- Work shifts create table shifts ( "id" serial primary key, "user_id" int references users, "scan_id_start" int references scans, -- clocking in "scan_id_end" int references scans, -- clocking out "attendance_status" text ); ``` In this case, you need to explicitly define the join because the joining column on `shifts` is ambiguous as they are both referencing the `scans` table. To fetch all the `shifts` with `scan_id_start` and `scan_id_end` related to a specific `scan`, use the following syntax: ```js const { data, error } = await supabase.from('shifts').select( ` *, start_scan:scans!scan_id_start ( id, user_id, badge_scan_time ), end_scan:scans!scan_id_end ( id, user_id, badge_scan_time ) ` ) ``` ```dart final data = await supabase.from('shifts').select(''' *, start_scan:scans!scan_id_start ( id, user_id, badge_scan_time ), end_scan:scans!scan_id_end ( id, user_id, badge_scan_time ) '''); ``` ```swift struct Shift: Codable { let id: Int let userId: Int let attendanceStatus: String? let scans: [Scan] struct Scan: Codable { let id: Int let userId: Int let badgeScanTime: TimeInterval enum CodingKeys: String, CodingKey { case id case userId = "user_id" case badgeScanTime = "badge_scan_time" } } enum CodingKeys: String, CodingKey { case id case userId = "user_id" case attendanceStatus = "attendance_status" } } let shifts: [Shift] = try await supabase .from("shifts") .select( """ *, start_scan:scans!scan_id_start ( id, user_id, badge_scan_time ), scans: scan_id_end ( id, user_id, badge_scan_time ) """ ) .execute() .value ``` ```kotlin val data = supabase.from("shifts").select(Columns.raw(''' *, start_scan:scans!scan_id_start ( id, user_id, badge_scan_time ), end_scan:scans!scan_id_end ( id, user_id, badge_scan_time ) ''')); ``` ```python data = supabase.from_('shifts').select(""" *, start_scan:scans!scan_id_start ( id, user_id, badge_scan_time ), end_scan:scans!scan_id_end ( id, user_id, badge_scan_time ) """).execute() ``` ```javascript const Query = ` query { shiftsCollection { edges { node { id user_id attendance_status scan_id_start { id user_id badge_scan_time } scan_id_end { id user_id badge_scan_time } } } } } ` ```