Skip to content
/ Chapter 22 · Lists, Structs, and JSON
Lesson 22.3 · movie_rentals
Lesson 22.3

Structs in SQL

A struct is a small row inside one value: named fields, each with its own type. A list of structs keeps several columns together for every item.

A list holds many values of one type. A struct holds a few values of different types, each with a name, like a row packed into a single column. Curly braces build one, with the field names in quotes:

Pattern
select title,
       {'genre': genre, 'year': release_year, 'rating': rating} as details
from movies
order by movie_id
limit 3

Each film has one details value with three fields: a text genre, a whole-number year, and a fractional rating.

Reading fields

A dot reads one field, the way m.title reads a column from a table alias. Square brackets with the name in quotes do the same thing:

Dot and bracket access
with films as (
  select title,
         {'genre': genre, 'year': release_year, 'rating': rating} as details
  from movies
)
select title,
       details.genre,
       details['year'] as released
from films
where details.rating >= 8.5

Only Paper Lanterns is rated 8.5 or higher. A field the struct doesn't have stops the query: details.director fails with Could not find key "director" in struct, and the message suggests a field the struct does have.

To turn every field back into its own column, use .*:

One column per field
with films as (
  select title,
         {'genre': genre, 'rating': rating} as details
  from movies
)
select title, details.*
from films
order by title
limit 3

A list of structs

Structs earn their keep inside a list. list(m.title order by r.rental_date) would give a customer's titles but lose the dates. Collect a struct instead, and each item keeps its title and its date together:

Customer 1's rental history
select r.customer_id,
       list({'title': m.title, 'rented_on': r.rental_date}
            order by r.rental_date) as rentals
from rentals as r
join movies as m on m.movie_id = r.movie_id
where r.customer_id = 1
group by r.customer_id

That's one row holding four rentals, earliest first. Index the list, then read a field: rentals[1].title is the first film customer 1 rented, and rentals[-1].rented_on is the date of the latest one.

For a single value, such as only the latest title, arg_max(m.title, r.rental_date) is shorter. The list is worth building when you need several things from the same ordered history, as the exercise does.

In other databases

BigQuery has STRUCT with the same dot access, and arrays of structs are how it stores nested data. PostgreSQL has composite types: row(title, rating) builds an unnamed one, and named fields need CREATE TYPE first. Snowflake, MySQL, and SQL Server use a JSON object instead, which is the next lesson.

Schema · Movie Rentals Tables · rentals, movies, customers 3 tables
Table · rentals

One row per rental. Movies still out have a null returned_date.

6 columns · 26 rows
rental_id int customer_id int movie_id int rental_date date returned_date date amount decimal
Table · movies

One row per movie in the catalog.

5 columns · 12 rows
movie_id int title text genre text release_year int rating decimal
Table · customers

One row per customer.

5 columns · 10 rows
customer_id int first_name text last_name text city text joined_date date
Your task

For each customer, build their rental history as a list of structs with a title and a rented_on date, in rental order. From it, return the customer's first_name, the number of rentals, the title of their first rental as first_title, and the title of their latest as latest_title. Sort by first_name.

·

Run your query to inspect the result.

Expected answer
  • Columns: first_name, rentals, first_title, latest_title.
  • Rows: 10 customers, by first_name.
  • Ava rented 4 films, from Neon Harbor to Midnight Diner.