GreatSQL分页查询优化案例实战
导语
随着国产化替代的深入,很多 Oracle 数据库迁移到 GreatSQL 上,GreatSQL 也做了大量的 Oracle 语法兼容,以减少国产化改造工作。
本文说一下 Oracle 的分页查询语句迁到 GreatSQL 上来的表现以及如何用 GreatSQL 实现高效分页查询。
SQL 案例
SQL语句
SELECT *
FROM (SELECT t.*, ROWNUM rn
FROM (SELECT * FROM t_pagequery ORDER BY log_time DESC) t
WHERE ROWNUM <= 10)
WHERE RN > 0
语句分析
这是 Oracle12c 以前典型的分页查询的写法,借助 ROWNUM 伪列的三层嵌套查询。因为 ROWNUM 是对结果集加的伪列,是先有结果集,返回给客户端时加上去的一个列。
如果在最内层查询上加上 ROWNUM 条件(SELECT * FROM t_pagequery WHERE ROWNUM<=10 ORDER BY log_time DESC)
,这样的效果是先取10行再排序,不是先排序再取10行,要对排序后的结果集取前10行,需要将ROWNUM放在第二层。因为 ROWNUM 是结果集的序号,不能用大于的条件,所以只能在第二层对其起别名,再套一层查询对别名施加大于的条件实现分页查询。
说实话,Oracle 这种写法,还挺复杂的,GreatSQL 原生的分页查询写法就相对简单了,只需一层查询即可实现。
SELECT t.* FROM t_pagequery t ORDER BY log_time DESC LIMIT 0,10;
GreatSQL 的 LIMIT 子句不是 WHERE 条件,对查询结果的一个分页截取。如果非要跟 Oracle 对标,把序号加上,需要使用 ROW_NUMBER
窗口函数。
SELECT t.*,ROW_NUMBER() OVER() rn FROM t_pagequery t ORDER BY log_time DESC LIMIT 0,10;
需要注意的一点,GreatSQL 的 sql_mode 如果设置为 Oracle 模式,会导致上面的查询无法使用索引排序。
据研发人员解释 Oracle 的索引与 GreatSQL 有所不同,Oracle 的普通索引,如果索引列的数据都是 NULL 值,不会放在索引树上,而 GreatSQL 上没有这个限制,即使都为 NULL,也可以存放在索引树上,因为 GreatSQL 的索引结构中除了存储索引列信息,还存储了主键信息。另外Oracle的默认排序是把NULL值放在后面,而 GreatSQL 的默认排序是把 NULL 值放在前面。基于索引结构与排序方式的不同,GreatSQL 在兼容 Oracle 的模式时,没有用索引排序。
GreatSQL 对 Oracle 的 ROWNUM 函数语法做了兼容,在 Oracle 上执行的分页查询语句,在 GreatSQL 上执行是完全没有问题的,但是 ROWNUM 函数的使用也会导致无法使用索引排序,小表使用文件排序也没有性能问题,遇到百万千万级大表时性能问题会显现,此时要将 Oracle 分页查询的写法,改为 GreatSQL 原生的分页查询,来提升性能。
实验验证
下面通过实验验证上面的结论。
创建测试表,插入10万行测试数据。
CREATE TABLE t_pagequery(id NUMBER(10) PRIMARY KEY,
log_time DATETIME,
c1 VARCHAR(10),
key idx_logtime(log_time)
);
SET sql_mode=Oracle;
DELIMITER //
CREATE OR REPLACE PROCEDURE p1() IS
BEGIN
FOR i IN 1..100000 LOOP
INSERT INTO t_pagequery(id,log_time,c1) VALUES(i,SYSDATE-RAND()*1000,'a');
END LOOP;
END;
//
DELIMITER ;
CALL p1;
1.验证 Oracle 分页写法与 GreatSQL 原生分页写法的结果集是一致的。
greatsql> SELECT *
-> FROM (SELECT t.*, ROWNUM rn
-> FROM (SELECT * FROM t_pagequery order by log_time desc) t
-> where ROWNUM <= 10)
-> WHERE RN > 0;
+-------+---------------------+------+------+
| id | log_time | c1 | rn |
+-------+---------------------+------+------+
| 24513 | 2025-07-11 11:08:19 | a | 1 |
| 78625 | 2025-07-11 11:06:48 | a | 2 |
| 96674 | 2025-07-11 10:39:17 | a | 3 |
| 57955 | 2025-07-11 10:30:58 | a | 4 |
| 41217 | 2025-07-11 10:30:51 | a | 5 |
| 34115 | 2025-07-11 10:22:08 | a | 6 |
| 39214 | 2025-07-11 10:19:31 | a | 7 |
| 2032 | 2025-07-11 10:12:18 | a | 8 |
| 7805 | 2025-07-11 09:53:12 | a | 9 |
| 74703 | 2025-07-11 09:44:34 | a | 10 |
+-------+---------------------+------+------+
10 rows in set (0.14 sec)
greatsql> SELECT t.*,ROW_NUMBER() OVER() rn FROM t_pagequery t ORDER BY log_time DESC LIMIT 0,10;
+-------+---------------------+------+----+
| id | log_time | c1 | rn |
+-------+---------------------+------+----+
| 24513 | 2025-07-11 11:08:19 | a | 1 |
| 78625 | 2025-07-11 11:06:48 | a | 2 |
| 96674 | 2025-07-11 10:39:17 | a | 3 |
| 57955 | 2025-07-11 10:30:58 | a | 4 |
| 41217 | 2025-07-11 10:30:51 | a | 5 |
| 34115 | 2025-07-11 10:22:08 | a | 6 |
| 39214 | 2025-07-11 10:19:31 | a | 7 |
| 2032 | 2025-07-11 10:12:18 | a | 8 |
| 7805 | 2025-07-11 09:53:12 | a | 9 |
| 74703 | 2025-07-11 09:44:34 | a | 10 |
+-------+---------------------+------+----+
10 rows in set (0.00 sec)
从上面查询的执行时间来看,GreatSQL原生的写法耗时0.00s,性能更好。
2.非 Oracle 模式下,查询一下执行计划。
greatsql> SELECT @@sql_mode;
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| @@sql_mode |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
greatsql> explain
-> SELECT *
-> FROM (SELECT t.*, ROWNUM rn
-> FROM (SELECT * FROM t_pagequery order by log_time desc) t
-> where ROWNUM <= 10)
-> WHERE RN > 0;
+----+-------------+-------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| 1 | PRIMARY | <derived2> | NULL | ALL | NULL | NULL | NULL | NULL | 100406 | 33.33 | Using where |
| 2 | DERIVED | <derived3> | NULL | ALL | NULL | NULL | NULL | NULL | 100406 | 100.00 | Using where |
| 3 | DERIVED | t_pagequery | NULL | ALL | NULL | NULL | NULL | NULL | 100406 | 100.00 | Using filesort |
+----+-------------+-------------+------------+------+---------------+------+---------+------+--------+----------+----------------+
3 rows in set, 1 warning (0.00 sec)
greatsql> explain SELECT t.*,ROW_NUMBER() OVER() rn FROM t_pagequery t ORDER BY log_time DESC LIMIT 0,10;
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+---------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+---------------------+
| 1 | SIMPLE | t | NULL | index | NULL | idx_logtime | 6 | NULL | 10 | 100.00 | Backward index scan |
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+---------------------+
1 row in set, 2 warnings (0.01 sec)
Oracle 分页查询的写法,在 GreatSQL 上执行,虽然只取前几行数据,却需要会对表进行全表扫描,再进行filesort,如果是个千万级别的大表,代价是很大的。而GreatSQL原生写法,使用了”Backward index scan”,倒序扫描索引10行就可以了,显然这种效率更高效。
3.Oracle 模式下,GreatSQL 原生写法的执行计划。
greatsql> SET sql_mode=Oracle;
Query OK, 0 rows affected (0.00 sec)
greatsql> SELECT @@sql_mode;
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| @@sql_mode |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,ONLY_FULL_GROUP_BY,Oracle,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
greatsql> explain SELECT t.* FROM t_pagequery t ORDER BY log_time DESC LIMIT 0,10;
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| 1 | SIMPLE | t | NULL | ALL | NULL | NULL | NULL | NULL | 100406 | 100.00 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+----------------+
1 row in set, 1 warning (0.00 sec)
Oracle模式下,GreatSQL原生写法也用不上索引,从Oracle迁移过来的系统,为了更好的兼容Oracle语法,一般是要开启Oracle模式的,那为了使用索引排序,可以在语句级别加hint设置sql_mode来解决,随便指定一个sql_mode即可,但不能设置成空串。
greatsql> explain SELECT /*+ set_var(sql_mode="ONLY_FULL_GROUP_BY") */ t.*,row_number() over() FROM t_pagequery t ORDER BY log_time DESC LIMIT 0,10;
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+---------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+---------------------+
| 1 | SIMPLE | t | NULL | index | NULL | idx_logtime | 6 | NULL | 10 | 100.00 | Backward index scan |
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+---------------------+
1 row in set, 2 warnings (0.00 sec)
greatsql> explain SELECT /*+ set_var(sql_mode="") */ t.*,row_number() over() FROM t_pagequery t ORDER BY log_time DESC LIMIT 0,10;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------+
| 1 | SIMPLE | t | NULL | ALL | NULL | NULL | NULL | NULL | 94474 | 100.00 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------+
1 row in set, 3 warnings (0.00 sec)
再提及一点细节,Oracle的普通索引,如果索引列都是NULL值,则不会存储在索引Tree结构中。而GreatSQL没有这个限制,所以我在建测试表时并没有指定索引列log_time带NOT NULL约束,照样用上了索引排序。而Oracle如果这样建表则不会使用索引排序,Oracle要么有非空约束,要么语句中加条件IS NOT NULL,要么与其他NOT NULL列或常数列建联合索引,才能用上索引排序。这一点上来看GreatSQL处理起来还是有优势的。
总结
- Oracle的分页查询借助ROWNUM做三层嵌套查询,GreatSQL的原生分页查询是使用LIMIT子句,GreatSQL虽然兼容Oracle这种分页查询语法,却无法使用索引排序,小表没有问题,但是对百万千万级别的大表,使用文件排序会很耗资源,需要改成GreatSQL原生的写法来提升效率。
- GreatSQL的sql_mode为Oracle模式时,无法使用索引排序,需要用hin语句级别指定sql_mode来解决,注意不能指定成空串。
- GreatSQL的二级索引中会存储索引列都为NULL的数据,在使用索引排序时,不用考虑带着索引列条件IS NOT NULL或者加非空约束。
Enjoy GreatSQL :)
关于 GreatSQL
GreatSQL是适用于金融级应用的国内自主开源数据库,具备高性能、高可靠、高易用性、高安全等多个核心特性,可以作为MySQL或Percona Server的可选替换,用于线上生产环境,且完全免费并兼容MySQL或Percona Server。
相关链接: GreatSQL社区 Gitee GitHub Bilibili
GreatSQL社区:
社区有奖建议反馈: https://greatsql.cn/thread-54-1-1.html
社区博客有奖征稿详情: https://greatsql.cn/thread-100-1-1.html
(对文章有疑问或者有独到见解都可以去社区官网提出或分享哦~)
技术交流群:
微信&QQ群:
QQ群:533341697
微信群:添加GreatSQL社区助手(微信号:wanlidbc
)好友,待社区助手拉您进群。
</div>