Skip to content

🧠 Case Study Buổi 5: SQL — SELECT, JOIN & Aggregation

Bài học thực tế từ các nền tảng công nghệ hàng đầu thế giới và Việt Nam

Tổng quan

Trong buổi học này, chúng ta đã tìm hiểu về relational database, viết SELECT/WHERE để lọc dữ liệu, các loại JOIN (INNER, LEFT, RIGHT, FULL) để kết nối bảng, GROUP BY + aggregate functions (COUNT, SUM, AVG, MIN, MAX) để tổng hợp, HAVING để lọc kết quả aggregation, và subquery. Lý thuyết cho thấy SQL là ngôn ngữ chung của Data — bất kỳ DA nào cũng phải thành thạo. Nhưng chỉ khi nhìn vào thực tế, bạn mới thấy SQL không chỉ là "viết query" — mà là cách các công ty công nghệ hàng đầu ra quyết định kinh doanh trị giá hàng tỷ đô la mỗi ngày.

Ba case study dưới đây minh họa cách SQL được áp dụng ở 3 bối cảnh khác nhau — từ nền tảng ride-hailing toàn cầu (Uber), super app Đông Nam Á (Grab), đến sàn thương mại điện tử hàng đầu Việt Nam (Tiki). Mỗi case study sẽ trả lời 3 câu hỏi cốt lõi: (1) Dữ liệu gì được lưu trong database? (2) SQL query nào được viết để phân tích? (3) Quyết định kinh doanh nào đã thay đổi nhờ kết quả query?

#Công tyVấn đềKỹ thuật SQL chính
1UberQuery hàng tỷ trip records để phân tích supply/demandSELECT, WHERE, GROUP BY, aggregate functions, subquery
2GrabJOIN data riders + drivers + payments cho analysis đa chiềuINNER JOIN, LEFT JOIN, multi-table JOIN, GROUP BY + HAVING
3TikiPhân tích đơn hàng bằng SQL: GMV, conversion rate, return rateSELECT + CASE, JOIN, GROUP BY, subquery, aggregate functions

Case Study 1: Uber — SQL query hàng tỷ trip records để phân tích supply/demand

🏷️ Thông tin

Tiêu chíChi tiết
Công tyUber Technologies, Inc.
NgànhRide-hailing / Mobility — Gọi xe công nghệ
Quy mô150+ triệu monthly active users, 6+ triệu drivers, ~28 triệu chuyến xe/ngày, revenue ~$40 tỷ USD (2025)
Thị trường70+ quốc gia, 10,000+ thành phố toàn cầu
Chủ đề DA liên quanRelational database, SELECT/WHERE, GROUP BY, aggregate functions (COUNT, SUM, AVG), subquery, supply/demand analysis

📋 Bối cảnh

Uber là nền tảng gọi xe công nghệ lớn nhất thế giới, vận hành trên 70+ quốc gia với hơn 28 triệu chuyến xe mỗi ngày. Toàn bộ dữ liệu hoạt động — từ yêu cầu gọi xe (trip request), chuyến xe thực tế (completed trip), vị trí GPS, giá cước, đánh giá — đều được lưu trong relational database quy mô cực lớn (petabyte-scale).

Bộ phận Marketplace Analytics tại Uber chịu trách nhiệm phân tích cung (supply — số driver online) và cầu (demand — số trip request từ riders) để tối ưu hóa trải nghiệm người dùng. Vấn đề cốt lõi của Uber là cân bằng supply/demand theo thời gian thực: nếu demand vượt supply → rider phải chờ lâu, trải nghiệm kém, chuyển sang đối thủ (Lyft, Bolt); nếu supply vượt demand → driver không có khách, thu nhập thấp, rời nền tảng.

Data Analyst team sử dụng SQL hàng ngày để query dữ liệu trip, phân tích pattern, và cung cấp insight cho team Product và Operations ra quyết định. Trong công ty công nghệ như Uber, SQL không phải "công cụ phụ" — nó là ngôn ngữ giao tiếp chính giữa DA với database chứa hàng tỷ records.

⚡ Thách thức

  1. Quy mô dữ liệu khổng lồ: ~28 triệu chuyến/ngày × 365 ngày = ~10 tỷ trip records/năm. Mỗi trip record có 30+ cột (pickup location, dropoff location, timestamps, fare, surge pricing, driver info, rider info, payment...). Không thể mở bằng Excel — chỉ có SQL mới truy vấn được.
  2. Phân tích supply/demand theo thời gian và địa điểm: Demand ở Manhattan lúc 8AM khác hoàn toàn với Brooklyn lúc 2AM. Cần GROUP BY theo nhiều chiều (city, zone, hour, day_of_week) đồng thời để thấy pattern chính xác.
  3. Surge pricing optimization: Khi demand > supply, Uber áp dụng surge pricing (hệ số nhân giá) để khuyến khích thêm driver online. Nhưng surge quá cao → rider không đặt xe → demand giảm giả tạo. Cần SQL query để phân tích mối quan hệ giữa surge multiplier và completion rate.
  4. Phát hiện anomaly nhanh: Một sự kiện lớn (concert, football match, severe weather) có thể tăng demand 500% trong 30 phút tại 1 khu vực nhỏ. Cần query phát hiện anomaly real-time để trigger surge pricing hoặc re-route driver từ vùng thừa sang vùng thiếu.
  5. Chất lượng dữ liệu: Trips bị cancel, GPS lỗi, duplicate records, driver fraud (fake trips) — cần WHERE conditions chính xác để lọc data sạch trước khi phân tích.

🛠️ Giải pháp

Dữ liệu gì? (Database schema)

┌──────────────────┐       ┌──────────────────────┐       ┌──────────────────┐
│      riders      │       │        trips          │       │     drivers      │
├──────────────────┤       ├──────────────────────┤       ├──────────────────┤
│ rider_id (PK)    │───┐   │ trip_id (PK)         │   ┌───│ driver_id (PK)   │
│ rider_name       │   └──>│ rider_id (FK)        │   │   │ driver_name      │
│ email            │       │ driver_id (FK)       │<──┘   │ vehicle_type     │
│ signup_date      │       │ request_time         │       │ city             │
│ city             │       │ pickup_time          │       │ signup_date      │
│ rider_rating     │       │ dropoff_time         │       │ driver_rating    │
└──────────────────┘       │ pickup_lat/lng       │       │ status           │
                           │ dropoff_lat/lng      │       └──────────────────┘
                           │ trip_status          │
                           │ fare_amount          │
                           │ surge_multiplier     │
                           │ city                 │
                           │ zone_id              │
                           └──────────────────────┘
  • trips: ~28 triệu records/ngày — bảng chính cho mọi phân tích
  • riders: ~150 triệu records — thông tin người đặt xe
  • drivers: ~6 triệu records — thông tin tài xế
  • zones: zone_id, zone_name, city, geofence — phân vùng địa lý
  • surge_history: zone_id, timestamp, surge_multiplier, demand_count, supply_count — lịch sử surge

Phân tích bằng SQL query nào?

Query 1: Tổng quan hoạt động ngày — SELECT + WHERE + Aggregate Functions

sql
-- Tổng quan hoạt động trong 1 ngày tại TP.HCM
-- Đây là query đầu tiên mọi DA viết mỗi sáng (daily health check)

SELECT
    DATE(request_time) AS trip_date,
    COUNT(*) AS total_requests,
    COUNT(CASE WHEN trip_status = 'completed' THEN 1 END) AS completed_trips,
    COUNT(CASE WHEN trip_status = 'cancelled_rider' THEN 1 END) AS rider_cancels,
    COUNT(CASE WHEN trip_status = 'cancelled_driver' THEN 1 END) AS driver_cancels,
    ROUND(
        COUNT(CASE WHEN trip_status = 'completed' THEN 1 END) * 100.0
        / COUNT(*), 2
    ) AS completion_rate_pct,
    ROUND(AVG(fare_amount), 2) AS avg_fare,
    ROUND(SUM(fare_amount), 2) AS total_revenue
FROM trips
WHERE city = 'HCMC'
  AND DATE(request_time) = '2026-02-17'
  AND trip_status != 'test'          -- Loại bỏ test trips
GROUP BY DATE(request_time);

Kết quả ví dụ:

trip_datetotal_requestscompleted_tripsrider_cancelsdriver_cancelscompletion_rate_pctavg_faretotal_revenue
2026-02-17185,000142,45028,35014,20077.0065,2009,288,490,000

Query 2: Phân tích supply/demand theo giờ — GROUP BY + ORDER BY

sql
-- Phân tích demand (requests) và supply (available drivers) theo từng giờ
-- Mục tiêu: tìm giờ nào demand vượt supply → cần surge pricing hoặc thêm driver

SELECT
    EXTRACT(HOUR FROM request_time) AS hour_of_day,
    COUNT(*) AS total_requests,
    COUNT(CASE WHEN trip_status = 'completed' THEN 1 END) AS completed,
    COUNT(CASE WHEN trip_status = 'no_driver' THEN 1 END) AS no_driver_available,
    ROUND(AVG(surge_multiplier), 2) AS avg_surge,
    ROUND(AVG(
        EXTRACT(EPOCH FROM (pickup_time - request_time)) / 60.0
    ), 1) AS avg_wait_minutes
FROM trips
WHERE city = 'HCMC'
  AND DATE(request_time) = '2026-02-17'
  AND trip_status IN ('completed', 'cancelled_rider', 'cancelled_driver', 'no_driver')
GROUP BY EXTRACT(HOUR FROM request_time)
ORDER BY hour_of_day;

Kết quả ví dụ (trích):

hour_of_daytotal_requestscompletedno_driver_availableavg_surgeavg_wait_minutes
715,20011,8001,8501.86.2
818,50013,2003,1002.38.5
1212,80010,9008001.13.2
1716,90012,1002,7002.17.8
228,2007,5002501.02.5

Insight: Giờ 7–8AM và 5–6PM (rush hour) có tỷ lệ no_driver_available cao nhất (~17%), avg_surge lên 2.0–2.3x, thời gian chờ 7–8 phút. Giờ trưa và đêm khuya ổn định. Operations team dùng insight này để chạy campaign khuyến khích driver online sớm (bonus 6:30–8:30AM).

Query 3: Surge pricing effectiveness — Subquery + Aggregate

sql
-- Phân tích: Surge pricing có thực sự tăng completion rate không?
-- So sánh completion rate ở các mức surge khác nhau

SELECT
    surge_bracket,
    total_requests,
    completed_trips,
    ROUND(completed_trips * 100.0 / total_requests, 2) AS completion_rate_pct,
    ROUND(avg_fare, 0) AS avg_fare_vnd,
    ROUND(avg_wait, 1) AS avg_wait_minutes
FROM (
    SELECT
        CASE
            WHEN surge_multiplier = 1.0 THEN '1.0x (No surge)'
            WHEN surge_multiplier BETWEEN 1.1 AND 1.5 THEN '1.1–1.5x (Low)'
            WHEN surge_multiplier BETWEEN 1.6 AND 2.0 THEN '1.6–2.0x (Medium)'
            WHEN surge_multiplier BETWEEN 2.1 AND 3.0 THEN '2.1–3.0x (High)'
            ELSE '3.0x+ (Extreme)'
        END AS surge_bracket,
        COUNT(*) AS total_requests,
        COUNT(CASE WHEN trip_status = 'completed' THEN 1 END) AS completed_trips,
        AVG(fare_amount) AS avg_fare,
        AVG(EXTRACT(EPOCH FROM (pickup_time - request_time)) / 60.0) AS avg_wait
    FROM trips
    WHERE city = 'HCMC'
      AND DATE(request_time) BETWEEN '2026-02-01' AND '2026-02-17'
    GROUP BY
        CASE
            WHEN surge_multiplier = 1.0 THEN '1.0x (No surge)'
            WHEN surge_multiplier BETWEEN 1.1 AND 1.5 THEN '1.1–1.5x (Low)'
            WHEN surge_multiplier BETWEEN 1.6 AND 2.0 THEN '1.6–2.0x (Medium)'
            WHEN surge_multiplier BETWEEN 2.1 AND 3.0 THEN '2.1–3.0x (High)'
            ELSE '3.0x+ (Extreme)'
        END
) AS surge_analysis
ORDER BY surge_bracket;

Kết quả ví dụ:

surge_brackettotal_requestscompleted_tripscompletion_rate_pctavg_fare_vndavg_wait_minutes
1.0x (No surge)1,850,0001,517,00082.0052,0003.1
1.1–1.5x (Low)620,000489,80079.0068,5004.2
1.6–2.0x (Medium)310,000226,30073.0095,2005.8
2.1–3.0x (High)95,00055,10058.00138,0006.5
3.0x+ (Extreme)25,0008,75035.00195,0007.2

Insight: Completion rate giảm dần khi surge tăng — đặc biệt giảm mạnh ở mức >2.0x (rider từ chối đặt xe vì quá đắt). Surge "sweet spot" là 1.1–1.5x — driver có incentive để online thêm, rider vẫn chấp nhận trả thêm. Trên 2.0x bắt đầu phản tác dụng. Uber dùng insight này để cap surge multiplier tại 2.0x ở một số thị trường.

Query 4: Peak demand zones — GROUP BY + HAVING + ORDER BY

sql
-- Tìm top 10 zones có demand cao nhất nhưng supply thấp
-- (supply gap lớn nhất → cần ưu tiên dispatch driver)

SELECT
    z.zone_name,
    z.city,
    COUNT(*) AS total_requests,
    COUNT(CASE WHEN t.trip_status = 'completed' THEN 1 END) AS completed,
    COUNT(CASE WHEN t.trip_status = 'no_driver' THEN 1 END) AS unserved,
    ROUND(
        COUNT(CASE WHEN t.trip_status = 'no_driver' THEN 1 END) * 100.0
        / COUNT(*), 2
    ) AS unserved_rate_pct
FROM trips t
JOIN zones z ON t.zone_id = z.zone_id
WHERE DATE(t.request_time) = '2026-02-17'
  AND t.city = 'HCMC'
GROUP BY z.zone_name, z.city
HAVING COUNT(*) >= 500                         -- Chỉ zones có demand đủ lớn
   AND COUNT(CASE WHEN t.trip_status = 'no_driver' THEN 1 END) * 100.0
       / COUNT(*) > 15                          -- Unserved rate > 15%
ORDER BY unserved_rate_pct DESC
LIMIT 10;

Insight: Quận 1, Quận 7 (Phú Mỹ Hưng), và Thủ Đức (khu công nghệ cao) là 3 zones có unserved rate >20% vào giờ cao điểm. Operations team phân bổ thêm driver incentive ($2 bonus/trip) tại các zones này vào khung giờ 7–9AM.

📊 Kết quả

Chỉ sốTrước (phân tích ad-hoc, chậm)Sau (SQL queries chuẩn hóa)
Thời gian daily health check2–3 giờ (export CSV → Excel)10 phút (chạy SQL dashboard query)
Phát hiện supply gap theo zoneCuối tuần (weekly report)Real-time (query chạy mỗi 15 phút)
Surge pricing optimizationDựa rule cứng (demand > X → surge Y)Data-driven: cap surge 2.0x → completion rate tăng 12%
Rider wait time (avg)6.5 phút4.2 phút (nhờ re-distribute driver đúng zone)
Driver idle time (avg)18 phút giữa chuyến11 phút (nhờ dispatch chính xác hơn)
Revenue impact+$2.8 triệu/tháng tại thị trường HCMC (nhờ giảm unserved trips)

SQL queries giúp Uber Marketplace Analytics team biến 28 triệu chuyến xe/ngày thành insight có thể hành động — từ cân bằng supply/demand, tối ưu surge pricing, đến phát hiện anomaly theo vùng. Mọi quyết định về pricing, driver incentive, và product feature đều bắt đầu từ một câu SQL query.

💡 Bài học cho Data Analyst Việt Nam

  1. SELECT + WHERE + Aggregate là "bộ ba quyền lực" cho mọi phân tích. Uber hàng ngày chỉ dùng 4 hàm: COUNT, SUM, AVG, ROUND — kết hợp với CASE WHEN để tạo conditional counting. Không cần hàm phức tạp — chỉ cần tổ hợp đúng.
  2. GROUP BY đa chiều = Pivot Table trên steroid. GROUP BY theo hour, zone, surge_bracket cho phép nhìn dữ liệu từ nhiều góc mà Pivot Table Excel không thể xử lý ở quy mô tỷ dòng.
  3. HAVING khác WHERE — hiểu đúng mới query đúng. WHERE lọc dòng TRƯỚC khi aggregate, HAVING lọc nhóm SAU khi aggregate. Query tìm zones có unserved_rate > 15% bắt buộc dùng HAVING — viết WHERE sẽ lỗi.
  4. Subquery giúp "tính trước, phân tích sau". Thay vì viết 1 query khổng lồ, tách thành inner query (tính toán) và outer query (format/lọc) — dễ đọc, dễ maintain, dễ debug.

Case Study 2: Grab — JOIN data riders + drivers + payments cho phân tích đa chiều

🏷️ Thông tin

Tiêu chíChi tiết
Công tyGrab Holdings Inc.
NgànhSuper App — Ride-hailing, Food Delivery, Payments, Financial Services
Quy mô35+ triệu monthly active users (Đông Nam Á), 13 triệu driver-partners, revenue ~$2.7 tỷ USD (2025)
Thị trường8 quốc gia Đông Nam Á: Singapore, Malaysia, Indonesia, Philippines, Thailand, Vietnam, Cambodia, Myanmar
Chủ đề DA liên quanJOIN (INNER, LEFT, RIGHT), multi-table JOIN, GROUP BY + HAVING, aggregate functions, phân tích cross-service

📋 Bối cảnh

Grab là "super app" lớn nhất Đông Nam Á, cung cấp dịch vụ gọi xe (GrabCar, GrabBike), giao đồ ăn (GrabFood), giao hàng (GrabExpress), thanh toán (GrabPay), và tài chính (GrabFin). Tại Việt Nam, Grab chiếm ~60% thị phần ride-hailing và là nền tảng giao đồ ăn phổ biến nhất.

Khác với Uber (chủ yếu ride-hailing), Grab vận hành đa dịch vụ trên cùng 1 nền tảng — nghĩa là 1 rider có thể vừa đặt xe, vừa đặt đồ ăn, vừa thanh toán hóa đơn, tất cả trong 1 app. Điều này tạo ra một relational database cực kỳ phức tạp với hàng chục bảng liên kết với nhau:

  • Bảng riders liên kết với bảng trips (ride-hailing) VÀ bảng food_orders (food delivery)
  • Bảng drivers liên kết với tripsfood_deliveries
  • Bảng payments liên kết với trips, food_orders, VÀ grabpay_transactions

Bộ phận Regional Analytics tại Grab Vietnam cần JOIN nhiều bảng để trả lời câu hỏi kinh doanh xuyên service: "Rider nào dùng cả GrabCar và GrabFood? Driver nào vừa chở khách vừa giao đồ ăn có thu nhập tốt hơn? Phương thức thanh toán nào phổ biến nhất theo dịch vụ?" — Những câu hỏi mà mỗi bảng đơn lẻ không thể trả lời được.

⚡ Thách thức

  1. Dữ liệu phân tán nhiều bảng: Thông tin 1 chuyến xe nằm ở ≥4 bảng: trips (chi tiết chuyến), riders (thông tin khách), drivers (thông tin tài xế), payments (thanh toán). Phải JOIN 4 bảng mới có bức tranh toàn cảnh — viết sai JOIN sẽ cho kết quả duplicate hoặc thiếu dữ liệu.
  2. LEFT JOIN vs INNER JOIN — chọn sai = insight sai: Nếu dùng INNER JOIN giữa tripspayments, sẽ mất các trips chưa thanh toán (payment pending). Nếu dùng LEFT JOIN, giữ lại đầy đủ nhưng phải xử lý NULL values. Mỗi business question yêu cầu loại JOIN khác nhau.
  3. Cross-service analysis (phân tích xuyên dịch vụ): Một rider có thể có records ở bảng trips (GrabCar) VÀ food_orders (GrabFood). Cần JOIN 2 bảng này qua rider_id để phân tích hành vi đa dịch vụ — nhưng schema khác nhau (trip có pickup/dropoff, food order có restaurant/delivery address).
  4. Duplicate records khi JOIN nhiều bảng: Nếu 1 trip có 2 payment records (split payment), JOIN sẽ tạo 2 dòng cho 1 trip → SUM(fare) bị nhân đôi. Cần hiểu cardinality (1-to-1, 1-to-many, many-to-many) trước khi JOIN.
  5. Performance với dữ liệu lớn: Grab Vietnam xử lý ~5 triệu rides + ~3 triệu food orders/ngày. JOIN 4 bảng × hàng triệu records = query có thể chạy hàng giờ nếu không tối ưu (WHERE conditions, indexing).

🛠️ Giải pháp

Dữ liệu gì? (Database schema)

┌────────────────┐     ┌──────────────────────┐     ┌────────────────┐
│     riders     │     │        trips          │     │    drivers     │
├────────────────┤     ├──────────────────────┤     ├────────────────┤
│ rider_id (PK)  │──┐  │ trip_id (PK)         │  ┌──│ driver_id (PK) │
│ rider_name     │  └─>│ rider_id (FK)        │  │  │ driver_name    │
│ phone          │     │ driver_id (FK)       │<─┘  │ vehicle_type   │
│ city           │     │ service_type         │     │ city           │
│ signup_date    │     │ request_time         │     │ total_trips    │
│ tier (Silver/  │     │ pickup_time          │     │ driver_rating  │
│  Gold/Platinum)│     │ dropoff_time         │     │ status         │
└────────────────┘     │ trip_status          │     └────────────────┘
                       │ fare_amount          │
        ┌──────────────│ payment_id (FK)      │     ┌────────────────────┐
        │              │ city                 │     │    food_orders     │
        │              └──────────────────────┘     ├────────────────────┤
        │                                           │ order_id (PK)      │
        v                                           │ rider_id (FK)      │
┌────────────────────┐                              │ driver_id (FK)     │
│     payments       │                              │ restaurant_id (FK) │
├────────────────────┤                              │ order_time         │
│ payment_id (PK)    │                              │ delivery_time      │
│ trip_id (FK)       │                              │ total_amount       │
│ payment_method     │                              │ order_status       │
│ amount             │                              │ payment_id (FK)    │
│ currency           │                              │ city               │
│ payment_status     │                              └────────────────────┘
│ transaction_time   │
└────────────────────┘

Phân tích bằng SQL query nào?

Query 1: INNER JOIN — Phân tích chuyến xe với thông tin rider + driver

sql
-- Mục tiêu: Xem chi tiết mỗi chuyến xe kèm thông tin rider và driver
-- INNER JOIN: chỉ lấy trips có CẢ rider VÀ driver (loại trips lỗi không có driver)

SELECT
    t.trip_id,
    t.request_time,
    t.service_type,
    r.rider_name,
    r.city AS rider_city,
    r.tier AS rider_tier,
    d.driver_name,
    d.vehicle_type,
    d.driver_rating,
    t.fare_amount,
    t.trip_status
FROM trips t
INNER JOIN riders r ON t.rider_id = r.rider_id
INNER JOIN drivers d ON t.driver_id = d.driver_id
WHERE t.city = 'Hanoi'
  AND DATE(t.request_time) = '2026-02-17'
  AND t.trip_status = 'completed'
ORDER BY t.request_time DESC
LIMIT 100;

Tại sao INNER JOIN? Vì chỉ quan tâm trips hoàn thành (có cả rider lẫn driver). Trips bị lỗi hệ thống (thiếu driver_id) tự động bị loại — đúng ý đồ phân tích.

Query 2: LEFT JOIN — Tìm riders không có chuyến xe trong 30 ngày (churn risk)

sql
-- Mục tiêu: Tìm riders đã đăng ký nhưng KHÔNG đặt xe 30 ngày qua → churn risk
-- LEFT JOIN: giữ TẤT CẢ riders, kể cả those không có trips → trips columns = NULL

SELECT
    r.rider_id,
    r.rider_name,
    r.city,
    r.tier,
    r.signup_date,
    COUNT(t.trip_id) AS trips_last_30d,
    MAX(t.request_time) AS last_trip_date,
    CURRENT_DATE - DATE(MAX(t.request_time)) AS days_since_last_trip
FROM riders r
LEFT JOIN trips t
    ON r.rider_id = t.rider_id
    AND t.request_time >= CURRENT_DATE - INTERVAL '30 days'
    AND t.trip_status = 'completed'
WHERE r.city = 'HCMC'
  AND r.signup_date <= CURRENT_DATE - INTERVAL '60 days'   -- Đã đăng ký > 60 ngày
GROUP BY r.rider_id, r.rider_name, r.city, r.tier, r.signup_date
HAVING COUNT(t.trip_id) = 0                                -- Không có trip nào 30 ngày qua
ORDER BY r.signup_date;

Tại sao LEFT JOIN, không phải INNER JOIN? Nếu dùng INNER JOIN, riders không có trips sẽ biến mất — nhưng đó chính là nhóm cần tìm! LEFT JOIN giữ lại tất cả riders, trips columns trả về NULL khi không có match → HAVING COUNT(t.trip_id) = 0 lọc đúng nhóm inactive.

Insight: Query phát hiện ~180,000 riders inactive 30 ngày+ tại HCMC. Marketing team gửi push notification "Ưu đãi 30% cho chuyến đi tiếp theo" → win-back rate 12% (~21,600 riders quay lại).

Query 3: Multi-table JOIN — Phân tích revenue theo payment method + rider tier

sql
-- Mục tiêu: Revenue breakdown theo phương thức thanh toán VÀ hạng rider
-- JOIN 3 bảng: trips + riders + payments

SELECT
    r.tier AS rider_tier,
    p.payment_method,
    COUNT(t.trip_id) AS total_trips,
    ROUND(SUM(p.amount), 0) AS total_revenue,
    ROUND(AVG(p.amount), 0) AS avg_trip_value,
    ROUND(
        SUM(p.amount) * 100.0 / SUM(SUM(p.amount)) OVER (), 2
    ) AS revenue_share_pct
FROM trips t
INNER JOIN riders r ON t.rider_id = r.rider_id
INNER JOIN payments p ON t.payment_id = p.payment_id
WHERE t.city = 'HCMC'
  AND DATE(t.request_time) BETWEEN '2026-02-01' AND '2026-02-17'
  AND t.trip_status = 'completed'
  AND p.payment_status = 'success'
GROUP BY r.tier, p.payment_method
ORDER BY r.tier, total_revenue DESC;

Kết quả ví dụ:

rider_tierpayment_methodtotal_tripstotal_revenueavg_trip_valuerevenue_share_pct
PlatinumGrabPay85,20012,780,000,000150,00018.50
PlatinumCredit Card42,1007,578,000,000180,00010.97
GoldGrabPay156,30014,067,000,00090,00020.36
GoldCash98,7007,896,000,00080,00011.43
SilverCash245,60012,280,000,00050,00017.78
SilverGrabPay134,2008,052,000,00060,00011.66

Insight: Platinum riders trả trung bình 150K–180K/trip (trips dài, service cao cấp) — chiếm 29.5% revenue dù chỉ chiếm ~8% riders. GrabPay là payment method #1 ở mọi tier. Silver riders vẫn dùng Cash 50% → cơ hội convert sang GrabPay bằng cashback campaign.

Query 4: Cross-service analysis — Riders dùng cả GrabCar và GrabFood

sql
-- Mục tiêu: Tìm riders dùng CẢ ride-hailing VÀ food delivery
-- Đây là "super users" có LTV (Lifetime Value) cao nhất

SELECT
    r.rider_id,
    r.rider_name,
    r.tier,
    ride_stats.total_rides,
    ride_stats.ride_revenue,
    food_stats.total_food_orders,
    food_stats.food_revenue,
    (ride_stats.ride_revenue + food_stats.food_revenue) AS combined_revenue
FROM riders r
INNER JOIN (
    -- Subquery: thống kê ride-hailing per rider
    SELECT
        rider_id,
        COUNT(*) AS total_rides,
        SUM(fare_amount) AS ride_revenue
    FROM trips
    WHERE trip_status = 'completed'
      AND DATE(request_time) BETWEEN '2026-01-01' AND '2026-02-17'
    GROUP BY rider_id
) ride_stats ON r.rider_id = ride_stats.rider_id
INNER JOIN (
    -- Subquery: thống kê food delivery per rider
    SELECT
        rider_id,
        COUNT(*) AS total_food_orders,
        SUM(total_amount) AS food_revenue
    FROM food_orders
    WHERE order_status = 'delivered'
      AND DATE(order_time) BETWEEN '2026-01-01' AND '2026-02-17'
    GROUP BY rider_id
) food_stats ON r.rider_id = food_stats.rider_id
WHERE r.city = 'HCMC'
ORDER BY combined_revenue DESC
LIMIT 20;

Insight: ~22% riders HCMC dùng cả 2 dịch vụ (ride + food). Nhóm này có combined revenue trung bình gấp 3.2x so với nhóm chỉ dùng 1 dịch vụ. Grab chạy campaign "Order GrabFood, get 15% off GrabCar" để cross-sell — tăng tỷ lệ multi-service users thêm 8%.

Query 5: Driver performance — HAVING lọc drivers underperform

sql
-- Mục tiêu: Tìm drivers có rating thấp + completion rate thấp → cần coaching
-- HAVING lọc SAU khi đã aggregate (GROUP BY)

SELECT
    d.driver_id,
    d.driver_name,
    d.vehicle_type,
    d.city,
    COUNT(t.trip_id) AS total_trips,
    COUNT(CASE WHEN t.trip_status = 'completed' THEN 1 END) AS completed,
    ROUND(
        COUNT(CASE WHEN t.trip_status = 'completed' THEN 1 END) * 100.0
        / COUNT(t.trip_id), 2
    ) AS completion_rate,
    ROUND(AVG(d.driver_rating), 2) AS avg_rating,
    SUM(t.fare_amount) AS total_earnings
FROM drivers d
INNER JOIN trips t ON d.driver_id = t.driver_id
WHERE d.city = 'HCMC'
  AND DATE(t.request_time) BETWEEN '2026-02-01' AND '2026-02-17'
GROUP BY d.driver_id, d.driver_name, d.vehicle_type, d.city
HAVING COUNT(t.trip_id) >= 50                              -- Ít nhất 50 trips (đủ data)
   AND (
       COUNT(CASE WHEN t.trip_status = 'completed' THEN 1 END) * 100.0
       / COUNT(t.trip_id) < 70                             -- Completion rate < 70%
       OR AVG(d.driver_rating) < 4.0                       -- Rating < 4.0
   )
ORDER BY completion_rate ASC;

Insight: Query phát hiện ~1,200 drivers (3% tổng) có completion rate <70% hoặc rating <4.0. Operations team phân loại: 800 drivers cần training (navigation, customer service), 400 drivers có dấu hiệu fraud (cancel liên tục lấy cancellation fee) → chuyển team Trust & Safety xử lý.

📊 Kết quả

Chỉ sốTrước (single-table analysis)Sau (multi-table JOIN analysis)
Thời gian phân tích cross-serviceKhông thể (data tách rời)15 phút (1 SQL query)
Phát hiện churn risk ridersCuối tháng (monthly report)Hàng tuần (LEFT JOIN query tự động)
Win-back campaign effectiveness5% (spray-and-pray)12% (target đúng nhóm inactive 30 ngày+)
Cross-sell rate (ride ↔ food)16% riders dùng multi-service24% (+8% nhờ targeted campaign)
Phát hiện driver underperformCuối tháng (manual review)Hàng tuần (HAVING query tự động)
Revenue từ Platinum ridersChưa đo được (thiếu JOIN)29.5% revenue từ 8% riders → tập trung chăm sóc
Chuyển đổi Cash → GrabPay35% dùng GrabPay48% (+13% nhờ cashback campaign target đúng tier)

JOIN nhiều bảng cho phép Grab Vietnam nhìn toàn cảnh hành vi người dùng xuyên dịch vụ — điều mà từng bảng đơn lẻ không thể làm được. Mỗi loại JOIN (INNER, LEFT) phục vụ business question khác nhau, và sự lựa chọn đúng JOIN type quyết định chất lượng insight.

💡 Bài học cho Data Analyst Việt Nam

  1. Mỗi loại JOIN có "câu hỏi kinh doanh" riêng. INNER JOIN trả lời "Ai đã làm X?" (completed trips). LEFT JOIN trả lời "Ai CHƯA làm X?" (inactive riders). Trước khi viết JOIN, hãy hỏi: "Tôi CÓ muốn giữ records không match không?" — Có → LEFT JOIN, Không → INNER JOIN.
  2. Multi-table JOIN = phân tích đa chiều. Join riders + trips + payments cho phép nhìn 1 chuyến xe từ 3 góc: khách hàng là ai (riders), chuyến xe như thế nào (trips), thanh toán ra sao (payments). Càng nhiều chiều, insight càng sâu — nhưng cũng phức tạp hơn, cần hiểu rõ cardinality.
  3. Subquery + JOIN = phân tích cross-service. Khi cần so sánh hành vi trên 2 service khác nhau (ride vs food), dùng subquery tính toán riêng rồi JOIN kết quả — cấu trúc rõ ràng, tránh duplicate, dễ debug.
  4. HAVING là WHERE cho kết quả GROUP BY. Nhiều DA mới nhầm lẫn WHERE và HAVING. Quy tắc: WHERE lọc dòng gốc (TRƯỚC GROUP BY), HAVING lọc nhóm (SAU GROUP BY). Query tìm driver có completion_rate < 70% bắt buộc dùng HAVING vì completion_rate tính từ COUNT — một aggregate function.

Case Study 3: Tiki — Phân tích đơn hàng bằng SQL: GMV, conversion rate, return rate

🏷️ Thông tin

Tiêu chíChi tiết
Công tyTiki Corporation (thuộc Tiki Global)
NgànhE-commerce — Thương mại điện tử
Quy mô~20 triệu monthly active users, ~100,000 sellers, ~15 triệu SKUs, top 2 sàn TMĐT Việt Nam
Thị trườngViệt Nam — 63 tỉnh thành, tập trung TP.HCM và Hà Nội
Chủ đề DA liên quanSELECT + CASE, JOIN, GROUP BY, subquery, aggregate functions, GMV analysis, conversion funnel, return rate

📋 Bối cảnh

Tiki là một trong những sàn thương mại điện tử hàng đầu Việt Nam, nổi tiếng với chương trình TikiNOW (giao hàng trong 2 giờ) và mô hình hybrid: vừa tự vận hành kho hàng (1P — first-party, như Amazon), vừa cho phép sellers đăng bán (3P — third-party, như marketplace). Mỗi ngày, Tiki xử lý hàng trăm nghìn đơn hàng, từ lúc khách hàng browse sản phẩm → thêm vào giỏ hàng → đặt mua → thanh toán → giao hàng → nhận hàng (hoặc trả hàng).

Bộ phận Business Intelligence tại Tiki liên tục phân tích dữ liệu đơn hàng để trả lời các câu hỏi kinh doanh cốt lõi:

  • GMV (Gross Merchandise Value) — Tổng giá trị hàng hoá giao dịch trên sàn là bao nhiêu? Tăng hay giảm so với tháng trước?
  • Conversion Rate — Bao nhiêu % người xem sản phẩm thực sự mua hàng? Đâu là bottleneck trong funnel?
  • Return Rate — Bao nhiêu % đơn hàng bị trả lại? Lý do chính là gì? Category nào có return rate cao nhất?

Các câu hỏi này yêu cầu kết hợp nhiều kỹ thuật SQL: SELECT với CASE WHEN cho conditional logic, JOIN để liên kết đơn hàng với sản phẩm và khách hàng, GROUP BY + aggregate để tổng hợp theo nhiều chiều, và subquery để tính các metric phức tạp như conversion rate.

⚡ Thách thức

  1. GMV phải loại bỏ "noise": GMV thô (tổng giá trị đơn đặt) khác GMV thực (sau khi trừ đơn hủy, đơn trả, đơn fraud). Nếu báo cáo GMV thô cho CEO, con số sẽ inflate 15–20% → quyết định sai. Cần CASE WHEN + WHERE chính xác để tính Net GMV.
  2. Conversion funnel nhiều bước: Từ page_view → add_to_cart → checkout → payment → completed → delivered — mỗi bước có drop-off. Dữ liệu nằm ở nhiều bảng khác nhau (events, carts, orders, deliveries). Cần JOIN + subquery để tính conversion rate giữa các bước.
  3. Return rate phải drill-down theo category: Return rate trung bình 8% — nhưng Fashion (quần áo) có thể 18% (sai size), Electronics 3%, Books 1%. Phân tích tổng hợp sẽ che giấu vấn đề thực sự. Cần GROUP BY category + HAVING để highlight outlier.
  4. Seller performance analysis: 100,000 sellers có chất lượng khác nhau — seller tốt vs seller nhiều return/complaint. Cần JOIN orders + sellers + returns để đánh giá và xếp hạng seller.
  5. Seasonality và campaign impact: Doanh thu ngày 11/11, 12/12 (mega sale) có thể gấp 10x ngày thường. Phân tích phải tách "organic growth" vs "campaign-driven" — cần SQL điều kiện phức tạp.

🛠️ Giải pháp

Dữ liệu gì? (Database schema)

┌──────────────────┐     ┌──────────────────────┐     ┌──────────────────┐
│   customers      │     │       orders          │     │    products      │
├──────────────────┤     ├──────────────────────┤     ├──────────────────┤
│ customer_id (PK) │──┐  │ order_id (PK)        │  ┌──│ product_id (PK)  │
│ customer_name    │  └─>│ customer_id (FK)     │  │  │ product_name     │
│ email            │     │ order_date           │  │  │ category         │
│ city             │     │ total_amount         │  │  │ subcategory      │
│ signup_date      │     │ order_status         │  │  │ seller_id (FK)   │
│ customer_tier    │     │ payment_method       │  │  │ price            │
└──────────────────┘     │ shipping_fee         │  │  │ cost_price       │
                         │ discount             │  │  └──────────────────┘
                         │ delivery_date        │  │
┌──────────────────┐     │ is_tikinow           │  │  ┌──────────────────┐
│    sellers       │     └──────────────────────┘  │  │   order_items    │
├──────────────────┤                               │  ├──────────────────┤
│ seller_id (PK)   │     ┌──────────────────────┐  │  │ item_id (PK)     │
│ seller_name      │     │      returns         │  │  │ order_id (FK)    │
│ seller_type      │     ├──────────────────────┤  └──│ product_id (FK)  │
│  (1P/3P)         │     │ return_id (PK)       │     │ quantity         │
│ join_date        │     │ order_id (FK)        │     │ unit_price       │
│ seller_rating    │     │ return_date          │     │ subtotal         │
└──────────────────┘     │ return_reason        │     └──────────────────┘
                         │ return_status        │
                         │ refund_amount        │
                         └──────────────────────┘

Phân tích bằng SQL query nào?

Query 1: GMV Analysis — SELECT + CASE WHEN + Aggregate

sql
-- Tính GMV theo nhiều cấp độ:
-- Gross GMV (tổng đơn đặt), Net GMV (trừ hủy + trả), Actual Revenue (trừ discount + shipping)

SELECT
    DATE_TRUNC('month', o.order_date) AS month,

    -- Gross GMV: tổng giá trị đơn hàng (kể cả hủy, trả)
    SUM(o.total_amount) AS gross_gmv,

    -- Net GMV: chỉ tính đơn completed + delivered
    SUM(CASE
        WHEN o.order_status IN ('completed', 'delivered')
        THEN o.total_amount ELSE 0
    END) AS net_gmv,

    -- Cancelled GMV: giá trị đơn hủy
    SUM(CASE
        WHEN o.order_status = 'cancelled'
        THEN o.total_amount ELSE 0
    END) AS cancelled_gmv,

    -- Returned GMV: giá trị đơn trả
    SUM(CASE
        WHEN o.order_status = 'returned'
        THEN o.total_amount ELSE 0
    END) AS returned_gmv,

    -- Actual Revenue (Net GMV - Discount)
    SUM(CASE
        WHEN o.order_status IN ('completed', 'delivered')
        THEN o.total_amount - o.discount ELSE 0
    END) AS actual_revenue,

    -- Số đơn hàng
    COUNT(DISTINCT o.order_id) AS total_orders,
    COUNT(DISTINCT CASE
        WHEN o.order_status IN ('completed', 'delivered')
        THEN o.order_id
    END) AS successful_orders,

    -- AOV (Average Order Value) — chỉ tính đơn thành công
    ROUND(
        SUM(CASE WHEN o.order_status IN ('completed', 'delivered')
            THEN o.total_amount ELSE 0 END)
        / NULLIF(COUNT(DISTINCT CASE
            WHEN o.order_status IN ('completed', 'delivered')
            THEN o.order_id END), 0)
    , 0) AS aov

FROM orders o
WHERE o.order_date BETWEEN '2025-09-01' AND '2026-02-17'
GROUP BY DATE_TRUNC('month', o.order_date)
ORDER BY month;

Kết quả ví dụ:

monthgross_gmvnet_gmvcancelled_gmvreturned_gmvactual_revenuetotal_orderssuccessful_ordersaov
2025-09850B714B85B51B642B2,850,0002,394,000298,000
2025-10920B773B92B55B696B3,050,0002,562,000302,000
2025-111,580B1,375B127B78B1,238B5,200,0004,524,000304,000
2025-121,420B1,207B142B71B1,086B4,680,0003,978,000303,000
2026-011,050B882B105B63B794B3,500,0002,940,000300,000

Insight: Tháng 11 (11/11 mega sale) GMV tăng gấp 1.7x bình thường, nhưng cancelled_gmv cũng tăng (impulse buying rồi hủy). Net GMV / Gross GMV = ~84% — nghĩa là 16% giá trị đơn bị "bay hơi" do cancel + return. Bài học: không bao giờ báo cáo Gross GMV cho CEO — luôn dùng Net GMV.

Query 2: GMV breakdown theo category — JOIN + GROUP BY

sql
-- GMV phân tích theo category sản phẩm
-- JOIN orders + order_items + products để có thông tin category

SELECT
    p.category,
    COUNT(DISTINCT o.order_id) AS total_orders,
    SUM(oi.subtotal) AS category_gmv,
    ROUND(
        SUM(oi.subtotal) * 100.0 / SUM(SUM(oi.subtotal)) OVER (), 2
    ) AS gmv_share_pct,
    ROUND(AVG(oi.subtotal), 0) AS avg_item_value,
    COUNT(DISTINCT o.customer_id) AS unique_customers
FROM orders o
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
WHERE o.order_status IN ('completed', 'delivered')
  AND o.order_date BETWEEN '2026-01-01' AND '2026-02-17'
GROUP BY p.category
ORDER BY category_gmv DESC;

Kết quả ví dụ:

categorytotal_orderscategory_gmvgmv_share_pctavg_item_valueunique_customers
Electronics420,000315B32.50750,000280,000
Fashion580,000174B17.95300,000410,000
Home & Living310,000155B15.99500,000220,000
Books & Stationery650,00097.5B10.06150,000520,000
Beauty & Health290,00087B8.98300,000195,000
Mom & Baby180,00072B7.43400,000130,000
Others270,00068.7B7.09255,000190,000

Insight: Electronics chiếm 32.5% GMV nhưng chỉ 420K orders (avg value 750K/item). Books & Stationery có nhiều orders nhất (650K) nhưng GMV thấp (avg 150K) — volume driver, not value driver. Fashion có nhiều unique customers nhất (410K) — acquisition driver. Mỗi category có vai trò khác nhau trong chiến lược.

Query 3: Conversion Funnel — Subquery + Aggregate

sql
-- Phân tích conversion funnel: view → cart → order → payment → delivered
-- Mỗi bước là 1 subquery, rồi combine kết quả

SELECT
    funnel_step,
    user_count,
    ROUND(
        user_count * 100.0 / FIRST_VALUE(user_count) OVER (ORDER BY step_order), 2
    ) AS pct_of_total,
    ROUND(
        user_count * 100.0 / LAG(user_count) OVER (ORDER BY step_order), 2
    ) AS step_conversion_pct,
    ROUND(
        100.0 - user_count * 100.0 / LAG(user_count) OVER (ORDER BY step_order), 2
    ) AS drop_off_pct
FROM (
    -- Step 1: Product page views
    SELECT 1 AS step_order, 'Page View' AS funnel_step,
           COUNT(DISTINCT customer_id) AS user_count
    FROM page_views
    WHERE event_date BETWEEN '2026-02-01' AND '2026-02-17'
      AND page_type = 'product_detail'

    UNION ALL

    -- Step 2: Add to Cart
    SELECT 2, 'Add to Cart',
           COUNT(DISTINCT customer_id)
    FROM cart_events
    WHERE event_date BETWEEN '2026-02-01' AND '2026-02-17'
      AND action = 'add'

    UNION ALL

    -- Step 3: Place Order
    SELECT 3, 'Place Order',
           COUNT(DISTINCT customer_id)
    FROM orders
    WHERE order_date BETWEEN '2026-02-01' AND '2026-02-17'

    UNION ALL

    -- Step 4: Payment Success
    SELECT 4, 'Payment Success',
           COUNT(DISTINCT customer_id)
    FROM orders
    WHERE order_date BETWEEN '2026-02-01' AND '2026-02-17'
      AND order_status NOT IN ('payment_failed', 'cancelled')

    UNION ALL

    -- Step 5: Delivered
    SELECT 5, 'Delivered',
           COUNT(DISTINCT customer_id)
    FROM orders
    WHERE order_date BETWEEN '2026-02-01' AND '2026-02-17'
      AND order_status = 'delivered'
) funnel
ORDER BY step_order;

Kết quả ví dụ:

funnel_stepuser_countpct_of_totalstep_conversion_pctdrop_off_pct
Page View5,200,000100.00
Add to Cart1,560,00030.0030.0070.00
Place Order780,00015.0050.0050.00
Payment Success702,00013.5090.0010.00
Delivered655,00012.6093.306.70

Insight:

  • Page View → Add to Cart: 30% — 70% người xem không thêm giỏ hàng. Đây là bottleneck lớn nhất → cải thiện product page (ảnh, reviews, pricing).
  • Add to Cart → Place Order: 50% — Một nửa bỏ giỏ hàng (cart abandonment). Nguyên nhân phổ biến: shipping fee, cần so sánh giá, quên.
  • Payment → Delivered: 93.3% — Tỷ lệ tốt, chỉ 6.7% failed delivery (sai địa chỉ, không liên lạc được, từ chối nhận hàng).
  • Overall conversion: 12.6% — Cứ 100 người xem sản phẩm, chỉ ~13 người nhận được hàng.

Query 4: Return Rate Analysis — JOIN + GROUP BY + HAVING

sql
-- Phân tích return rate theo category và lý do trả hàng
-- JOIN: orders + order_items + products + returns
-- HAVING: lọc categories có return rate > threshold

SELECT
    p.category,
    COUNT(DISTINCT o.order_id) AS total_delivered_orders,
    COUNT(DISTINCT r.return_id) AS total_returns,
    ROUND(
        COUNT(DISTINCT r.return_id) * 100.0
        / COUNT(DISTINCT o.order_id), 2
    ) AS return_rate_pct,
    -- Top return reasons per category
    MODE() WITHIN GROUP (ORDER BY r.return_reason) AS top_return_reason,
    SUM(COALESCE(r.refund_amount, 0)) AS total_refund_amount
FROM orders o
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
LEFT JOIN returns r ON o.order_id = r.order_id
WHERE o.order_status IN ('delivered', 'returned')
  AND o.order_date BETWEEN '2026-01-01' AND '2026-02-17'
GROUP BY p.category
HAVING COUNT(DISTINCT o.order_id) >= 1000       -- Đủ data
ORDER BY return_rate_pct DESC;

Kết quả ví dụ:

categorytotal_delivered_orderstotal_returnsreturn_rate_pcttop_return_reasontotal_refund_amount
Fashion580,000104,40018.00wrong_size31,320,000,000
Electronics420,00029,4007.00defective22,050,000,000
Beauty & Health290,00017,4006.00not_as_described5,220,000,000
Home & Living310,00015,5005.00damaged_shipping7,750,000,000
Mom & Baby180,0007,2004.00wrong_item2,880,000,000
Books & Stationery650,0006,5001.00damaged_shipping975,000,000

Insight: Fashion có return rate 18% — gấp 3x trung bình sàn. Lý do #1: wrong_size — khách chọn sai size khi mua online. Giải pháp: yêu cầu seller Fashion phải có size chart chuẩn + ảnh thực → giảm return rate Fashion xuống 12% sau 3 tháng, tiết kiệm ~10 tỷ VNĐ refund.

Query 5: Seller Performance Ranking — Subquery + CASE + ORDER BY

sql
-- Xếp hạng seller dựa trên nhiều chỉ số: GMV, return rate, rating
-- Dùng subquery tính từng metric rồi combine

SELECT
    s.seller_id,
    s.seller_name,
    s.seller_type,
    seller_metrics.total_orders,
    seller_metrics.seller_gmv,
    seller_metrics.return_rate_pct,
    s.seller_rating,
    CASE
        WHEN seller_metrics.seller_gmv >= 5000000000
             AND seller_metrics.return_rate_pct <= 5
             AND s.seller_rating >= 4.5
        THEN 'Gold Seller'
        WHEN seller_metrics.seller_gmv >= 1000000000
             AND seller_metrics.return_rate_pct <= 10
             AND s.seller_rating >= 4.0
        THEN 'Silver Seller'
        WHEN seller_metrics.return_rate_pct > 20
             OR s.seller_rating < 3.5
        THEN 'Warning'
        ELSE 'Standard'
    END AS seller_tier
FROM sellers s
INNER JOIN (
    SELECT
        p.seller_id,
        COUNT(DISTINCT o.order_id) AS total_orders,
        SUM(oi.subtotal) AS seller_gmv,
        ROUND(
            COUNT(DISTINCT r.return_id) * 100.0
            / NULLIF(COUNT(DISTINCT o.order_id), 0), 2
        ) AS return_rate_pct
    FROM orders o
    INNER JOIN order_items oi ON o.order_id = oi.order_id
    INNER JOIN products p ON oi.product_id = p.product_id
    LEFT JOIN returns r ON o.order_id = r.order_id
    WHERE o.order_status IN ('completed', 'delivered', 'returned')
      AND o.order_date BETWEEN '2026-01-01' AND '2026-02-17'
    GROUP BY p.seller_id
) seller_metrics ON s.seller_id = seller_metrics.seller_id
ORDER BY seller_metrics.seller_gmv DESC
LIMIT 50;

Insight: Trong 100,000 sellers, chỉ ~2,000 sellers đạt "Gold Seller" (2%) — nhưng đóng góp 45% tổng GMV. ~5,000 sellers ở mức "Warning" (return rate >20% hoặc rating ❤️.5) → Tiki gửi cảnh báo, yêu cầu cải thiện trong 30 ngày hoặc hạn chế hiển thị sản phẩm trên homepage.

📊 Kết quả

Chỉ sốTrước (phân tích thủ công)Sau (SQL queries chuẩn hóa)
Thời gian tính GMV daily2 giờ (export + Excel)5 phút (SQL query tự động)
GMV accuracy±15% (dùng Gross GMV)<1% sai số (Net GMV + loại trừ fraud)
Conversion rate visibilityChỉ biết "bao nhiêu đơn"Full funnel 5 steps: view → cart → order → pay → deliver
Phát hiện category return caoCuối quý (manual review)Hàng tuần (HAVING auto-flag return rate > 10%)
Seller quality monitoringCuối thángHàng ngày (auto-ranking Gold/Silver/Warning)
Fashion return rate18%12% (nhờ insight → bắt buộc size chart)
Cart abandonment reduction50% abandonment42% (nhờ insight → gửi reminder email)
Tiết kiệm chi phí refund~10 tỷ VNĐ/quý (giảm return rate Fashion)

SQL queries giúp Tiki BI team biến hàng triệu đơn hàng thành bức tranh kinh doanh rõ ràng — từ GMV thực (không phải GMV "ảo"), đến conversion funnel (biết bottleneck ở đâu), đến return rate (biết category nào cần fix). Mỗi insight đều dẫn đến 1 action cụ thể, đo lường được.

💡 Bài học cho Data Analyst Việt Nam

  1. CASE WHEN là "IF/IFS của SQL" — biến dữ liệu thô thành insight. CASE WHEN trong SELECT cho phép phân loại (order_status → GMV type), phân hạng (seller → Gold/Silver/Warning), và tạo conditional aggregation (SUM chỉ completed orders). Học CASE WHEN là bước nhảy vọt từ SQL cơ bản sang SQL phân tích.
  2. Luôn phân biệt Gross vs Net metrics. Gross GMV, gross orders, gross revenue — tất cả đều inflate nếu không trừ cancel + return + fraud. Tiki chênh 16% giữa Gross GMV và Net GMV. Báo cáo con số nào cho CEO quyết định chất lượng insight — và uy tín — của DA.
  3. Conversion funnel cần UNION ALL + subquery. Mỗi step trong funnel nằm ở bảng khác → UNION ALL gộp kết quả → window function (LAG, FIRST_VALUE) tính drop-off. Đây là pattern SQL chuẩn cho funnel analysis — áp dụng được ở mọi e-commerce.
  4. LEFT JOIN cho return analysis là bắt buộc. Nếu dùng INNER JOIN giữa orders và returns, chỉ thấy đơn bị trả — mất đơn không bị trả → không tính được return rate. LEFT JOIN giữ tất cả orders, returns columns = NULL khi không bị trả → tính return rate chính xác.

📝 So sánh & tổng hợp

Tiêu chíUberGrabTiki
NgànhRide-hailing / MobilitySuper App (Ride + Food + Pay)E-commerce
Quy mô dữ liệu~28 triệu trips/ngày~8 triệu rides + food orders/ngày~500K orders/ngày
Số bảng chính3 (trips, riders, drivers)5+ (trips, food_orders, riders, drivers, payments)6+ (orders, items, products, customers, sellers, returns)
SQL technique nổi bậtSELECT + WHERE + GROUP BY + SubqueryINNER/LEFT JOIN + Multi-table JOIN + HAVINGCASE WHEN + JOIN + Subquery + UNION ALL
Business question chínhSupply/demand balance, surge pricingCross-service behavior, churn predictionGMV accuracy, conversion funnel, return rate
JOIN sử dụngJOIN zones (trips ↔ zones)INNER JOIN 3+ bảng, LEFT JOIN churnINNER JOIN orders ↔ items ↔ products, LEFT JOIN returns
Aggregate functionsCOUNT, SUM, AVG, ROUNDCOUNT, SUM, AVG + window (OVER)COUNT, SUM, AVG + CASE WHEN conditional
HAVING dùng choFilter zones có demand > 500 requestsFilter drivers completion_rate < 70%Filter categories return_rate > 10%
Subquery dùng choSurge bracket analysisCross-service rider statsConversion funnel, seller metrics
Kết quả nổi bậtGiảm rider wait time 6.5' → 4.2', +$2.8M/thángWin-back 21,600 riders, cross-sell +8%Giảm Fashion return 18% → 12%, tiết kiệm 10 tỷ VNĐ/quý

Điểm chung của cả 3 case study:

  • Đều sử dụng relational database với multiple tables liên kết qua Primary Key / Foreign Key — không thể phân tích bằng 1 bảng Excel đơn lẻ.
  • Đều chứng minh SQL là công cụ phân tích #1 trong công ty công nghệ — từ startup đến enterprise, từ ride-hailing đến e-commerce. Mọi DA đều viết SQL hàng ngày.
  • Đều kết hợp nhiều kỹ thuật SQL: SELECT + WHERE để lọc, JOIN để liên kết, GROUP BY để tổng hợp, HAVING để lọc kết quả, CASE WHEN để phân loại, subquery để tính toán phức tạp.
  • Đều cho thấy chọn đúng loại JOIN quyết định chất lượng insight: INNER JOIN cho "đã xảy ra", LEFT JOIN cho "chưa xảy ra" (churn, no returns).
  • Đều nhấn mạnh: SQL query tốt = business question rõ ràng. Trước khi viết query, luôn xác định: "Tôi cần trả lời câu hỏi gì? Dữ liệu nằm ở bảng nào? Bảng nào cần JOIN? Aggregate theo chiều nào?"

🤔 Câu hỏi thảo luận

  1. INNER JOIN vs LEFT JOIN: Trong case Grab, query tìm riders inactive dùng LEFT JOIN. Nếu thay bằng INNER JOIN, kết quả sẽ khác như thế nào? Hãy giải thích bằng ví dụ cụ thể với 5 riders mẫu (3 có trips, 2 không có trips) — viết ra kết quả từng dòng cho cả 2 loại JOIN.

  2. WHERE vs HAVING: Uber dùng HAVING để lọc zones có demand > 500 requests. Tại sao không viết WHERE thay cho HAVING? Hãy viết lại query sai (dùng WHERE thay HAVING) và giải thích tại sao nó lỗi hoặc cho kết quả sai.

  3. Subquery thực hành: Tiki dùng subquery để tính seller metrics rồi JOIN với bảng sellers. Nếu bạn viết tất cả trong 1 query đơn (không dùng subquery), SQL sẽ dài và phức tạp hơn bao nhiêu? Hãy thử viết lại query Seller Performance không dùng subquery — so sánh readability.

  4. CASE WHEN cho business logic: Tiki phân hạng seller thành Gold/Silver/Warning/Standard bằng CASE WHEN. Nếu bạn quản lý 1 quán cà phê với bảng orders (order_id, customer_id, order_date, total_amount), hãy viết SQL dùng CASE WHEN để phân loại khách hàng thành "VIP" (tổng chi tiêu > 5 triệu), "Regular" (1–5 triệu), "New" (< 1 triệu).

  5. Conversion Funnel: Tiki có conversion rate Page View → Delivered là 12.6%. Bottleneck lớn nhất là Page View → Add to Cart (70% drop-off). Nếu bạn là DA tại Tiki, bạn sẽ đề xuất 3 action cụ thể nào để tăng conversion từ Page View → Add to Cart? Action nào có thể đo lường bằng SQL query tiếp theo?


📚 Nguồn tham khảo

  1. Uber Engineering Blog. "Uber's Big Data Platform: 100+ Petabytes with Minute Latency." — Kiến trúc data platform phục vụ hàng tỷ trip records.
  2. Grab Tech Blog. "How Grab Uses Data to Improve Driver Allocation." — Phân tích supply/demand và driver dispatch optimization bằng SQL + ML.
  3. Tiki Engineering. Thông tin về hệ thống e-commerce platform và data analytics tại Tiki Vietnam.
  4. Date, C.J. (2003). An Introduction to Database Systems. Addison-Wesley. — Giáo trình kinh điển về relational database.
  5. Forta, B. (2013). Sams Teach Yourself SQL in 10 Minutes. Pearson. — SQL cơ bản cho người mới, focus SELECT/JOIN/GROUP BY.
  6. Molinaro, A. & Graaf, R. (2020). SQL Cookbook. O'Reilly Media. — Patterns SQL nâng cao: subquery, window functions, analytical queries.
  7. Google Data Analytics Certificate. "Prepare Data for Exploration — SQL and Databases." — Module SQL trong chương trình Google DA.
  8. Mode Analytics SQL Tutorial. mode.com/sql-tutorial — Tutorial tương tác SQL với business datasets thực tế.
  9. CafeBiz, TechInAsia, DealStreetAsia — Các bài viết về Grab, Tiki, và hệ sinh thái công nghệ Đông Nam Á.
  10. PostgreSQL Documentation. postgresql.org/docs — Tài liệu chính thức PostgreSQL — RDBMS phổ biến nhất cho DA.

🔗 Xem thêm Buổi 5

📘 Nội dung chính📝 Blog🏆 Tiêu chuẩn🛠 Workshop🎮 Mini Game