mysql 5.7及更早版本禁止子查询中直接使用limit,属解析器硬限制;须外层嵌套派生表并显式命名别名,如where id in (select t.id from (select id from log order by ts desc limit 10) as t)。

为什么直接用 LIMIT 在子查询里会报错
MySQL 5.7 及更早版本不支持在子查询中直接写 LIMIT,比如:SELECT * FROM (SELECT * FROM goods ORDER BY price DESC LIMIT 3) t 会报错 This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'。这不是语法写错了,是引擎限制——子查询若作为 IN、JOIN 或标量子查询的来源,不能带 LIMIT。
解决思路不是绕开子查询,而是把排序 + 截断逻辑移到能支持 LIMIT 的上下文中,比如派生表(derived table)或 CTE(MySQL 8.0+)。
MySQL 8.0+ 推荐用 ROW_NUMBER() 窗口函数
这是目前最清晰、可读性最强且性能可控的方式。它天然支持“按分类分组、按字段排序、取前 N”的语义,不需要嵌套多层子查询。
- 必须用
PARTITION BY category_id划分组,否则全局编号 -
ORDER BY created_at DESC决定“前 N 条”的顺序,别漏掉DESC导致取到最老的记录 - 过滤时必须用
WHERE rn ,不能写成 <code>HAVING rn (窗口函数不能在 <code>HAVING中引用)
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY created_at DESC) AS rn FROM goods ) t WHERE t.rn <h3>MySQL 5.7 兼容方案:相关子查询 + 计数比较</h3><p>核心逻辑是:对每条记录,统计同分类中比它“更优”(如时间更新、价格更高)的记录数,若 ≤ N−1,则它是前 N 条之一。虽然写法略绕,但兼容性好、语义明确。</p>
- 子查询里必须包含和外层相同的分类条件:
WHERE g2.category_id = g1.category_id - 比较字段要和排序逻辑一致,比如外层按
created_at DESC,子查询就得用g2.created_at > g1.created_at - 注意去重:如果
created_at可能重复,需加入辅助字段(如id)避免漏行或重复,例如:g2.created_at > g1.created_at OR (g2.created_at = g1.created_at AND g2.id > g1.id)
SELECT g1.* FROM goods g1 WHERE (
SELECT COUNT(*) FROM goods g2
WHERE g2.category_id = g1.category_id
AND (g2.created_at > g1.created_at OR (g2.created_at = g1.created_at AND g2.id > g1.id))
) <h3>性能陷阱:索引没建对,再好的写法也慢</h3><p>无论用窗口函数还是相关子查询,没有合适索引时都会全表扫描。关键不是“有没有索引”,而是索引字段顺序是否匹配查询模式。</p>
- 对
ROW_NUMBER()方案,最佳索引是:(category_id, created_at DESC, id)—— 前两个字段对应PARTITION BY + ORDER BY,id用于避免排序不稳定 - 对相关子查询方案,同样需要
(category_id, created_at, id),且注意 MySQL 8.0 支持降序索引,5.7 只能靠DESC字段放在最后并接受 filesort - 别只给
category_id单列索引——它只能定位分类,无法加速组内排序
实际执行前务必用 EXPLAIN 看 type 是否为 ref 或 range,Extra 里有没有 Using filesort 或 Using temporary。











