Checklist (high level)- Validate unique user identifiers and possible alternative keys- Identify duplicate user_ids and merge rules- Detect timezone coverage and inconsistent timestamp formats- Measure event lateness distribution (ingest_time − event_time)- Decide lateness window for retention (e.g., 7 days)- Apply transformations before cohort aggregation and keep audit logsStrategies & SQL/ETL snippets1) Deduplicate users (resolve multiple accounts → canonical_id)- Rule: prefer verified email, latest activity, or highest trust scoresql
-- create canonical user mapping: choose latest active profile
WITH ranked AS (
SELECT user_id, email, last_active_at,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY last_active_at DESC) rn
FROM users_raw
)
SELECT user_id AS canonical_user_id, email
INTO user_canonical
FROM ranked WHERE rn = 1;
Join events to canonical_user before cohorts.2) Normalize timestamps & timezones- Store all times in UTC; keep original_tz field when available- Convert at ingest using tz offset or IANA tz databasesql
-- convert string timestamp with timezone info to UTC (Postgres)
SELECT event_id,
(event_time AT TIME ZONE event_tz) AT TIME ZONE 'UTC' AS event_utc
FROM events_raw;
If only local_time + country, map country→tz using lookup table.3) Detect & handle late-arriving events- Measure lateness and choose cutoff (e.g., 7 days). Backfill policy: update retention when late events within cutoff arrive; beyond cutoff, annotate but exclude from primary retentions.sql
-- lateness distribution
SELECT DATE_TRUNC('day', event_utc) AS event_day,
DATE_TRUNC('day', ingest_utc) - DATE_TRUNC('day', event_utc) AS delay_days,
COUNT(*) FROM events GROUP BY 1,2 ORDER BY 1,2;
ETL approach:- Use staging with watermark: accept events where ingest_time <= current_time - watermark for cohort builds.- Keep incremental backfill jobs that reprocess cohorts for recent N days.4) Cohort build example (after cleaning)sql
WITH events_clean AS (
SELECT e.*, c.canonical_user_id,
event_utc::date AS event_date
FROM events_raw e
JOIN user_canonical c USING (user_id)
WHERE e.event_utc IS NOT NULL
)
-- first event per user = cohort_day
, first_event AS (
SELECT canonical_user_id, MIN(event_date) AS cohort_date
FROM events_clean
GROUP BY canonical_user_id
)
SELECT f.cohort_date,
e.event_date,
COUNT(DISTINCT e.canonical_user_id) AS users
FROM first_event f
JOIN events_clean e
ON e.canonical_user_id = f.canonical_user_id
AND e.event_date BETWEEN f.cohort_date AND f.cohort_date + INTERVAL '30 day'
GROUP BY 1,2;
Other practical notes- Keep provenance: store raw vs cleaned, transformation versions- Monitor with data quality checks: duplicate rates, percent missing tz, lateness percent- Communicate assumptions (dedupe rules, lateness window) to stakeholders and document backfill behavior.