滴滴:分析顺风车订单中实际行驶时长比预估时长多 20% 以上的订单占比。
Didi: Analyze the percentage of Hitch orders where the actual trip duration is more than 20
考察要点
这道题面试官想看你是否懂:
- 业务逻辑转换能力:能否将“实际时长比预估时长多 20% 以上”这个业务需求,精确地翻译成 SQL 中的
WHERE或CASE WHEN条件。 - 条件聚合函数:是否熟练掌握使用
CASE WHEN结合COUNT或SUM在一次全表扫描中完成复杂统计,这是衡量 SQL 熟练度的重要指标。 - 数据精度与边界处理:是否注意到整数除法可能导致结果为 0 的问题,以及如何处理
NULL值或分母为 0 等异常情况。
核心概念
本题的核心是条件聚合(Conditional Aggregation),即在一次聚合查询中,根据不同条件计算多个指标。
-
CASE WHEN表达式: 这是实现条件聚合的基石。其标准语法如下:sql1CASE2 WHEN condition1 THEN result13 WHEN condition2 THEN result24 ...5 ELSE result_else6END在本题中,我们用它来“标记”出符合条件的订单。
-
结合聚合函数:
CASE WHEN可以无缝嵌套在COUNT,SUM,AVG等聚合函数内部。COUNT(CASE WHEN ... THEN 1 END):CASE表达式在满足条件时返回 1,否则返回NULL。COUNT函数只计算非NULL的值,因此这等价于计算满足条件的行数。这是最标准、最高效的写法。SUM(CASE WHEN ... THEN 1 ELSE 0 END):CASE表达式在满足条件时返回 1,不满足时返回 0。SUM将这些 1 和 0 加起来,同样得到满足条件的行数。性能上与COUNT写法几乎无差别。
-
浮点数除法: 在 SQL 中,两个整数相除的结果默认可能还是整数(取决于具体数据库实现,但为了可移植性和准确性,应始终注意)。例如
5 / 10结果是0。- 解决方案: 必须将分子或分母至少一个转换为浮点数或高精度小数。
- 乘以
1.0:COUNT(...) * 1.0 / COUNT(*) - 使用
CAST:CAST(COUNT(...) AS DECIMAL(10, 4)) / COUNT(*)
- 乘以
- 解决方案: 必须将分子或分母至少一个转换为浮点数或高精度小数。
-
数据表结构(假设): 面试时可以先和面试官确认表结构,如果未提供,可以主动提出一个合理的假设。 表名:
hitch_orders列名 类型 描述 order_idBIGINT订单唯一 ID estimated_durationINT预估行驶时长(分钟) actual_durationINT实际行驶时长(分钟) city_idINT城市 ID create_timeDATETIME订单创建时间
多种解法
解法 1:单次扫描 + 条件聚合(最优解)
这是最推荐的解法,因为它只需要对表进行一次扫描,性能最高,代码也最简洁。
- 思路: 使用
COUNT(CASE WHEN ...)计算分子,COUNT(*)计算分母,并在同一次查询中完成除法。 - 版本: ✅ 所有主流数据库版本(MySQL, PostgreSQL, Oracle, SQL Server)均支持。
- SQL 代码:
sql1SELECT2 COUNT(CASE WHEN actual_duration > estimated_duration * 1.2 THEN 1 END) * 100.0 / COUNT(*) AS overtime_percentage3FROM4 hitch_orders5WHERE6 -- 增加过滤条件保证数据质量,避免除零或负数等无效数据干扰7 estimated_duration > 0 AND actual_duration > 0;
- 时间复杂度: ,其中 N 是表
hitch_orders的行数。需要一次全表扫描。 - 空间复杂度: ,聚合计算仅需常数级别的内存。
- 索引依赖: 由于需要计算
COUNT(*),必须扫描全表,无法通过索引来显著优化此特定查询。即使(estimated_duration, actual_duration)上有索引,也无法避免全表扫描来获取总数。
解法 2:CTE(通用表表达式)+ AVG(代码可读性更佳)
当判断逻辑变得复杂时,使用 CTE 可以让 SQL 结构更清晰,更易于维护和调试。
- 思路: 先用一个
CTE将每一行标记为是否超时(1 或 0),然后在主查询中对这个标记列求平均值AVG(),结果乘以 100 就是百分比。 - 版本: ✅ MySQL 8.0+, PostgreSQL, Oracle, SQL Server 等支持
CTE的版本。 - SQL 代码:
sql1WITH OrderFlags AS (2 SELECT3 CASE4 WHEN actual_duration > estimated_duration * 1.2 THEN 15 ELSE 06 END AS is_overtime7 FROM8 hitch_orders9 WHERE10 estimated_duration > 0 AND actual_duration > 011)12SELECT13 AVG(is_overtime) * 100 AS overtime_percentage14FROM15 OrderFlags;
- 时间复杂度: 。数据库优化器通常会将此查询视作与解法 1 等效,执行计划基本一致,也是一次全表扫描。
- 空间复杂度: 。逻辑上存在中间表,但物理执行时通常是流式处理,不会物化整个
CTE。 - 索引依赖: 同解法 1,无法通过索引优化。
常见低分回答
-
整数除法导致结果错误:
sql1-- 错误示例:100 / 300 的结果是 0,而不是 0.33332SELECT COUNT(CASE WHEN actual_duration > estimated_duration * 1.2 THEN 1 END) * 100 / COUNT(*) FROM hitch_orders;错误原因:
COUNT()返回整数,* 100也是整数,两个整数相除结果被截断为整数 0。没有正确处理浮点数运算。 -
错误使用
WHERE子句:sql1-- 错误示例:这只计算了超时订单的数量,而不是占比2SELECT COUNT(*) FROM hitch_orders WHERE actual_duration > estimated_duration * 1.2;错误原因: 逻辑混淆。将筛选条件放在
WHERE中,导致COUNT(*)计算的是分子(超时订单数),而不是分母(总订单数)。 -
多次查询,效率低下:
sql1-- 错误示例:在应用代码中执行两次查询2// 第一次查询3SELECT COUNT(*) FROM hitch_orders WHERE actual_duration > estimated_duration * 1.2;4// 第二次查询5SELECT COUNT(*) FROM hitch_orders;6// 在代码中计算百分比错误原因: 对同一张大表进行了两次全表扫描,网络开销和数据库负载都增加了一倍。面试官期望在数据库端用一条 SQL 完成计算。
-
忽略数据质量和边界条件:
sql1-- 有风险的示例2SELECT COUNT(CASE WHEN actual_duration / estimated_duration > 1.2 THEN 1 END) * 100.0 / COUNT(*) FROM hitch_orders;错误原因: 如果
estimated_duration存在为 0 的行,这将导致“division by zero”错误。虽然题目给的条件actual_duration > estimated_duration * 1.2巧妙地避免了这个问题,但如果候选人改写成除法形式,就必须考虑分母为 0 的情况,并使用NULLIF(estimated_duration, 0)或WHERE estimated_duration > 0来规避。
工程实践与优化
- 数据量级问题: 当
hitch_orders表达到十亿、百亿级别时,每次都进行全表扫描是不可接受的,会导致查询缓慢并消耗大量数据库资源。 - 解决方案:
- 预聚合/物化视图: 这是最常见的
OLAP解决方案。创建一个每日(或每小时)的聚合结果表dws_hitch_order_kpi。通过定时任务(如 Airflow, DolphinScheduler)在业务低峰期运行聚合 SQL,将结果(如总订单数、超时订单数、城市等维度)写入该表。线上的查询直接从这个小得多的结果表中获取数据,响应时间可以做到毫秒级。 - 采样查询: 如果不需要 100% 精确的结果,只是为了趋势分析,可以进行采样。
注意:sql1-- MySQL 中一种不精确但快速的采样方式2SELECT AVG(CASE WHEN actual_duration > estimated_duration * 1.2 THEN 1 ELSE 0 END) * 1003FROM hitch_orders WHERE order_id % 100 = 1; -- 假设 order_id 分布均匀,进行 1% 采样
WHERE RAND() < 0.01在 MySQL 中性能很差,因为它需要为每一行计算RAND(),无法有效利用索引。 - 使用列存数据库/
OLAP引擎: 对于海量数据的分析场景,应使用专门的OLAP系统,如 ClickHouse, Doris, StarRocks。它们的列式存储和并行计算能力天生适合这类全列扫描的聚合查询,性能比行存的 MySQL 高出几个数量级。
- 预聚合/物化视图: 这是最常见的
- 执行计划分析:
- 对于解法 1 或 2,在 MySQL 中
EXPLAIN会显示:type: ALL:表示全表扫描。Extra: Using where:表示应用了WHERE子句中的过滤条件。
- 这是符合预期的,因为没有索引可以完全覆盖这个查询。优化的重点不在于 SQL 本身,而在于改变数据架构(预聚合)或更换技术栈(
OLAP引擎)。
- 对于解法 1 或 2,在 MySQL 中
常见追问与延伸题目
-
追问:如果要按城市分析这个超时订单占比,SQL 该怎么写?
- 回答要点: 这考察的是
GROUP BY的使用。我们只需在查询中加入city_id列,并在末尾添加GROUP BY city_id即可。 - SQL 示例:
sql1SELECT2 city_id,3 COUNT(CASE WHEN actual_duration > estimated_duration * 1.2 THEN 1 END) * 100.0 / COUNT(*) AS overtime_percentage4FROM5 hitch_orders6WHERE7 estimated_duration > 0 AND actual_duration > 08GROUP BY9 city_id10HAVING11 COUNT(*) > 1000; -- 可以增加 HAVING 子句过滤掉订单量过少的城市,使统计更有意义
- 回答要点: 这考察的是
-
追问:如果数据量增长 100 倍,达到百亿级别,你这条 SQL 会有什么问题?怎么优化?
- 回答要点:
- 问题: 单机 MySQL 无法在可接受的时间内完成百亿级表格的全表扫描。查询会非常慢(可能数小时),并可能拖垮整个数据库实例。
- 优化方案:
- 架构层面: 引入
OLAP解决方案。将数据从业务库(如 MySQL)通过 ETL 同步到 ClickHouse、Doris 或大数据平台(Hive/Spark)。在这些专为分析设计的系统上,百亿级聚合查询可以在秒级或分钟级完成。 - 应用层面: 如果不能引入新组件,唯一的方法是预计算。创建一张日维度的城市级聚合表
daily_city_hitch_summary,包含dt,city_id,total_orders,overtime_orders等字段。每天凌晨定时任务计算前一天的数据并插入此表。查询时直接查这张汇总表,即使原始数据有百亿行,汇总表也只有城市数量 * 天数行,查询极快。
- 架构层面: 引入
- 回答要点:
-
追问:如果
actual_duration和estimated_duration列存在NULL值,你的查询会如何处理?这符合预期吗?- 回答要点:
- SQL 的行为: 在
CASE WHEN actual_duration > estimated_duration * 1.2 THEN 1 END中,如果actual_duration或estimated_duration为NULL,那么>比较的结果是UNKNOWN(等同于NULL),CASE表达式的结果也是NULL。COUNT(NULL)的结果是 0,所以这些行不会被计为分子(超时订单)。同时,COUNT(*)会把这些行计入分母。 - 是否符合预期: 这取决于业务需求。通常,数据不完整的订单应该被排除在统计之外,即分子和分母都不应包含它们。
- 修正后的查询: 应该在
WHERE子句中明确排除NULL值,这比依赖CASE的隐式行为更清晰、更安全。sql1SELECT2 COUNT(CASE WHEN actual_duration > estimated_duration * 1.2 THEN 1 END) * 100.0 / COUNT(*) AS overtime_percentage3FROM4 hitch_orders5WHERE6 actual_duration IS NOT NULL7 AND estimated_duration IS NOT NULL8 AND estimated_duration > 0;
NULL语义的深刻理解和代码的严谨性。 - SQL 的行为: 在
- 回答要点: