找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4607

积分

0

好友

604

主题
发表于 昨天 05:12 | 查看: 10| 回复: 0

问题1:LEFT JOIN后数据缺失(预期保留左表所有行,实际仅返回匹配行)

LEFT JOIN 后,我们使用 WHERE 子句对右表字段进行过滤(如:WHERE o.order_time >= '2024-01-01'),这会导致左表中未匹配到右表的记录被意外排除。这是因为 WHERE 子句在 JOIN 操作之后执行,此时右表的字段值为 NULL,不满足 WHERE 条件,从而被过滤掉。

正确做法:将针对右表的筛选条件移至 ON 子句中,确保在关联阶段就完成过滤,从而真正保留左表的全部行。如果需要同时对左表字段进行过滤(例如,只想查询活跃用户),该条件则应保留在 WHERE 子句中,因为它是对最终结果集的筛选,不影响 LEFT JOIN 的语义。

示例

错误写法

SELECT u.user_id, o.order_id
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_time >= '2024-01-01';

正确写法

SELECT u.user_id, o.order_id
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id AND o.order_time >= '2024-01-01'
WHERE u.user_status = 'active';

注意:如果右表字段本身有非空约束(NOT NULL),那么 ONWHERE 的效果可能一致。但在通用场景下,我们仍应严格遵循“筛选右表用 ON”的原则,以保证代码意图清晰和跨数据兼容性。

验证方法:可以分别执行 SELECT COUNT(*) FROM users WHERE user_status = 'active'SELECT COUNT(*) FROM (上述 LEFT JOIN 查询) AS t 进行对比,确认左表的有效行数没有减少。

问题2:GROUP BY后结果“随机”或报错

MySQL 中,当 sql_mode 未启用 ONLY_FULL_GROUP_BY 时,它允许 SELECT 列表中出现非 GROUP BY 字段,系统会返回该分组中某一行的值(看起来是“随机”的)。但从 MySQL 5.7 起,该模式默认开启,如果 SELECT 字段既不在 GROUP BY 中,又非聚合函数,则会直接报错

因此,常说的“升级后报错”需要修正为:“sql_mode 包含 ONLY_FULL_GROUP_BY 时会报错”,并非所有 5.7+ 版本的实例都必然报错。

解决方法:针对非分组字段,我们必须使用聚合函数(如 MAXMIN)或 GROUP_CONCAT 来合并多值。对于更复杂的“取整行”需求,在支持窗口函数的数据库(如 MySQL 8.0+PostgreSQL)中,可以使用 ROW_NUMBER() 来精准获取。

示例

错误写法(依赖数据库的非标准宽松模式)

SELECT user_id, order_id
FROM orders
GROUP BY user_id;

正确写法

-- 方法一:取最新订单ID(使用聚合函数)
SELECT user_id, MAX(order_id) AS latest_order_id
FROM orders
GROUP BY user_id;

-- 方法二:合并所有订单ID(适用于数据量小的场景)
SELECT user_id, GROUP_CONCAT(order_id ORDER BY order_time DESC) AS all_orders
FROM orders
GROUP BY user_id;

-- 方法三:精准取最新订单的整行信息(MySQL 8.0+ 使用窗口函数)
SELECT user_id, order_id, order_time
FROM (
    SELECT *,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn
    FROM orders
) t
WHERE rn = 1;

验证方法:结合业务逻辑,确认聚合结果是否符合预期。可以通过执行 SELECT user_id, COUNT(*) FROM orders GROUP BY user_id 来验证分组数量的一致性。

问题3:LIMIT OFFSET 分页越往后越慢

LIMIT 100000, 10 这样的查询,数据库需要先扫描并丢弃前 100000 行,这导致性能随着偏移量 (OFFSET) 的增大而急剧下降。

游标分页(也叫“seek method”,基于主键或唯一索引进行过滤)是高效的替代方案,但它有局限性:仅适用于按单调递增主键排序,且数据没有频繁删除的场景。如果主键不连续(如使用 UUID),或者需要按其他字段(如 order_time DESC)排序,就不能简单地用 id > last_id。此外,游标分页不支持跳页,只适用于“上一页/下一页”式的连续翻页。

更通用的优化建议

  • 场景1:按主键排序
    这是最理想的情况,可以直接使用游标分页。

    SELECT user_id, order_id
    FROM orders
    WHERE id > 100000
    ORDER BY id
    LIMIT 10;
  • 场景2:按非主键字段排序(如 order_time DESC
    此时可以采用“延迟关联” (deferred join) 技术,并结合覆盖索引来优化。

    SELECT o.user_id, o.order_id
    FROM orders o
    INNER JOIN (
        SELECT id
        FROM orders
        ORDER BY order_time DESC
        LIMIT 100000, 10
    ) tmp ON o.id = tmp.id
    ORDER BY o.order_time DESC;

    关键前提:必须在 (order_time, id) 上建立复合索引,且顺序为“排序字段在前,主键在后”。这样,子查询可以完全通过索引完成(无需回表),实现高效定位。
    重要提示:尽管子查询已经按 order_time DESC 排序并返回了有序的 id 列表,但 SQL 不保证 JOIN 操作会保留输入的顺序。因此,主查询中我们仍然需要显式使用 ORDER BY o.order_time DESC 来确保最终结果的顺序正确。即使 idorder_time 在业务上存在严格的单调关系,也建议保留此排序,以避免因执行计划变更或数据异常导致结果乱序。

验证方法:使用 EXPLAINEXPLAIN ANALYZE 查看执行计划,确认子查询是否使用了索引扫描(显示 Using index)。同时,对比查询不同页码时的执行时间,确保响应时间稳定(例如,始终小于 100ms)。

问题4:ORDER BY NULL 排序位置在不同数据库中不一致

这个问题的核心在于不同数据库对 ORDER BYNULL 值的默认处理方式不同:

  • MySQL 默认将 NULL 视为最小值(排在最前面)。
  • PostgreSQL 默认将 NULL 视为最大值(排在最后面)。

如果不加处理,跨数据库迁移或协作时,查询结果的排序就会出现差异。

解决方法一(推荐,遵循 PostgreSQL/Oracle/标准SQL):显式控制 NULL 的位置。

SELECT category_id, COUNT(*)
FROM products
GROUP BY category_id
ORDER BY category_id NULLS FIRST;  -- 或 NULLS LAST

解决方法二(追求跨库兼容):使用 COALESCE 函数将 NULL 替换为一个特定值。注意,替换值(如 -1)必须不在原字段的有效取值范围内,以免造成错误分组。

SELECT COALESCE(category_id, -1) AS cate_id, COUNT(*)
FROM products
GROUP BY COALESCE(category_id, -1)
ORDER BY cate_id;

解决方法三(MySQL 兼容性模拟):通过条件表达式来控制排序优先级。

-- 让NULL排在最后
SELECT category_id, COUNT(*)
FROM products
GROUP BY category_id
ORDER BY IF(category_id IS NULL, 1, 0), category_id;

-- 让NULL排在最前
ORDER BY IF(category_id IS NULL, 0, 1), category_id;

验证方法:在 MySQLPostgreSQL 中分别执行优化后的语句,确认 NULL 分组(或其替换值)的排序位置是否一致。

问题5:递归 CTE 导致 CPU 飙升或无限循环

根本原因通常是数据中存在环形引用(例如:部门 A 的父部门是 B,而部门 B 的父部门又是 A),导致递归无法自行终止。

一个常见的错误是使用 path NOT LIKE CONCAT('%', d.dept_id, '%') 来检测环路。这种方法存在子串误匹配风险,例如路径 '1,2,3' 会被误判为包含部门 ID 12。正确的做法是在路径的首尾都添加分隔符,以确保精确匹配。

正确写法(适用于 MySQL 8.0+ / PostgreSQL

WITH RECURSIVE dept_tree AS (
    SELECT
        dept_id,
        parent_id,
        CAST(CONCAT(',', dept_id, ',') AS VARCHAR(1000)) AS path,
        1 AS level
    FROM dept
    WHERE dept_id = 1 -- 递归起点

    UNION ALL

    SELECT
        d.dept_id,
        d.parent_id,
        CONCAT(t.path, d.dept_id, ','),
        t.level + 1
    FROM dept d
    INNER JOIN dept_tree t ON d.parent_id = t.dept_id
    WHERE t.path NOT LIKE CONCAT('%,', d.dept_id, ',%')
      AND t.level < 100 -- 设置递归深度上限,防止失控
)
SELECT dept_id, parent_id FROM dept_tree;

说明与注意事项

  1. 路径字段类型:使用 VARCHAR(1000) 而非 CHAR(255),因为固定长度的 CHAR 在路径过长时会被截断,可能导致环路检测失效。VARCHAR(1000) 的长度应根据业务中可能的最大层级进行预估(例如:100层 * 每个ID最多10位字符 + 分隔符 ≈ 1100字符)。
  2. 分隔符选择:如果部门 ID 本身是字符串且可能包含逗号(如 ”A,B”),则应选用一个绝对不会在 ID 中出现的字符作为分隔符,例如竖线 |、井号 # 或不可见字符 \x01
  3. 数据库支持:此语法需要数据库支持公用表表达式(CTE),MySQL 5.7 及以下版本不支持。
  4. 递归成员:递归部分必须使用 UNION ALL,且递归成员中只能引用一次 CTE 表名。

验证方法:执行查询时,应在合理时间内返回有限的结果集(例如,部门层级 ≤ 10),并且没有观察到数据库 CPU 或内存资源耗尽的现象。验证的标准是查询能够安全终止,且返回的层级结构符合业务预期。

掌握这些 SQL 查询的“潜规则”,能帮助你在 MySQLPostgreSQL数据库中进行技术文档编写和性能调优时更加得心应手,避免踩坑。如果你想深入学习更多数据库内核原理或实战案例,可以到技术社区交流和查找资料。




上一篇:AI编程助手如何处理超长任务?解析Cursor Composer的自总结训练
下一篇:C++内存管理:堆与栈的核心差异及性能分析 | 面试八股文深入解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 18:14 , Processed in 0.942235 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表