因为count()返回整数,多数sql引擎对整数相除默认截断小数,如3/5得0;须显式转浮点(如cast或乘1.0)并用nullif或case防除零,才能获得正确百分比。

为什么直接用 COUNT() 相除结果总是 0 或 NULL
因为多数 SQL 引擎(如 MySQL、PostgreSQL)默认对两个整数做除法时执行的是整数除法,COUNT() 返回的永远是 INT,所以 COUNT(visit)/COUNT(conversion) 在 MySQL 中会截断小数,比如 3/5 得 0;在 PostgreSQL 中则可能报错或返回 0.0(取决于版本和隐式转换规则)。
真正要的是浮点除法,必须显式把至少一个操作数转成小数类型。
- MySQL:用
CAST(COUNT(...) AS DECIMAL)或乘1.0 - PostgreSQL:用
CAST(... AS NUMERIC)或::NUMERIC - SQLite:用
CAST(... AS REAL) - 注意:别用
FLOAT,精度不可控;优先选DECIMAL(p,s)或NUMERIC
渠道转化率的标准写法(带分母为 0 保护)
假设表 events 有字段 channel、event_type(值为 'page_view' 或 'purchase'),要算「每个渠道的购买转化率 = 购买人数 / 浏览人数」:
SELECT
channel,
COUNT(CASE WHEN event_type = 'purchase' THEN 1 END) AS conversions,
COUNT(CASE WHEN event_type = 'page_view' THEN 1 END) AS visits,
CASE
WHEN COUNT(CASE WHEN event_type = 'page_view' THEN 1 END) = 0
THEN 0.0
ELSE CAST(COUNT(CASE WHEN event_type = 'purchase' THEN 1 END) AS DECIMAL(5,4))
/ COUNT(CASE WHEN event_type = 'page_view' THEN 1 END)
END AS conversion_rate
FROM events
GROUP BY channel;
- 用
CASE WHEN ... THEN 1 END配合COUNT()实现条件计数,比SUM(IF(...))更跨数据库兼容 -
CAST(... AS DECIMAL(5,4))表示最多 5 位数字、小数点后 4 位(即 0.0000~9.9999),避免科学计数显示 -
CASE WHEN ... = 0 THEN 0.0防止除零错误,且确保返回类型是小数
用 JOIN 或子查询统计更复杂的漏斗(比如注册→下单→支付)
如果转化路径涉及多个独立表(如 signups、orders、payments),不能只靠 CASE,得用左连接或关联子查询。例如统计「各渠道注册用户中完成支付的比例」:
SELECT s.channel, COUNT(s.id) AS signups, COUNT(p.order_id) AS paid_orders, CAST(COUNT(p.order_id) AS DECIMAL(5,4)) / NULLIF(COUNT(s.id), 0) AS payment_rate FROM signups s LEFT JOIN payments p ON s.user_id = p.user_id GROUP BY s.channel;
-
NULLIF(COUNT(s.id), 0)是比CASE WHEN ... = 0 THEN NULL ELSE ... END更简洁的除零防护写法,多数引擎都支持 - 必须用
LEFT JOIN,否则没支付记录的注册用户会被丢掉,导致分母变小 - 如果某渠道有注册但无对应支付表记录,
p.order_id是NULL,COUNT(p.order_id)自然为 0 —— 这正是我们想要的
性能与可读性兼顾的小技巧
当数据量大、渠道多、事件类型杂时,嵌套太多 CASE 会让 SQL 难维护。可以拆成 CTE 或物化中间结果:
WITH channel_stats AS (
SELECT
channel,
COUNT(*) FILTER (WHERE event_type = 'page_view') AS visits,
COUNT(*) FILTER (WHERE event_type = 'purchase') AS conversions
FROM events
GROUP BY channel
)
SELECT
channel,
ROUND(CAST(conversions AS DECIMAL) / NULLIF(visits, 0), 4) AS conv_rate
FROM channel_stats;
- PostgreSQL 支持
COUNT(*) FILTER (WHERE ...),比CASE更直观;MySQL 不支持,得回退到CASE -
ROUND(..., 4)控制小数位数,但注意:它只是显示控制,不改变数值精度;真要四舍五入存值,才该用ROUND包裹CAST - CTE 不一定提升性能,但能大幅降低理解成本——尤其当你后续还要加留存、分群等逻辑时
最常被忽略的一点:转化率分母必须严格对应业务定义。比如「浏览首页」还是「任意页面」?「点击按钮」还是「进入下单页」?SQL 写得再对,事件埋点口径错了,结果就全偏了。










