Database

Querying Joins and Nested 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 orchestral_sections and instruments:

Orchestral sections

idname
1strings
2woodwinds

Instruments

idnamesection_id
1violin1
2viola1
3flute2
4oboe2

The APIs will automatically detect relationships based on the foreign keys:

1
const { data, error } = await supabase.from('orchestral_sections').select(`
2
id,
3
name,
4
instruments ( id, name )
5
`)

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 orchestral sections and instruments:

1
create table orchestral_sections (
2
"id" serial primary key,
3
"name" text
4
);
5
6
create table instruments (
7
"id" serial primary key,
8
"name" text,
9
"section_id" int references "orchestral_sections"
10
);

We can get the nested SectionsWithInstruments type like this:

1
import { QueryResult, QueryData, QueryError } from '@supabase/supabase-js'
2
3
const sectionsWithInstrumentsQuery = supabase.from('orchestral_sections').select(`
4
id,
5
name,
6
instruments (
7
id,
8
name
9
)
10
`)
11
type SectionsWithInstruments = QueryData<typeof sectionsWithInstrumentsQuery>
12
13
const { data, error } = await sectionsWithInstrumentsQuery
14
if (error) throw error
15
const sectionsWithInstruments: SectionsWithInstruments = data

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):

1
create table users (
2
"id" serial primary key,
3
"name" text
4
);
5
6
create table teams (
7
"id" serial primary key,
8
"team_name" text
9
);
10
11
create table members (
12
"user_id" int references users,
13
"team_id" int references teams,
14
primary key (user_id, team_id)
15
);

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:

1
const { data, error } = await supabase.from('teams').select(`
2
id,
3
team_name,
4
users ( id, name )
5
`)

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:

1
-- Employees
2
create table users (
3
"id" serial primary key,
4
"name" text
5
);
6
7
-- Badge scans
8
create table scans (
9
"id" serial primary key,
10
"user_id" int references users,
11
"badge_scan_time" timestamp
12
);
13
14
-- Work shifts
15
create table shifts (
16
"id" serial primary key,
17
"user_id" int references users,
18
"scan_id_start" int references scans, -- clocking in
19
"scan_id_end" int references scans, -- clocking out
20
"attendance_status" text
21
);

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:

1
const { data, error } = await supabase.from('shifts').select(
2
`
3
*,
4
start_scan:scans!scan_id_start (
5
id,
6
user_id,
7
badge_scan_time
8
),
9
end_scan:scans!scan_id_end (
10
id,
11
user_id,
12
badge_scan_time
13
)
14
`
15
)