GROUP BY chỉ sinh ra dòng cho dữ liệu đã tồn tại — tháng không có đơn thì không có dòng nào để gom.
Cách chuẩn là tự sinh dải thời gian rồi LEFT JOIN dữ liệu vào.
sql
WITH months AS (
SELECT generate_series(
date_trunc('month', :from_date::date),
date_trunc('month', :to_date::date),
interval '1 month'
) AS month
),
revenue AS (
SELECT date_trunc('month', created_at) AS month, SUM(total) AS amount
FROM orders
WHERE status = 'paid'
AND created_at >= :from_date AND created_at < :to_date
GROUP BY 1
)
SELECT
m.month,
COALESCE(r.amount, 0) AS revenue
FROM months m
LEFT JOIN revenue r ON r.month = m.month
ORDER BY m.month;Ba chi tiết quyết định đúng/sai:
- LEFT JOIN phải bắt đầu từ bảng lịch (months), không phải từ orders — đảo lại là mất tác dụng.
- COALESCE(r.amount, 0) để tháng trống hiện 0 thay vì NULL.
- Điều kiện thời gian dùng nửa mở >= from AND < to để không lệ thuộc độ chính xác của timestamp và vẫn dùng được index trên created_at.
MySQL không có generate_series; thay bằng bảng lịch (calendar_months) tạo sẵn hoặc recursive CTE.