N048-M3 Tier 4 · Advanced · medium analytics · Streamhub

Return one row per session that contains at least one event, showing the user's ID, name, the session ID, and the total event count for that session

Part of LATERAL Joins in SQL

The problem

Streamhub's product analytics team is measuring user engagement at the session level.

Write a query to return one row per session that contains at least one event, showing the user's ID, name, the session ID, and the total event count for that session.

Assumptions:

  • A session's event count is the number of events linked to that session. Sessions with zero events do not appear, and users with no qualifying sessions also do not appear.

Output:

  • One row per session with at least one event, with columns user_id, name, session_id, and event_count.
Schema · analytics 5 tables
users
id integer
name text
email text
country text
plan text
signed_up_at timestamptz
is_active boolean
conversions
id integer
user_id integer
converted_at timestamptz
plan text
amount numeric
sessions
id integer
user_id integer
started_at timestamptz
ended_at? timestamptz
event_count integer
events
id integer
user_id integer
session_id? integer
event_type text
occurred_at timestamptz
properties? jsonb
periods
id integer
name text
start_month integer
end_month integer

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution Try it yourself first
Solution query
SELECT
  u.id AS user_id,
  u.name,
  session_data.session_id,
  session_data.event_count
FROM
  users u
  CROSS JOIN LATERAL (
    SELECT
      s.id AS session_id,
      COUNT(*) AS event_count
    FROM
      sessions s
      JOIN events e ON e.session_id = s.id
    WHERE
      s.user_id = u.id
    GROUP BY
      s.id
  ) session_data

The shape

The lateral runs per user and groups that user's sessions by session id, returning one row per session that contains at least one event. The outer user row is duplicated once per qualifying session, which is the per-session engagement shape the analytics team needs. Users with no qualifying sessions return zero rows from the lateral and CROSS JOIN LATERAL drops them, which is what the prompt asks for.

Clause by clause

  • SELECT u.id AS user_id, u.name, session_data.session_id, session_data.event_count returns the user's identity from the outer table and the per-session result from the lateral.
  • FROM users u is the driving table; the lateral is evaluated once per user.
  • CROSS JOIN LATERAL ( ... ) session_data is the lateral. Inside it, FROM sessions s JOIN events e ON e.session_id = s.id pairs each session with its events, WHERE s.user_id = u.id is the correlated filter that scopes the work to the outer user, GROUP BY s.id collapses to one row per session, and the inner SELECT returns the session id and its event count. The inner join drops sessions with zero events, and users with no qualifying sessions get an empty lateral and disappear under the CROSS JOIN.

Why this and not a three-table flat join

FROM users u JOIN sessions s ON ... JOIN events e ON ... GROUP BY u.id, u.name, s.id returns the same rows on this data and is a valid answer. The lateral form maps more cleanly to the question the analytics team is asking: "for each user, summarise each of their sessions." It also scales: if the inner work later needs the most recent event per session via ORDER BY ... LIMIT 1 or a per-session CASE rollup, the lateral already has the per-row scope to express it without restructuring the outer query.

The trap

The u.id reference inside the lateral is the load-bearing detail. PostgreSQL only allows it because of the LATERAL keyword; remove LATERAL and the planner refuses the query because a FROM item cannot reference a sibling FROM item's columns. Whenever the inner subquery needs to use an outer row's value, LATERAL is the keyword that grants that access.

You practiced CROSS JOIN LATERAL over a multi-table per-user breakdown — the lateral returns one row per qualifying session, multiplying the outer user record once per matching session.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.

Practice, feedback, mastery, review. That's the loop that turns reading into real skill.

Start free

No account, no credit card. Start solving in under a minute.