AdvancedSQL · 24 min · 19 challenges

Dates: Advanced

Uber pays drivers for a week that runs from Monday 4:00 a.m. to the following Monday 3:59 a.m., in the driver's local time. A Chicago driver finishes a fare at 11:50 p.m. on Sunday, and the trip log stamps it 05:50 UTC Monday. Which statement does that fare land on? Converting the zone, snapping to the week, and printing the date the driver expects all decide it.

Timezone Control

Daily Life
Interviews

An instant read on 2 wall clocks

A TIMESTAMP WITH TIME ZONE value is a point on the global timeline plus the zone it should be read in. AT TIME ZONE swaps that zone and keeps the point, so the clock digits change while the moment stays where it was. Every conversion question then has 2 parts: which instant, and whose wall clock reads it.

The trips table stores each end time as text that carries its zone, such as 2024-03-04 05:50:00 UTC. A CAST to TIMESTAMP WITH TIME ZONE reads that text as an instant, AT TIME ZONE 'America/Chicago' reads the instant on a Chicago clock, and a CAST of the result to DATE keeps the Chicago calendar day.

WITH local_trips AS (
SELECT
trip_id,
ended_at,
CAST(ended_at AS TIMESTAMP WITH TIME ZONE) AT TIME ZONE 'America/Chicago' AS ended_local
FROM trips
)
SELECT
trip_id,
ended_at,
ended_local,
CAST(ended_local AS DATE) AS local_date
FROM local_trips
Result
trip_idended_atended_locallocal_date
1012024-03-04 05:50:00 UTCNULLNULL
1022024-03-10 18:05:00 UTCNULLNULL
1032024-03-11 04:50:00 UTCNULLNULL
3 rows
Trips 101 and 103 both end at 23:50 on a Sunday in Chicago, yet their UTC times of day differ by an hour: 05:50 and 04:50. Chicago ran 6 hours behind UTC before 2:00 a.m. local time on March 10, 2024. After that moment it ran 5 hours behind. Both rows also carry a UTC date 1 day later than their Chicago date, so a report grouped by UTC day books Sunday night's fares on Monday.

Named zones follow daylight saving rules

A fixed offset such as -06:00 applies the same shift all year. A named zone such as America/Chicago carries the region's rule history, so it moves from 6 to 5 hours behind UTC at the instant the clocks jumped. Both tabs below read 07:30 and 08:30 UTC on March 10, 2024, half an hour before and after Chicago's jump.

The named zone reads 01:30 before the jump and 03:30 after it: 1 hour of UTC time covers 2 hours of Chicago clock, because 02:00 to 03:00 never happened that night.
SELECT
TIMESTAMP '2024-03-10 07:30:00 UTC' AT TIME ZONE 'America/Chicago' AS before_jump,
TIMESTAMP '2024-03-10 08:30:00 UTC' AT TIME ZONE 'America/Chicago' AS after_jump
Result
before_jumpafter_jump
NULLNULL
1 row
The jump in March has a mirror in November. On November 3, 2024, Chicago clocks reached 2:00 a.m. and fell back to 1:00 a.m., so every minute between 1:00 and 2:00 happened twice. Converting from UTC still gives a single answer per instant, as these 2 readings taken an hour apart show.
SELECT
TIMESTAMP '2024-11-03 06:30:00 UTC' AT TIME ZONE 'America/Chicago' AS first_pass,
TIMESTAMP '2024-11-03 07:30:00 UTC' AT TIME ZONE 'America/Chicago' AS second_pass
Result
first_passsecond_pass
NULLNULL
1 row
Both columns read 01:30, and they remain different instants, 1 hour apart. The reverse trip has no single answer: a wall-clock 01:30 on that night names 2 instants, so a pipeline that stores only local digits can no longer tell which of 2 fares came first. Keep the instant, and derive local time from it.
TIP
Store IANA zone names in dimension tables, never offsets. America/Chicago is one such name, and so are Europe/London and Asia/Tokyo. An offset captured in January is wrong in Chicago from March to November.

A zone stored on every row

AT TIME ZONE accepts a zone name written as a literal, so it can't read the zone from a column. When each row carries its own zone, route the value through Unix time, which the intermediate lesson covered: to_unixtime turns the instant into seconds since 1970-01-01 UTC, and from_unixtime(seconds, zone) builds a TIMESTAMP WITH TIME ZONE read in the zone that row names. Seconds since the epoch belong to no zone, so the instant passes through unchanged.

SELECT
trip_id,
city,
FROM_UNIXTIME(
TO_UNIXTIME(CAST(ended_at AS TIMESTAMP WITH TIME ZONE)),
zone
) AS ended_local
FROM trips_global
Result
trip_idcityended_local
201ChicagoNULL
202LondonNULL
203TokyoNULL
204Los AngelesNULL
4 rows
Tokyo's trip ends on March 11 locally while its UTC stamp says March 10. The Los Angeles trip at 09:30 UTC reads 01:30, 30 minutes before that city's clocks jumped at 10:00 UTC. Each row resolves against its own zone's rules.
Bare timestamps borrow the session zone
A plain TIMESTAMP holds clock digits with no zone attached. Apply AT TIME ZONE to one and the database first reads those digits in the session's zone, a setting that never appears in the query text, so 2 people running the same query can get different rows. Put the zone in the data, the way trips carries its UTC suffix, before converting.
Multiple Choice

A fare ends at 08:45 UTC on March 10, 2024. What does this query return?

SELECT
CAST(
TIMESTAMP '2024-03-10 08:45:00 UTC' AT TIME ZONE 'America/Chicago'
AS DATE
) AS local_date,
HOUR(
TIMESTAMP '2024-03-10 08:45:00 UTC' AT TIME ZONE 'America/Chicago'
) AS local_hour

DATE_TRUNC()

Daily Life
Interviews

date_trunc(unit, value) keeps every field down to the named unit and resets the smaller ones to their minimum: day 1, hour 0, minute 0, second 0. The result has the input's type, so a truncated timestamp can still be sorted and compared, and interval arithmetic works on it. Every value inside the same unit lands on the same start point, which turns the truncated column into a grouping key.

SELECT
DATE_TRUNC('minute', TIMESTAMP '2024-03-14 17:42:09') AS to_minute,
DATE_TRUNC('hour', TIMESTAMP '2024-03-14 17:42:09') AS to_hour,
DATE_TRUNC('day', TIMESTAMP '2024-03-14 17:42:09') AS to_day,
DATE_TRUNC('week', TIMESTAMP '2024-03-14 17:42:09') AS to_week,
DATE_TRUNC('month', TIMESTAMP '2024-03-14 17:42:09') AS to_month,
DATE_TRUNC('quarter', TIMESTAMP '2024-03-14 17:42:09') AS to_quarter
Result
to_minuteto_hourto_dayto_weekto_monthto_quarter
2024-03-14 17:42:002024-03-14 17:00:002024-03-142024-03-112024-03-012024-01-01
1 row

March 14, 2024 is a Thursday, and 'week' snaps it back to Monday, March 11: date_trunc always starts weeks on Monday, the convention the ISO 8601 section covers in full. 'quarter' lands on January 1 because March sits in the first quarter, and 'year' would land there too.

The units are 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter' and 'year'.

Truncation keeps the year; month() drops it

Grouping by month(ordered_at), from the intermediate lesson, returns 3 for every March. That puts March 2023 and March 2024 in 1 group. date_trunc('month', ordered_at) returns a full timestamp such as 2024-03-01 00:00:00, which still names its year and sorts across years in calendar order.

Returns 1 row: month 3 with 4 orders, placed a year apart and counted together.
SELECT
MONTH(ordered_at) AS order_month,
COUNT(*) AS orders
FROM orders
GROUP BY MONTH(ordered_at)
ORDER BY order_month
Result
order_monthorders
34
1 row

The GROUP BY repeats the whole date_trunc expression because GROUP BY runs before the SELECT list names order_month. Change the unit string and the same query counts at another grain, such as week or quarter. Every key stays a timestamp, so later steps can filter on it or format it.

Truncate on the local clock

On a TIMESTAMP WITH TIME ZONE, date_trunc cuts at the boundaries of the zone the value carries. Truncate a UTC value to 'day' and days break at UTC midnight, which is 6:00 p.m. in Chicago during standard time, so an evening shift splits across 2 days. Convert with AT TIME ZONE before truncating, and days break at Chicago midnight.

SELECT
trip_id,
DATE_TRUNC('day', CAST(ended_at AS TIMESTAMP WITH TIME ZONE)) AS utc_day,
DATE_TRUNC(
'day',
CAST(ended_at AS TIMESTAMP WITH TIME ZONE) AT TIME ZONE 'America/Chicago'
) AS chicago_day
FROM trips
Result
trip_idutc_daychicago_day
101NULLNULL
102NULLNULL
103NULLNULL
3 rows

Trips 101 and 103 each move back 1 day once the cut reads Chicago's clock, because both ended at 23:50 local. Trip 102 ended at 1:05 p.m. in Chicago and keeps March 10 either way. A daily total grouped by utc_day would credit 2 Sunday-night fares to Monday.

Weeks that open Monday at 4:00 a.m.

A pay week that runs Monday 4:00 a.m. to the next Monday 3:59 a.m. matches no unit. Shift every timestamp back 4 hours so the boundary lands on midnight, truncate to 'week', then add the 4 hours back. The fares table below holds Chicago wall-clock times as plain timestamps; with UTC data, convert before shifting. Here are 3 fares walked through by hand.

FareEnds (local)Minus 4 hoursTruncated to weekPlus 4 hours
2Mon 03-04 02:15Sun 03-03 22:15Mon 02-26 00:00Mon 02-26 04:00
3Mon 03-04 09:40Mon 03-04 05:40Mon 03-04 00:00Mon 03-04 04:00
6Mon 03-11 04:00Mon 03-11 00:00Mon 03-11 00:00Mon 03-11 04:00
SELECT
DATE_TRUNC('week', ended_local - INTERVAL '4' HOUR) + INTERVAL '4' HOUR AS pay_week_start,
COUNT(*) AS fares,
SUM(amount) AS earnings
FROM fares
GROUP BY DATE_TRUNC('week', ended_local - INTERVAL '4' HOUR) + INTERVAL '4' HOUR
ORDER BY pay_week_start
fares5 rows
fare_id
ended_local
amount
1
2024-03-03 23:50:00
18
2
2024-03-04 02:15:00
22
3
2024-03-04 09:40:00
15
5
2024-03-11 03:59:00
12
6
2024-03-11 04:00:00
9
Preparing the animation
Fare 2 ended on Monday, March 4, before 4:00 a.m., so it joins the week of February 26 with Sunday night's fare 1. Fare 5 at 03:59 on March 11 still counts toward the March 4 week, and fare 6, 1 minute later, opens the next one.
Filter with ranges, not truncated columns
  • ▸WHERE date_trunc('month', ordered_at) = TIMESTAMP '2024-03-01 00:00:00' computes the function on every row before comparing.
  • ▸WHERE ordered_at >= TIMESTAMP '2024-03-01 00:00:00' AND ordered_at < TIMESTAMP '2024-04-01 00:00:00' returns the same rows.
  • ▸The range compares the raw column, so the engine can check each file's stored min and max and skip files that can't match.
  • ▸Close the range with <: BETWEEN includes its upper end and would pull in rows stamped exactly midnight on April 1.
Multiple Choice

Using the 4-hour shift from the fares query, which pay week start does a fare that ends at 2024-03-18 03:30:00 get?

Parsing and Formatting

Daily Life
Interviews

Dates arrive as text, and each source picks its own layout: a CSV export writes them one way, a partner feed another. date_parse(text, format) reads text into a TIMESTAMP by matching it against a format string: each % specifier stands for one field, and every other character must appear in the text exactly as written.

SpecifierReadsExample
%Y4-digit year2024
%mmonth 01-1203
%dday 01-3104
%Hhour 00-2323
%hhour 01-1211
%iminutes 00-5950
%sseconds 00-5909
%pAM or PMPM

Minutes are %i because %m already means month. A format that writes %m in the minutes slot reads that slot as a second month field, and any minute value above 12 fails to parse.

SELECT
event_id,
raw_time,
DATE_PARSE(raw_time, '%m/%d/%Y %h:%i %p') AS parsed
FROM raw_events
Result
event_idraw_timeparsed
103/04/2024 11:50 PMNULL
203/10/2024 01:30 AMNULL
312/31/2024 07:05 PMNULL
3 rows

%h with %p turns 11:50 PM into 23:50 and 07:05 PM into 19:05. The slashes and the colon outside the specifiers matched the text exactly, and so did the space between date and time.

Real feeds drift. Suppose a second upstream system starts writing 2024-03-10 01:30 into the same column. That text has hyphens where the format expects slashes, so date_parse raises an error at that row and the whole query fails, however many good rows came before it. TRY wraps any expression and turns an error inside it into NULL for that row, the way TRY_CAST handles a bad cast.

SELECT
event_id,
raw_time,
TRY(DATE_PARSE(raw_time, '%m/%d/%Y %h:%i %p')) AS parsed
FROM raw_events

With that row added to raw_events, the 3 rows above parse as before and the new one returns NULL in parsed. Counting those NULLs after each load tells you how many rows the format missed, and a count that jumps from 0 to thousands overnight means a source changed its layout.

The format string decides ambiguous dates

The text 03/04/2024 means March 4 to a US system and April 3 to most of Europe. Nothing in its 10 characters says which, so the format string records the decision, and only the source system's documentation can settle it.

Reads month then day and returns 2024-03-04.
SELECT
DATE_PARSE('03/04/2024', '%m/%d/%Y') AS parsed
Result
parsed
2024-03-04
1 row
TIP
A first field above 12 settles the order: 25/03/2024 parses only as day first. Before trusting a format on a new feed, scan a sample of the column for such values.

Pattern letters: the second format language

parse_datetime(text, pattern) and format_datetime(value, pattern) use letter patterns in place of % codes, as in yyyy-MM-dd HH:mm:ss. Letter case carries meaning: MM is month and mm is minutes, so the pattern yyyy-mm-dd reads 2024-03-10 as minute 3 of January 10. parse_datetime returns a TIMESTAMP WITH TIME ZONE, and its Z letter reads a numeric offset such as -0600 from the text.

SELECT
PARSE_DATETIME('2024-03-10 01:30:00 -0600', 'yyyy-MM-dd HH:mm:ss Z') AS parsed,
PARSE_DATETIME('2024-03-10 01:30:00 -0600', 'yyyy-MM-dd HH:mm:ss Z') AT TIME ZONE 'UTC' AS parsed_utc,
FORMAT_DATETIME(TIMESTAMP '2024-03-10 07:30:00 UTC', 'yyyy/MM/dd/HH') AS path_part

The offset in the text becomes part of the value, so converting to UTC lands on 07:30. Writing runs the same machinery in reverse: format_datetime turns a timestamp into 2024/03/10/07, an hour-level folder layout for raw files. Code that reads those folder names back needs the identical pattern, so define it once and use it in both directions.

The case slip in yyyy-mm-dd raises no error, so nothing in the load stops it. These 2 columns parse the same text and differ only in the case of the month letters.

SELECT
PARSE_DATETIME('2024-03-10', 'yyyy-MM-dd') AS month_upper,
PARSE_DATETIME('2024-03-10', 'yyyy-mm-dd') AS month_lower
Result
month_uppermonth_lower
2024-03-10 00:00:002024-03-10 00:00:00
1 row

month_lower lands on January 10 at 00:03: the mm slot read 03 as minutes, and the month, never read, defaulted to January. Every row of a feed parsed this way counts as parsed, so a NULL check passes while the whole feed piles into January.

✓Do
  • Write the format string from a documented sample of the source, field by field
  • Wrap parsing in TRY and alert when the NULL count rises after a load
  • Keep the raw text column next to the parsed one so failures can be inspected
✗Don't
  • Guess day-first or month-first from a handful of rows
  • Mix % specifiers and pattern letters in one format string
  • Write mm for months in a letter pattern; it means minutes
Multiple Choice

Which format string makes date_parse('2024-03-10 13:05:00', ...) return 2024-03-10 13:05:00?

ISO 8601 Functions

Daily Life
Interviews

ISO 8601 is the international standard for writing dates and times as text. It puts the largest unit first in fixed-width fields and ends with an explicit offset. 2024-03-10T01:30:00-06:00 names its instant with no outside context, and values written in the same offset sort correctly as plain text, which is why many APIs and log formats use it.

Reading timestamps without a format string

from_iso8601_timestamp(text) reads ISO 8601 timestamp text into a TIMESTAMP WITH TIME ZONE with no format string. It accepts the variations the standard allows within one column, so Z for UTC and a numeric offset can sit side by side, with or without fractional seconds. A date_parse format matches exactly one of those layouts.

SELECT
event_id,
sent_at,
FROM_ISO8601_TIMESTAMP(sent_at) AT TIME ZONE 'UTC' AS sent_utc
FROM webhook_events
Result
event_idsent_atsent_utc
e12024-03-10T07:30:00Z2024-03-10 07:30:00
e22024-03-10T01:30:00-06:002024-03-10 07:30:00
e32024-03-10T16:30:00.250+09:002024-03-10 07:30:00
e42024-03-10T09:45:00+01:002024-03-10 08:45:00
4 rows
Rows e1 and e2 look nothing alike as text, yet they name the same instant, and e3 lands 250 milliseconds later. Compared as strings, all 3 differ; compared after parsing, e1 equals e2.

Sorting the raw text puts e2 first and e3 last, because text compares the clock digits as written: 01:30, 07:30, 09:45, 16:30. On the UTC timeline e1 and e2 tie at 07:30, with e3 250 milliseconds behind them. Row e4 comes last at 08:45. ORDER BY on the text column reports the first order, and MIN and MAX do the same.

Calendar, week and ordinal dates

from_iso8601_date(text) returns a DATE and reads 3 ISO spellings of a day: the calendar form 2024-12-30, the week form 2025-W01-1 (week-year 2025, week 1, day 1 for Monday), and the ordinal form 2024-366, the 366th day of 2024.

SELECT
label,
FROM_ISO8601_DATE(label) AS as_date
FROM plan_dates
Result
labelas_date
2024-12-302024-12-30
2025-W01-1NULL
2024-366NULL
3 rows
The first 2 labels name the same Monday, and the week form files it under 2025. That filing rule is the one ISO week numbers follow.

ISO weeks and the year they belong to

ISO weeks start on Monday, and week 1 of a year is the week that holds the year's first Thursday. The last days of December can therefore sit in week 1 of the next year, and the first days of January in week 52 or 53 of the previous one. week(d) returns the ISO week number, year_of_week(d) returns the ISO year that number belongs to, and day_of_week(d) returns 1 for Monday through 7 for Sunday.

The Thursday rule gives a way to compute the ISO year by hand. Every ISO week holds exactly 1 Thursday, and the week belongs to the year that Thursday falls in. date_add('day', 4 - day_of_week(d), d) moves any date to the Thursday of its week, 3 days forward from a Monday and 3 back from a Sunday, and year() of that Thursday is the value year_of_week(d) returns. The query below computes it that way so the rule is visible in every row.

SELECT
d,
YEAR(d) AS cal_year,
YEAR(DATE_ADD('day', 4 - DAY_OF_WEEK(d), d)) AS iso_year,
WEEK(d) AS iso_week,
DAY_OF_WEEK(d) AS iso_dow
FROM edge_days
ORDER BY d
Result
dcal_yeariso_yeariso_weekiso_dow
2020-12-3120202020524
2021-01-012021202005
2021-01-042021202111
2024-12-2920242025520
2024-12-3020242025531
2025-01-012025202503
6 rows

On 2024-12-30, cal_year says 2024 while iso_week says 1. A weekly report grouped by year(d), week(d) files that Monday under week 1 of 2024, a week that ended almost a year earlier, and it splits Friday 2021-01-01 away from the Thursday before it. Grouping by year_of_week(d), week(d) keeps every week whole.

Complete the SQL

> Count the days in edge_days per ISO week, and keep each week whole across New Year by taking the ISO year from the Thursday of the week. Two tiles don't belong.

1
2
3
4
5
6
TIP
date_trunc('week', d) produces the same weekly groups as year_of_week(d) with week(d), as a single date: the Monday that opens the ISO week.

Writing ISO text

to_iso8601(value) writes the standard form back out. A DATE becomes 2024-03-10, and a TIMESTAMP WITH TIME ZONE keeps its offset, with UTC written as Z. Use it whenever a timestamp leaves the database for another system, since the receiver needs no agreement beyond the standard to parse it.

SELECT
TO_ISO8601(DATE '2024-03-10') AS date_text,
TO_ISO8601(
FROM_ISO8601_TIMESTAMP('2024-03-10T01:30:00-06:00') AT TIME ZONE 'UTC'
) AS utc_text
Result
date_textutc_text
2024-03-10T00:00:002024-03-10T07:30:00
1 row
✓Do
  • Send timestamps to other systems with to_iso8601, in UTC
  • Parse ISO text with from_iso8601_timestamp and skip hand-written formats
  • Group weekly data by year_of_week(d) and week(d) together
✗Don't
  • Compare ISO strings that carry different offsets as text
  • Pair week(d) with year(d) in a weekly report
  • Strip the offset before parsing to make the text look uniform
Multiple Choice

What does this query return?

SELECT
YEAR(DATE '2021-01-02') AS cal_year,
YEAR_OF_WEEK(DATE '2021-01-02') AS iso_year,
WEEK(DATE '2021-01-02') AS iso_week

DATE_FORMAT()

Daily Life
Interviews

date_format(value, format) turns a timestamp into the text a person reads on a statement or a chart axis. It shares its % specifiers with date_parse; the table adds the name and unpadded-number specifiers that matter most on output, and shows each one for 2024-03-04 16:07:09.

SpecifierShowsOutput
%Wweekday nameMonday
%ashort weekdayMon
%Mmonth nameMarch
%bshort monthMar
%eday, no padding4
%cmonth, no padding3
%y2-digit year24
%l12-hour, no padding4
%k24-hour, no padding16
%jday of year064
%x-W%vISO year and week2024-W10

Formatting a pay statement

The weekly totals built in the DATE_TRUNC section arrive with week_start as a Chicago wall-clock timestamp. Each statement line needs both ends of its week in the form drivers read: the Monday 4:00 a.m. opening, and the close 1 minute short of 7 days later.

SELECT
DATE_FORMAT(week_start, '%a, %b %e %l:%i %p') AS opens,
DATE_FORMAT(
week_start + INTERVAL '7' DAY - INTERVAL '1' MINUTE,
'%a, %b %e %l:%i %p'
) AS closes,
earnings
FROM pay_weeks
ORDER BY week_start
Result
opensclosesearnings
Mon, Feb 26 4:00 AMMon, Mar 4 3:59 AM40
Mon, Mar 4 4:00 AMMon, Mar 11 3:59 AM27
Mon, Mar 11 4:00 AMMon, Mar 18 3:59 AM9
3 rows

%e and %l drop leading zeros, so the text reads Mar 4 3:59 AM. With %d and %h it would print Mar 04 03:59 AM. The query still orders by the timestamp week_start and formats only inside the SELECT list.

Multiple Choice

What does this query return?

SELECT
DATE_FORMAT(TIMESTAMP '2024-03-04 16:05:00', '%l:%i %p') AS label

Sort by the value, show the text

A formatted date is a VARCHAR, and text sorts character by character. Apr comes before Jan because A precedes J, so ordering by the label scrambles the calendar. Both tabs below show the same labels and differ only in the ORDER BY column.

Alphabetical order: Apr, Feb, Jan, Mar, Nov.
SELECT
DATE_FORMAT(month_start, '%b %Y') AS month_label,
revenue
FROM monthly
ORDER BY month_label
Result
month_labelrevenue
Apr 2024101
Feb 202495
Jan 2024120
Mar 2024143
Nov 2024160
5 rows
TIP
Keep dates typed through every CTE and format in the final SELECT. Once a date becomes a string you can't truncate it or convert it to another zone, and comparisons across months go wrong. The timestamp it came from has none of those problems.
Layouts for common readers
  • ▸Statements and receipts: %a, %b %e %l:%i %p gives Mon, Mar 4 4:00 AM.
  • ▸Monthly chart axes: %b %Y gives Mar 2024.
  • ▸Weekly report labels: %x-W%v gives 2024-W10. That label matches year_of_week and week.
  • ▸Day-first audiences: %d/%m/%Y gives 04/03/2024.

The weekly label in that list depends on %x. %Y prints the calendar year and %x the year a week number belongs to, the value year_of_week returns, so the specifiers agree on most days and split only on the days around New Year that ISO files under a neighbouring year. Monday, December 30, 2024 is one such day.

✓%Y-W%v on 2024-12-30
  • Prints 2024-W01
  • Points at the week of January 1, 2024, almost a year earlier
  • Puts the last Monday of 2024 at the top of a 2024 list
✗%x-W%v on 2024-12-30
  • Prints 2025-W01
  • Points at the week that opens on this Monday
  • Agrees with year_of_week and week for the same date

Format after converting the zone

date_format prints the clock digits of the value it receives. On a TIMESTAMP WITH TIME ZONE that means the digits in the zone it carries, so a UTC value formats as a UTC time. Apply AT TIME ZONE first and the label shows the reader's local time, as in this query on the trips table from the first section.

SELECT
trip_id,
DATE_FORMAT(
CAST(ended_at AS TIMESTAMP WITH TIME ZONE),
'%a %l:%i %p'
) AS utc_label,
DATE_FORMAT(
CAST(ended_at AS TIMESTAMP WITH TIME ZONE) AT TIME ZONE 'America/Chicago',
'%a %l:%i %p'
) AS chicago_label
FROM trips
ORDER BY trip_id
Result
trip_idutc_labelchicago_label
101NULLNULL
102NULLNULL
103NULLNULL
3 rows

Trip 101 shows Mon 5:50 AM in UTC and Sun 11:50 PM in Chicago. Only the second matches what the driver saw on the dashboard, and it decides which pay week the fare joins.

Complete the SQL

> Build a query that lists each trip with its end time as a Chicago label such as Sun 11:50 PM, in the order the trips ended. 2 tiles don't belong.

1
2
3
4
5
❯❯❯PUTTING IT ALL TOGETHER

> A pipeline for the Uber weekly driver statement from the intro could work this way: trip end times arrive as ISO 8601 text, drivers work in many cities, and each statement must cover Monday 4:00 a.m. to Monday 3:59 a.m. on the driver's own clock.

Parsing comes first: from_iso8601_timestamp turns each trip's text into an instant with its offset honored, and TRY flags malformed rows as NULLs.
Zone control comes next: from_unixtime(to_unixtime(...), zone) re-reads every instant in the driver's city, so the Chicago fare at Sunday 11:50 p.m. stays on Sunday.
Truncation assigns the week. Truncating to 'week' after a 4-hour shift back, then adding the 4 hours again, puts a Monday 02:15 fare on the previous statement.
Week labels follow ISO rules: %x-W%v labels the statement that opens Monday, December 30, 2024 as 2025-W01, matching year_of_week.
Formatting comes last: date_format writes Mon, Mar 4 4:00 AM from the typed timestamp, and statements sort by week_start, never by the label.
KEY TAKEAWAYS
AT TIME ZONE keeps the instant and changes the clock digits; a named IANA zone applies daylight saving, a fixed offset never does.
For a zone stored on each row, convert through from_unixtime(to_unixtime(ts), zone).
date_trunc resets the smaller fields and keeps the year; for boundaries like Monday 4:00 a.m., shift the value before truncating and shift it back afterward.
date_parse uses % specifiers and parse_datetime uses case-sensitive letters; wrap parsing in TRY so failures show up as NULLs.
from_iso8601_timestamp reads every ISO variation with no format string, and weekly groups need year_of_week alongside week.
Format with date_format in the final SELECT, after converting the zone, and sort by the timestamp.

Convert across time zones, snap to custom boundaries, and parse, label and print dates the way people read them.

Timezones, truncation, and global time handling

Category
SQL
Difficulty
advanced
Duration
24 minutes
Challenges
19 hands-on challenges

Topics covered: Timezone Control, DATE_TRUNC(), Parsing and Formatting, ISO 8601 Functions, DATE_FORMAT()

Lesson Sections

  1. Timezone Control (concepts: Timezones)

    An instant read on 2 wall clocks Trips 101 and 103 both end at 23:50 on a Sunday in Chicago, yet their UTC times of day differ by an hour: 05:50 and 04:50. Chicago ran 6 hours behind UTC before 2:00 a.m. local time on March 10, 2024. After that moment it ran 5 hours behind. Both rows also carry a UTC date 1 day later than their Chicago date, so a report grouped by UTC day books Sunday night's fares on Monday. Named zones follow daylight saving rules The jump in March has a mirror in November. On

  2. DATE_TRUNC() (concepts: Date Truncation)

    The units are 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter' and 'year'. Truncation keeps the year; month() drops it Truncate on the local clock Weeks that open Monday at 4:00 a.m. Fare 2 ended on Monday, March 4, before 4:00 a.m., so it joins the week of February 26 with Sunday night's fare 1. Fare 5 at 03:59 on March 11 still counts toward the March 4 week, and fare 6, 1 minute later, opens the next one.

  3. Parsing and Formatting (concepts: Date Formatting)

    The format string decides ambiguous dates Pattern letters: the second format language

  4. ISO 8601 Functions (concepts: Date Formatting)

    Reading timestamps without a format string Rows e1 and e2 look nothing alike as text, yet they name the same instant, and e3 lands 250 milliseconds later. Compared as strings, all 3 differ; compared after parsing, e1 equals e2. Calendar, week and ordinal dates The first 2 labels name the same Monday, and the week form files it under 2025. That filing rule is the one ISO week numbers follow. ISO weeks and the year they belong to Writing ISO text

  5. DATE_FORMAT() (concepts: Date Formatting)

    Formatting a pay statement Sort by the value, show the text Format after converting the zone