Học SQL Qua Challenges

SQL Project: Reactivation Campaign Analysis (07 Tasks) – Task 01

List 07 Tasks trong Project Reactivation Campaign Analysis -> Task 01Task 02 – Task 03 – Task 04 – Task 05 – Task 06 – Task 07

Các Task này mô phỏng lại 1 Case trong thực tế, các bạn có thể sử dụng để áp dụng vào công việc cũng như làm Portfolio Project dùng để phỏng vấn vị trí Data Analyst.

Task 01: Xác Định Dormant Users

Giả sử bạn đang là 1 Data Analyst cho một ứng dụng tập gym có 500.000 người dùng. Nhiều người dùng đã ngừng sử dụng app sau một thời gian. Đội Marketing thiết kế chiến dịch gửi 3 email liên tiếp để kéo họ quay lại.

Trước khi gửi email, bạn cần xác định những người dùng nào đã ngừng hoạt động từ 14 ngày trở lên. Đây là bước đầu tiên và quan trọng nhất của chiến dịch Reactivation – nếu xác định sai đối tượng, toàn bộ chiến dịch sẽ sai.

Bạn hãy viết câu truy vấn tìm tất cả người dùng đã không đăng nhập hoặc hoàn thành bất kỳ bài tập nào trong vòng 14 ngày gần nhất. Loại trừ những người đã hủy tài khoản hoặc đã bị chặn (banned).

WITH LastActivity AS (
    SELECT 
        u.user_id, -- lay user_id tu bang users
        u.full_name, -- lay full_name tu bang users
        u.email, -- lay email tu bang users
        u.city, -- lay city tu bang users
        u.subscription_type, -- lay subscription_type tu bang users
        u.account_status, -- lay account_status tu bang users
        u.signup_date, -- lay signup_date tu bang users
        ll.last_login_at, -- lay last_login_at tu bang user_login_logs
        lw.last_workout_at, -- lay last_workout_at tu bang workout_completions
        -- xac dinh ngay hoat dong cuoi cung giua dang nhap va tap luyen
        CASE 
            -- neu ca 2 deu null => lay signup_date
            WHEN ll.last_login_at IS NULL AND lw.last_workout_at IS NULL THEN u.signup_date
            -- neu login >= workout (hoac workout null) => lay login
            WHEN ll.last_login_at >= ISNULL(lw.last_workout_at, '1900-01-01') THEN ll.last_login_at
            -- nguoc lai (workout > login) => lay workout
            ELSE lw.last_workout_at
        END AS last_activity_at
    FROM users u
    -- lay ngay dang nhap cuoi cung cua moi nguoi dung
    -- dung group by de gom nhom theo user_id va lay ngay dang nhap lon nhat trong moi nhom
    LEFT JOIN (SELECT user_id, MAX(login_at) as last_login_at FROM user_login_logs GROUP BY user_id) ll 
        ON u.user_id = ll.user_id
    -- lay ngay tap luyen cuoi cung cua moi nguoi dung
    -- dung group by de gom nhom theo user_id va lay ngay tap luyen lon nhat trong moi nhom
    LEFT JOIN (SELECT user_id, MAX(completed_at) as last_workout_at FROM workout_completions GROUP BY user_id) lw 
        ON u.user_id = lw.user_id
    -- dung left join de lay tat ca nguoi dung, ke ca nhung nguoi chua dang nhap hay tap luyen lan nao
    -- cu phap: not in (list) -> lay cac gia tri khong thuoc list
    WHERE u.account_status NOT IN ('deleted', 'banned') -- loai bo nguoi dung da bi xoa hoac cam
)
SELECT 
    user_id,
    full_name,
    email,
    city,
    subscription_type,
    account_status,
    signup_date,
    last_login_at,
    last_workout_at,
    last_activity_at,
    -- tinh so ngay khong hoat dong bang cach tinh hieu giua ngay hien tai va ngay hoat dong cuoi cung
    -- cu phap: datediff(day, ngay_hoat_dong, getdate()) -> lay so ngay tu ngay_hoat_dong den ngay hien tai
    DATEDIFF(DAY, last_activity_at, GETDATE()) AS days_inactive,
    CASE
        -- phan loai muc do dormancy dua tren so ngay khong hoat dong
        -- cu phap: dateadd(day, -so_ngay, getdate()) -> lay ngay hien tai tru di so_ngay
        WHEN last_activity_at <= DATEADD(DAY, -61, GETDATE()) THEN 'severe' -- neu khong hoat dong hon 60 ngay => severe
        WHEN last_activity_at <= DATEADD(DAY, -31, GETDATE()) THEN 'moderate' -- neu khong hoat dong hon 30 ngay => moderate
        ELSE 'mild' -- nguoc lai => mild
    END AS dormancy_level
FROM LastActivity
WHERE last_activity_at <= DATEADD(DAY, -14, GETDATE())
ORDER BY last_activity_at DESC;

Output Mẫu

user_idfull_nameemailcitysubscription_typeaccount_statussignup_datelast_login_atlast_workout_atlast_activity_atdays_inactivedormancy_level
5Hoang Van Emem.hoang@gmail.comHo Chi Minhfreeactive2026-01-092026-02-07 23:01:52.9702026-02-08 00:01:52.9832026-02-08 00:01:52.98315mild
3Le Van Cuongcuong.le@gmail.comDa Nangpremium_yearlyactive2025-10-262026-02-03 06:01:52.9702026-02-03 07:01:52.9832026-02-03 07:01:52.98320mild
12Mai Thi Ngocngoc.mai@gmail.comHa Noifreeactive2026-02-032026-02-03 00:00:00.00020mild
7Dang Van Giagia.dang@gmail.comHa Noifreeactive2025-09-262026-01-24 07:01:52.9702026-01-24 08:01:52.9832026-01-24 08:01:52.98330mild
4Pham Thi Dungdung.pham@gmail.comHa Noifreeactive2025-12-252026-01-14 01:01:52.9702026-01-14 02:01:52.9832026-01-14 02:01:52.98340moderate
8Bui Thi Hanhhanh.bui@gmail.comDa Nangpremium_monthlyactive2025-12-052026-01-09 02:01:52.9702026-01-09 03:01:52.9832026-01-09 03:01:52.98345moderate
6Vo Thi Phuongphuong.vo@gmail.comCan Thopremium_monthlyactive2025-11-152025-12-25 04:01:52.9702025-12-25 05:01:52.9832025-12-25 05:01:52.98360moderate
11Truong Van Lamlam.truong@gmail.comHo Chi Minhfreeinactive2025-11-252025-11-25 00:00:00.00090severe
CộtÝ nghĩa
user_idMã định danh duy nhất của người dùng
full_nameHọ tên đầy đủ
emailĐịa chỉ email để gửi chiến dịch
cityThành phố, có thể dùng để phân nhóm theo vùng miền
subscription_typeLoại gói (free/premium) – giúp ưu tiên người premium
account_statusTrạng thái tài khoản – đã lọc bỏ deleted/banned
signup_dateNgày đăng ký – dùng làm fallback khi không có hoạt động nào
last_login_atLần đăng nhập cuối – NULL nếu chưa bao giờ đăng nhập
last_workout_atLần tập cuối – NULL nếu chưa bao giờ tập
last_activity_atNgày hoạt động gần nhất (MAX của login và workout, hoặc signup_date)
days_inactiveSố ngày không hoạt động – chỉ số cốt lõi để xác định dormant
dormancy_levelPhân loại mild (14-30 ngày)/moderate (31-60 ngày)/severe (>60 ngày) – quyết định chiến lược email

Kiến Thức Viết Query Tối Ưu – Thứ Mà Chỉ Khi Đi Làm Bạn Mới Biết

Cách viết 01:

WHERE DATEDIFF(DAY, last_activity_at, GETDATE()) >= 14
  • Cơ chế: Áp dụng hàm DATEDIFF lên cột last_activity_at.
  • Hệ quả: SQL Server buộc phải thực hiện tính toán trên từng dòng một (row-by-row) trước khi nó có thể quyết định dòng đó có thỏa mãn điều kiện hay không.
  • Kế hoạch thực thi: Nó khiến Query Optimizer không thể sử dụng thứ tự sắp xếp sẵn có của Index (B-Tree). Do đó, nó buộc phải chọn Index Scan hoặc Table Scan. Đây là thao tác rất tốn I/O và CPU khi dữ liệu lớn.

Cách viết 02: SARGable

WHERE last_activity_at <= DATEADD(DAY, -14, GETDATE())
  • Cơ chế: Cột last_activity_at đứng ở bên trái toán tử so sánh. Bên phải là một biểu thức hằng số (DATEADD chỉ tính 1 lần khi query bắt đầu chạy).
  • Hệ quả: SQL Server có thể sử dụng thống kê (statistics) và cấu trúc của Index để biết chính xác dữ liệu nằm ở đâu.
  • Kế hoạch thực thi: Nó có thể chọn Index Seek. Thay vì quét hết, nó nhảy thẳng đến đoạn dữ liệu cần thiết và chỉ đọc những dòng thỏa mãn.

-> Thay đổi từ DATEDIFF sang DATEADD trong mệnh đề WHERE giúp query của bạn tối ưu hơn.

Tới đây, bạn đã hoàn thành xong Task 01 trong Project Reactivation Campaign Analysis. Hãy bước tiếp sang Task 02.

Nếu bạn muốn học thực chiến SQL qua Challenges và Cases thì có thể đăng ký khóa học SQL Data Analyst Thực Chiến 100+ Challenges hoặc Data Analyst BootCamp. Inbox mình Tại Đây