新闻详情

新闻详情

首页 / 资讯中心 / 详情

电商后端分页实战:从SQL原理到深分页优化与失效排查

发布时间:2026/9/13 15:00:22来源:尧图网络
电商后端分页实战:从SQL原理到深分页优化与失效排查
做电商商城后端这几年有些功能看着不起眼但一到数据量大就问题不断商品列表分页绝对是其中之一。刚接手商城项目那会儿商品表才几万条数据代码里直接LIMIT 20 OFFSET 40简单粗暴一点问题没有。等到数据涨到百万级、大促流量一进来问题全都冒出来了接口超时、商品列表翻页数据重复、订单明细和列表对不上、MyBatis Plus 分页莫名其妙失效……这篇就以电商系统展示商品为例把 web 分页从原理到实操完整拆一遍讲清楚分页到底在解决什么问题、不同数据库的分页怎么写、分页插件怎么用、分页失效怎么排查、深分页怎么优化以及前端组件怎么配合才不出幺蛾子。新手可以照着抄老手也可以看看有没有踩过同样的坑。1. 分页到底在解决什么问题1.1 三个维度的压力数据库、网络、用户体验先谈本质。不分页直接一把梭SELECT * FROM product在几千条数据的 Demo 项目里毫无压力但在真实电商系统里商品表动辄几十万、上百万甚至上千万。一次全量查询意味着数据库要把所有匹配的行都扫描出来哪怕用户只需要看 20 条。这就像去图书馆查资料你为了看第一页内容非要把整本几百万字的书全部翻一遍然后只摘录第 1 到 20 页的句子。扫描的行越多磁盘 I/O 和内存消耗越大把 buffer pool 挤爆之后连旁边的订单查询都要跟着遭殃。网络传输也是隐形杀手。一条商品记录在 MySQL 里可能就几百字节加上关联的 SKU、品牌、标签之后单条记录能到 1-2KB。你查 10 万条记录可能就要传输接近 100MB 的数据浏览器端解析这么大体量的 JSON页面直接卡死。用户体验层面更不用多说移动端屏幕就那么大用户一次只看 20 条左右一次性渲染 10 万条 DOM 节点只会让手机发热、列表滚动掉帧。分页的诉求就在这里每次只拿当前页需要的数据把数据库压力、网络开销、前端渲染都控制在一个合理范围内。1.2 分页方案的分类前端分页、后端分页、游标分页分页不是只有一种做法。以电商商品列表为例常见的有三类前端分页后端一次性返回全量数据前端在内存里切片展示。适用于数据量很小且基本不变的场景比如后台管理系统的字典表、配置项。商品列表这种数据量基本不适合。后端传统分页通过页码 page 和每页条数 pageSize 查询数据库返回当前页数据。这是绝大多数系统在用的方式也是本文主要展开的方式。游标分页 / Keyset 分页前端不传页码而是传上一条记录的某个排序字段值作为游标比如id12345后端查询时加条件id 12345 ORDER BY id LIMIT 20。这种方案在深分页和加载更多场景下性能极好后面单独讲。选型上没有银弹。传统的 page/pageSize 对中小数据量和后台管理系统的跳页需求很友好游标分页适合数据量巨大且用户只关心下一页的场景比如信息流、电商搜索结果。电商商品列表其实两种都会用到普通列表页用传统分页搜索结果页用游标分页或者加载更多。2. SQL 层分页不同数据库的写法与原理2.1 MySQLLIMIT 和 OFFSETMySQL 最经典的分页写法是SELECT id, product_name, price FROM product WHERE status 1 ORDER BY id LIMIT 20 OFFSET 40;含义从第 41 条记录开始取 20 条。也可以缩写为LIMIT 40, 20第一个数是偏移量第二个数是条数。其中 page 和 pageSize 的换算关系是offset (page - 1) * pageSize比如 page3、pageSize20就是LIMIT 20 OFFSET 40。这里有个很容易踩的坑如果前端页码从 0 开始后端从 1 开始换算一定要统一。我见过有的系统前端传的 page 从 0 开始后端直接用LIMIT #{page}, #{pageSize}去拼 SQL数据永远错位一页。这个最好在接口层统一掉。MySQL 8.0.31 之后也支持了标准 SQL 的OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY写法但实际项目里LIMIT已经深入人心兼容性也最好暂时不需要特意改。注意一个细节MySQL 里 OFFSET 超过表的总行数时不会报错只会返回空结果。这个特性在写接口时要注意比如用户手动把页码调成了 99999接口应该返回空列表而不是报错。2.2 Oracle 分页从 ROWNUM 到 FETCH FIRSTOracle 的写法比 MySQL 啰嗦一些因为早期的 Oracle 没有 LIMIT。经典写法是三层嵌套SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT id, product_name, price FROM product WHERE status 1 ORDER BY id ) t WHERE ROWNUM 60 ) WHERE rn 40;这里有个特别重要的坑ROWNUM 是在结果出来之后才赋值的而且是在 WHERE 条件过滤之前还是之后很容易搞混。如果直接写WHERE ROWNUM 40你会得到一个空结果集因为 ROWNUM 在第一行被过滤掉之前就已经赋值了条件永远不成立。所以一定要先取前 60 条再在外面过滤掉前 40 条。Oracle 12c 之后提供了标准写法SELECT id, product_name, price FROM product ORDER BY id OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;如果遇到老系统还在用三层嵌套的也不用觉得奇怪很多金融、政企项目里这套写法服役了十几年逻辑完全正确只是不好读。2.3 SQL Server 分页OFFSET...FETCH 语法SQL Server 从 2012 开始支持 OFFSET...FETCHSELECT id, product_name, price FROM product ORDER BY id OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;和 MySQL 的 LIMIT OFFSET 逻辑完全一致但有个明显区别ORDER BY 在这个语法里是必须的没有 ORDER BY 就不允许使用 OFFSET。这一点和 MySQL 不一样MySQL 即使没有 ORDER BY 也能 LIMIT只是结果顺序不确定。顺带提一个偏门的知识点SQL Server 内存管理中有一个非分页缓冲池占用过高的问题。名字里带分页但和 SQL 分页查询一点关系都没有它指的是 SQL Server 缓冲池里不可分页内存Nonpaged Pool过高通常和驱动、第三方组件以及某些系统元数据分配有关。遇到数据库内存持续上涨可以先从 DBCC MEMORYSTATUS 的输出看起常见原因是非分页池里积压了大量待释放内存。虽然是另一个层面的问题但在排查数据库性能时容易被分页这两个字带偏提前打个预防针。2.4 分页 SQL 的本质数据库是怎么执行的不管哪种写法数据库执行分页查询的本质都包含两步按 WHERE 条件筛选出候选行按 ORDER BY 排好序后跳过 offset 个记录取 limit 个记录。跳过 offset 这个动作实际上是要把前面所有行都走一遍这也是为什么深分页会慢的根本原因后面第 5 章会重点讲。理解了这个本质能推导出两个结论分页查询的性能很大程度取决于 WHERE 和 ORDER BY 能不能用上索引如果条件匹配的行特别多OFFSET 又很大性能必然差。第一步扫描的性能和你要第 1 页还是第 100 万页关系不大只和 OFFSET 大小有关系。很多人以为LIMIT 20很快其实LIMIT 1000000, 20一点都不快。从数据库执行计划的视角看传统分页的本质就是牺牲已经扫描过的行来换取规则的切片这是它在大数据量下天然的软肋。3. MyBatis Plus 分页电商 Java 项目的主流落地方式3.1 为什么选 MyBatis Plus 分页插件国内电商 Java 项目里MyBatis Plus 的使用率极高。用它做分页的好处很直接不用手写每个查询的 LIMIT、ROWNUM代码统一自动生成 count 查询返回 total帮你屏蔽 MySQL、Oracle、SQL Server 的方言差异。但注意MyBatis Plus 的分页不是开箱即用的它依赖内置的分页插件而且必须显式配置拦截器。很多项目分页失效就是栽在这一步。3.2 分页插件配置与核心 API先看配置。Spring Boot 3.x 项目中MyBatis Plus 3.5.3Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setOverflow(false); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }几个关键点DbType 一定要写对。有的项目是 MySQL配置时写了 OracleSQL 生成出来就会不对劲。maxLimit 可以设置单页最大条数防止有人传 pageSize100000 直接把服务打崩。overflow 代表超出总页数时是否回正到第一页比较适合后台管理系统。3.3 商品分页查询完整实操以电商后端为例一个典型的商品分页查询长这样。实体类略直接看 Mapperpublic interface ProductMapper extends BaseMapperProduct { }ServiceService public class ProductService { Autowired private ProductMapper productMapper; public PageResultProductVO pageProduct(int page, int pageSize, ProductQuery query) { PageProduct pageParam new Page(page, pageSize); LambdaQueryWrapperProduct wrapper Wrappers.ProductlambdaQuery() .eq(Product::getStatus, 1) .eq(StringUtils.hasText(query.getBrandId()), Product::getBrandId, query.getBrandId()) .like(StringUtils.hasText(query.getKeyword()), Product::getProductName, query.getKeyword()) .orderByAsc(Product::getSortOrder) .orderByDesc(Product::getId); IPageProduct result productMapper.selectPage(pageParam, wrapper); ListProductVO records result.getRecords().stream() .map(product - convertToVO(product)) .collect(Collectors.toList()); PageResultProductVO pageResult new PageResult(); pageResult.setRecords(records); pageResult.setTotal(result.getTotal()); pageResult.setPage(page); pageResult.setPageSize(pageSize); pageResult.setPages(result.getPages()); return pageResult; } }ControllerRestController RequestMapping(/api/product) public class ProductController { GetMapping(/page) public ResultPageResultProductVO page( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 20) int pageSize, ProductQuery query) { return Result.ok(productService.pageProduct(page, pageSize, query)); } }这里有一个实际开发中容易忽略的细节selectPage返回的 IPage 里包含 records、total、current、size、pages 等字段但自定义 VO 转换后如果你直接 new 一个 PageResult记得把 total、pages 一起带上。很多前端只显示 total 就够了但如果你做的是共 xx 件商品这种展示total 丢了体验会很差。还有一个常见问题查询条件里如果用like要确认在商品名称这类字段上是否有索引。电商场景里名称模糊搜索经常是全表扫描的元凶之一尤其%关键字%这种写法MySQL 默认用不上索引。分页本身没有错但和模糊搜索结合时要结合搜索引擎或 ES 去解决这里先提个醒。3.4 分页插件到底帮你做了什么MyBatis Plus 分页插件的原理说白了就是一次 SQL 改写。当它检测到 Mapper 方法第一个参数是 IPage就会在原 SQL 基础上生成一条SELECT COUNT(*)的 count 语句用来算 total根据配置好的 DbType把原 SQL 改写成带 LIMIT / ROWNUM / OFFSET 的方言分页语句执行两条 SQLcount 和分页查询把结果封装成 IPage。这一步改写在拦截器层面完成对业务开发来说完全透明。但也正因为透明很多人出了问题根本不知道去哪里排查。后面第 4 章专门讲。4. 分页失效问题排查实录分页失效是网络检索很多的情况很真实。这里结合我踩过的坑整理一下。4.1 分页失效的常见原因对照表现象常见原因解决方式分页参数传了但返回全量数据MybatisPlusInterceptor 没注册或拦截器顺序不对检查 Bean 配置多租户等拦截器放在 PaginationInnerInterceptor 之前自定义 SQL 分页失效方法签名没有第一个参数 IPage或返回类型不是 IPageMapper 接口方法第 1 个参数改为 Page返回类型改为 IPage分页 SQL 生成错误DbType 配置错误比如 MySQL 配成了 Oracle检查 DbType 与数据库实际类型一致翻页后数据重复或缺失ORDER BY 字段不唯一排序字段后面追加主键比如 ORDER BY sort_order, idcount 结果不对自定义 SQL 里有 group by 或 join手动提供 count SQL或拆成两条查询单页数据超过限制maxLimit 未配置被恶意传 pageSize配置 pagination.setMaxLimit(500L)这张表基本可以覆盖大多数分页失效的现场。下面挑几个细说。4.2 自定义 SQL 分页失效方法签名是关键很多人用 MyBatis Plus 自带 selectPage 没问题但一写多表 join 就改用自定义 SQLSelect(SELECT p.*, s.stock_count FROM product p LEFT JOIN sku_stock s ON p.id s.product_id WHERE p.status 1) IPageProductDetailVO pageProductDetail(Page? page, Param(query) ProductQuery query);这里注意两点Page 参数必须是方法第一个参数MyBatis Plus 的分页插件是靠参数位置识别哪个是分页参数的返回类型要是 IPage不能是 List。如果写成ListProduct pageProductDetail(...)分页插件不会生效直接返回全量。另外如果 select 里头有 GROUP BY分页插件的自动 count 可能不准。这种情况我一般手动写 count SQL或者把查询拆成两步先查分页后的 id 列表再根据 id 查详情。后一种方式在性能上也更好下面第 5 章会用到这个思路。4.3 连接查询 嵌套结果映射导致分页记录数不对还有一个更隐蔽的坑一对多关联查询。比如一条商品对应多张 SKU 图SQL join 之后商品记录会被放大成多条分页插件在数据库层做了 LIMIT但 MyBatis 做嵌套结果映射时可能把同一个商品的多条 SKU 记录折叠成一条然后 Page 对象里的 records 数量就少于 pageSizetotal 也可能对不上。解决方式也很简单不要在分页 SQL 里直接 join 一对多表。拆成两步第一步分页查询商品主表第二步再对结果集批量查 SKU 图并装配。这也是一个通用经验分页查询尽量只查主表关联子表在内存里要么用 IN 批量查要么加缓存。列表页的主流性能问题一半都出在这种使劲 join的写法上。4.4 排查思路先看 SQL再看拦截器遇到分页失效我建议按这个顺序排查把 MyBatis 打印 SQL 打开看执行日志里到底有没有 LIMIT。如果没有 LIMIT99% 是拦截器没生效或者方法签名不对。看方法签名。IPage 是不是第一个参数返回类型是不是 IPage。看拦截器配置。MybatisPlusInterceptor 是否被注册DbType 是否正确。如果是 Spring Boot 3.x 项目检查 MyBatis Plus 版本和 Spring Boot 版本的兼容性。有些旧版本在 Spring Boot 3 下扫描不到拦截器。这里再额外说一个我踩过的坑如果你在同一个 SQL 里使用了多个 InnerInterceptor比如多租户插件和分页插件插件的顺序很重要。多租户插件必须放在分页插件之前因为多租户插件需要先改 SQL 添加租户条件分页插件再基于改完的 SQL 去生成 count 和分页语句。顺序反了count 可能带不上租户条件导致越权看了眼别人的数据那是线上事故级别的。5. 深分页性能优化百万商品怎么翻页不卡5.1 深分页为什么慢偏移扫描与回表假设商品表有 200 万条数据你要查第 50000 页不现实但用来解释原理很直观SELECT id, product_name, price FROM product WHERE status 1 ORDER BY id LIMIT 20 OFFSET 999980;数据库要扫描 100 万行然后丢掉前 999980 行只把最后 20 行给你。前面那些行每一行都经历了读出 - 判断 status - 参与排序 - 被丢弃。更麻烦的是如果 select 的列不在索引里每扫描一行都要回表去主键索引取完整数据。LIMIT 1000000, 20可能触发百万次回表I/O 堆积接口能不慢吗。深分页慢的本质就是扫描了很多注定要丢弃的数据这个开销无论 MySQL、Oracle 还是 SQL Server 都躲不掉。5.2 覆盖索引和延迟关联优化第一种优化思路让扫描过程尽量在索引里完成。对上面的 SQL可以给(status, id)建一个联合索引查询改成SELECT id FROM product WHERE status 1 ORDER BY id LIMIT 999980, 20;此时扫描的全部是二级索引的数据不需要回表速度会快很多。但只拿到 id 不够还要用 id 去取完整记录。这时候再用延迟关联SELECT p.* FROM product p INNER JOIN ( SELECT id FROM product WHERE status 1 ORDER BY id LIMIT 999980, 20 ) t ON p.id t.id;内存里实际只保留 20 个 id再用这 20 个 id 去主键索引回表回表成本可以忽略。实测在百万级数据下这种写法比直接 LIMIT 快一到两个数量级。缺点是需要两层 SQL复杂筛选条件时不太好拼。5.3 游标分页Keyset改造方案如果产品交互允许建议直接改成游标分页几乎可以根治深分页问题。思路是不用页码而用最后一条记录的排序值作为下一页的起点。-- 第一页 SELECT id, product_name, price FROM product WHERE status 1 ORDER BY id LIMIT 20; -- 下一页带上上一页最后一条记录的 id SELECT id, product_name, price FROM product WHERE status 1 AND id #{lastId} ORDER BY id LIMIT 20;只需(status, id)联合索引数据库从 lastId 那一刻才开始扫描一次最多只扫 20 条无论翻到多深都很快。代价是失去了跳页能力只能下一页不能直接点第 100000 页如果有多个排序字段游标要携带多个字段的值数据新增/删除时游标分页本身比页码分页更稳定。因为你不会因为中间插入一条数据而导致后面页的数据整体位移。电商商品列表、搜索结果这类移动端场景产品上本来就有点击加载更多的习惯用游标分页体验一级棒。PC 端电商列表依然保留页码跳转那就在后台展示页码接口层做一层转换。5.4 Redis 缓存分页的思路大促时商品列表的查询压力非常大。很多团队会把热门分类的商品列表缓存到 Redis再做分页。Redis 分页有两种主流姿势LIST 类型用LPUSH把商品 id 按顺序塞进 List然后用LRANGE key start stop取指定区间。第一页LRANGE hot_products 0 19第二页LRANGE hot_products 20 39。这种方式最像传统分页性能极好每次只取 20 个元素。ZSET 类型如果列表需要按价格、销量、上架时间排序用 ZSETmember 是商品 idscore 是排序值。用ZRANGE key start stop或者ZRANGEBYSCORE key min max LIMIT offset count取数据。ZSET 天然支持范围查询排序和分页都能做。注意缓存分页有几个实践限制第 100 页之后的数据几乎没人看没必要全量缓存。业界常见做法是只缓存前 N 页比如前 20 页一共 500 条翻到底直接穿透到数据库。缓存更新要处理好。商品上新、下架、价格变化需要同步更新缓存或者用缓存双删 过期时间兜底的策略。Redis 的 list/zset 里存的是 id拿到 id 之后还要批量回 MySQL 查商品详情。这一步可以走本地缓存或 Redis 的 hash避免每条商品都查一次数据库。这里再补充一个经验如果你用了 Redis 分页前端加载更多可以直接用游标思路下一页时把当前最后一个 score 值传给后端后端用ZRANGEBYSCORE ... (lastScore继续往后取比 LRANGE 每次从头算 offset 更稳。5.5 count 查询的性能优化分页接口除了查询当前页数据还要回总数。count 在数据量大时也很贵尤其是 where 条件复杂时。优化的思路用覆盖索引count 查询只扫二级索引不要让它回表。大促期间允许近似total 不要求精确时可以走缓存里的近似值或者直接返回一个totalnull前端只显示加载更多。有些场景甚至可以直接去掉 total做无限滚动。分页插件优化MyBatis Plus 默认 optimizeCountSql 会把 order by 去掉但如果你有复杂的 join/group by手动写 count SQL 可能更靠谱。如果有只显示前 1000 条的业务规则可以在 count SQL 里加个上限判断业务上明确告知用户有多少条即可未必非要精确统计全表。6. 前端分页组件与后端接口的配合细节6.1 分页接口的字段约定前端和后端之间分页参数和返回结构最好统一约定。电商项目里常见的约定请求参数参数名含义示例page页码从1开始1pageSize每页条数20返回结构{ code: 0, data: { records: [ { id: 1, productName: xxx, price: 18.8 } ], total: 1024, page: 1, pageSize: 20, pages: 52 } }字段命名要统一。有的后端喜欢返回current、sizeMyBatis Plus 默认命名有的喜欢page、pageSize。如果不统一前端每次都晕。我一般建议自定义一个 PageResult 统一转成page/pageSize/total/pages/records这样对前端最友好。6.2 前端分页组件怎么接数据以 Vue 3 Element Plus 为例script setup import { ref } from vue import { getProductPage } from /api/product const page ref(1) const pageSize ref(20) const total ref(0) const records ref([]) async function loadData() { const res await getProductPage({ page: page.value, pageSize: pageSize.value, keyword: keyword.value }) records.value res.data.records total.value res.data.total } function handlePageChange(p) { page.value p loadData() } function handleSizeChange(size) { pageSize.value size page.value 1 // 每页条数变化后通常要回到第一页 loadData() } /script template el-pagination v-model:current-pagepage :page-sizepageSize :totaltotal layouttotal, prev, pager, next, sizes current-changehandlePageChange size-changehandleSizeChange / /template这里有两个小细节条数变化时一定要重置页码到 1。如果 pageSize 从 20 改成 50而当前在第 10 页那偏移量 (10-1)*50450用户大概率看不到自己想看的内容。搜索条件变化比如关键词、分类、价格区间变了也要重置页码到 1。不重置的话用户搜索出来的第 8 页完全没有上下文体验很差。6.3 快速切换页码的竞态处理这个问题很隐蔽我前后端联调的时候踩过好几次。用户快速点击下一页再上一页两个请求同时发出。如果接口 A 先返回但接口 B 的后返回页面会被陈旧数据覆盖。表现就是明明点了第 2 页页面上显示的却是第 3 页的数据。处理方式有几种利用请求序号每次发请求前seq响应时只保留当前 seq 匹配的那次。用 AbortController 取消上一个请求浏览器原生支持。后端返回数据时带上请求的 page 参数前端比对。我推荐前两种。注意竞态问题在分页组件上要特别小心因为用户翻页很快而商品列表接口在深分页时响应时间可能差异很大迟到的响应会覆盖更早的响应。let seq 0 async function loadData() { const currentSeq seq const res await getProductPage(params) if (currentSeq seq) { records.value res.data.records total.value res.data.total } }这样只有最后一次请求能更新数据旧请求即使先返回也会被丢弃。这个写法很简单但能避免很多匪夷所思的页面数据乱跳问题。6.4 电商前端的加载更多模式移动端电商更喜欢加载更多用传统的上一页下一页组件反而少见。加载更多本质上就是游标分页的前端形态。记录 lastId 或 score触底时带上 lastId 请求下一页接口返回的新数据追加到列表末尾而不是替换列表。实现注意点追加数据时要防止重复。翻页过程中如果后端排序不稳定可能会有重复商品出现。除了后端保证排序稳定前端也可以用商品 id 做一层去重。这种兜底逻辑看起来不优雅但确实能在数据有问题时不至于把商品重复展示给用户。7. 电商场景下分页的几个隐藏坑7.1 排序字段不唯一导致的错乱这是一个非常常见的坑。商品列表一般会提供综合排序、销量、价格等多个排序维度。比如按销量排序SELECT * FROM product ORDER BY sales_count DESC LIMIT 20 OFFSET 40;如果 sales_count 相同的有几十条MySQL 只能按某种内部顺序返回那么第一页和第二页之间可能有重复或漏掉。更麻烦的是如果同时有新的订单进来导致 sales_count 变化翻页时数据会跳动。解决方案很简单排序字段加唯一主键。SELECT * FROM product ORDER BY sales_count DESC, id ASC LIMIT 20 OFFSET 40;用 id 做 tie breaker让顺序彻底确定。这个规则同样适用于游标分页游标需要同时携带 sales_count 和 id。7.2 大促场景下的缓存与分页结合大促时商品列表页的 qps 非常高纯数据库分页是扛不住的。常见的组合方案是热数据列表分类页、搜索词页用 Redis ZSET 缓存前 N 页的商品 id商品详情用 Redis Hash 缓存商品核心信息批量 mget数据库兜底超过缓存范围后走延迟关联优化过的分页 SQL。这里有一个取舍缓存一致性在大促期间不能要求太严。商品价格变动、库存变化如果很久才生效用户体验反而不好。所以缓存超时时间不能太长大促期间普通列表建议 30~60 秒过期秒杀类商品单独走库存接口不做列表缓存。7.3 分页与索引设计是一对好兄弟分页优化到最后几乎都落在索引设计上。单字段筛选WHERE status 1 ORDER BY id加(status, id)索引多条件筛选品牌、分类、价格区间等尽量把高频过滤字段 排序字段放到一个联合索引里注意联合索引的最左前缀原则(status, category_id, id)的索引不能跳过 category_id 直接用status id范围查用了也不高效。模糊搜索不进索引LIKE %关键词%规划到 ES 或者 MySQL 的全文索引。我见过太多系统分页写得没毛病但 SQL 没有可用索引导致 LIMIT 20 也要扫全表。排查这类问题时EXPLAIN是第一步。看 key 是不是 null看 rows 是不是接近全表基本一眼就知道问题在哪。最后说点实在的。分页功能看起来简单但把它做到位涉及 SQL 原理、索引设计、框架插件、前后端协作、缓存策略一整条链路。我这些年最大的体会是遇到分页问题不要急着在代码层面打补丁先弄清楚数据量和访问模式再选合适的分页形态。几万条数据的后台管理系统用传统 page/pageSize 就够了别为了炫技去搞游标分页百万级商品列表首选覆盖索引 延迟关联前端配合加载更多体验和性能都能兼顾。希望这篇以电商商品为例的分享能帮你在做 web 分页时少踩几个坑。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Authelia `storage migrate` 命令详解:SQLite / MySQL / PostgreSQL 数据库 Schema 迁移管理指南 2026/9/13 15:45:27

Authelia `storage migrate` 命令详解:SQLite / MySQL / PostgreSQL 数据库 Schema 迁移管理指南

Authelia storage migrate 命令详解:SQLite / MySQL / PostgreSQL 数据库 Schema 迁移管理指南 【免费下载链接】authelia The Single Sign-On Multi-Factor portal for web apps. OpenID Certified™ and Post-Quantum Cryptography Ready. 项目地址: https://gi…

阅读更多 →
PostHog HogQL 解析器:基于 ANTLR 的语法生成流程与 C++/WASM 双端解析器构建管线 2026/9/13 15:45:27

PostHog HogQL 解析器:基于 ANTLR 的语法生成流程与 C++/WASM 双端解析器构建管线

PostHog HogQL 解析器:基于 ANTLR 的语法生成流程与 C/WASM 双端解析器构建管线 【免费下载链接】posthog :hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay,…

阅读更多 →
Teable Standalone 独立版 Docker 自托管部署指南:开箱即用的单机部署方案 2026/9/13 15:45:27

Teable Standalone 独立版 Docker 自托管部署指南:开箱即用的单机部署方案

Teable Standalone 独立版 Docker 自托管部署指南:开箱即用的单机部署方案 【免费下载链接】teable ✨ AI Spreadsheet for Business 项目地址: https://gitcode.com/GitHub_Trending/te/teable Teable 是一个构建在 PostgreSQL 之上的业务数据平台&#xff…

阅读更多 →
Backstage v1.35.0-next.1 变更深度解读:GitLab Catalog 提供者能力演进与升级实践 2026/9/13 15:45:27

Backstage v1.35.0-next.1 变更深度解读:GitLab Catalog 提供者能力演进与升级实践

Backstage v1.35.0-next.1 变更深度解读:GitLab Catalog 提供者能力演进与升级实践 【免费下载链接】backstage Backstage is an open framework for building developer portals 项目地址: https://gitcode.com/GitHub_Trending/ba/backstage 本文以 Backst…

阅读更多 →
Arduino-ESP32 ESP_Video 库实战指南:基于 MIPI-CSI 与 DVP 接口的 V4L2 风格摄像头采集 2026/9/13 15:45:27

Arduino-ESP32 ESP_Video 库实战指南:基于 MIPI-CSI 与 DVP 接口的 V4L2 风格摄像头采集

Arduino-ESP32 ESP_Video 库实战指南:基于 MIPI-CSI 与 DVP 接口的 V4L2 风格摄像头采集 【免费下载链接】arduino-esp32 Arduino core for the ESP32 family of SoCs 项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32 ESP_Video 是 arduino…

阅读更多 →
Tasmota 中的 ScioSense ENS16x 驱动库:ENS160/ENS161 四通道 MOX 气体传感器 I2C 使用指南 2026/9/13 15:42:27

Tasmota 中的 ScioSense ENS16x 驱动库:ENS160/ENS161 四通道 MOX 气体传感器 I2C 使用指南

Tasmota 中的 ScioSense ENS16x 驱动库:ENS160/ENS161 四通道 MOX 气体传感器 I2C 使用指南 【免费下载链接】Tasmota Alternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or r…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞