1. LIMIT/OFFSET 的语义,跳过 N 行取 M 行的代价?
说明 LIMIT/OFFSET 的语义与代价?
- OFFSET 跳过
- LIMIT 取行
- 代价
LIMIT M OFFSET N 表示跳过前 N 行,取接下来的 M 行。代价:数据库仍需扫描并丢弃前 N 行(除非索引),OFFSET 越大代价越高(深分页)。
OFFSET 深分页是性能陷阱,需 keyset 分页。
说明 LIMIT/OFFSET 的语义与代价?
LIMIT M OFFSET N 表示跳过前 N 行,取接下来的 M 行。代价:数据库仍需扫描并丢弃前 N 行(除非索引),OFFSET 越大代价越高(深分页)。
OFFSET 深分页是性能陷阱,需 keyset 分页。
说明 MySQL 中 LIMIT 的优化?
当 LIMIT 极大(如 LIMIT 1000000)时,优化器可能认为全表扫描比走索引更快(因为索引+回表 vs 全表顺序扫描),从而选择全表扫描。小 LIMIT 倾向索引。
大 LIMIT 改变优化器选择,影响深分页性能。
说明 PostgreSQL 中 OFFSET 的实现?
PostgreSQL 的 OFFSET 在执行器层面仍会扫描并丢弃所有跳过的行(不返回客户端),因此 OFFSET 大时仍消耗扫描代价。优化器无法跳过,除非用 keyset 谓词。
OFFSET 只是"丢弃返回",扫描成本仍在。
说明 SQL Server 的 OFFSET ... FETCH NEXT?
SQL Server 用 OFFSET...FETCH NEXT 实现分页:ORDER BY ... OFFSET N ROWS FETCH NEXT M ROWS ONLY。需配 ORDER BY,是 SQL 标准分页语法。
SQL Server 用标准 OFFSET...FETCH NEXT 分页,需配 ORDER BY。
SELECT * FROM t ORDER BY id OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY;
说明 Keyset 分页的实现?
Keyset 分页用 WHERE (col1, col2) > (last_col1, last_col2) 记录上次位置,配合 ORDER BY 和 LIMIT 取下一页,避免 OFFSET 扫描。利用复合索引 (col1, col2) 高效定位。
Keyset 用行值比较定位下一页,避免 OFFSET 深扫描。
SELECT * FROM t
WHERE (created_at, id) > (?last_created, ?last_id)
ORDER BY created_at, id
LIMIT 20;
说明深分页的优化方案:游标(keyset)分页、ID 范围分页、二级索引延迟关联各自如何避免 OFFSET 大偏移扫描?
深分页优化:基于游标(keyset 分页,记录上次位置)、基于 ID 范围(WHERE id > last_id)、基于二级索引(延迟关联,先取主键再回表)。避免 OFFSET 大偏移扫描。
三种方案都避免 OFFSET 逐行跳过:keyset 记录上次位置、ID 范围天然有序、延迟关联先取主键再回表;答题核心是把大偏移转化为条件过滤,并说明 keyset 分页要求排序列唯一确定顺序。
说明深分页(Deep Pagination)的性能问题:OFFSET 很大时为什么慢,即使走索引代价为何依然很高?
OFFSET 1000000 时数据库需扫描并丢弃前 100 万行,即使走索引也需大量回表,代价高。深分页导致性能急剧下降,需用 keyset/游标分页。
深分页是经典性能问题,OFFSET 越大越慢。
说明 MySQL 中 LIMIT 与索引的协同?
LIMIT 配合 ORDER BY 命中索引时,可用索引有序扫描直接取前 N 行,避免排序。覆盖索引可减少回表。但大 OFFSET 仍会扫描跳过行。
索引 + LIMIT 是高效分页的基础。
说明延迟关联的深分页优化?
延迟关联(Deferred Join):先按索引取页的主键(LIMIT/OFFSET 在索引上),再回表关联取完整行,避免大 OFFSET 时的大量回表。减少扫描数据量。
先在索引上取主键页,再回表取完整行,减少回表量。
SELECT t.* FROM t
JOIN (SELECT id FROM t WHERE ... ORDER BY id LIMIT 1000000, 20) tmp
ON t.id = tmp.id;
说明 LIMIT 10 OFFSET 20 的语义?
LIMIT 10 OFFSET 20 跳过前 20 行,返回接下来的 10 行(即第 21-30 行)。先 OFFSET 后 LIMIT。
先跳过 20 行再取 10 行,即第 21-30 行。
说明 PageHelper 分页插件的原理?
PageHelper 等分页插件通过拦截 SQL,自动改写为带 LIMIT(MySQL)/ROWNUM(Oracle)的分页语句,并自动执行 count 查询统计总数。基于 MyBatis 拦截器实现。
分页插件基于 ORM 拦截器机制,在 SQL 执行前自动改写为 LIMIT/ROWNUM 分页语句并附加 count 统计;理解拦截-改写-计数三步流程,才能解释插件为何透明,也能分析其性能隐患。
说明分页缓存(Page Cache)的设计思路:缓存常用页可减少查询,但如何保证缓存失效与数据一致性?
分页缓存可缓存常用页结果(如 Redis 缓存前几页),减少数据库查询。但需处理缓存失效与数据一致性(数据变化时缓存过期)。适合高频稳定数据。
分页缓存以数据一致性为代价换取查询加速,只适合高频且稳定的首页或前几页数据;缓存失效策略(TTL、主动失效)是设计核心,答题时强调缓存不是银弹,一致性方案必须同步设计。
说明深分页的近似方案 next cursor:客户端如何记录上次位置并基于该键取下一页,避免 OFFSET 深度扫描?
深分页近似方案用"next cursor"(记录上次位置游标)而非 OFFSET,客户端保存上次返回的最后一条记录的键,下次基于该键取下一页。避免 OFFSET 深度扫描。
next cursor 是 keyset 分页的实践形态。
说明 UNION ALL 与后续 DISTINCT 的取舍?
UNION ALL 保留重复行,不去重;若需去重可用 UNION(去重)或后续 SELECT DISTINCT。UNION ALL 更快(无去重开销),需要去重时权衡性能。
不需要去重时用 UNION ALL 更高效。
说明 UNION 与 JOIN 的根本差异?
UNION 纵向拼接(行数相加,列数相同);JOIN 横向拼接(列数相加,按条件连接行)。UNION 合并结果集,JOIN 关联表。
UNION 是集合运算,JOIN 是表连接。
说明集合运算的 ORDER BY?
UNION/INTERSECT/EXCEPT 的 ORDER BY 只能写在最后一个 SELECT 之后,对整个结果排序。不能在每个分支单独 ORDER BY(除非用子查询)。
ORDER BY 作用于整个合并结果,只能写在最后且按第一个 SELECT 列名。
SELECT a FROM t1 UNION SELECT b FROM t2 ORDER BY a;
说明 INTERSECT ALL 与 EXCEPT ALL 语义?
INTERSECT 去重后取交集;INTERSECT ALL 保留重复次数(取两集合中出现的较小次数)。EXCEPT 去重后取差集;EXCEPT ALL 保留差集重复次数。ALL 保留重复。
ALL 变体保留重复语义,与不带 ALL 的去重不同。
说明 INTERSECT 与 EXISTS 的等价?
INTERSECT 与 EXISTS 在某场景等价(如找两表共同元素),但 INTERSECT 返回去重后的共同行,EXISTS 判断存在性。优化器可能将 INTERSECT 转为半连接/去重连接。
需要共同行的值用 INTERSECT,存在性用 EXISTS。
说明 UNION ALL 与 CTE 的性能对比?
UNION ALL 直接合并多个查询结果;CTE 可复用子查询(物化与否)。若 CTE 被多次引用且物化,可能比 UNION ALL 更高效(避免重复计算);否则 UNION ALL 开销小。
性能取决于 CTE 是否物化与复用次数。
说明 UNION 的列名规则?
UNION 结果集的列名采用第一个 SELECT 的列名(或别名),后续 SELECT 的列名被忽略。后续 ORDER BY 用第一个 SELECT 的列名。
UNION 结果集列名一律取第一个 SELECT 的列名,后续列名被静默忽略,ORDER BY 也须引用第一个 SELECT 的列名;这是常见陷阱,回答时点明以第一个 SELECT 为准即可。
说明 CTE 中的数据修改?
PostgreSQL 支持 CTE 中的数据修改(INSERT/UPDATE/DELETE),配合 RETURNING 供后续查询使用。MySQL 8.0 不支持 CTE 内 DML。这是方言差异。
PostgreSQL 支持 CTE 内 DML 并配合 RETURNING,MySQL 不支持。
WITH del AS (DELETE FROM t WHERE id=1 RETURNING *)
SELECT * FROM del;
说明多个 CTE 的链式依赖?
多个 CTE 可链式依赖:WITH cte1 AS (...), cte2 AS (SELECT ... FROM cte1) SELECT ...。后定义的 CTE 可引用前面定义的 CTE。
后定义 CTE 可引用前面 CTE,实现分层复用。
WITH cte1 AS (SELECT id FROM t1),
cte2 AS (SELECT * FROM cte1 WHERE id > 10)
SELECT * FROM cte2;
说明 CTE 与临时 VIEW 的差异?
CTE 在单条语句内有效,不持久化;视图(VIEW)是持久化的命名查询,可复用。CTE 适合一次性复用,VIEW 适合长期共享。
CTE 作用域是语句,VIEW 是数据库对象。
说明 CTE 在 OLAP 的应用?
CTE 在 OLAP 中用于多步聚合:先 CTE 预聚合,再在后续查询中复用,提高可读性与可维护性。避免嵌套子查询。
CTE 让复杂 OLAP 查询分层清晰。
说明 CTE 在函数中的可见性?
CTE 在 PostgreSQL 函数内作为 SQL 语句的一部分可见,其作用域为所在语句。函数内多条语句各自可有自己的 CTE,不能跨语句引用。
CTE 的作用域仅限所在单条 SQL 语句,函数内多条语句各自维护自己的 CTE,不能跨语句引用;理解作用域边界才能避免写出引用上一条语句 CTE 的错误代码,这也是 CTE 与临时表的关键区别之一。
说明 CTE 在 SQL Server 的实现差异?
SQL Server 的 CTE 支持非递归与递归(递归 CTE 用 WITH 自身引用)。CTE 在语句内有效,可被多次引用。SQL Server 的 CTE 默认不强制物化,优化器可内联。
递归 CTE 是 SQL Server 的常用特性。
说明 CTE 的物化控制?
PostgreSQL 12+ 允许 AS MATERIALIZED(强制物化,缓存 CTE 结果)与 AS NOT MATERIALIZED(强制内联,避免物化)。默认优化器决定。物化适合多次引用,内联适合单次。
物化缓存结果适用多次引用,内联避免物化适用单次引用。
WITH cte AS MATERIALIZED (SELECT ...) SELECT ...
WITH cte AS NOT MATERIALIZED (SELECT ...) SELECT ...
说明 CTE 别名引用?
CTE 的别名(如 cte AS (...))可在主查询和后续 CTE 中作为表名引用,也可在 FROM 中引用。
CTE 名作为表名可在主查询与后续 CTE 中引用。
WITH cte AS (SELECT 1 AS x) SELECT x FROM cte;
说明 WITH ... INSERT 数据修改 CTE?
语法为 WITH ... INSERT ... RETURNING,可先定义 CTE 再插入,或用 INSERT 配合 RETURNING 供后续操作。PostgreSQL 支持。
前置 WITH 定义源后插入,配合 RETURNING 返回插入行。
WITH src AS (SELECT id, val FROM staging)
INSERT INTO t (id, val) SELECT id, val FROM src RETURNING id;
说明 Oracle 的 ROWNUM 与 FETCH FIRST?
ROWNUM 是 Oracle 的伪列,在行返回时分配序号,需在 WHERE 中过滤(ROWNUM <= n);FETCH FIRST n ROWS ONLY 是标准分页语法,Oracle 12c+ 支持,更简洁。ROWNUM 限制(不能 ROWNUM > n 直接使用)。
ROWNUM 是伪列需在 WHERE 过滤,FETCH FIRST 是标准语法更简洁。
SELECT * FROM t WHERE ROWNUM <= 10;
SELECT * FROM t FETCH FIRST 10 ROWS ONLY;
说明 EXCEPT 的语义?
EXCEPT 返回左集合有但右集合无的行(差集),默认去重。Oracle 用 MINUS 关键字。等价于 NOT IN/反连接(去重后)。
EXCEPT 返回左集合有而右集合无的行并默认去重,Oracle 中写作 MINUS;回答时与 INTERSECT、UNION 对比区分三种集合运算,并说明它等价于去重后的 NOT IN 或反连接。
说明 INTERSECT 的语义与实现?
INTERSECT 返回两集合共同的行(交集),默认去重。实现:Hash Intersection(哈希去重交集)或 Sort-based Intersection(排序后合并)。优化器按数据选择。
INTERSECT 返回两集合共同的行并默认去重,可用哈希或排序两种算法实现;与 EXCEPT 同为集合运算,答题时一并说明共同行加去重的语义,以及算法选择依赖数据量与内存。
说明 UNION 的列兼容性?
UNION 的各 SELECT 列数必须相同,对应列类型必须兼容(可隐式转换)。不满足会报错。这是集合运算的硬性要求。
UNION 要求各 SELECT 列数相同且对应列类型可隐式转换,否则直接报错,这是集合运算的硬约束;回答时说明列数一致与类型兼容两个条件,并区分 UNION 与 UNION ALL 在去重上的差异。
说明 INTERSECT 与 DISTINCT 的差异?
INTERSECT 是集合运算,返回交集并去重;DISTINCT 是对单查询结果去重。INTERSECT 隐含去重,DISTINCT 是显式去重子句。两者概念不同。
INTERSECT 是集合运算,DISTINCT 是去重。
说明 MySQL 用 NOT IN 等模拟 EXCEPT/INTERSECT?
MySQL 8.0.31 前无原生 EXCEPT/INTERSECT,可用 NOT IN/NOT EXISTS/LEFT JOIN 模拟差集,用 IN 模拟交集。陷阱:NOT IN 在子查询含 NULL 时结果为空(需排除 NULL);NOT EXISTS 不受影响。
模拟时注意 NULL 处理,优先用 NOT EXISTS。
说明 LIMIT 0 的用途?
LIMIT 0 返回空结果但保留列结构,用于快速获取表结构(不取数据)。常用于元数据检查、测试。
LIMIT 0 返回空集但保留列结构,用于取表结构。
SELECT * FROM t LIMIT 0;
说明 count(*) 总数与分页的取舍?
分页常需 count(*) 总数,但大表 count 代价高,可能全表扫描。取舍:可缓存总数、近似估算、或去掉总数(无限滚动)。优化 count 用索引/覆盖。
大表 count(*) 可能全表扫描,代价高,与分页性能直接冲突;常用取舍是缓存总数、近似估算或去掉总数改为无限滚动,答题核心是识别 count 成本并用工程手段规避,而非盲目优化 SQL。
说明分页查询的 ORDER BY 必要性?
分页必须有稳定的 ORDER BY,否则各页顺序不确定,产生重复/遗漏。最好加唯一键(如主键)作为末级排序保证稳定。
无 ORDER BY 的分页结果不稳定。
说明 UNION ALL 在分库分表的应用?
分库分表后,跨库/跨表查询可用 UNION ALL 合并各分片结果,再在应用层或外层处理。UNION ALL 不去重,适合合并分片数据。常用于全量扫描场景。
UNION ALL 合并分片结果,需注意排序与分页。
说明 UNION 中 NULL 的处理与类型推导?
UNION 去重时 NULL 视为相同值(多个 NULL 合并为一个);UNION ALL 保留多个 NULL。结果列类型由各 SELECT 的对应列类型推导(取兼容类型)。跨库/异构数据源时需保证 NULL 语义一致(如数据类型转换、NULL 表示一致)。
NULL 在去重时合并,类型取兼容类型。