资讯动态

SQL LIMIT用法详解:从基础分页到深分页优化与数据库兼容

发布时间:2026/10/15 20:13:14 来源:尧图企业网站定制
LIMIT这玩意儿平时写SQL基本都会碰。但真要说起来我发现大多数人只停留在LIMIT 10这种最基础的玩法上稍微绕一点的分页写法、不同数据库的语法差异、性能陷阱还真不一定讲得清楚。这篇文章就专门把LIMIT用法掰开揉碎好好聊一遍从最简单的取前N行到进阶的分页、深分页优化、不同数据库的兼容写法尽量一次说透。如果你工作里经常跟SQL打交道或者是正在学数据库的新手这篇值得耐心看完很多细节都是我实操中踩过坑之后总结出来的。1. LIMIT字句的本质与基本形态1.1 一个关键字解决只要前几行的需求LIMIT从字面意思上看就是限制它限制的是查询结果返回的行数。数据库表里动辄几万、几十万行但很多时候我们只需要看前几条。比如刚上线一个新功能想看最新的几条订单记录或者做数据分析时只关心销售额排名前十的商品这时候LIMIT就是最直接的手段。它的核心语法在不同数据库里长得不太一样但逻辑是共通的。MySQL里最经典的是这样SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;意思很直白把orders表按创建时间倒序排好之后只留第1到第10行。10就是返回的最大行数。稍微进阶一点的写法是带偏移量SELECT * FROM orders ORDER BY created_at DESC LIMIT 10 OFFSET 20;这个偏移量的含义是跳过前20行从第21行开始取10行。但这里有个特别容易踩的坑——在MySQL里LIMIT 20, 10和LIMIT 10 OFFSET 20是等价的但参数顺序极容易搞反。很多人会把LIMIT 20, 10误读成取20行跳过10行实际上MySQL的第一参数是偏移量第二参数才是行数这一点非常反直觉。PostgreSQL、SQLite、MariaDB这些数据库语法大体一致OFFSET子句也支持。SQL Server和Oracle是另一套写法后文会专门讲这里先不展开。1.2 写在最后面的执行铁律LIMIT不是想放哪就放哪的。一条完整的SQL语句各子句有固定的执行顺序LIMIT一定出现在所有子句之后也就是WHERE、GROUP BY、HAVING、ORDER BY全部搞定之后才轮到它。SELECT department, COUNT(*) AS cnt FROM employees WHERE status active GROUP BY department HAVING COUNT(*) 5 ORDER BY cnt DESC LIMIT 3;这个SQL的执行过程是先筛选在职员工按部门分组计数把数量小于等于5的部门丢掉按数量倒序排最后留前3个。LIMIT只有可能作用于最终结果它永远不会在WHERE之前做预裁剪——虽然有些数据库优化器确实会尝试下推但这属于优化层面的事逻辑语义上不存在LIMIT先执行的可能。理解这个顺序对排错很有帮助。如果你发现加上LIMIT之后结果依然很慢或者LIMIT的结果不符合预期先检查是不是因为前面的JOIN、子查询、排序本身就已经把开销吃完了。1.3 常被忽略的“0行”和“负数”边界LIMIT能做很多让人意想不到的事情比如LIMIT 0。SELECT * FROM users LIMIT 0;这个查询不返回任何行但它在MySQL里是合法的。最有用的场景是CREATE TABLE ... SELECT时只想要表结构不想要数据或者做数据迁移前先试跑一下SQL、验证它有没有语法错误而不触发大量数据扫描。类似场景下LIMIT 0能省下很多不必要的IO。负数在MySQL的LIMIT里是非法参数直接报错。但PostgreSQL有个有趣的例外LIMIT ALL和LIMIT NULL都表示不限制行数。这一点在写通用代码时容易出兼容性问题需要格外注意。2. 分页场景下的LIMIT撑起后台列表的半边天2.1 经典的分页公式是怎么来的后台管理系统里分页查询是最常见的LIMIT应用场景。假设你现在要写一个用户列表页面每页显示20条前端传过来一个page参数页码从1开始后端要拼出正确的LIMIT参数。公式其实很简单offset (page - 1) * page_size limit page_size第1页LIMIT 20 OFFSET 0 第2页LIMIT 20 OFFSET 20 第3页LIMIT 20 OFFSET 40这个公式我建议不要死记理解一下就好第一页不需要跳过任何数据所以offset是0到了第二页得把第一页已经看过的20条跳过所以offset是20以此类推。实践中很多人会忽略一个重要问题——如果用户正在浏览第1页同时新数据不断写入翻到第2页时可能出现某条数据重复出现或漏掉的情况。原因很简单排序依据比如created_at有相同值时LIMIT切分的位置会漂移。解决这个问题最通用的做法是给ORDER BY加上一个唯一性字段作为第二排序键比如SELECT * FROM products ORDER BY created_at DESC, id DESC LIMIT 20 OFFSET 20;2.2 大页码翻不动了深分页的性能陷阱LIMIT分页对小页码非常友好但随着页码越来越大查询会越来越慢。下面这个查询我见很多团队踩过SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;这种写法在数据量大的表上甚至会直接把数据库拖垮。原因不复杂LIMIT的偏移量是1000000数据库引擎必须老老实实地从第一行开始数数够100万行之后才取后面的20行。前面那100万行虽然不会返回给客户端但它们扫描和排序的开销一点都不少。本质上这就是个趋势问题页码越深数过的行越多耗时越大。网上有个说法叫深分页指的就是偏移量很大的分页。更准确地说这类查询让数据库做了一大堆无用功——排序、回表、逐行跳过。一个有效的优化思路是延迟关联把LIMIT作用在索引字段或者主键上然后用JOIN把完整行捞回来SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders ORDER BY id LIMIT 1000000, 20 ) t ON o.id t.id;子查询只查了主键id走覆盖索引不需要回表速度会快很多。然后把查到的20个id跟原表做关联取出完整记录。这个方案相比直接LIMIT省掉了对100万行完整数据的扫描排序效果立竿见影。2.3 更优雅的游标分页写法深分页还有一个更彻底的解法不靠页码靠上一次看到的最后一条数据来定位。这就是游标分页也叫keyset pagination。假设数据按id排序每页20条。请求第一页SELECT * FROM orders ORDER BY id ASC LIMIT 20;客户端拿到这20条之后记住最后一条的id比如是100040。请求第二页时把id作为条件传进来SELECT * FROM orders WHERE id 100040 ORDER BY id ASC LIMIT 20;这个方案的意义在于无论表有多大查询都能直接通过主键索引定位到id100040的位置接着往后取20条。MySQL的B树索引找一条主键的开销是log级别的后面的数据又是顺序连续的性能非常稳定。不管翻到多深的页查询速度都跟第一页差不多完全不会出现深分页劣化。缺点也很明显——没办法直接跳到第100页只能一页一页往后翻。但绝大多数面向用户的业务用户根本不会翻到那么深。游标分页是用户体验和数据性能的最优折中值得大家在设计分页接口时认真考虑。3. 不同数据库里的限制行数方案LIMIT这个关键字虽然太常见了但真出了MySQL在别的数据库里就得换个写法。很多做数据库迁移的朋友在这块吃了不少苦头。3.1 MySQL系LIMIT OFFSET全家桶MySQL、MariaDB的写法最灵活它支持两种等价风格SELECT * FROM t LIMIT 10; SELECT * FROM t LIMIT 10 OFFSET 20; SELECT * FROM t LIMIT 20, 10;第二种和第三种完全等价都是跳过20行取10行。但就像前面说的逗号风格容易把顺序搞混。我个人的建议是项目代码里统一用LIMIT ... OFFSET ...的写法把语义写在明面上减少后来维护的人犯错的概率。值得提一下的是MySQL 8.0.1开始支持FETCH FIRST子句这是SQL标准语法为跨数据库兼容铺了路。但在CURRENT_DATE这种写法上各家还有微妙差别。3.2 PostgreSQLLIMIT什么都能干PostgreSQL写LIMIT 10 OFFSET 20和MySQL没任何区别。但PostgreSQL还支持SQL标准里的FETCH FIRSTSELECT * FROM products ORDER BY price DESC FETCH FIRST 10 ROWS ONLY; SELECT * FROM products ORDER BY price DESC OFFSET 20 ROWS FETCH FIRST 10 ROWS ONLY;PostgreSQL还有一个加分项FETCH FIRST后面可以带WITH TIES意思是把和最后一条并列的数据一起取出来。这个非常智能SELECT * FROM products ORDER BY price DESC FETCH FIRST 5 ROWS WITH TIES;假设第5条和第6条价格一样这个查询会返回6行因为第6条和边界行并列。而LIMIT只会死板地返回5行。这种语义在需要排名靠前且并列也算的场景非常实用。3.3 SQL Server和Oracle各有各的规矩SQL Server 2012及以上用的是OFFSET ... FETCH NEXTSELECT * FROM products ORDER BY id OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;而且SQL Server的ORDER BY是必需的不写ORDER BY直接OFFSET会报错。这跟MySQL的宽松态度完全不同。Oracle的传统做法是ROWNUM在Oracle 12c之前只能这么写SELECT * FROM ( SELECT t.*, ROWNUM rn FROM (SELECT * FROM products ORDER BY id) t ) WHERE rn 10 AND rn 20;这个写法相当费劲内外两层嵌套且ROWNUM要在排序完成之后才编号。Oracle 12c之后终于支持了现代语法SELECT * FROM products ORDER BY id OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;做兼容层的同学可以放宽心了新版本的Oracle、SQL Server、PostgreSQL在分页语法上已经高度接近。4. LIMIT操作中的隐形坑与实用技巧4.1 没有ORDER BY的随机取一行这是一个在开发中经常被忽视的问题。LIMIT 1看起来很简单但如果查询里没有ORDER BY它返回的到底是哪一行答案是不确定。数据库存储引擎返回行的顺序可能受物理存储位置、索引扫描顺序、并行执行计划影响在没有任何排序约束时哪怕同一句SQL跑两次结果都可能不一样。很多人写SELECT * FROM users LIMIT 1取一个用户做测试结果每次取到的都不是同一个人就是这个原因。要取最新一条必须显式加ORDER BY id DESC或者ORDER BY created_at DESC。任何依赖隐含顺序的写法都是拿系统的确定性开玩笑。4.2 LIMIT和聚合、子查询的边界问题有几个场景LIMIT的表现容易让人迷惑。一是GROUP BY和LIMIT一起出现时LIMIT作用于分组后的结果。二是UNION的结果集跟LIMIT搭配时如果直接写SELECT ... UNION SELECT ... LIMIT 10LIMIT作用于整个UNION结果。但要想每个子查询各自LIMIT再合并必须加括号(SELECT * FROM t1 ORDER BY id LIMIT 5) UNION ALL (SELECT * FROM t2 ORDER BY id LIMIT 5);注意MySQL里带括号的UNION子句写法对版本有要求这个语法在8.0.29之后才能写而且第一个子查询的ORDER BY需要配合LIMIT才生效。三是子查询里不能用LIMIT的场景。比如MySQL的某些版本不允许UPDATE ... WHERE id IN (SELECT id FROM ... LIMIT 1)这种写法。我实际碰到过报错内容是LIMIT clause in subquery is not allowed。解法通常是包一层派生表UPDATE t1 WHERE id IN ( SELECT id FROM ( SELECT id FROM t2 ORDER BY id LIMIT 1 ) tmp );4.3 性能优化的两个额外杀招LIMIT和大排序一起用时排序本身往往是瓶颈。给排序字段建合适的索引LIMIT的性能会直接上一个台阶。比如这个查询SELECT * FROM events WHERE user_id 123 ORDER BY event_time DESC LIMIT 10;如果存在(user_id, event_time)的联合索引数据库可以沿着索引顺序直接定位到user_id123对应的最大event_time然后往后扫10条几乎不费力气。没有这个索引它要把所有event_time排序后取前10代价大得多。另外一个技巧是LIMIT配合RAND()做随机抽样SELECT * FROM users ORDER BY RAND() LIMIT 5;这个方法在数据量小时很香但表一大就非常痛苦因为RAND()要对整个表做一次全表扫描和排序。替代方案是SELECT * FROM users WHERE id ( SELECT FLOOR(RAND() * (SELECT MAX(id) FROM users)) ) ORDER BY id LIMIT 1;它是先随机选一个id起点再取后面第一条。性能很快代价是均匀随机的质量会差一点。业务对随机性要求不是极端严格时这个方案非常划算。4.4 常见问题速查表问题现象大概率原因解决方案分页重复或漏数据排序列有重复值ORDER BY追加唯一列如id页码越深越慢偏移量过大导致扫描大量行延迟关联或游标分页没有ORDER BY的LIMIT 1结果不稳定引擎没有保证返回顺序显式加ORDER BYSQL Server报错OFFSET requires ORDER BYSQL Server语法限制补上ORDER BY子句Oracle里写了LIMIT直接报错老版本不支持LIMIT用ROWNUM或者升级后FETCH FIRST逗号写法语义对不上LIMIT a, b参数顺序混淆统一用LIMIT b OFFSET a4.5 隐藏的小众场景LIMIT用于数据校验和抽样除了分页LIMIT在几个日常不起眼的场景里也很有价值。EXPLAIN配合LIMIT可以快速看执行计划的前几条路径。数据同步任务里你用LIMIT 1提前探明目标表的字段结构和数据格式可以避免正式任务跑挂。做数据质量校验时用LIMIT抽样几百行做快速检查比全量扫描快得多。还有在一个DELETE语句里先SELECT LIMIT确认影响范围再真正执行这种先看后删的习惯能救你很多次。关于LIMIT的性能问题很多场景不仅取决于语法本身还跟连接缓存、查询计划缓存、索引使用情况密切相关不能指望单独一个LIMIT包治百病。但把LIMIT理解到位很多看起来玄学的查询性能问题其实会豁然开朗。我自己的习惯是凡是线上要跑的带LIMIT的查询都会先跑一遍EXPLAIN确认索引使用情况再把偏移量控制在一个合理的业务时间窗口内。在某个做过统计报表的系统里历史数据一天天膨胀如果一直用深分页访问用户翻到几十页之后页面就卡得不能看。后来切换到游标分页整体体验才稳定下来。这些经验说到底就一句话LIMIT不是简单的取N条它背后连着排序、索引、SQL标准和分页设计。能把LIMIT用明白的人写出来的SQL通常也差不到哪去。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑