What You'll Master Here
what happened, when it happened, when you saw it, and which payload details need to become columns.
This chapter is about the kind of tables you meet once data comes from a real product: a stream of events (a user viewed something, checked out, asked for a refund), each stamped with a time and often carrying a little bundle of extra details in JSON.
Two things make these tables tricky. First, an event has two times — when it happened, and when your system actually received it — and those can be days apart. Second, the extra details are not neat columns; they are flexible, nested data you have to reach into. This chapter is about handling both safely.
The live labs run on SQLite, and the text points out where bigger databases (Postgres, BigQuery, Snowflake) spell things differently. The wording changes; the idea does not.
Separate four things: what happened, when it happened, when you saw it, and which payload details need to become columns.
Event tables sit behind product dashboards, marketing reports, and alerts. A small mistake with a timestamp or a JSON field does not throw an error — it just quietly shifts the numbers, which is far harder to catch.
- event_time
- When the user or system action actually happened.
- ingested_at
- When your pipeline received the event (which can be later than when it happened).
- JSON
- A text way of storing data as name:value pairs, e.g. {"device":"ios","campaign":"winter"}. It can nest and hold lists.
- semi-structured
- Data that is not neat rows and columns. It has some structure (named fields, like JSON) but stays flexible and can be nested.
- payload
- The bundle of extra details, usually JSON, attached to an event.
- late-arriving data
- A record that reaches your pipeline after the report for its event_time has already been counted.
| event_id | user_id | event_name | event_time | ingested_at |
|---|---|---|---|---|
| 1 | 1 | view | 2026-01-01 03:30 | 2026-01-01 03:31 |
| 2 | 1 | checkout | 2026-01-01 04:10 | 2026-01-01 04:12 |
| 3 | 2 | view | 2026-01-01 22:15 | 2026-01-02 08:00 |
| 4 | 2 | checkout | 2026-01-02 00:30 | 2026-01-02 00:31 |
| 5 | 3 | view | 2026-01-02 05:00 | 2026-01-04 09:00 |
| 6 | 1 | refund | 2026-01-03 18:00 | 2026-01-03 18:05 |
Events 3 and 5 were ingested a day or more after they happened — that lag is the whole subject of this chapter.
select
event_id,
event_name,
event_time,
ingested_at
from events
order by event_time, ingested_at;| event_id | event_name | event_time | ingested_at |
|---|---|---|---|
| 1 | view | 2026-01-01 03:30 | 2026-01-01 03:31 |
| 2 | checkout | 2026-01-01 04:10 | 2026-01-01 04:12 |
| 3 | view | 2026-01-01 22:15 | 2026-01-02 08:00 |
| 4 | checkout | 2026-01-02 00:30 | 2026-01-02 00:31 |
| 5 | view | 2026-01-02 05:00 | 2026-01-04 09:00 |
| 6 | refund | 2026-01-03 18:00 | 2026-01-03 18:05 |
Six events across three users. Keep this stream in mind — every example below queries it.
Treating event time, ingestion time, and processing time as the same time. Late data, timezone boundaries, and backfills become impossible to reason about.
Name which timestamp you are using.
Order events deterministically.
Decide how late-arriving data should be handled.
Most event bugs are not syntax bugs. They are semantic bugs about time, ordering, payload shape, or whether a record arrived too late for its reporting window.
Use the topic menu as a checklist. Each topic is an event-data habit you should be able to demonstrate on the stream above.
Event SQL is trustworthy when time semantics and payload extraction are visible in the query.
