分页查询 LIMIT

# 分页查询 LIMIT

# 概述

分页查询是数据库查询中的重要技术,用于限制查询结果的数量,避免一次性返回大量数据造成性能问题。LIMIT子句是实现分页查询的核心语法。

# 基本语法

# MySQL语法

-- 基本LIMIT语法
SELECT column1, column2, ...
FROM table_name
WHERE condition
ORDER BY column_name
LIMIT row_count;

-- LIMIT with OFFSET
SELECT column1, column2, ...
FROM table_name
WHERE condition
ORDER BY column_name
LIMIT offset, row_count;

-- 或者使用OFFSET关键字
SELECT column1, column2, ...
FROM table_name
WHERE condition
ORDER BY column_name
LIMIT row_count OFFSET offset;

# 其他数据库语法

-- SQL Server
SELECT TOP 10 column1, column2
FROM table_name
WHERE condition
ORDER BY column_name;

-- SQL Server 2012+
SELECT column1, column2
FROM table_name
WHERE condition
ORDER BY column_name
OFFSET 10 ROWS
FETCH NEXT 10 ROWS ONLY;

-- PostgreSQL
SELECT column1, column2
FROM table_name
WHERE condition
ORDER BY column_name
LIMIT 10 OFFSET 10;

-- Oracle
SELECT column1, column2
FROM (
    SELECT column1, column2, ROWNUM as rn
    FROM table_name
    WHERE condition
    ORDER BY column_name
)
WHERE rn BETWEEN 11 AND 20;

-- Oracle 12c+
SELECT column1, column2
FROM table_name
WHERE condition
ORDER BY column_name
OFFSET 10 ROWS
FETCH NEXT 10 ROWS ONLY;

# 测试数据准备

-- 创建用户表
CREATE TABLE users (
    user_id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50),
    email VARCHAR(100),
    age INT,
    city VARCHAR(50),
    registration_date DATE,
    status VARCHAR(20)
);

-- 插入大量测试数据
INSERT INTO users (username, email, age, city, registration_date, status) VALUES
('user001', 'user001@example.com', 25, '北京', '2023-01-15', 'active'),
('user002', 'user002@example.com', 30, '上海', '2023-01-16', 'active'),
('user003', 'user003@example.com', 28, '广州', '2023-01-17', 'inactive'),
('user004', 'user004@example.com', 35, '深圳', '2023-01-18', 'active'),
('user005', 'user005@example.com', 22, '杭州', '2023-01-19', 'active'),
('user006', 'user006@example.com', 27, '成都', '2023-01-20', 'inactive'),
('user007', 'user007@example.com', 33, '武汉', '2023-01-21', 'active'),
('user008', 'user008@example.com', 29, '西安', '2023-01-22', 'active'),
('user009', 'user009@example.com', 26, '南京', '2023-01-23', 'inactive'),
('user010', 'user010@example.com', 31, '重庆', '2023-01-24', 'active'),
('user011', 'user011@example.com', 24, '天津', '2023-01-25', 'active'),
('user012', 'user012@example.com', 32, '苏州', '2023-01-26', 'inactive'),
('user013', 'user013@example.com', 28, '青岛', '2023-01-27', 'active'),
('user014', 'user014@example.com', 30, '大连', '2023-01-28', 'active'),
('user015', 'user015@example.com', 27, '厦门', '2023-01-29', 'inactive'),
('user016', 'user016@example.com', 34, '宁波', '2023-01-30', 'active'),
('user017', 'user017@example.com', 25, '无锡', '2023-01-31', 'active'),
('user018', 'user018@example.com', 29, '佛山', '2023-02-01', 'inactive'),
('user019', 'user019@example.com', 31, '东莞', '2023-02-02', 'active'),
('user020', 'user020@example.com', 26, '石家庄', '2023-02-03', 'active');

-- 创建订单表
CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    product_name VARCHAR(100),
    order_amount DECIMAL(10,2),
    order_date DATE,
    order_status VARCHAR(20)
);

-- 插入订单数据
INSERT INTO orders (user_id, product_name, order_amount, order_date, order_status) VALUES
(1, 'iPhone 14', 5999.00, '2023-02-01', 'completed'),
(2, 'MacBook Pro', 12999.00, '2023-02-02', 'completed'),
(3, 'iPad Air', 3999.00, '2023-02-03', 'pending'),
(4, 'AirPods Pro', 1999.00, '2023-02-04', 'completed'),
(5, 'Apple Watch', 2999.00, '2023-02-05', 'completed'),
(1, 'iPhone Case', 199.00, '2023-02-06', 'completed'),
(2, 'Magic Mouse', 599.00, '2023-02-07', 'shipped'),
(3, 'Magic Keyboard', 1299.00, '2023-02-08', 'completed'),
(4, 'USB-C Cable', 199.00, '2023-02-09', 'completed'),
(5, 'Power Adapter', 399.00, '2023-02-10', 'pending');

# 基本分页查询

# 获取前N条记录

-- 获取前5个用户
SELECT user_id, username, email, city
FROM users
ORDER BY user_id
LIMIT 5;

-- 获取最新注册的10个用户
SELECT user_id, username, registration_date
FROM users
ORDER BY registration_date DESC
LIMIT 10;

-- 获取年龄最大的3个用户
SELECT username, age, city
FROM users
ORDER BY age DESC
LIMIT 3;

# 跳过前N条记录

-- 跳过前5条,获取接下来的5条记录(第6-10条)
SELECT user_id, username, email
FROM users
ORDER BY user_id
LIMIT 5 OFFSET 5;

-- 等价写法
SELECT user_id, username, email
FROM users
ORDER BY user_id
LIMIT 5, 5;

# 分页查询实现

# 基本分页逻辑

-- 分页参数
SET @page_size = 5;     -- 每页显示5条记录
SET @page_number = 2;   -- 第2页(从1开始计数)
SET @offset = (@page_number - 1) * @page_size;

-- 执行分页查询
SELECT user_id, username, email, city
FROM users
ORDER BY user_id
LIMIT @page_size OFFSET @offset;

-- 或者直接计算
SELECT user_id, username, email, city
FROM users
ORDER BY user_id
LIMIT 5 OFFSET 5;  -- 第2页,每页5条

# 获取总记录数

-- 获取总记录数(用于计算总页数)
SELECT COUNT(*) as total_records
FROM users
WHERE status = 'active';

-- 计算总页数
SELECT 
    COUNT(*) as total_records,
    CEIL(COUNT(*) / 5) as total_pages
FROM users
WHERE status = 'active';

# 完整分页查询

-- 分页查询活跃用户
SET @page_size = 5;
SET @page_number = 2;
SET @offset = (@page_number - 1) * @page_size;

-- 获取分页数据
SELECT 
    user_id, 
    username, 
    email, 
    city, 
    registration_date
FROM users
WHERE status = 'active'
ORDER BY registration_date DESC
LIMIT @page_size OFFSET @offset;

-- 获取总数信息
SELECT 
    COUNT(*) as total_records,
    CEIL(COUNT(*) / @page_size) as total_pages,
    @page_number as current_page,
    @page_size as page_size
FROM users
WHERE status = 'active';

# 高级分页技术

# 游标分页(Cursor-based Pagination)

-- 基于ID的游标分页(更高效,适合大数据量)
-- 第一页
SELECT user_id, username, email
FROM users
WHERE status = 'active'
ORDER BY user_id
LIMIT 10;

-- 下一页(假设上一页最后一条记录的user_id是10)
SELECT user_id, username, email
FROM users
WHERE status = 'active' AND user_id > 10
ORDER BY user_id
LIMIT 10;

-- 上一页(假设当前页第一条记录的user_id是21)
SELECT user_id, username, email
FROM users
WHERE status = 'active' AND user_id < 21
ORDER BY user_id DESC
LIMIT 10;

# 基于时间的游标分页

-- 基于注册时间的分页
-- 第一页
SELECT user_id, username, registration_date
FROM users
ORDER BY registration_date DESC, user_id DESC
LIMIT 10;

-- 下一页(假设上一页最后一条记录的时间是'2023-01-20',ID是6)
SELECT user_id, username, registration_date
FROM users
WHERE (registration_date < '2023-01-20') 
   OR (registration_date = '2023-01-20' AND user_id < 6)
ORDER BY registration_date DESC, user_id DESC
LIMIT 10;

# 窗口函数分页

-- 使用ROW_NUMBER()实现分页
SELECT 
    user_id,
    username,
    email,
    city,
    row_num
FROM (
    SELECT 
        user_id,
        username,
        email,
        city,
        ROW_NUMBER() OVER (ORDER BY user_id) as row_num
    FROM users
    WHERE status = 'active'
) ranked_users
WHERE row_num BETWEEN 6 AND 10;  -- 第2页,每页5条

-- 使用RANK()处理并列情况
SELECT 
    user_id,
    username,
    age,
    age_rank
FROM (
    SELECT 
        user_id,
        username,
        age,
        RANK() OVER (ORDER BY age DESC) as age_rank
    FROM users
) ranked_users
WHERE age_rank BETWEEN 6 AND 10;

# 实战案例

# 案例1:用户列表分页

-- 用户管理系统的分页查询
DELIMITER //
CREATE PROCEDURE GetUsersPaginated(
    IN p_page_number INT,
    IN p_page_size INT,
    IN p_status VARCHAR(20),
    IN p_city VARCHAR(50)
)
BEGIN
    DECLARE v_offset INT;
    SET v_offset = (p_page_number - 1) * p_page_size;
    
    -- 获取分页数据
    SELECT 
        user_id,
        username,
        email,
        age,
        city,
        registration_date,
        status
    FROM users
    WHERE (p_status IS NULL OR status = p_status)
      AND (p_city IS NULL OR city = p_city)
    ORDER BY registration_date DESC
    LIMIT p_page_size OFFSET v_offset;
    
    -- 获取总数
    SELECT 
        COUNT(*) as total_records,
        CEIL(COUNT(*) / p_page_size) as total_pages,
        p_page_number as current_page,
        p_page_size as page_size
    FROM users
    WHERE (p_status IS NULL OR status = p_status)
      AND (p_city IS NULL OR city = p_city);
END //
DELIMITER ;

-- 调用存储过程
CALL GetUsersPaginated(2, 5, 'active', NULL);

# 案例2:订单历史分页

-- 用户订单历史分页查询
SELECT 
    o.order_id,
    o.product_name,
    o.order_amount,
    o.order_date,
    o.order_status,
    u.username
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.user_id = 1
ORDER BY o.order_date DESC
LIMIT 5 OFFSET 0;

-- 获取用户订单总数
SELECT COUNT(*) as total_orders
FROM orders
WHERE user_id = 1;

-- 复杂条件的订单分页
SELECT 
    o.order_id,
    o.product_name,
    o.order_amount,
    o.order_date,
    o.order_status,
    u.username,
    u.city
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.order_date >= '2023-02-01'
  AND o.order_status IN ('completed', 'shipped')
  AND o.order_amount >= 500
ORDER BY o.order_amount DESC, o.order_date DESC
LIMIT 10 OFFSET 0;

# 案例3:搜索结果分页

-- 用户搜索分页
SET @search_keyword = 'user';
SET @page_size = 8;
SET @page_number = 1;
SET @offset = (@page_number - 1) * @page_size;

-- 搜索用户
SELECT 
    user_id,
    username,
    email,
    city,
    MATCH(username, email) AGAINST(@search_keyword) as relevance_score
FROM users
WHERE MATCH(username, email) AGAINST(@search_keyword)
   OR username LIKE CONCAT('%', @search_keyword, '%')
   OR email LIKE CONCAT('%', @search_keyword, '%')
ORDER BY relevance_score DESC, registration_date DESC
LIMIT @page_size OFFSET @offset;

-- 搜索结果总数
SELECT COUNT(*) as total_results
FROM users
WHERE MATCH(username, email) AGAINST(@search_keyword)
   OR username LIKE CONCAT('%', @search_keyword, '%')
   OR email LIKE CONCAT('%', @search_keyword, '%');

# 案例4:数据导出分页

-- 大数据量导出时的分页处理
DELIMITER //
CREATE PROCEDURE ExportUsersInBatches(
    IN p_batch_size INT
)
BEGIN
    DECLARE v_total_records INT;
    DECLARE v_current_offset INT DEFAULT 0;
    DECLARE v_batch_number INT DEFAULT 1;
    
    -- 获取总记录数
    SELECT COUNT(*) INTO v_total_records FROM users;
    
    -- 分批导出
    WHILE v_current_offset < v_total_records DO
        SELECT 
            CONCAT('Batch ', v_batch_number) as batch_info,
            user_id,
            username,
            email,
            city,
            registration_date
        FROM users
        ORDER BY user_id
        LIMIT p_batch_size OFFSET v_current_offset;
        
        SET v_current_offset = v_current_offset + p_batch_size;
        SET v_batch_number = v_batch_number + 1;
    END WHILE;
END //
DELIMITER ;

-- 调用导出过程
CALL ExportUsersInBatches(5);

# 性能优化

# 1. 索引优化

-- 为ORDER BY列创建索引
CREATE INDEX idx_registration_date ON users(registration_date);
CREATE INDEX idx_status_regdate ON users(status, registration_date);

-- 复合索引支持WHERE + ORDER BY + LIMIT
CREATE INDEX idx_status_city_regdate ON users(status, city, registration_date);

-- 覆盖索引减少回表
CREATE INDEX idx_cover_users ON users(status, registration_date, user_id, username, email);

# 2. 避免大偏移量

-- 问题:大偏移量性能差
SELECT * FROM users ORDER BY user_id LIMIT 10 OFFSET 100000;  -- 慢

-- 解决方案1:使用游标分页
SELECT * FROM users 
WHERE user_id > 100000 
ORDER BY user_id 
LIMIT 10;  -- 快

-- 解决方案2:子查询优化
SELECT u.*
FROM users u
JOIN (
    SELECT user_id
    FROM users
    ORDER BY user_id
    LIMIT 10 OFFSET 100000
) t ON u.user_id = t.user_id
ORDER BY u.user_id;

# 3. 计数优化

-- 问题:每次都COUNT(*)很慢
SELECT COUNT(*) FROM large_table WHERE condition;  -- 慢

-- 解决方案1:缓存总数
-- 在应用层缓存总数,定期更新

-- 解决方案2:估算总数
SELECT 
    table_rows as estimated_count
FROM information_schema.tables
WHERE table_schema = 'your_database'
  AND table_name = 'users';

-- 解决方案3:分页时不显示总数
-- 只提供"下一页"功能,不显示总页数

# 4. 查询优化

-- 优化复杂分页查询
-- 原查询(慢)
SELECT 
    u.user_id,
    u.username,
    u.email,
    COUNT(o.order_id) as order_count,
    SUM(o.order_amount) as total_amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE u.status = 'active'
GROUP BY u.user_id, u.username, u.email
ORDER BY total_amount DESC
LIMIT 10 OFFSET 20;

-- 优化后(快)
-- 先分页再关联
SELECT 
    u.user_id,
    u.username,
    u.email,
    COALESCE(os.order_count, 0) as order_count,
    COALESCE(os.total_amount, 0) as total_amount
FROM (
    SELECT user_id, username, email
    FROM users
    WHERE status = 'active'
    ORDER BY user_id  -- 使用索引排序
    LIMIT 10 OFFSET 20
) u
LEFT JOIN (
    SELECT 
        user_id,
        COUNT(*) as order_count,
        SUM(order_amount) as total_amount
    FROM orders
    GROUP BY user_id
) os ON u.user_id = os.user_id
ORDER BY total_amount DESC;

# 不同数据库的分页实现

# MySQL

-- MySQL LIMIT语法
SELECT * FROM users
ORDER BY user_id
LIMIT 10 OFFSET 20;

-- 或者
SELECT * FROM users
ORDER BY user_id
LIMIT 20, 10;

# PostgreSQL

-- PostgreSQL语法
SELECT * FROM users
ORDER BY user_id
LIMIT 10 OFFSET 20;

# SQL Server

-- SQL Server 2012之前
SELECT TOP 10 *
FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY user_id) as rn
    FROM users
) t
WHERE rn > 20;

-- SQL Server 2012+
SELECT * FROM users
ORDER BY user_id
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;

# Oracle

-- Oracle 11g及之前
SELECT *
FROM (
    SELECT a.*, ROWNUM rn
    FROM (
        SELECT * FROM users ORDER BY user_id
    ) a
    WHERE ROWNUM <= 30
)
WHERE rn > 20;

-- Oracle 12c+
SELECT * FROM users
ORDER BY user_id
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;

# 常见错误

# 1. 忘记ORDER BY

-- 错误:没有ORDER BY的分页结果不确定
SELECT * FROM users LIMIT 10 OFFSET 10;  -- 结果不可预测

-- 正确:总是使用ORDER BY
SELECT * FROM users 
ORDER BY user_id 
LIMIT 10 OFFSET 10;

# 2. 大偏移量性能问题

-- 问题:大偏移量导致性能下降
SELECT * FROM users ORDER BY user_id LIMIT 10 OFFSET 1000000;

-- 解决:使用游标分页
SELECT * FROM users 
WHERE user_id > @last_seen_id 
ORDER BY user_id 
LIMIT 10;

# 3. 计数查询性能问题

-- 问题:每次分页都执行COUNT(*)
SELECT COUNT(*) FROM large_table WHERE complex_condition;

-- 解决:缓存计数或使用估算

# 最佳实践

# 1. 性能优化

  • 为ORDER BY列创建适当的索引
  • 避免使用大偏移量,考虑游标分页
  • 对于大表,考虑缓存总数或使用估算
  • 使用覆盖索引减少回表操作

# 2. 用户体验

  • 提供合理的默认页面大小(通常10-50条)
  • 允许用户自定义页面大小
  • 提供跳转到指定页面的功能
  • 显示当前页面信息和总页数

# 3. 安全性

  • 验证分页参数,防止SQL注入
  • 限制最大页面大小,防止资源滥用
  • 对敏感数据进行权限检查

# 4. 可维护性

  • 封装分页逻辑到存储过程或应用层
  • 使用参数化查询
  • 记录慢查询日志,监控分页性能

# 总结

分页查询是处理大数据量的重要技术。正确实现分页不仅能提高查询性能,还能改善用户体验。在实际应用中,需要根据数据量大小、查询复杂度和性能要求选择合适的分页策略。对于大数据量场景,游标分页通常比传统的OFFSET分页更高效。