MySQL 常用sql注意坑点
1. LEFT JOIN ON 中左表条件"不生效"?
条件写到On里还是Where里,这是一个常见且容易误解的点。实际上左表条件不是不生效,而是生效的逻辑位置与预期不同。
一、现象重现
准备数据
-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY, -- 用户ID,主键
name VARCHAR(50) NOT NULL -- 用户名,非空
);
-- 插入数据
INSERT INTO users (id, name) VALUES
(101, 'Alice'),
(102, 'Bob'),
(103, 'Carol');
------------
-- 创建订单表
CREATE TABLE IF NOT EXISTS orders (
id INT PRIMARY KEY, -- 订单ID,主键
user_id INT NOT NULL, -- 关联的用户ID
amount DECIMAL(10, 2) NOT NULL -- 订单金额(保留2位小数)
);
-- 插入示例数据
INSERT INTO orders (id, user_id, amount) VALUES
(1, 101, 50.00),
(2, 102, 200.00),
(3, 103, 300.00);
查询示例
示例:查询订单和用户信息
-- 左表条件在 ON 中
SELECT *
FROM orders o
LEFT JOIN users u ON o.user_id = u.id AND o.amount > 100;
-- 对比 WHERE 中的写法
SELECT *
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.amount > 100; -- 左表条件在 WHERE 中
实际执行效果对比:
-- 查询1结果:左表条件在 ON 中
id | amount | name
1 | 50 | NULL -- amount=50不满足>100,但订单记录依然保留
2 | 200 | Bob -- 满足条件,正常连接
3 | 300 | Carol -- 满足条件,正常连接
看起来"不生效"的原因:
- 期望:过滤掉 amount ≤ 100 的订单
- 实际:amount ≤ 100 的订单仍然出现,只是对应的用户信息为 NULL
二、ON 中左表条件的真实作用
正确理解:ON 条件控制的是连接匹配,不是行过滤
-- LEFT JOIN 的逻辑分解
FROM orders o LEFT JOIN users u ON [条件]
-- 等价于:
1. 遍历 orders 的每一行
2. 检查 ON 条件:
- 如果条件为 TRUE:连接 users 的对应行
- 如果条件为 FALSE:users 所有列填 NULL
3. 无论 ON 条件结果如何,orders 的这一行都会出现在结果中
LEFT JOIN 的设计原则:永远保留左表所有行
-- 这是 LEFT JOIN 的核心语义
-- 无论 ON 条件是什么,左表的行必须出现
三、对比两种 JOIN 类型:
-- INNER JOIN:ON 中的左表条件会过滤行
SELECT * FROM orders o
INNER JOIN users u ON o.user_id = u.id AND o.amount > 100;
-- 结果:只有 amount>100 的订单出现
-- LEFT JOIN:ON 中的左表条件只影响是否连接右表
SELECT * FROM orders o
LEFT JOIN users u ON o.user_id = u.id AND o.amount > 100;
-- 结果:所有订单都出现,但只有 amount>100 的订单有用户信息
四、如何正确使用?
场景 1:想要过滤左表行 → 用 WHERE
-- 正确:只查询金额>100的订单及其用户
SELECT *
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.amount > 100; -- 这里过滤订单
场景 2:想要根据左表条件控制是否连接右表 → ON 中写左表条件
-- 适用场景:显示所有订单,但只连接大额订单的用户详情
SELECT
o.*,
CASE WHEN o.amount > 100 THEN u.detail ELSE '标准用户' END as user_detail
FROM orders o
LEFT JOIN user_details u ON o.user_id = u.id AND o.amount > 100;
-- 这里:所有订单显示,但只有大额订单显示详细用户信息
场景 3:复杂条件判断
-- 示例:根据订单类型决定连接逻辑
SELECT *
FROM orders o
LEFT JOIN premium_users pu ON o.user_id = pu.user_id AND o.type = 'premium'
LEFT JOIN regular_users ru ON o.user_id = ru.user_id AND o.type = 'regular';
-- 每种类型的订单只连接对应的用户表
黄金法则:
- 要过滤左表行 → 用 WHERE
- 要控制是否连接右表(基于左表值)→ 用 ON 中的左表条件
- 大多数情况下,左表过滤条件应该放在 WHERE 中
2. group by后取的不是对应行的完整记录?
一、先看:直接用 GROUP BY 为什么不行?
CREATE TABLE `user_records` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`operate_time` datetime DEFAULT CURRENT_TIMESTAMP,
`operate_content` varchar(255) DEFAULT NULL COMMENT '操作内容描述',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户操作记录表';
-- 写入数据
INSERT INTO user_records (user_id, operate_content) VALUES
(100, '登录系统'),
(100, '修改密码'), -- id=2(user_id=100的最大id)
(101, '查看订单'),
(101, '提交退款'); -- id=4(user_id=101的最大id);
如果直接写这样的SQL:
-- 错误示范:看似能查最大id,但无法直接获取完整记录,直接报错
SELECT user_id, MAX(id) AS max_id, operate_content
FROM user_records
GROUP BY user_id;
核心问题:
GROUP BY user_id会把同一user_id的所有行合并成一行,- 聚合函数(如
MAX(id))能计算出该组的最大值;
但 GROUP BY 只能拿到「分组字段 + 聚合结果」,但拿不到「聚合结果对应的完整行数据」。
二、需要 JOIN —— 找回完整行
子查询 SELECT user_id, MAX(id) AS max_id FROM user_records GROUP BY user_id 的作用是:
精准拿到「每个user_id对应的最大id」(这是一组“正确的键值对”)。
但我们需要的是「这个最大id对应的整行记录」,所以需要:
- 用子查询得到“正确的user_id + max_id”;
- 把这个结果和原表 JOIN
SELECT *
FROM user_records AS u
INNER JOIN
(SELECT user_id,
MAX(id) AS max_id
FROM user_records
GROUP BY user_id) AS t
ON u.id = t.max_id;
3. 字段类型转换的坑
当 SQL 中 WHERE 条件的字段类型和条件值类型不一致时,数据库会触发「隐式类型转换」—— 看似能查到数据,实则暗藏两大坑:索引失效(性能雪崩)、结果错误(逻辑漏洞)。
先看两个典型场景
- 某个字段在表中是varchar类型,sql语句中where条件使用成了 int类型;
- 或者反过来,字段是int类型,sql语句中where 条件使用成了 varchar类型
| 表字段类型 | WHERE 条件值类型 | 示例 SQL | 隐式转换逻辑 |
|---|---|---|---|
VARCHAR(20)(如 user_id 存 "1001") |
INT(如 1001) |
SELECT * FROM t WHERE user_id = 1001; |
数据库会把所有行的 user_id 转成 INT,再和 1001 比较 |
INT(如 status 是 0、1) |
VARCHAR(如 "a") |
SELECT * FROM t WHERE status = "a"; |
数据库会把条件值 "a" 转成 INT 0,再和 status 比较 |
初步结论: MySQL 在字符串和数字类型比较时,会把字符串往数字上转,再比较。
坑1:索引失效 慢查询
假设 t.user_id 是 VARCHAR 类型,且建了索引 idx_user_id,执行:
-- 索引失效!全表扫描
SELECT * FROM t WHERE user_id = 1001;
- 原因:隐式转换是「把字段值转成条件值类型」(而非反过来),即数据库需要先遍历全表,把每一行的
user_id(字符串)转成 INT,再判断是否等于 1001 —— 索引完全用不上。
坑2:查询到了结果(逻辑漏洞),但非预期是错误的(超坑)
我早年入行时,因此出过生产case…
VARCHAR 转 INT 的诡异规则
MySQL 中字符串转数字时,会从左到右提取数字,遇到非数字字符则停止。无数字则转成 0,尤其这个转0真的坑。。
| VARCHAR 字段值 | 转 INT 结果 |
|---|---|
"1001" |
1001 |
"1001abc" |
1001 |
"abc1001" |
0 |
""(空字符串) |
0 |
"a" |
0 |
执行 SELECT * FROM t WHERE status = "a"; 时,status = 0 的行都会被命中,比如查询后判断是否有记录做逻辑处理,本该没有记录的但是会查到,可能导致很严重的错误。
这种隐式转换会导致「看似不合理的条件却查到数据」(比如 status="a" 查到 status=0 的行),属于典型的逻辑坑。