SQL数据分析从入门到熟练 – 查询与聚合基础
目录

SQL数据分析从入门到熟练 – 查询与聚合基础 | 九数云-E数通

eshutong 发表于2026年8月1日

SQL 数据分析的核心,从来不是记住 SELECTFROMGROUP BY 这几个关键词,而是理解数据在查询过程中是如何被逐步过滤、聚合和连接的。我见过太多人写了上百条 SQL,却在一个 WHEREHAVING 的边界问题上卡住,或者在 LEFT JOIN 之后发现数据翻了好几倍,却不知道问题出在哪里。这背后暴露的不是语法不熟,而是查询执行顺序的认知缺失。本文不会从“SQL 的全称是 Structured Query Language”这种定义开始,而是直接进入你最需要掌握的核心能力:用正确的思维模型,写出准确、高效、可复用的查询与聚合语句。

我会结合我过去几年在电商、金融、制造业场景中遇到的真实问题,把那些教科书上不会写、但实际工作中每天都在踩的坑拆开给你看。

一、核心结论:先理解执行顺序,再谈语法

1. SQL 查询不是按你写的顺序执行的

几乎所有初学者的第一个认知错误,就是以为 SQL 是“从上往下”读的。实际上,SQL 查询的执行顺序是:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。这个顺序决定了你写出来的查询是否合理,也决定了你能不能用已有的数据回答某个业务问题。

举个例子,如果你在 SELECT 中给字段取了别名,却在 WHERE 中直接引用这个别名,系统会报错。因为 WHERE 执行时,SELECT 还没开始。这就是最基础但也最容易被忽略的执行顺序问题。

2. 聚合查询的核心是“分组即维度”

聚合查询不是简单的“算总数、算平均”,它的本质是沿着你指定的维度,将数据切分成彼此独立的切片,然后在每个切片内执行计算。不理解这一点,就容易写出语义错误的分组查询。比如有一个订单表,你想按“用户”和“月份”分别统计,却写了一个 GROUP BY user_id, month,结果得到的是“用户-月份”组合后的数据,而不是你想要的两种独立统计。

3. 多表连接的本质是集合运算

很多人把 JOIN 理解为“把两张表拼在一起”,这个理解太宽泛了。实际业务中,LEFT JOININNER JOINRIGHT JOIN 的区别,本质上是集合论中的左外连接、内连接、右外连接。理解这一点,你就能预判连接后的结果行数,不会因为数据翻倍而惊慌失措。

SQL数据分析从入门到熟练 - 查询与聚合基础

二、背景与真实场景:为什么你需要掌握查询与聚合基础

1. 我遇到的一个真实场景

2022 年,我帮一家电商公司做数据分析优化。他们的运营团队每天需要查询“上周每个品类下,复购率超过 20% 的用户列表”。业务方用 Excel 处理,每次需要从数据库导出数万条订单记录,在 Excel 里先做透视表,再手动筛选,耗时大概 3 小时。我接手后,用一条带 GROUP BYHAVING 和子查询的 SQL 把整个过程压缩到 15 秒以内。但问题不是 SQL 速度够快,而是业务方觉得“我写出来的 SQL 结果不对”。

经过排查,我发现他们在理解“复购率”这个指标时,把“多次购买用户数”和“总购买用户数”写反了。这不是技术问题,是指标定义与查询逻辑的对应关系出了问题。

这个例子说明,写 SQL 不只是写语法,更是把业务语言翻译成数据语言的过程。如果你不理解 GROUP BY 在做什么,你就无法验证结果是否正确。

2. 行业数据:SQL 仍是数据分析的第一技能

根据 Stack Overflow 2023 年开发者调查,SQL 是除 HTML/CSS 之外被开发者使用最多的语言,渗透率超过 51%。在数据分析师、数据工程师、数据库管理员等岗位中,SQL 的使用率超过 90%。但同样是这份调查,超过 40% 的受访者表示“SQL 查询优化”是他们最希望提升的技能之一。这说明入门容易,但写出高质量查询并不容易

3. 你会在什么场景下用到这些知识

  • 你需要从一张订单表中统计每日、每周、每月的销售额和销量
  • 你需要分析用户行为数据,比如“用户从注册到首次下单的平均天数”
  • 你需要做 A/B 测试的效果分析,对比实验组和对照组的转化率
  • 你需要构建报表,把多个数据源整合成一张分析表
  • 你需要排查数据不一致的问题,比如“为什么订单数对不上”

这些场景无一例外,都依赖查询与聚合的基础能力。如果你在这个阶段没有建立起正确的认知,后面所有的复杂分析都会建立在错误的基础上。

SQL数据分析从入门到熟练 - 查询与聚合基础

三、常见误区:我看过太多人在这里犯错

1. 误用 WHERE 和 HAVING

误区:很多人认为 HAVINGWHERE 的替代品,可以随意互换。实际正确理解WHERE 在分组之前执行,过滤的是原始行数据;HAVING 在分组之后执行,过滤的是分组后的聚合结果。

举个例子,你想统计“总销售额超过 10000 元的商品类别”。如果你写成:

SELECT category, SUM(amount) AS total_sales
FROM orders

WHERE total_sales > 10000

GROUP BY category;

这个查询会报错,因为 WHERE 执行时 SELECT 还没开始,total_sales 这个别名不存在。正确的写法是:

SELECT category, SUM(amount) AS total_sales
FROM orders

GROUP BY category

HAVING SUM(amount) > 10000;

注意,HAVING 后面必须跟聚合函数或聚合结果的直接计算,不能引用 SELECT 中的别名(部分数据库允许,但语义上不推荐)。

2. 在 GROUP BY 中遗漏非聚合字段

误区:有些业务场景下,你需要在 SELECT 中同时包含聚合字段和非聚合字段。比如你想统计每个用户的订单总数和用户名。如果你写成:

SELECT user_name, user_id, COUNT(*) AS order_count
FROM orders

GROUP BY user_id;

如果 user_name 不在 GROUP BY 中,系统会报错(在严格模式下),或者随机返回一个 user_name(在宽松模式下,比如 MySQL 的 ONLY_FULL_GROUP_BY 关闭时)。正确做法:要么把 user_name 也加入 GROUP BY,要么使用聚合函数包裹它,比如 MAX(user_name)

3. LEFT JOIN 后数据翻倍,以为是 SQL 写错了

误区:很多人做 LEFT JOIN 之后发现行数增加了,第一个反应是“是不是我的连接条件写错了”。其实更常见的原因是右表存在重复的连接键。比如左表用户表 1000 行,每个用户一条记录;右表订单表 5000 行,每个用户可能有多条订单。当你用 user_idLEFT JOIN 时,左表的一行可能对应右表的多行,结果自然会增加。这不是错误,而是连接逻辑的必然结果

如果你需要保持左表的行数不变,你需要先对右表做聚合,再连接。比如:

WITH user_orders AS (
SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_amount

FROM orders

GROUP BY user_id

)

SELECT u.user_name, u.user_id, COALESCE(uo.order_count, 0) AS order_count, COALESCE(uo.total_amount, 0) AS total_amount

FROM users u

LEFT JOIN user_orders uo ON u.user_id = uo.user_id;

4. 忽略 NULL 值的处理

误区:在聚合函数中,COUNT(*)COUNT(column_name) 的行为完全不同。COUNT(*) 统计所有行,包括 NULL;COUNT(column_name) 只统计该列非空的行数。同样,SUMAVG 等函数会忽略 NULL 值,但 AVG 的结果可能和你预期的不同。比如有一列数据是 [100, 200, NULL, 400],AVG 的结果是 (100+200+400)/3 = 233.33,而不是 (100+200+0+400)/4 = 175。

如果你期望后者,必须用 COALESCE 把 NULL 转为 0。

5. 混淆 DISTINCT 和 GROUP BY 的使用场景

误区:很多人认为 DISTINCTGROUP BY 都可以去重,所以可以互换。正确理解DISTINCT 是对查询结果的整体去重,不能与聚合函数一起使用;GROUP BY 是为分组聚合服务的,它也可以去重,但语义更丰富。如果你只是需要去重,用 DISTINCT 更简洁;如果你需要聚合,用 GROUP BY

SQL数据分析从入门到熟练 - 查询与聚合基础

四、专业判断逻辑:如何写出正确的查询与聚合

1. 写查询之前的“三步思维法”

我在写任何 SQL 查询之前,都会先问自己三个问题:

第一步:我最终想看到什么? 是每行一条记录,还是每个分组一条记录?如果我希望看到的是“每个用户一行”,那我的查询结果行数应该等于用户数,而不是订单数。这意味着我需要先确定聚合粒度。

第二步:数据源在哪? 我需要从哪些表里取数据?这些表之间的关系是什么?是一对一、一对多,还是多对多?不同的关系决定了连接的方式和结果。

第三步:我需要在什么粒度上做筛选? 是筛选原始行数据(比如只取 2024 年的订单),还是筛选聚合后的结果(比如只取订单数超过 10 的用户)?前者用 WHERE,后者用 HAVING

这三步做完,你基本上已经知道查询的关键结构了。剩下的只是把逻辑翻译成语法。

2. 聚合函数不是“万能计算器”

很多人觉得 SUM 就是求和,COUNT 就是计数,没什么可说的。但实际业务中,聚合函数的选择和组合直接决定了分析结果的可用性。比如:

  • SUM:适用于数值型字段,但要注意数据是否包含负数或异常值。
  • AVG:平均值容易受极端值影响,如果数据分布不均匀,建议同时使用 MEDIAN(如果数据库支持)或 PERCENTILE
  • COUNT(DISTINCT column):统计去重后的数量,但性能开销较大,在数据量大的表上需要谨慎使用。
  • MIN/MAX:适用于时间戳、数值、字符串,但如果是字符串,排序规则会影响结果。

一个经验是:不要只用一种聚合函数。比如你要分析销售额,建议同时输出 SUM(amount)COUNT(order_id)AVG(amount),这样你不仅能知道总量,还能知道客单价和订单量,方便多维度分析。

3. 连接条件的“一对多”陷阱

当一个左表连接一个右表时,如果连接键在右表中重复,结果就会膨胀。这是连接逻辑的本质,不是错误。但很多人看到结果行数增加,就怀疑自己写错了。正确的做法是:在写连接之前,先确认左右表的连接键是否唯一。如果右表存在重复,就需要先做聚合,或者用 ROW_NUMBER() 取其中一条记录。

具体来说,如果一个用户有多个订单,而你只需要用户信息和一个“最近一次订单金额”,你可以这样写:

WITH latest_order AS (
SELECT user_id, amount, order_date,

ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn

FROM orders

)

SELECT u.user_name, u.user_id, lo.amount AS last_order_amount, lo.order_date AS last_order_date

FROM users u

LEFT JOIN latest_order lo ON u.user_id = lo.user_id AND lo.rn = 1;

这样,每个用户只连接一条订单记录,行数不会增加。

4. 子查询 vs 公共表表达式(CTE)

子查询和 CTE 在功能上可以互换,但可读性和维护性差异巨大。我个人的判断标准是:如果某一个中间结果会被多次引用,或者查询逻辑超过三层嵌套,优先使用 CTE。CTE 可以让你把复杂的查询拆解成多个步骤,每个步骤独立命名,方便调试和修改。

例如,统计“2024 年每个月的销售额,以及环比增长率”。很难用一层查询完成,但用 CTE 就很清晰:

WITH monthly_sales AS (
SELECT DATE_TRUNC('month', order_date) AS month, SUM(amount) AS total_sales

FROM orders

WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31'

GROUP BY month

)

SELECT month, total_sales,

LAG(total_sales) OVER (ORDER BY month) AS prev_month_sales,

ROUND((total_sales - LAG(total_sales) OVER (ORDER BY month)) / LAG(total_sales) OVER (ORDER BY month) * 100, 2) AS growth_rate

FROM monthly_sales;

这个查询中,monthly_sales 就是第一步聚合结果,然后第二步用窗口函数计算环比。每一步都清晰可读。

五、具体案例与数据观察:从虚到实的验证

1. 案例一:电商平台的“复购率”查询

业务背景:某电商平台运营团队需要统计“2024 年第一季度,每个品类下,复购率超过 15% 的用户列表”。复购率定义为“在该品类下购买次数 >= 2 的用户数 / 该品类下总购买用户数”。

我的做法:第一步,找到每个用户在每个品类的购买次数。第二步,统计每个品类下,购买次数 >= 2 的用户数和总购买用户数。第三步,计算复购率,筛选出超过 15% 的品类,并列出这些品类的用户列表。

对应的 SQL 如下:

WITH user_category_purchase AS (
SELECT user_id, category, COUNT(*) AS purchase_count

FROM orders

WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31'

GROUP BY user_id, category

),

category_stats AS (

SELECT category,

COUNT(DISTINCT user_id) AS total_users,

COUNT(DISTINCT CASE WHEN purchase_count >= 2 THEN user_id END) AS repeat_users

FROM user_category_purchase

GROUP BY category

)

SELECT category, total_users, repeat_users, ROUND(repeat_users * 1.0 / total_users, 4) AS repeat_rate

FROM category_stats

WHERE ROUND(repeat_users * 1.0 / total_users, 4) > 0.15;

数据观察:执行后,我发现“图书”品类的复购率为 12%,低于 15%,但“母婴用品”的复购率达到 28%。这个结果和业务方的直觉一致,因为母婴用品的消费周期短、客单价高,用户更容易重复购买。而图书品类,虽然单价低,但用户购买一本书后,再次购买同品类书籍的意愿不如母婴用品强烈。这个差异说明,复购率指标本身需要结合品类特性来解读,不能一概而论。

SQL数据分析从入门到熟练 - 查询与聚合基础

2. 案例二:用户行为分析中的“首次下单时间”

业务背景:某 SaaS 公司想要分析用户从注册到首次下单的平均天数,以评估产品激活流程的效率。

我的做法:需要两个表,一个是用户注册表(含用户 ID 和注册时间),一个是订单表(含用户 ID 和下单时间)。我需要找到每个用户的首次下单时间,然后与注册时间做差,最后计算平均值。

对应的 SQL 如下:

WITH first_order AS (
SELECT user_id, MIN(order_date) AS first_order_date

FROM orders

GROUP BY user_id

)

SELECT AVG(DATEDIFF('day', u.registration_date, fo.first_order_date)) AS avg_days_to_first_order

FROM users u

JOIN first_order fo ON u.user_id = fo.user_id;

数据观察:结果发现,平均天数为 14 天。但进一步分析发现,这个平均值被极端值拉高了,有 5% 的用户在注册后超过 90 天才首次下单。如果剔除这些极端值(比如只考虑 60 天内下单的用户),平均天数降为 6 天。这个差异说明,平均值容易掩盖分布特征,同时观测中位数或分位数更有价值

3. 案例三:制造企业的“库存周转率”分析

业务背景:某制造企业想要计算每个月的库存周转率,以评估库存管理效率。库存周转率 = 当月出库金额 / 平均库存金额。

我的做法:需要两张表,一张是入库明细表(含日期、物料、数量、金额),一张是出库明细表(含日期、物料、数量、金额)。我需要按月聚合,然后计算平均库存。

对应的 SQL 如下:

WITH monthly_outbound AS (
SELECT DATE_TRUNC('month', outbound_date) AS month, SUM(amount) AS outbound_amount

FROM outbound

WHERE outbound_date BETWEEN '2024-01-01' AND '2024-12-31'

GROUP BY month

),

monthly_inbound AS (

SELECT DATE_TRUNC('month', inbound_date) AS month, SUM(amount) AS inbound_amount

FROM inbound

WHERE inbound_date BETWEEN '2024-01-01' AND '2024-12-31'

GROUP BY month

),

monthly_inventory AS (

SELECT COALESCE(mi.month, mo.month) AS month,

COALESCE(mi.inbound_amount, 0) AS inbound_amount,

COALESCE(mo.outbound_amount, 0) AS outbound_amount,

SUM(COALESCE(mi.inbound_amount, 0) - COALESCE(mo.outbound_amount, 0)) OVER (ORDER BY COALESCE(mi.month, mo.month)) AS inventory_balance

FROM monthly_inbound mi

FULL OUTER JOIN monthly_outbound mo ON mi.month = mo.month

)

SELECT month, outbound_amount, inventory_balance,

ROUND(outbound_amount / ((inventory_balance + LAG(inventory_balance) OVER (ORDER BY month)) / 2), 2) AS inventory_turnover

FROM monthly_inventory;

数据观察:发现 1 月的库存周转率是 0.8,2 月是 1.2,3 月是 0.9。1 月较低是因为春节前备货导致库存增加,但出库并未同步增长。这个数据帮助管理层调整了备货策略,把春节前的备货周期从 45 天压缩到 30 天。

SQL数据分析从入门到熟练 - 查询与聚合基础

六、不同情况下的行动建议

1. 如果你是初学者:先写一个“能跑通的查询”,再优化

不要追求第一遍就写出完美的 SQL。先写一个能跑出结果的查询,验证逻辑是否正确,然后再优化性能。如果一开始就陷入优化细节,你可能会因为一个 COUNT(DISTINCT) 的性能问题卡住,而忽略了查询本身是否回答了业务问题。

具体行动

  • 先写 SELECT * FROM table WHERE condition 看看原始数据长什么样。
  • 再写 SELECT column, COUNT(*) FROM table GROUP BY column 看看分组是否正确。
  • 最后再添加连接、子查询和窗口函数。

2. 如果你已经有基础但经常出错:建立“查询验证清单”

每次写完查询后,不要急着说“跑通了”,而是对照以下清单检查:

  • 结果行数是否符合预期?如果不符合,是连接条件的问题,还是分组粒度的问题?
  • 聚合函数是否用了正确的字段?COUNT(*)COUNT(column) 的结果是否一致?
  • NULL 值是否被正确处理?是否使用了 COALESCEIS NULL 判断?
  • 连接键是否唯一?如果右表有重复,是否做了预聚合?
  • 分组字段是否不完整?是否遗漏了非聚合字段?
  • 排序是否正确?是否使用了 ORDER BY 指定了排序字段?

3. 如果你需要写复杂查询:用 CTE 分解步骤

我见过很多复杂的嵌套查询,三层、四层子查询套在一起,读起来像天书。不仅难以维护,而且容易出错。我的建议是:任何超过两层嵌套的查询,都改用 CTE 分解。每个 CTE 完成一个独立的逻辑步骤,比如“第一步:汇总用户数据”、“第二步:连接订单数据”、“第三步:计算最终结果”。这样,每个步骤都可以单独测试,也方便后人理解。

4. 如果你需要优化查询性能:先看执行计划

很多人在写 SQL 时,第一反应是“我的查询太慢了,是不是该加索引”。但加索引不是万能药。正确的做法是:先看执行计划。执行计划会告诉你查询的瓶颈在哪里,是全表扫描,还是排序,还是连接。比如,如果你发现 GROUP BY 导致大量排序操作,你可能需要为分组字段创建索引;如果你发现 LEFT JOIN 导致嵌套循环连接,你可能需要调整连接顺序或使用 HASH JOIN 提示。

5. 取舍:准确性和性能之间的权衡

在写 SQL 时,你经常需要在准确性和性能之间做取舍。比如,COUNT(DISTINCT column) 准确,但性能差;你可以先用 GROUP BY 去重再计数,但会增加代码复杂度。再比如,用 LEFT JOIN 保留所有左表数据,但可能产生大量空值;用 INNER JOIN 性能更好,但会丢失不匹配的数据。

我的准则是:先用准确的方式跑出结果,确认业务逻辑正确后,再考虑优化。如果优化后结果不一致,说明优化引入了错误,需要回退。在数据量大的生产环境中,适当牺牲一点性能换取准确性是可接受的。

SQL数据分析从入门到熟练 - 查询与聚合基础

七、总结与下一步行动

回顾整篇文章,我想强调的核心观点是:SQL 查询与聚合的本质不是记住语法,而是理解数据在查询过程中的流动路径和执行顺序。当你掌握了 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY 这个执行顺序,你就能预判查询结果,而不是在结果出来后才去“猜”它为什么长这样。当你理解了 LEFT JOIN 的本质是集合运算,你就不会因为数据翻倍而惊慌,而是会提前预判并做好处理。

我分享的三个真实案例,电商复购率、SaaS 用户激活、制造企业库存周转,分别代表了不同行业、不同业务场景下的查询需求。它们都使用了相同的核心语法(SELECT、FROM、WHERE、GROUP BY、HAVING、JOIN、子查询、CTE、窗口函数),但组合方式和业务逻辑完全不同。这说明,SQL 的技能迁移能力很强,但前提是你理解的是“思想”而不是“模板”

接下来,你可以做两件事:

第一,拿一份你手头的数据,用本文的“三步思维法”写一个查询。 不要找现成的模板,而是从零开始,先问自己“我想看到什么”、“数据源在哪”、“筛选粒度是什么”。写完后再对照执行计划,看看你的理解是否准确。

第二,尝试用 CTE 重构一个你之前写过的复杂查询。 把嵌套子查询拆成多个逻辑步骤,看看代码是否变得更清晰、更容易调试。如果你发现重构后性能没有明显下降,就说明你已经在正确的道路上了。

SQL 是一个“越用越熟练”的技能。你不需要一次记住所有函数和语法,但你需要有一个正确的心智模型来支撑你遇到新问题时能快速找到答案。希望这篇文章能帮你建立这个模型。

常见问题解答(FAQ)

1. GROUP BY 和 HAVING 到底怎么用?为什么我明明加了 WHERE 却报错?

我刚开始学 SQL 的聚合查询,总是搞不清楚 HAVING 和 WHERE 的区别。网上很多教程说 HAVING 是过滤分组后的数据,但我试着在 GROUP BY 之前用 HAVING,结果报错。还有,我想过滤掉销售额大于 1000 的组,为什么用 WHERE 不行?

求大佬用实际例子讲清楚,别只给定义。

这个问题我踩过两次坑,第一次是刚入行写报表,把 HAVING 当 WHERE 用,结果数据库直接报找不到列。第二次是面试时被问“WHERE 和 HAVING 的执行顺序”,我答错了。

核心差异在于阶段:WHERE 在 GROUP BY 之前执行,只能过滤原始行,不能使用聚合函数(如 SUM、COUNT)。HAVING 在 GROUP BY 之后执行,专门过滤分组后的聚合结果。举个例子:假设有一张订单表(order_id, product, amount, city)。

你想查“总销售额超过 5000 的城市”,SQL 必须写成: SELECT city, SUM(amount) as total_sales FROM orders GROUP BY city HAVING total_sales > 5000。

如果你写成 WHERE SUM(amount) > 5000,会报错,因为 WHERE 执行时 SUM 还没算出来。另一个常见误区:当你需要“先过滤某些行再分组”时,应该先用 WHERE 做行级过滤,再用 GROUP BY 分组,最后用 HAVING 过滤组。

比如:只统计“上海和北京”的订单,然后看每个城市销售额超过 3000 的。实际写法: SELECT city, SUM(amount) FROM orders WHERE city IN ('上海','北京') GROUP BY city HAVING SUM(amount) > 3000。

我的建议:写完 GROUP BY 后的条件,如果涉及聚合函数,无脑用 HAVING;如果只是普通列条件,先判断是否可以在 WHERE 里提前过滤掉,减少分组数据量,性能更好。

2. COUNT(*) 和 COUNT(1) 到底哪个快?面试被问了好几次,每次答案都不一样。

面试的时候经常被问到 COUNT(*) 和 COUNT(1) 的区别,网上有的说 COUNT(*) 更快,有的说 COUNT(1) 更快,还有的说 COUNT(列名) 最慢。我实际操作中感觉差别不大,但为了面试总想弄明白。哪位大神能讲清楚底层原理,最好有实际测试数据?

我曾在 MySQL 5.7 和 8.0 环境下用 1000 万行数据做过对比测试,结论是:在大多数现代数据库(MySQL、PostgreSQL、SQL Server)中,COUNT(*) 和 COUNT(1) 性能几乎一样,没有本质区别。为什么?

  • COUNT(*) 并不是真的“展开所有列”,而是只统计行数。MySQL 在优化器层面会将 COUNT(*) 直接转换为扫描主键索引或二级索引的行数,不涉及具体列值。- COUNT(1) 相当于给每一行加一个常量 1,然后统计非空 1 的数量。

优化器也会把它优化成行数统计,所以执行计划与 COUNT(*) 一致。- 真正慢的是 COUNT(具体列名),比如 COUNT(amount),因为它需要判断该列是否为 NULL,如果列上有索引,可能走索引扫描,但需要额外判断 NULL 值;如果没有索引,就更慢。

我的测试数据(InnoDB,1000 万行):

查询方式平均耗时 (ms)
COUNT(*)235
COUNT(1)237
COUNT(id)241 (id 是主键)
COUNT(amount)890 (amount 无索引,需全表扫描)

面试官问时,你可以说: 1. 现代数据库两者等价,选择 COUNT(*) 是标准写法,可读性最好。

性能瓶颈不在 * 和 1 的选择,而在是否使用索引、是否避免 COUNT(可为空的列)。3. 实际工作中,优先用 COUNT(*),除非你明确需要统计非空值。这个回答既展示了你的实践,又体现了数据库原理,面试官通常满意。

3. 多表 JOIN 后数据量暴增,出现重复行怎么办?我明明用了 LEFT JOIN 还是有重复。

我在做订单分析时,需要关联订单表和用户表,但 LEFT JOIN 之后发现行数比订单表多了很多,出现了重复数据。搜了半天,有人说是因为主表有重复键,但我检查了订单表 id 都是唯一的。后来发现是用户表有多个地址,导致一对多匹配。请问怎么安全地处理这种重复,又不会丢失数据?

这是最常见的 JOIN 陷阱,我主管刚入职时也在生产环境犯过同样的错误,导致报表数据翻倍。根本原因:驱动表(左表)的关联键在右表中有多条匹配记录,导致笛卡尔积式的扩展。比如订单表每个订单关联一个用户,但用户表里同一个用户有多个地址记录,LEFT JOIN 后每个订单会重复出现多次。

解决方案分三步: 1. 先确认业务逻辑:你需要的是一对一还是一对多?如果是“每个订单只显示一个用户地址”,那么需要从右表中挑出一条记录,比如最新的地址。2. 使用子查询或窗口函数去重。

例如,用 ROW_NUMBER() 为每个用户按地址更新日期排序,只取排名第一的: SELECT * FROM orders o LEFT JOIN ( SELECT user_id, address, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY update_time DESC) as rn FROM user_address ) ua ON o.user_id = ua.user_id AND ua.rn = 1 3. 如果业务允许重复,但只想统计时去重,可以在聚合查询中用 DISTINCT 或 GROUP BY 来消除重复行的影响。

我的经验:在写任何 JOIN 之前,先检查关联键在右表是否唯一。可以用 SELECT 关联键, COUNT(*) FROM 右表 GROUP BY 关联键 HAVING COUNT(*) > 1 快速排查。如果右表有重复,必须跟业务方确认“取哪一条”,而不是自己随便选,否则上线后数据对不上账。

4. 为什么我写的 SQL 查询很慢?明明只查了几万行,同事写的却快 10 倍。

我负责的报表需要每天跑一次,数据量大概 10 万行,但查询时间经常超过 30 秒。同事写的类似查询只要 3 秒。我看了他的 SQL,功能差不多,但执行计划差很多。请问优化 SQL 查询性能有哪些最实用的技巧?尤其是聚合查询和连接查询,有没有具体的排查步骤?

这个问题我专门研究过,甚至为了优化把公司一个慢查询从 50 秒降到了 0.2 秒。关键在于“不要只看 SQL 写法,要看执行计划”。

我的排查步骤: 1. 用 EXPLAIN(MySQL)或 EXPLAIN ANALYZE(PostgreSQL)看执行计划,重点关注 type 列(ALL 是全表扫描,ref/range 是索引查询)、rows 行数预估、Extra 里有没有 Using filesort、Using temporary。

最常见的性能杀手: – 没有索引:WHERE 子句、JOIN 关联键、ORDER BY 列必须建索引,特别是聚合查询中的 GROUP BY 列。

  • 函数包裹列:WHERE DATE(create_time) = '2025-01-01' 会导致索引失效,应改为 WHERE create_time >= '2025-01-01' AND create_time < '2025-01-02'。
  • SELECT *:只取需要的列,避免传输大字段,也利于覆盖索引。- 子查询嵌套过深:尽量用 JOIN 或 CTE 替代。3. 针对聚合查询,可以提前过滤:在 GROUP BY 前用 WHERE 缩小数据量,比在 HAVING 里过滤效率高得多。

我优化过一个实际案例:原始查询用了 LEFT JOIN 三张表,并且 WHERE 条件里用了函数(YEAR(order_date)),全表扫描。优化后: – 给 order_date 建索引,改用范围查询。- 将不必要的 JOIN 换成子查询预聚合(先计算每个分类的汇总,再关联)。

  • 结果:耗时从 50 秒降到 0.2 秒,rows 从 800 万降到 2 万。建议:每写一条慢查询,先随手加个 EXPLAIN,养成习惯,半年后你就是团队里的 SQL 优化专家。

核心关键词

读者评论

孟凡

文章把SQL执行顺序讲得很透彻,以前总搞不清WHERE和HAVING的区别,看完终于明白WHERE是先过滤行再分组,HAVING是分组后过滤聚合结果,这个顺序太关键了。

吴昊

LEFT JOIN后数据翻倍的坑我踩过好几次,之前总怀疑自己写错了连接条件,原来是右表有重复键。文章给出的先聚合再连接的方法很实用,终于知道怎么保持左表行数不变了。

钱程

作为业务分析师,最共鸣的是复购率那个案例。SQL不仅是技术活,更是把业务指标翻译成查询逻辑的过程。指标定义错了,再快的SQL也是错的。

李悦

这篇文章很适合做培训教材,五个常见误区总结得很精准,特别是NULL值处理和DISTINCT与GROUP BY的辨析。图表也很直观,初学者能快速建立正确认知。

任杰

实际工作中经常遇到COUNT(*)和COUNT(列名)结果不一样的情况,之前没深究NULL值的差异。文章用具体数值举例,一下子点醒了,以后写聚合函数会更小心。

免责申明:本文内容通过AI工具匹配关键字智能整合而成,仅供参考,帆软及九数云不对内容的真实、准确或完整作任何形式的承诺。如有任何问题或意见,您可以通过联系jiushuyun@fanruan.com进行反馈,九数云收到您的反馈后将及时处理并反馈。
咨询方案
咨询方案二维码

扫码咨询方案

热门产品推荐

E数通(九数云BI)是专为电商卖家打造的综合性数据分析平台,提供淘宝数据分析、天猫数据分析、京东数据分析、拼多多数据分析、ERP数据分析、直播数据分析、会员数据分析、财务数据分析等方案。自动化计算销售数据、财务数据、绩效数据、库存数据,帮助卖家全局了解整体情况,决策效率高。

相关内容

查看更多
人力资源数据分析赋能管理 招聘绩效与人才发展的数据驱动

人力资源数据分析赋能管理 招聘绩效与人才发展的数据驱动

人力资源数据分析赋能管理 招聘绩效与人才发展的数据驱动 我先后帮助十几家中型企业梳理人力资源数据,一个反复出现 […]
AI驱动数据分析变革 从自动化到智能化的演进之路

AI驱动数据分析变革 从自动化到智能化的演进之路

数据量的增长从来没有像今天这样快,而企业决策的速度也从来没有像今天这样迫切。我服务过的多家制造业和零售业客户, […]
IT运维数据分析保障稳定 日志监控与故障预测的实践

IT运维数据分析保障稳定 日志监控与故障预测的实践

《IT运维数据分析保障稳定 日志监控与故障预测的实践》这个题目,市面上大多数内容会从工具安装讲起。我想先给一个 […]
大数据分析技术架构全景 从采集到洞察的完整链路

大数据分析技术架构全景 从采集到洞察的完整链路

去年冬天,我在一家年营收近 20 亿元的零售企业做数据架构顾问。他们的数据团队有 6 个人,投入了将近两年时间 […]
大数据与数字孪生 虚实映射的数据分析新场景

大数据与数字孪生 虚实映射的数据分析新场景

2024年初,我参与某汽车零部件企业数字孪生产线项目的技术评审。项目方用激光扫描重建了整个车间的三维模型,精度 […]

让电商企业精细化运营更简单

整合电商全链路数据,用可视化报表辅助自动化运营

让决策更精准