前言

MySQL排序分页作为企业级应用的核心功能之一,直接影响着用户体验和系统性能。通过深入理解MySQL分页的常见陷阱,掌握分页优化技巧,能够构建一个高效、稳定、可扩展的分页系统,确保企业级应用的流畅运行。本文从MySQL分页陷阱分析到性能优化,从基础原理到企业级应用,系统梳理MySQL分页的完整解决方案。

一、MySQL分页架构设计

1.1 MySQL分页整体架构

1.2 MySQL分页陷阱架构

二、MySQL分页陷阱分析

2.1 深分页性能陷阱

2.1.1 问题描述

当使用LIMIT offset, size进行深分页时,MySQL需要跳过大量记录,导致性能急剧下降。

2.1.2 代码示例

1
2
3
4
5
6
7
8
9
-- ❌ 错误:深分页查询,性能极差
SELECT * FROM users
ORDER BY create_time DESC
LIMIT 100000, 20;

-- 执行计划分析
EXPLAIN SELECT * FROM users
ORDER BY create_time DESC
LIMIT 100000, 20;

2.1.3 性能分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class PaginationService {

@Autowired
private UserMapper userMapper;

// ❌ 错误:深分页查询
public PageResult<User> getUsersDeepPagination(int page, int size) {
int offset = (page - 1) * size;

// 当page=5000, size=20时,offset=99980
// MySQL需要扫描前99980条记录,性能极差
List<User> users = userMapper.selectUsersWithPagination(offset, size);
long total = userMapper.countUsers();

return new PageResult<>(users, total, page, size);
}

// ✅ 正确:游标分页
public PageResult<User> getUsersCursorPagination(Long lastId, int size) {
List<User> users = userMapper.selectUsersWithCursor(lastId, size);
boolean hasMore = users.size() == size;

return new PageResult<>(users, hasMore);
}
}

2.1.4 解决方案

  1. 游标分页
  2. 延迟关联
  3. 分页缓存
  4. 索引优化

2.2 排序字段重复陷阱

2.2.1 问题描述

当排序字段存在重复值时,分页结果可能出现数据重复或遗漏。

2.2.2 代码示例

1
2
3
4
5
6
7
-- ❌ 错误:排序字段重复,分页结果不稳定
SELECT * FROM orders
ORDER BY status, create_time
LIMIT 0, 10;

-- 如果status字段值相同,create_time也相同
-- 分页结果可能不一致

2.2.3 解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class OrderService {

@Autowired
private OrderMapper orderMapper;

// ❌ 错误:排序不稳定
public List<Order> getOrdersUnstable(int page, int size) {
int offset = (page - 1) * size;
return orderMapper.selectOrdersUnstable(offset, size);
}

// ✅ 正确:添加唯一排序字段
public List<Order> getOrdersStable(int page, int size) {
int offset = (page - 1) * size;
return orderMapper.selectOrdersStable(offset, size);
}
}
1
2
3
4
-- ✅ 正确:添加唯一排序字段
SELECT * FROM orders
ORDER BY status, create_time, id
LIMIT 0, 10;

2.3 索引失效陷阱

2.3.1 问题描述

不当的排序字段组合可能导致索引失效,影响查询性能。

2.3.2 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 表结构
CREATE TABLE products (
id BIGINT PRIMARY KEY,
category_id BIGINT,
price DECIMAL(10,2),
create_time TIMESTAMP,
INDEX idx_category_price (category_id, price)
);

-- ❌ 错误:索引失效
SELECT * FROM products
WHERE category_id = 1
ORDER BY create_time DESC
LIMIT 0, 20;

-- ✅ 正确:使用复合索引
SELECT * FROM products
WHERE category_id = 1
ORDER BY price DESC, id DESC
LIMIT 0, 20;

2.3.3 索引优化策略

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class ProductService {

@Autowired
private ProductMapper productMapper;

// ✅ 正确:利用复合索引
public List<Product> getProductsOptimized(Long categoryId, int page, int size) {
int offset = (page - 1) * size;
return productMapper.selectProductsWithIndex(categoryId, offset, size);
}
}

2.4 内存溢出陷阱

2.4.1 问题描述

大数据量分页查询可能导致内存溢出,影响系统稳定性。

2.4.2 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Service
public class DataService {

@Autowired
private DataMapper dataMapper;

// ❌ 错误:一次性加载大量数据
public List<Data> getAllData(int page, int size) {
int offset = (page - 1) * size;
// 如果size过大,可能导致内存溢出
return dataMapper.selectAllData(offset, size);
}

// ✅ 正确:限制分页大小
public List<Data> getDataWithLimit(int page, int size) {
// 限制最大分页大小
int maxSize = 1000;
if (size > maxSize) {
size = maxSize;
}

int offset = (page - 1) * size;
return dataMapper.selectDataWithLimit(offset, size);
}
}

三、MySQL分页优化策略

3.1 游标分页优化

3.1.1 游标分页原理

使用唯一标识符作为游标,避免深分页问题。

3.1.2 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class CursorPaginationService {

@Autowired
private UserMapper userMapper;

public CursorPageResult<User> getUsersWithCursor(Long lastId, int size) {
List<User> users = userMapper.selectUsersAfterId(lastId, size);

boolean hasMore = users.size() == size;
Long nextCursor = hasMore ? users.get(users.size() - 1).getId() : null;

return new CursorPageResult<>(users, nextCursor, hasMore);
}
}
1
2
3
4
5
-- 游标分页SQL
SELECT * FROM users
WHERE id > #{lastId}
ORDER BY id ASC
LIMIT #{size};

3.1.3 游标分页优势

  1. 性能稳定:不受分页深度影响
  2. 结果一致:避免数据重复或遗漏
  3. 内存友好:只加载需要的数据

3.2 延迟关联优化

3.2.1 延迟关联原理

先通过索引获取主键,再关联查询完整数据。

3.2.2 代码实现

1
2
3
4
5
6
7
8
9
10
11
@Service
public class DeferredJoinService {

@Autowired
private UserMapper userMapper;

public List<User> getUsersWithDeferredJoin(int page, int size) {
int offset = (page - 1) * size;
return userMapper.selectUsersWithDeferredJoin(offset, size);
}
}
1
2
3
4
5
6
7
8
-- 延迟关联SQL
SELECT u.* FROM users u
INNER JOIN (
SELECT id FROM users
ORDER BY create_time DESC
LIMIT #{offset}, #{size}
) t ON u.id = t.id
ORDER BY u.create_time DESC;

3.3 分页缓存优化

3.3.1 缓存策略

缓存热点分页数据,减少数据库压力。

3.3.2 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
public class CachedPaginationService {

@Autowired
private UserMapper userMapper;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private static final String PAGE_CACHE_KEY = "users:page:";
private static final int CACHE_EXPIRE_SECONDS = 300;

public PageResult<User> getUsersWithCache(int page, int size) {
String cacheKey = PAGE_CACHE_KEY + page + ":" + size;

// 尝试从缓存获取
PageResult<User> cachedResult = (PageResult<User>) redisTemplate.opsForValue().get(cacheKey);
if (cachedResult != null) {
return cachedResult;
}

// 缓存未命中,查询数据库
int offset = (page - 1) * size;
List<User> users = userMapper.selectUsersWithPagination(offset, size);
long total = userMapper.countUsers();

PageResult<User> result = new PageResult<>(users, total, page, size);

// 缓存结果
redisTemplate.opsForValue().set(cacheKey, result, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);

return result;
}
}

3.4 索引优化策略

3.4.1 复合索引设计

根据查询条件设计合适的复合索引。

3.4.2 代码示例

1
2
3
4
5
6
7
8
-- 根据查询模式设计索引
CREATE INDEX idx_category_status_time ON products (category_id, status, create_time);

-- 支持以下查询
SELECT * FROM products
WHERE category_id = 1 AND status = 'ACTIVE'
ORDER BY create_time DESC
LIMIT 0, 20;

3.4.3 索引监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class IndexMonitor {

@Autowired
private DataSource dataSource;

public void monitorIndexUsage() {
try (Connection connection = dataSource.getConnection()) {
// 查询索引使用情况
String sql = "SHOW INDEX FROM products";
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet resultSet = statement.executeQuery();

while (resultSet.next()) {
String indexName = resultSet.getString("Key_name");
String columnName = resultSet.getString("Column_name");
System.out.println("索引: " + indexName + ", 字段: " + columnName);
}
} catch (SQLException e) {
log.error("监控索引使用情况失败", e);
}
}
}

四、企业级分页解决方案

4.1 分页组件设计

4.1.1 通用分页组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class PaginationComponent {

public <T> PageResult<T> paginate(List<T> data, int page, int size) {
int total = data.size();
int totalPages = (total + size - 1) / size;

int start = (page - 1) * size;
int end = Math.min(start + size, total);

List<T> pageData = data.subList(start, end);

return new PageResult<>(pageData, total, page, size, totalPages);
}

public <T> CursorPageResult<T> paginateWithCursor(List<T> data, int size) {
boolean hasMore = data.size() > size;
List<T> pageData = hasMore ? data.subList(0, size) : data;

return new CursorPageResult<>(pageData, hasMore);
}
}

4.1.2 分页结果封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Data
@AllArgsConstructor
public class PageResult<T> {
private List<T> data;
private long total;
private int page;
private int size;
private int totalPages;

public PageResult(List<T> data, long total, int page, int size) {
this.data = data;
this.total = total;
this.page = page;
this.size = size;
this.totalPages = (int) ((total + size - 1) / size);
}
}

@Data
@AllArgsConstructor
public class CursorPageResult<T> {
private List<T> data;
private Object nextCursor;
private boolean hasMore;
}

4.2 分页监控与告警

4.2.1 分页性能监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class PaginationMonitor {

private final MeterRegistry meterRegistry;
private final Timer paginationTimer;
private final Counter paginationCounter;

public PaginationMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.paginationTimer = Timer.builder("pagination.duration")
.description("分页查询耗时")
.register(meterRegistry);
this.paginationCounter = Counter.builder("pagination.count")
.description("分页查询次数")
.register(meterRegistry);
}

public <T> PageResult<T> monitorPagination(Supplier<PageResult<T>> paginationSupplier) {
return Timer.Sample.start(meterRegistry)
.stop(paginationTimer)
.recordCallable(() -> {
paginationCounter.increment();
return paginationSupplier.get();
});
}
}

4.2.2 分页告警配置

1
2
3
4
5
6
7
8
9
10
# application.yml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
export:
prometheus:
enabled: true

4.3 分页安全防护

4.3.1 分页参数验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class PaginationValidator {

private static final int MAX_PAGE_SIZE = 1000;
private static final int DEFAULT_PAGE_SIZE = 20;

public PaginationParams validateAndNormalize(int page, int size) {
// 参数验证
if (page < 1) {
page = 1;
}

if (size < 1) {
size = DEFAULT_PAGE_SIZE;
} else if (size > MAX_PAGE_SIZE) {
size = MAX_PAGE_SIZE;
}

return new PaginationParams(page, size);
}
}

4.3.2 分页限流控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class PaginationRateLimiter {

@Autowired
private RedisTemplate<String, String> redisTemplate;

public boolean isAllowed(String userId, int page) {
String key = "pagination:limit:" + userId;
String count = redisTemplate.opsForValue().get(key);

if (count == null) {
redisTemplate.opsForValue().set(key, "1", 60, TimeUnit.SECONDS);
return true;
}

int currentCount = Integer.parseInt(count);
if (currentCount >= 100) { // 每分钟最多100次分页请求
return false;
}

redisTemplate.opsForValue().increment(key);
return true;
}
}

4.4 分页性能优化

4.4.1 分页预加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class PreloadPaginationService {

@Autowired
private UserMapper userMapper;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Async
public void preloadNextPage(int currentPage, int size) {
// 异步预加载下一页数据
int nextPage = currentPage + 1;
String cacheKey = "users:page:" + nextPage + ":" + size;

if (redisTemplate.opsForValue().get(cacheKey) == null) {
int offset = (nextPage - 1) * size;
List<User> users = userMapper.selectUsersWithPagination(offset, size);
redisTemplate.opsForValue().set(cacheKey, users, 300, TimeUnit.SECONDS);
}
}
}

4.4.2 分页结果压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class PaginationCompressor {

public String compressPageResult(PageResult<?> result) {
try {
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(result);

// 使用GZIP压缩
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzos = new GZIPOutputStream(baos);
gzos.write(json.getBytes());
gzos.close();

return Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (Exception e) {
log.error("压缩分页结果失败", e);
return null;
}
}
}

五、MySQL分页最佳实践

5.1 分页设计原则

5.1.1 分页策略选择

  1. 游标分页:适用于实时数据、大数据量
  2. 传统分页:适用于小数据量、需要总数
  3. 缓存分页:适用于热点数据、读多写少

5.1.2 分页参数设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
public class PaginationRequest {
private int page = 1;
private int size = 20;
private String sortField = "id";
private String sortOrder = "ASC";
private Object cursor;

public int getOffset() {
return (page - 1) * size;
}

public boolean isCursorPagination() {
return cursor != null;
}
}

5.2 性能优化策略

5.2.1 数据库优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 1. 合理设计索引
CREATE INDEX idx_category_status_time ON products (category_id, status, create_time);

-- 2. 使用覆盖索引
CREATE INDEX idx_covering ON products (category_id, status, create_time, id, name);

-- 3. 分页查询优化
SELECT p.* FROM products p
INNER JOIN (
SELECT id FROM products
WHERE category_id = 1 AND status = 'ACTIVE'
ORDER BY create_time DESC
LIMIT 1000, 20
) t ON p.id = t.id;

5.2.2 应用层优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Service
public class OptimizedPaginationService {

@Autowired
private UserMapper userMapper;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

public PageResult<User> getUsersOptimized(PaginationRequest request) {
// 1. 参数验证
validatePaginationParams(request);

// 2. 缓存检查
String cacheKey = buildCacheKey(request);
PageResult<User> cached = getFromCache(cacheKey);
if (cached != null) {
return cached;
}

// 3. 数据库查询
PageResult<User> result = queryFromDatabase(request);

// 4. 缓存结果
cacheResult(cacheKey, result);

// 5. 异步预加载
preloadNextPage(request);

return result;
}
}

5.3 监控与运维

5.3.1 分页监控指标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
public class PaginationMetrics {

private final MeterRegistry meterRegistry;

public PaginationMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}

public void recordPaginationMetrics(String operation, int page, int size, long duration) {
// 记录分页查询耗时
Timer.Sample.start(meterRegistry)
.stop(Timer.builder("pagination.duration")
.tag("operation", operation)
.tag("page", String.valueOf(page))
.register(meterRegistry))
.record(duration, TimeUnit.MILLISECONDS);

// 记录分页查询次数
Counter.builder("pagination.count")
.tag("operation", operation)
.register(meterRegistry)
.increment();
}
}

5.3.2 分页健康检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Component
public class PaginationHealthIndicator implements HealthIndicator {

@Autowired
private DataSource dataSource;

@Override
public Health health() {
try {
// 检查分页查询性能
long startTime = System.currentTimeMillis();

try (Connection connection = dataSource.getConnection()) {
PreparedStatement statement = connection.prepareStatement(
"SELECT COUNT(*) FROM users LIMIT 1"
);
statement.executeQuery();
}

long duration = System.currentTimeMillis() - startTime;

if (duration < 1000) {
return Health.up()
.withDetail("pagination", "Healthy")
.withDetail("response_time", duration + "ms")
.build();
} else {
return Health.down()
.withDetail("pagination", "Slow")
.withDetail("response_time", duration + "ms")
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}

六、总结

MySQL排序分页是企业级应用开发中的核心功能,通过深入理解分页的各种陷阱,掌握有效的优化技巧,能够构建一个高效、稳定、可扩展的分页系统。

6.1 核心要点

  1. 分页陷阱:深分页性能、排序字段重复、索引失效、内存溢出
  2. 优化策略:游标分页、延迟关联、分页缓存、索引优化
  3. 解决方案:参数验证、性能监控、安全防护、预加载优化
  4. 最佳实践:分页设计原则、性能优化策略、监控运维

6.2 企业级应用建议

  1. 建立完善的分页监控体系,实时监控分页性能
  2. 制定分页设计规范,确保分页使用的一致性
  3. 实施定期的分页性能检查,及时发现和解决问题
  4. 建立分页性能优化机制,持续提升系统性能

通过以上措施,可以构建一个高效、稳定、可扩展的MySQL分页系统,为企业的各种业务场景提供流畅的分页体验。