🦏 第3章 查询操作
📄 一句话概括

limit n 是「最多取 n 行」,limit offset, n 是「跳过 offset 行再取 n 行」。 分页公式只有一条:offset = (page - 1) * size。 Demo 里每页 5 条翻了三页:第 1 页首行张伟、第 2 页首行杨帆、第 3 页只剩 2 行; 不加 order by 的 limit 这次碰巧一样,但那只是运气——标准里没有「默认顺序」这回事。

📝 本章和前面几章的关系

上一篇定顺序,这一篇定取几行。 两个一起用,才有「分页」这件事。 它是接口设计的底座:所有列表接口的参数都是 page 和 size, 背后就是这一句 limit offset, size。

一、limit n:只要前 n 行

LIMIT 3 就是「最多 3 行」。 是「最多」不是「正好」——表里只剩 2 行时它给 2 行,不报错。 分页最后一页常常不满,就靠这个行为。 limit 0 会返回空结果集, 但列名还在——有时候用它「只要结构不要数据」,比 where 1=0 干净。

二、limit offset, n:跳过再取

LIMIT 2, 3 的意思是「跳过前 2 行,再取 3 行」,等价于 LIMIT 3 OFFSET 2。 两个数谁在前?偏移在前、条数在后。 写反是最常见的错:LIMIT 3, 2 是「跳 3 取 2」, 结果完全不同。 推荐写 LIMIT n OFFSET m:顺序和语义一致,不用记谁在前, MySQL 和 SQLite 都支持这种写法。

三、分页公式与那一页两行

公式:offset = (page - 1) * size。 页码从 1 开始、偏移从 0 开始,所以必须先减 1。 Demo 的账:每页 5 条,第 1 页 offset 0 取 5 行、第 2 页 offset 5 取 5 行、 第 3 页 offset 10 只剩 2 行——12 个学生正好翻三页。 总量也要一起查:SELECT COUNT(*) 拿总行数,前端才能算出总页数(ceil(total / size))。 两个查询要放在同一个事务或同一时刻,否则数字对不上。

四、limit 必须配 order by

不加 order by 的 limit,取到哪几行是不确定的。 标准里没有「默认顺序」这回事—— 引擎想怎么给就怎么给。 Demo 里这次两边一样(都是 1 到 5),但那是运气: 数据量小、又刚好按主键顺序读。 换个数据量、加个索引,顺序就变了。 后果很具体: 翻页时同一行出现两次、另一行一直看不到。 这类 bug 在测试环境很难复现,上线才爆。

五、深翻页为什么慢,怎么救

LIMIT 100000, 20 这种大偏移:引擎要把前 10 万行都找出来再丢掉,越往后翻越慢, 第 5000 页可能比第 1 页慢几百倍。 救法一(最常用):记住上一页最后一行的主键, 下一页用 WHERE id > 上一页最后的值 ORDER BY id LIMIT 20。 这样每一页都一样快,代价是不能跳页。 救法二:先只查主键(走覆盖索引),再按主键回表取数据。 适合「必须支持跳页」的场景。 救法三:限制最大页数(比如只允许看前 100 页),产品上很常见,也最省事。

六、小结

教材小结:limit 限制返回行数,limit offset, count 可以跳行取,常用于分页。 本课补三条:一是分页必须配 order by;二是 LIMIT n OFFSET m 比 LIMIT m, n 好记; 三是深翻页要换成「按上一页最后的主键往下取」。

七、动手 Demo:每页五条,翻三页

Demo 五段:先 limit 3,再 limit 2, 3,然后按公式翻三页,最后对比加不加 order by。 第三段那三行是分页的全部账:offset、取到几行、首行是谁。

import _q46 as Q
c = Q.con()

c.execute("SELECT id, name FROM students ORDER BY id LIMIT 3")
c.execute("SELECT id, name FROM students ORDER BY id LIMIT 2, 3")

# 分页公式:offset = (page - 1) * size
for page in (1, 2, 3):
    off = (page - 1) * 5
    c.execute("SELECT id, name FROM students ORDER BY id "
              "LIMIT 5 OFFSET %d" % off)

# 不加 order by 的 limit 没保证
c.execute("SELECT id FROM students LIMIT 5")
c.execute("SELECT id FROM students ORDER BY id LIMIT 5")

实跑输出:

--------------------------------------------------------------------
第十三课:limit,以及分页那条公式
--------------------------------------------------------------------

  取前几行:limit n
  SQL> SELECT id, name FROM students ORDER BY id LIMIT 3
  id     name
  ------------
  1      张伟
  2      王芳
  3      李娜
  (3 行)

  **`limit 3` 就是「最多 3 行」。** **注意是「最多」——表里只有 2 行时,它给 2 行,不报错。**

  跳过几行再取:limit offset, n
  SQL> SELECT id, name FROM students ORDER BY id LIMIT 2, 3
  id     name
  ------------
  3      李娜
  4      刘洋
  5      陈静
  (3 行)

  **`limit 2, 3` 的意思是「跳过前 2 行,再取 3 行」,等价于 `LIMIT 3 OFFSET 2`。**
  **第一个数是偏移、第二个数是条数——顺序写反是常见错。**

  分页公式:第 page 页,每页 size 条
  每页 5 条,取第 1、2、3 页:
    第 1 页:offset = (1-1)*5 = 0 ,取到 5 行,首行是 张伟
    第 2 页:offset = (2-1)*5 = 5 ,取到 5 行,首行是 杨帆
    第 3 页:offset = (3-1)*5 = 10,取到 2 行,首行是 徐明

  **公式就是 `offset = (page - 1) * size`。** **页码从 1 开始,偏移从 0 开始,所以要先减 1。**

  不加 order by 的 limit,结果不保证
  不加 order by:[1, 2, 3, 4, 5]
  加 order by:  [1, 2, 3, 4, 5]

  **这次两边一样,但那是运气。** **SQL 标准里没有「默认顺序」这回事,引擎想怎么给就怎么给。**
  **分页必须配合 order by,否则翻页时会出现「同一行出现两次、另一行一直不出现」。**

  limit 的一个硬限制
  **`limit` 不能跟子查询里那种「先算再取」混着用**:
  `limit` 只能出现在整条语句的最外层(或子查询自己的最外层)。
  **还有一个性能常识**:`limit 100000, 20` 这种大偏移,
  **引擎要先把前 10 万行找出来再丢掉,越翻越慢。**
  **优化方向是「记住上一页最后一行的 id,用 where id > ? 往下取」。**

第一段 limit 3。 取前三行,1、2、3。 第二段 limit 2, 3。 跳过前两行,取到 3、4、5——和第一段的结果不重叠,这就是偏移的意义。 第三段翻页。 第 1 页 offset 0 首行张伟;第 2 页 offset 5 首行杨帆; 第 3 页 offset 10 只取到 2 行(12 个学生到底了)。 第四段不加 order by。 这次给 1 到 5,和加了 order by 一样, 但正文里说清了:这是运气,不是保证。 第五段深翻页与三个救法。

30 秒实验

  1. LIMIT 3, 2 取到的是哪几行?和 LIMIT 2, 3 差在哪?
  2. (先想再跑)每页 5 条、共 12 行,一共几页?第 3 页取到几行?
  3. LIMIT 100000, 20 慢在哪一步?

常见误区

  1. 把 LIMIT m, n 的两个数写反。 一个是偏移一个是条数,写反结果完全不同。
  2. 不加 order by 就分页。 顺序不保证,翻页时会出现重复行和漏行。
  3. 用大偏移做深翻页。 引擎要先把前面十几万行读出来再丢掉,越翻越慢。
自测三问(点开对答案) 1. `LIMIT 2, 3` 是什么意思? —— 跳过前 2 行,再取 3 行;等价于 `LIMIT 3 OFFSET 2`。 2. 分页公式是什么?为什么先减 1? —— offset = (page - 1) * size;页码从 1 开始、偏移从 0 开始,所以第一页偏移是 0。 3. 为什么 limit 一定要配 order by? —— SQL 没有默认顺序,不加排序时取到哪几行不确定,翻页会出现重复与漏行。

查询的四个子句(select/where/order by/limit)都会用了, 下一篇补最后一个:去重 distinct——以及它常常暴露出来的问题。

相关笔记