MySQL非空判断实战:从NULL陷阱到数据清洗最佳实践
1. 从一次数据清洗的“翻车”说起:为什么非空判断是基本功
那天下午,我正处理一个用户画像的数据清洗任务。需求很简单:从一张名为user_profile的表中,筛选出“手机号”和“邮箱”至少有一个不为空的用户,用于后续的营销触达。我随手写下了自以为万无一失的 SQL:
SELECT user_id, mobile, email FROM user_profile WHERE mobile != '' AND email != '';跑出来的结果让我心里咯噔一下:数量比预想的少了近三分之一。直觉告诉我,肯定有问题。排查后发现,表里有大量记录,mobile或email字段的值是NULL,而不是空字符串''。我的条件mobile != ''在面对NULL时,不会返回TRUE,也不会返回FALSE,而是返回NULL。在WHERE子句中,NULL被视为FALSE,于是这些记录就被默默地过滤掉了。这就是一个典型的因为混淆了“空值”(NULL)和“空字符串”('')而导致的逻辑错误。
这个看似低级的错误,恰恰暴露了数据处理中的一个核心痛点:对“空”的认知模糊。在 MySQL 乃至整个 SQL 领域,NULL是一个特殊的存在,它表示“未知”或“不适用”,它与任何值(包括它自己)的比较结果都是NULL。而空字符串''是一个确定的值,一个长度为0的字符串。很多业务逻辑的 Bug、统计结果的偏差,根源都在于此。因此,熟练掌握 MySQL 中的非空判断(IS NULL,IS NOT NULL)以及处理空值的函数(如COALESCE,IFNULL),绝不是可有可无的语法知识,而是保证数据操作准确性的第一道防线。无论你是刚入门的数据分析师,还是需要与数据库频繁交互的后端开发者,厘清这些概念,都能让你避开无数个潜在的坑。
2. 理解“空”的本质:NULL vs. 空字符串('')
在深入函数和技巧之前,我们必须先打好地基,彻底理解NULL和空字符串''的本质区别。这是所有后续正确操作的前提。
2.1 NULL:未知的“黑洞”
NULL在 SQL 标准中代表“缺失的未知值”。你可以把它想象成一个标签,上面写着“此处无信息”。因为它代表“未知”,所以任何与NULL进行的算术运算或比较操作,结果都是NULL。这是一个非常重要的三值逻辑(TRUE, FALSE, NULL)概念。
我们来通过几个查询直观感受一下:
-- 创建一个测试表 CREATE TABLE test_null ( id INT PRIMARY KEY, value_null INT, value_empty VARCHAR(10) ); INSERT INTO test_null (id, value_null, value_empty) VALUES (1, NULL, ''), -- id为1的记录,value_null是NULL,value_empty是空字符串 (2, 100, 'hello'), (3, NULL, NULL); -- 比较操作 SELECT id, value_null = NULL AS `eq_null`, -- 错误用法!永远返回NULL value_null IS NULL AS `is_null`, -- 正确用法 value_null = 100 AS `eq_100`, value_empty = '' AS `eq_empty` FROM test_null;执行结果可能出乎一些人的意料:
| id | eq_null | is_null | eq_100 | eq_empty |
|---|---|---|---|---|
| 1 | NULL | 1 (TRUE) | NULL | 1 (TRUE) |
| 2 | NULL | 0 (FALSE) | 1 (TRUE) | 0 (FALSE) |
| 3 | NULL | 1 (TRUE) | NULL | NULL |
关键点解析:
value_null = NULL: 这是最常见的错误写法。因为NULL与任何值的比较(包括它自己)都是NULL,所以这一列结果全是NULL,在WHERE条件中会被当作FALSE处理。你无法用=或!=来判断NULL。value_null IS NULL: 这是唯一正确的判断某个字段是否为NULL的方式。它返回明确的布尔值TRUE或FALSE。value_null = 100: 当value_null为NULL时,与100比较的结果是NULL(未知)。value_empty = '': 空字符串是一个确定的值,所以可以用等号比较。
实操心得:养成条件反射,看到
=和NULL在一起就要警惕。在代码审查时,这是一个重点检查项。很多隐晦的 Bug 就藏在这里。
2.2 空字符串(''):确定的“空盒子”
空字符串是一个有效的字符串值,只是它的长度为零。它在内存中占有空间(对于变长字符串,可能只有一个长度标识),在逻辑上它等于''。
SELECT LENGTH('') AS length_of_empty, -- 返回 0 CONCAT('Hello', '', 'World') AS concat_result, -- 返回 'HelloWorld' '' = '' AS empty_eq_empty; -- 返回 1 (TRUE)空字符串可以参与所有字符串操作,行为是可预测的。它与NULL的关键区别在于:NULL是“未知状态”,而''是“已知的空状态”。
2.3 业务场景中的抉择:何时用NULL,何时用''?
这是一个设计问题,没有绝对答案,但有一些通用的最佳实践:
使用 NULL:
- 信息缺失或不适用时:例如,用户的“中间名”字段,很多人没有,这属于信息缺失,用
NULL更合适。 - 数值型字段的默认值:例如,一个产品的“折扣率”字段,如果尚未设置折扣,用
NULL比用0更合理,因为0代表“零折扣”,是一个明确的业务含义。 - 外键字段:可选的关联关系,如订单的“推荐人ID”,如果没有推荐人,应为
NULL。
- 信息缺失或不适用时:例如,用户的“中间名”字段,很多人没有,这属于信息缺失,用
使用空字符串('')或默认值(如0):
- 必填但可为空的字符串字段:在某些设计中,为了简化查询,会将所有字符串字段默认设为
''。但这会模糊“用户未填写”和“用户填写了空内容”的区别。 - 有明确业务意义的默认值:如“状态”字段,默认值为 ‘pending’(待处理),这比
NULL更有意义。
- 必填但可为空的字符串字段:在某些设计中,为了简化查询,会将所有字符串字段默认设为
个人经验:在我的项目中,我更倾向于严格使用
NULL来表示“未知/缺失”。这迫使开发者在写 SQL 时必须显式地处理NULL情况,虽然初期会增加一些复杂度,但从长远看,数据的语义更清晰,能减少很多二义性 Bug。例如,统计“平均折扣率”时,AVG(discount)会自动忽略NULL值,这通常是我们想要的;而如果误用0,则会拉低平均值,导致统计错误。
3. 核心武器:IS NULL 与 IS NOT NULL 的精准使用
理解了NULL的特性后,IS NULL和IS NOT NULL这两个操作符就成了你驾驭“空值”最直接、最可靠的武器。
3.1 基础语法与查询过滤
它们的用法非常直接:
-- 查找所有邮箱为空的用户(包括NULL和''吗?注意区别!) SELECT * FROM users WHERE email IS NULL; -- 查找所有邮箱不为空的用户(不包括NULL,但包括'') SELECT * FROM users WHERE email IS NOT NULL; -- 查找所有邮箱为NULL或者为空字符串''的用户 SELECT * FROM users WHERE email IS NULL OR email = ''; -- 或者使用长度函数 SELECT * FROM users WHERE email IS NULL OR LENGTH(TRIM(email)) = 0;这里有一个关键点:IS NOT NULL只过滤掉NULL值,不会过滤掉空字符串''。如果你需要同时排除NULL和空字符串,必须组合条件。
3.2 在数据更新与删除中的应用
非空判断在数据维护中同样重要。
-- 场景:清理无效数据,删除没有邮箱且最近一年未登录的用户 DELETE FROM users WHERE (email IS NULL OR email = '') AND last_login_at < DATE_SUB(NOW(), INTERVAL 1 YEAR); -- 场景:数据补全,将手机号为NULL的用户的手机号更新为一个默认占位符 UPDATE users SET mobile = 'N/A' WHERE mobile IS NULL; -- 注意:这里用‘N/A’而不是NULL或'',是为了在后续查询中区分“未收集”和“无效值”。3.3 与聚合函数(Aggregate Functions)的配合
聚合函数如COUNT(),SUM(),AVG()等,在计算时会自动忽略NULL值。这是一个非常重要的特性。
CREATE TABLE sales ( id INT, amount DECIMAL(10, 2), -- 销售额,允许NULL(可能退款或未入账) region VARCHAR(50) ); INSERT INTO sales VALUES (1, 100.0, 'North'), (2, NULL, 'North'), (3, 150.0, 'South'), (4, NULL, 'South'); SELECT region, COUNT(*) AS total_rows, -- 计算所有行数,包括amount为NULL的 COUNT(amount) AS valid_sales_count, -- 只计算amount非NULL的行数 SUM(amount) AS total_amount, -- 对非NULL的amount求和,NULL被当作0处理(但实际是忽略) AVG(amount) AS avg_amount -- 对非NULL的amount求平均 FROM sales GROUP BY region;结果:
| region | total_rows | valid_sales_count | total_amount | avg_amount |
|---|---|---|---|---|
| North | 2 | 1 | 100.00 | 100.00 |
| South | 2 | 1 | 150.00 | 150.00 |
COUNT(*): 统计的是行数。COUNT(amount): 统计的是amount字段不为NULL的行数。这是统计“有效销售记录数”的正确方式。SUM(amount)和AVG(amount): 都只针对非NULL值进行计算。AVG(amount)等于SUM(amount) / COUNT(amount)。
避坑指南:如果你想统计“所有行的平均值,将NULL视为0”,那么不能直接用
AVG(amount)。你需要使用COALESCE函数(下文会讲)先将NULL转换为 0:AVG(COALESCE(amount, 0))。但务必想清楚,这在业务上是否合理(将未知的销售额视为0,可能会大幅拉低平均值)。
4. 进阶处理:COALESCE、IFNULL 等函数的场景化实战
仅仅判断是否为空往往不够,我们经常需要将NULL转换成一个有意义的默认值,或者根据是否为空进行条件分支。这时,就需要用到COALESCE和IFNULL这类函数。
4.1 COALESCE:返回参数列表中第一个非NULL值
COALESCE(value1, value2, ..., valueN)是 SQL 标准函数,在 MySQL、PostgreSQL 等数据库中通用。它从左到右检查参数,返回第一个不是NULL的值。如果所有参数都是NULL,则返回NULL。
典型场景1:数据展示时提供默认值
SELECT user_name, COALESCE(nick_name, user_name) AS display_name, -- 昵称为空则显示用户名 COALESCE(avatar_url, '/images/default-avatar.png') AS avatar, -- 头像为空用默认图 COALESCE(bio, '这个人很懒,什么都没写~') AS biography FROM users;典型场景2:多字段优先级取值在用户联系信息中,优先使用手机号,其次邮箱,最后是座机。
SELECT user_id, COALESCE(mobile, email, tel) AS primary_contact FROM user_contacts;典型场景3:在计算中避免 NULL 污染计算员工总薪资(基本工资+奖金),但奖金可能为NULL。
SELECT employee_id, base_salary, bonus, base_salary + COALESCE(bonus, 0) AS total_salary -- 如果bonus为NULL,则按0计算 FROM salaries;如果不使用COALESCE,base_salary + NULL的结果将是NULL,导致总薪资数据丢失。
4.2 IFNULL:COALESCE 的双参数简化版
IFNULL(expr1, expr2)是 MySQL 特有的函数,可以看作是COALESCE(expr1, expr2)的简写。如果expr1不是NULL,则返回expr1;否则返回expr2。
-- 与 COALESCE 等价 SELECT IFNULL(mobile, '未填写') FROM users; SELECT COALESCE(mobile, '未填写') FROM users; -- 结果相同选型建议:虽然
IFNULL更简洁,但我个人强烈推荐始终使用COALESCE。原因有二:1)COALESCE是 SQL 标准,具有更好的跨数据库兼容性(如迁移到 PostgreSQL 也能用)。2)COALESCE可以接受两个以上的参数,功能更强大,而IFNULL只能处理两个。养成使用COALESCE的习惯,代码更具可扩展性和可移植性。
4.3 NULLIF:主动制造NULL的“安全阀”
NULLIF(expr1, expr2)函数的作用与COALESCE相反:如果expr1等于expr2,则返回NULL;否则返回expr1。它常用于避免除零错误或清理数据。
场景:避免除零错误
-- 计算转化率,但访问次数可能为0 SELECT campaign_id, clicks, visits, -- 当visits为0时,NULLIF(visits, 0)返回NULL,导致整个除法结果为NULL,避免了错误。 clicks / NULLIF(visits, 0) AS conversion_rate FROM campaign_stats;当visits为 0 时,NULLIF(visits, 0)返回NULL,任何数与NULL相除结果还是NULL(而不是报错),查询可以正常执行,转化率显示为NULL,这比程序因除零错误而崩溃要好得多。
场景:将特定值标准化为NULL
-- 将历史数据中的‘N/A’、‘未知’等占位符统一转换为NULL UPDATE products SET manufacturer = NULLIF(NULLIF(manufacturer, 'N/A'), '未知') WHERE manufacturer IN ('N/A', '未知');4.4 函数组合使用:应对复杂业务逻辑
实战中,这些函数常常组合使用,以构建健壮的数据处理逻辑。
案例:构建用户完整的地址信息假设我们有多个可能为NULL的地址字段。
SELECT user_id, CONCAT( COALESCE(province, ''), COALESCE(city, ''), COALESCE(district, ''), COALESCE(street_address, '') ) AS full_address_raw, -- 更优雅的做法:使用CONCAT_WS,它自动忽略NULL值,并用指定分隔符连接非NULL值 TRIM(CONCAT_WS(' ', NULLIF(province, ''), -- 先处理空字符串,如果是''则转为NULL NULLIF(city, ''), NULLIF(district, ''), street_address )) AS full_address_better FROM user_address;CONCAT_WS(' ', ...)是“With Separator”的缩写,用空格连接非NULL值,完美地处理了字段缺失的情况。- 先用
NULLIF将无意义的空字符串转为NULL,再利用CONCAT_WS忽略NULL的特性,可以生成更干净、无多余空格的地址字符串。
5. 索引、性能与常见陷阱深度剖析
掌握了语法和函数,我们还需要从数据库性能和维护的角度来审视非空判断。
5.1 索引对 IS NULL / IS NOT NULL 的影响
这是一个性能关键点。MySQL 中索引的行为会影响这类查询的效率。
对于允许为 NULL 的列:
- 在单列索引上,
IS NULL条件可以使用索引。 IS NOT NULL条件是否使用索引,取决于数据分布。如果表中绝大多数行都是NOT NULL,优化器可能认为全表扫描比走索引更快。你可以使用FORCE INDEX提示或使用EXPLAIN命令查看执行计划。
- 在单列索引上,
对于定义为 NOT NULL 的列:
- 查询
IS NULL不会有任何结果,优化器能快速识别。 IS NOT NULL等价于查询所有行,同样可能走全表扫描。
- 查询
示例与建议:
-- 假设在email字段上有一个索引 ALTER TABLE users ADD INDEX idx_email (email); -- 使用EXPLAIN分析查询计划 EXPLAIN SELECT * FROM users WHERE email IS NULL; EXPLAIN SELECT * FROM users WHERE email IS NOT NULL;查看EXPLAIN输出中的key列,如果显示了idx_email,说明使用了索引。
性能心得:对于需要频繁使用
IS NOT NULL进行查询的字段,如果该字段的NULL值比例很低(比如<5%),可以考虑将其改为NOT NULL DEFAULT '',并建立索引。这样,查询“非空”就变成了查询“不等于默认值”,索引利用率可能会更高。但这需要权衡业务语义的清晰度。
5.2 联合索引中的NULL值陷阱
在联合(复合)索引中,NULL值的行为有些特殊。MySQL 的 InnoDB 引擎认为所有NULL值在索引中是相等的。这意味着:
- 如果你在
(a, b)上建立了联合索引,且a字段允许为NULL,那么所有a IS NULL的记录在索引中会被视为具有相同的“值”。 - 这可能导致索引的效率在某些查询中下降,因为基于
NULL的索引部分无法提供有效的排序或范围过滤。
5.3 开发中的高频陷阱与解决方案
陷阱一:IN 子查询与 NULL
SELECT * FROM table_a WHERE id IN (SELECT id FROM table_b WHERE ...);如果子查询返回的结果集中包含NULL,整个IN条件的结果会是NULL或FALSE(取决于table_a.id是否允许为NULL),可能导致查询结果不符合预期。解决方案是确保子查询不返回NULL,或在外部查询中处理。
陷阱二:NOT IN 子查询与 NULL(致命陷阱)
-- 这是一个经典陷阱! SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM blacklist);如果blacklist.user_id中有任何一行是NULL,那么整个NOT IN条件的结果将永远是FALSE或NULL,导致查询结果为空集!这是因为NOT IN等价于!= ALL(...),而NULL参与比较会得到NULL。绝对安全的写法是使用NOT EXISTS:
SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM blacklist b WHERE b.user_id = u.id);NOT EXISTS对子查询中的NULL是安全的。
陷阱三:排序(ORDER BY)中的NULL在排序时,NULL值被视为最小值(在ASC升序中排在最前,在DESC降序中排在最后)。如果你希望改变这种行为,可以使用ORDER BY ... IS NULL, ...或ORDER BY COALESCE(field, default_value)。
-- 将NULL值排在最后(升序时) SELECT * FROM products ORDER BY price IS NULL, price ASC; -- 先按price是否为NULL排序(FALSE(0)在前,TRUE(1)在后),再按price值排序。陷阱四:DISTINCT, GROUP BY 与 NULLDISTINCT和GROUP BY会将所有的NULL值归为一组。这意味着多个NULL行会被视为具有相同的值,只返回一行。
6. 设计最佳实践:从表结构定义开始规避问题
很多关于NULL的麻烦,其实可以在设计表结构时就进行规避或规范。
6.1 字段定义:NOT NULL 与默认值
- 原则:尽可能地将字段定义为
NOT NULL。这能强制数据完整性,简化查询逻辑,并可能带来一些性能好处(例如,InnoDB 存储固定大小的行时,NOT NULL字段可能更高效)。 - 方法:为每个
NOT NULL字段选择一个合理的默认值。- 数字类型:
0,-1(如果0有业务含义)。 - 字符串类型:
''(空字符串)。但需注意,这牺牲了“未知”和“空值”的区分。 - 日期时间类型:
'1970-01-01','0000-00-00'(需注意MySQL的SQL模式是否允许)或业务上的一个特殊日期。 - 枚举类型:定义一个明确的默认状态,如
‘pending’。
- 数字类型:
CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled') NOT NULL DEFAULT 'pending', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 以下字段允许为NULL,因为信息可能后续才补充 paid_at TIMESTAMP NULL, shipping_address TEXT NULL, invoice_no VARCHAR(64) NULL );6.2 使用CHECK约束(MySQL 8.0.16+)
从 MySQL 8.0.16 开始,支持标准的CHECK约束,可以用于实现更复杂的非空或数据验证逻辑。
CREATE TABLE employees ( id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, salary DECIMAL(10,2) NOT NULL, -- 确保薪水为正数,且如果奖金字段有值,则必须大于0 bonus DECIMAL(10,2) NULL, CONSTRAINT chk_salary_positive CHECK (salary > 0), CONSTRAINT chk_bonus CHECK (bonus IS NULL OR bonus > 0) );CHECK约束能保证数据在进入数据库时就符合业务规则,比在应用层校验更可靠。
6.3 使用触发器(Trigger)进行复杂校验
对于更复杂的、涉及多表或动态条件的非空逻辑,可以使用触发器。
DELIMITER // CREATE TRIGGER before_insert_user BEFORE INSERT ON users FOR EACH ROW BEGIN -- 确保邮箱和手机号至少有一个不为空 IF NEW.email IS NULL AND NEW.mobile IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'User must have either email or mobile number.'; END IF; END; // DELIMITER ;这个触发器会在插入新用户时,强制要求email和mobile至少有一个有值。
6.4 视图(View)封装通用空值处理逻辑
如果某些复杂的空值处理逻辑在多个查询中重复出现,可以创建一个视图来封装。
CREATE VIEW v_user_contact AS SELECT id, user_name, COALESCE(mobile, 'N/A') AS primary_mobile, COALESCE(email, 'N/A') AS primary_email, CONCAT_WS('/', NULLIF(mobile, ''), NULLIF(email, '') ) AS contact_info, CASE WHEN mobile IS NOT NULL AND email IS NOT NULL THEN 'both' WHEN mobile IS NOT NULL THEN 'mobile_only' WHEN email IS NOT NULL THEN 'email_only' ELSE 'none' END AS contact_status FROM users;这样,业务查询可以直接使用v_user_contact视图,无需每次都写冗长的COALESCE和CASE WHEN语句,保证了逻辑的一致性和可维护性。
7. 实战演练:一个完整的数据清洗与报表案例
让我们通过一个模拟的电商订单数据清洗和报表生成的完整流程,串联运用前面所学的所有知识。
场景:有一张原始的订单表raw_orders,数据质量较差,存在大量NULL和无效值。我们需要清洗数据,并生成一份每日有效订单金额的报表。
步骤1:审视原始数据
-- 假设表结构如下 DESC raw_orders; -- | Field | Type | Null | Key | Default | Extra | -- | order_id | varchar(20) | YES | | NULL | | -- | user_id | int | YES | | NULL | | -- | amount | decimal(10,2) | YES | | NULL | | -- | status | varchar(20) | YES | | NULL | | -- | create_date | date | YES | | NULL | | -- 查看数据问题 SELECT COUNT(*) AS total_rows, COUNT(order_id) AS valid_order_id, COUNT(user_id) AS valid_user_id, COUNT(amount) AS valid_amount, COUNT(create_date) AS valid_date, SUM(CASE WHEN status IS NULL OR status = '' THEN 1 ELSE 0 END) AS invalid_status_count FROM raw_orders;步骤2:制定清洗规则并创建干净表
-- 创建清洗后的目标表,字段均设为NOT NULL并设默认值 CREATE TABLE clean_orders ( order_id VARCHAR(20) NOT NULL PRIMARY KEY, user_id INT NOT NULL DEFAULT 0, -- 无效用户ID归为0(匿名用户) amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status ENUM('created', 'paid', 'shipped', 'completed', 'cancelled', 'invalid') NOT NULL DEFAULT 'invalid', create_date DATE NOT NULL, INDEX idx_date (create_date), INDEX idx_user (user_id) ); -- 执行数据清洗与导入 INSERT INTO clean_orders (order_id, user_id, amount, status, create_date) SELECT -- 1. 处理order_id:必须存在,去除空格 TRIM(COALESCE(order_id, '')) AS order_id, -- 2. 处理user_id:NULL或非正数视为无效匿名用户(0) COALESCE(NULLIF(user_id, 0), 0) AS user_id, -- 先处理0,再处理NULL -- 3. 处理amount:NULL或负数视为0 GREATEST(COALESCE(amount, 0), 0) AS amount, -- COALESCE处理NULL,GREATEST处理负数 -- 4. 处理status:映射并清理,无效状态默认为‘invalid’ CASE WHEN status IS NULL THEN 'invalid' WHEN LOWER(TRIM(status)) IN ('create', 'new') THEN 'created' WHEN LOWER(TRIM(status)) = 'pay' THEN 'paid' WHEN LOWER(TRIM(status)) IN ('complete', 'done') THEN 'completed' WHEN LOWER(TRIM(status)) IN ('cancel', 'cancelled') THEN 'cancelled' WHEN LOWER(TRIM(status)) IN ('ship', 'shipped') THEN 'shipped' ELSE 'invalid' END AS status, -- 5. 处理create_date:NULL或极早日期视为昨天(假设数据是近期补录的) COALESCE(NULLIF(create_date, '0000-00-00'), CURDATE() - INTERVAL 1 DAY) AS create_date FROM raw_orders -- 6. 最终过滤:order_id清洗后不能为空 WHERE TRIM(COALESCE(order_id, '')) != '';这个清洗脚本综合运用了COALESCE、NULLIF、CASE WHEN、TRIM、GREATEST等函数,并设定了明确的业务规则来处理各种NULL和无效值情况。
步骤3:基于清洗后数据生成报表
-- 生成每日有效订单(状态为'paid', 'shipped', 'completed')的统计报表 SELECT create_date AS `date`, COUNT(*) AS total_orders, COUNT(DISTINCT user_id) AS unique_customers, SUM(amount) AS total_gmv, AVG(amount) AS avg_order_value, -- 计算有购买行为的真实用户(user_id > 0)的平均客单价 SUM(CASE WHEN user_id > 0 THEN amount ELSE 0 END) / NULLIF(COUNT(DISTINCT CASE WHEN user_id > 0 THEN user_id END), 0) AS arpu FROM clean_orders WHERE status IN ('paid', 'shipped', 'completed') AND create_date BETWEEN '2023-10-01' AND '2023-10-31' GROUP BY create_date ORDER BY create_date;在计算arpu(每用户平均收入)时,我们再次使用了NULLIF来防止除零错误。COUNT(DISTINCT CASE WHEN ...)是一种常用的条件去重计数技巧。
通过这个完整的案例,你可以看到,对“空”和“非空”的严谨处理,是构建可靠数据管道和生成准确业务洞察的基石。它从最细微的字段定义开始,贯穿于数据清洗、转换、查询和分析的每一个环节。忽略它,你的数据就仿佛建立在流沙之上;掌握它,你才能从数据中挖掘出真正可信的价值。
