现在数据已经可以查询。我们先不急着写复杂语句,而是用 SELECT、WHERE、GROUP BY 和 HAVING 回答几个具体问题:有多少完成订单、各支付方式有多少订单、哪些月份取消较多。
SELECT
order_id,
customer_id,
order_date,
status,
payment_method
FROM v_orders_clean
ORDER BY order_date, order_id
LIMIT 10;SELECT 决定返回哪些列,FROM 指定数据来源,ORDER BY 决定展示顺序,LIMIT 控制查看行数。探索数据时先看 10 行,比直接导出整表更容易发现日期、状态或编号是否异常。
经营收入通常只统计已完成订单,所以先过滤订单状态。
SELECT COUNT(*) AS completed_orders
FROM v_orders_clean
WHERE status = 'COMPLETED';结果是 509 笔。取消和退货订单仍保留在数据库里,但不进入销售额。这个口径以后会写进销售明细视图,避免每个查询各写一遍。
多个条件可以用 AND 或 OR 组合:
SELECT
order_id,
order_date,
status
FROM v_orders_clean
WHERE order_date >= '2025-01-01'
AND status <> 'COMPLETED'
ORDER BY order_date;日期使用 YYYY-MM-DD 文本格式时可以直接比较。这个格式从年到日由大到小排列,字符串顺序与时间顺序一致。
SELECT
status,
COUNT(*) AS orders
FROM v_orders_clean
GROUP BY status
ORDER BY orders DESC;GROUP BY status 把相同状态的订单放进一组,COUNT(*) 再数每组有多少行。聚合之后,SELECT 中的普通列必须出现在 GROUP BY 中,否则结果的含义会不清楚。
再看已完成订单的支付方式:
SELECT
payment_method,
COUNT(*) AS orders,
ROUND(
100.0 * COUNT(*) /
SUM(COUNT(*)) OVER (),
1
) AS order_share_pct
FROM v_orders_clean
WHERE status = 'COMPLETED'
GROUP BY payment_method
ORDER BY这里的 SUM(COUNT(*)) OVER () 会把各组的订单数再次求和,用作占比的分母。乘以 100.0 是为了让 SQLite 使用小数除法。
WHERE 在分组前过滤明细行,HAVING 在分组后过滤汇总结果。下面的查询只保留取消或退货超过 1 笔的月份:
SELECT
strftime('%Y-%m', order_date) AS order_month,
COUNT(*) AS abnormal_orders
FROM v_orders_clean
WHERE status IN ('CANCELLED', 'RETURNED')
GROUP BY strftime('%Y-%m', order_date)
HAVING COUNT(*) > 1
ORDER BY order_month;如果把 COUNT(*) > 1 写进 WHERE,数据库会报错,因为分组尚未发生,还不存在每月的计数。
一个汇总最好能和总数对上。先按状态汇总,再把汇总结果相加:
WITH status_counts AS (
SELECT status, COUNT(*) AS orders
FROM v_orders_clean
GROUP BY status
)
SELECT SUM(orders) AS checked_total
FROM status_counts;checked_total 应该等于清洗订单表的 540 行。如果对不上,说明过滤条件或分组逻辑改变了范围。
SQL 写出来的顺序和数据库理解它的顺序不同。可以先记住这个简化版本:
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT先确定数据来源,再过滤明细行,然后分组。HAVING 过滤分组结果,SELECT 才决定最后展示哪些列。
这解释了两个常见疑问。
第一,为什么 COUNT(*) > 1 不能写进 WHERE?执行 WHERE 时还没有分组,也就没有每组计数。
第二,为什么有时不能在 WHERE 中直接使用 SELECT 别名?别名在更后面的 SELECT 阶段才产生。为了让查询更容易移植,筛选条件里直接写原表达式,或先放进 CTE。
下面两条查询都合法,但回答的问题不同。
按状态分组:
SELECT status, COUNT(*) AS orders
FROM v_orders_clean
GROUP BY status;结果一行一个状态。
按状态和支付方式分组:
SELECT
status,
payment_method,
COUNT(*) AS orders
FROM v_orders_clean
GROUP BY status, payment_method
ORDER BY status, orders DESC;结果一行是一个“状态 + 支付方式”组合。每增加一个分组字段,结果粒度就更细。
如果你想要一行一个状态,却把 payment_method 也放进 GROUP BY,行数会增加。这不是数据库算错,而是问题已经变成了另一件事。
SQL 中 NULL 表示未知。它既不等于某个值,也不等于另一个 NULL。
下面的写法找不到空值:
SELECT *
FROM v_customers_clean
WHERE channel = NULL;正确写法是:
SELECT *
FROM v_customers_clean
WHERE channel IS NULL;本项目的清洗视图已经把空渠道标记成“未归因”,所以查询清洗视图时应筛选这个明确类别:
SELECT COUNT(*) AS unattributed_customers
FROM v_customers_clean
WHERE channel = '未归因';不要把 NULL 与 0 混为一谈。0 是已知没有,NULL 是不知道。后面环比首月保持为空,也是同一个道理。
一条查询结果不对时,不要一次改很多地方。按下面的顺序缩小问题:
GROUP BY 和聚合函数。例如要统计 2025 年的非完成订单,可以分步运行:
SELECT COUNT(*)
FROM v_orders_clean
WHERE order_date >= '2025-01-01';再加入状态条件:
SELECT COUNT(*)
FROM v_orders_clean
WHERE order_date >= '2025-01-01'
AND status <> 'COMPLETED';这样能看出是哪一个条件改变了范围。直接从一条很长的查询开始,报错和口径问题会混在一起。
支付方式占比的分母应该是 509 笔完成订单。可以把结果包进 CTE 再核对:
WITH payment_counts AS (
SELECT
payment_method,
COUNT(*) AS orders
FROM v_orders_clean
WHERE status = 'COMPLETED'
GROUP BY payment_method
)
SELECT
SUM(orders) AS completed_orders,
ROUND(SUM(100.0 * orders / 509), 1)
完成订单应为 509,占比合计应接近 100%。因为四舍五入,展示后的占比合计可能有 0.1 个百分点差异,这与漏数不是同一种问题。
下一节会把订单、明细、客户和商品连接起来,开始计算销售额与毛利。连接后行数会显著增加,我们会专门检查这种增加是不是合理的一对多展开。