cswork / libraryOpen practice workspace

Coding questions · Meta

Write SQL and Python for data prep

This question evaluates data engineering competencies including SQL analytics with window functions and MERGE/INSERT patterns for idempotent incremental jobs, event-time handling (duplicates and late events), sessionization and retention metrics, plus Python-based JSON parsing, normalization, partitioned Parquet output and basic unit testing.

Given clickstream events (user_id, event_type, ts, properties) and a users table (user_id, signup_ts, plan), write SQL to compute DAU/WAU/MAU, D1/W1 retention cohorts, and sessionized metrics; implement an incremental daily job that updates aggregates idempotently using window functions and MERGE/INSERT patterns; and diagnose/handle duplicates and late events. Then, using Python (no heavy frameworks), implement a data-cleaning script that parses semi-structured JSON in the properties column, normalizes nested fields, and writes partitioned Parquet outputs with basic unit tests.

Compute DAU, WAU, and MAU from clickstream events

You are given a clickstream_events table with user-level events and a users table with signup information. Using these tables, write a SQL query to compute, for each calendar day from 2025-05-18 to 2025-05-24 (inclusive), the number of distinct active users for: (1) that day (DAU), (2) the trailing 7-day window ending on that day (WAU), and (3) the trailing 30-day window ending on that day (MAU). A user is considered active on a day if they have at least one event whose ts falls on that calendar date. Return one row per activity_date with columns activity_date, dau, wau_7d, and mau_30d.

Table users

columntypeconstraints
user_idINTPRIMARY KEY
signup_tsTIMESTAMPNOT NULL
planVARCHAR(20)NOT NULL

Sample data:

plansignup_tsuser_id
free2025-05-01 10:00:001
pro2025-05-02 12:00:002
free2025-05-08 09:30:003
pro2025-05-15 15:45:004
free2025-05-20 11:15:005

Table clickstream_events

columntypeconstraints
event_idBIGINTPRIMARY KEY
user_idINTNOT NULL
event_typeVARCHAR(50)NOT NULL
tsTIMESTAMPNOT NULL
ingested_atTIMESTAMPNOT NULL
propertiesVARCHAR(1000)

Sample data:

event_idevent_typeingested_atpropertiestsuser_id
1page_view2025-05-01 10:06:00{}2025-05-01 10:05:001
2login2025-05-18 09:00:05{}2025-05-18 09:00:001
3page_view2025-05-18 09:05:10{}2025-05-18 09:05:001
4click2025-05-19 10:00:05{}2025-05-19 10:00:001
5page_view2025-05-22 11:00:03{}2025-05-22 11:00:001
6signup_complete2025-05-02 12:30:10{}2025-05-02 12:30:002
7page_view2025-05-18 14:00:06{}2025-05-18 14:00:002
8page_view2025-05-18 14:01:00{}2025-05-18 14:00:002
9click2025-05-20 15:00:04{}2025-05-20 15:00:002
10page_view2025-05-23 16:00:01{}2025-05-23 16:00:002
11page_view2025-05-08 09:45:03{}2025-05-08 09:45:003
12click2025-05-21 13:00:02{}2025-05-21 13:00:003
13page_view2025-05-21 13:30:05{}2025-05-21 13:30:003
14page_view2025-05-15 16:00:02{}2025-05-15 16:00:004
15click2025-05-19 17:00:02{}2025-05-19 17:00:004
16login2025-05-24 18:00:01{}2025-05-24 18:00:004
17page_view2025-05-20 11:30:02{}2025-05-20 11:30:005
18click2025-05-20 11:45:02{}2025-05-20 11:45:005
19page_view2025-05-25 12:00:01{}2025-05-25 12:00:005
20click2025-05-20 08:00:00{}2025-05-18 09:02:001
21page_view2025-05-03 09:00:03{}2025-05-03 09:00:002
22page_view2025-05-21 10:00:03{}2025-05-21 10:00:005

Expected output

One row per calendar day from 2025-05-18 to 2025-05-24 with distinct active user counts for that day (dau), the trailing 7-day window (wau_7d), and the trailing 30-day window (mau_30d).

activity_datedaumau_30dwau_7d
2025-05-18243
2025-05-19243
2025-05-20254
2025-05-21255
2025-05-22155
2025-05-23155
2025-05-24155

Hints

  1. Derive a calendar date from ts and work at the (user_id, activity_date) grain first.
  2. Use conditional COUNT(DISTINCT ...) over date ranges to compute DAU/WAU/MAU.

Compute D1 and W1 retention cohorts

Using the users and clickstream_events tables, compute Day-1 (D1) and Week-1 (W1) retention for signup cohorts in May 2025. Define cohort_date as CAST(signup_ts AS DATE). For each cohort_date in 2025-05-01 through 2025-05-31, return: (1) the number of users who signed up on that date (signups), (2) the number who had at least one event on cohort_date + 1 day (d1_retained_users), (3) the D1 retention rate (d1_retained_users / signups), (4) the number who had at least one event between cohort_date + 1 day and cohort_date + 7 days inclusive (w1_retained_users), and (5) the W1 retention rate (w1_retained_users / signups). Return one row per cohort_date.

Table users

columntypeconstraints
user_idINTPRIMARY KEY
signup_tsTIMESTAMPNOT NULL
planVARCHAR(20)NOT NULL

Sample data:

plansignup_tsuser_id
free2025-05-01 10:00:001
pro2025-05-02 12:00:002
free2025-05-08 09:30:003
pro2025-05-15 15:45:004
free2025-05-20 11:15:005

Table clickstream_events

columntypeconstraints
event_idBIGINTPRIMARY KEY
user_idINTNOT NULL
event_typeVARCHAR(50)NOT NULL
tsTIMESTAMPNOT NULL
ingested_atTIMESTAMPNOT NULL
propertiesVARCHAR(1000)

Sample data:

event_idevent_typeingested_atpropertiestsuser_id
1page_view2025-05-01 10:06:00{}2025-05-01 10:05:001
2login2025-05-18 09:00:05{}2025-05-18 09:00:001
3page_view2025-05-18 09:05:10{}2025-05-18 09:05:001
4click2025-05-19 10:00:05{}2025-05-19 10:00:001
5page_view2025-05-22 11:00:03{}2025-05-22 11:00:001
6signup_complete2025-05-02 12:30:10{}2025-05-02 12:30:002
7page_view2025-05-18 14:00:06{}2025-05-18 14:00:002
8page_view2025-05-18 14:01:00{}2025-05-18 14:00:002
9click2025-05-20 15:00:04{}2025-05-20 15:00:002
10page_view2025-05-23 16:00:01{}2025-05-23 16:00:002
11page_view2025-05-08 09:45:03{}2025-05-08 09:45:003
12click2025-05-21 13:00:02{}2025-05-21 13:00:003
13page_view2025-05-21 13:30:05{}2025-05-21 13:30:003
14page_view2025-05-15 16:00:02{}2025-05-15 16:00:004
15click2025-05-19 17:00:02{}2025-05-19 17:00:004
16login2025-05-24 18:00:01{}2025-05-24 18:00:004
17page_view2025-05-20 11:30:02{}2025-05-20 11:30:005
18click2025-05-20 11:45:02{}2025-05-20 11:45:005
19page_view2025-05-25 12:00:01{}2025-05-25 12:00:005
20click2025-05-20 08:00:00{}2025-05-18 09:02:001
21page_view2025-05-03 09:00:03{}2025-05-03 09:00:002
22page_view2025-05-21 10:00:03{}2025-05-21 10:00:005

Expected output

One row per signup cohort_date in May 2025 with counts and rates for D1 and W1 retention.

cohort_dated1_retained_usersd1_retention_ratesignupsw1_retained_usersw1_retention_rate
2025-05-0100100
2025-05-0211111
2025-05-0800100
2025-05-1500111
2025-05-2011111

Hints

  1. First map each user to a cohort_date based on signup_ts.
  2. Join cohorts to events and use conditional COUNT(DISTINCT ...) over date ranges for D1 and W1.

Sessionize clickstream events with a 30-minute inactivity threshold

Using the clickstream_events table, define user sessions over the inclusive date range from 2025-05-18 through 2025-05-21. Consider only events whose ts falls on one of those dates.

For each user_id, sort events by ts. Start a new session when the gap from the previous event for that user is strictly greater than 30 minutes; otherwise keep the event in the same session. Session IDs should start at 1 for each user and increase chronologically.

Return one row per session with:

  • user_id
  • session_id
  • session_start formatted as YYYY-MM-DD HH24:MI:SS
  • session_end formatted as YYYY-MM-DD HH24:MI:SS
  • event_count
  • session_duration_minutes as the integer minute difference between session end and start

Order the output by user_id, then session_id.

Table clickstream_events

columntypeconstraints
event_idBIGINTPRIMARY KEY
user_idINTNOT NULL
event_typeVARCHAR(50)NOT NULL
tsTIMESTAMPNOT NULL
ingested_atTIMESTAMPNOT NULL
propertiesVARCHAR(1000)

Sample data:

event_idevent_typeingested_atpropertiestsuser_id
1page_view2025-05-01 10:06:00{}2025-05-01 10:05:001
2login2025-05-18 09:00:05{}2025-05-18 09:00:001
20click2025-05-20 08:00:00{}2025-05-18 09:02:001
3page_view2025-05-18 09:05:10{}2025-05-18 09:05:001
4click2025-05-19 10:00:05{}2025-05-19 10:00:001
6signup_complete2025-05-02 12:30:10{}2025-05-02 12:30:002
7page_view2025-05-18 14:00:06{}2025-05-18 14:00:002
8page_view2025-05-18 14:01:00{}2025-05-18 14:00:002
9click2025-05-20 15:00:04{}2025-05-20 15:00:002
11page_view2025-05-08 09:45:03{}2025-05-08 09:45:003
12click2025-05-21 13:00:02{}2025-05-21 13:00:003
13page_view2025-05-21 13:30:05{}2025-05-21 13:30:003
14page_view2025-05-15 16:00:02{}2025-05-15 16:00:004
15click2025-05-19 17:00:02{}2025-05-19 17:00:004
17page_view2025-05-20 11:30:02{}2025-05-20 11:30:005
18click2025-05-20 11:45:02{}2025-05-20 11:45:005
22page_view2025-05-21 10:00:03{}2025-05-21 10:00:005

Expected output

One row per inferred session between 2025-05-18 and 2025-05-21, per user, with start/end timestamps, event counts, and durations in minutes. Session IDs restart from 1 for each user.

event_countsession_duration_minutessession_endsession_idsession_startuser_id
352025-05-18 09:05:0012025-05-18 09:00:001
102025-05-19 10:00:0022025-05-19 10:00:001
202025-05-18 14:00:0012025-05-18 14:00:002
102025-05-20 15:00:0022025-05-20 15:00:002
2302025-05-21 13:30:0012025-05-21 13:00:003
102025-05-19 17:00:0012025-05-19 17:00:004
2152025-05-20 11:45:0012025-05-20 11:30:005
102025-05-21 10:00:0022025-05-21 10:00:005

Hints

  1. Use LAG(ts) over each user's chronological events to detect inactivity gaps.
  2. A running SUM of the new-session flag is a compact way to assign session IDs per user.
  3. Use TO_CHAR for timestamp display and EXTRACT(EPOCH FROM interval) / 60 for duration in minutes.

Incremental, idempotent DAU aggregation with deduplication and late-event handling

You maintain a daily DAU aggregate table, daily_user_activity, derived from raw clickstream events in clickstream_events. The raw feed has two data-quality issues you must handle:

  • Duplicates: the same logical event can be ingested more than once. A duplicate is any group sharing the same (user_id, event_type, ts); the rows differ only in event_id, ingested_at, and possibly properties. Keep only the row with the earliest ingested_at in each such group, and discard the rest.
  • Late events: an event's ingested_at can land on a day later than its activity day ts (e.g. event_id = 20 has ts on 2025-05-18 but ingested_at on 2025-05-20). Because of this you must recompute affected days from scratch rather than trust the previously stored DAU.

Assume today is 2025-06-01. Your incremental job recomputes DAU for activity_date in the inclusive window 2025-05-18 through 2025-05-21 using all events currently available, then upserts the recomputed values into daily_user_activity (idempotently: rows in that window are overwritten, any date in the window not yet present would be inserted). DAU for a day is the number of distinct user_ids whose deduplicated ts falls on that calendar day.

Write a single PostgreSQL SELECT that returns the full contents of daily_user_activity as it would look immediately after this upsert runs. Specifically:

  • For each activity_date in 2025-05-18 .. 2025-05-21, return the freshly recomputed dau and a last_updated_at of 2025-06-01 00:00:00 (the recompute timestamp).
  • Any daily_user_activity row whose activity_date is outside the recompute window must be passed through unchanged (in this dataset there are none, but your query must not drop them).

Return exactly the columns activity_date, dau, last_updated_at, one row per activity_date, sorted by activity_date ascending.

Table clickstream_events

columntypeconstraints
event_idBIGINTPRIMARY KEY
user_idINTNOT NULL
event_typeVARCHAR(50)NOT NULL
tsTIMESTAMPNOT NULL
ingested_atTIMESTAMPNOT NULL
propertiesVARCHAR(1000)

Sample data:

event_idevent_typeingested_atpropertiestsuser_id
1page_view2025-05-01 10:06:00{}2025-05-01 10:05:001
2login2025-05-18 09:00:05{}2025-05-18 09:00:001
3page_view2025-05-18 09:05:10{}2025-05-18 09:05:001
4click2025-05-19 10:00:05{}2025-05-19 10:00:001
5page_view2025-05-22 11:00:03{}2025-05-22 11:00:001
6signup_complete2025-05-02 12:30:10{}2025-05-02 12:30:002
7page_view2025-05-18 14:00:06{}2025-05-18 14:00:002
8page_view2025-05-18 14:01:00{}2025-05-18 14:00:002
9click2025-05-20 15:00:04{}2025-05-20 15:00:002
10page_view2025-05-23 16:00:01{}2025-05-23 16:00:002
11page_view2025-05-08 09:45:03{}2025-05-08 09:45:003
12click2025-05-21 13:00:02{}2025-05-21 13:00:003
13page_view2025-05-21 13:30:05{}2025-05-21 13:30:003
14page_view2025-05-15 16:00:02{}2025-05-15 16:00:004
15click2025-05-19 17:00:02{}2025-05-19 17:00:004
16login2025-05-24 18:00:01{}2025-05-24 18:00:004
17page_view2025-05-20 11:30:02{}2025-05-20 11:30:005
18click2025-05-20 11:45:02{}2025-05-20 11:45:005
19page_view2025-05-25 12:00:01{}2025-05-25 12:00:005
20click2025-05-20 08:00:00{}2025-05-18 09:02:001
21page_view2025-05-03 09:00:03{}2025-05-03 09:00:002
22page_view2025-05-21 10:00:03{}2025-05-21 10:00:005

Table daily_user_activity

columntypeconstraints
activity_dateDATEPRIMARY KEY
dauINTNOT NULL
last_updated_atTIMESTAMPNOT NULL

Sample data:

activity_datedaulast_updated_at
2025-05-1812025-05-19 00:00:00
2025-05-1922025-05-20 00:00:00
2025-05-2012025-05-21 00:00:00
2025-05-2112025-05-22 00:00:00

Expected output

Final contents of daily_user_activity after the 2025-06-01 idempotent recompute of activity_date 2025-05-18..2025-05-21: deduplicated DAU per day with last_updated_at set to 2025-06-01 00:00:00, ordered by activity_date.

activity_datedaulast_updated_at
2025-05-1822025-06-01T00:00:00
2025-05-1922025-06-01T00:00:00
2025-05-2022025-06-01T00:00:00
2025-05-2122025-06-01T00:00:00

Hints

  1. Deduplicate first: ROW_NUMBER() OVER (PARTITION BY user_id, event_type, ts ORDER BY ingested_at) and keep rn = 1 to retain the earliest-ingested copy.
  2. Bucket events by the activity day CAST(ts AS DATE) — not ingested_at — so late-arriving events land on the correct day; then COUNT(DISTINCT user_id) per day.
  3. Model the upsert without MERGE: UNION ALL the recomputed window rows with the pre-existing rows whose date is outside the window, then ORDER BY activity_date.