天天看点

一千万以上的数据量,你是如何做查询的?

作者:架构师成长历程

千万级别数据查询难点

一千万以上的数据量,你是如何做查询的?
  1. 数据库性能:在分页查询中,通常需要跳过前面的数据行,然后获取指定数量的数据行。对于千万级别的数据,如果没有适当的索引或查询优化,每次查询都需要扫描大量的数据,导致查询性能下降。针对偏移量大导致查询速度慢:这可能导致较慢的查询响应时间或高负载的数据库服务器。
  2. 数据排序:通常情况下,分页查询需要按照某个字段进行排序,以确保结果的顺序是一致的。对于大规模数据集,进行排序操作可能会对性能造成影响,尤其是如果没有适当的索引来支持排序操作。
  3. 数据一致性:当进行分页查询时,如果有新的数据被插入或旧数据被删除,可能会导致分页结果的不一致性。例如,如果在你查询的期间有新的数据插入到前面的页中,而你只查询后面的页,可能会导致某些数据的丢失或重复。

解决这些难点的方法包括:

  1. 使用适当的索引:为查询语句中涉及到的字段创建适当的索引,以加速数据检索。尤其是需要进行排序的字段,应该优先考虑创建索引。
  2. 查询优化:编写高效的查询语句,避免不必要的操作和子查询,合理使用JOIN操作,减少查询的数据量和复杂度。
  3. 使用数据库缓存:考虑使用缓存机制,如Redis或Memcached,缓存已经查询过的结果集,减少数据库的访问次数。
  4. 分页优化:根据具体业务需求和场景,考虑合适的分页策略。例如,可以使用游标分页(Cursor-based Pagination)而不是传统的基于页码的分页,以提高性能和避免数据一致性问题。
  5. 数据库拆分和分布式架构:如果数据量非常庞大且单台数据库无法满足需求,可以考虑将数据进行拆分,并采用分布式数据库架构,以提高查询性能和扩展性。

综上所述,分页查询千万级别的数据需要注意数据库性能、数据排序和数据一致性等难点。通过优化查询语句、使用适当的索引、合理分页策略以及采用缓存和分布式架构等方法,可以改善查询性能和应对数据量的挑战。

1 先给结论

对于1千万的数据查询,主要关注分页查询过程中的性能

针对偏移量大导致查询速度慢:

先对查询的字段创建唯一索引

根据业务需求,先定位查询范围(对应主键id的范围,比如大于多少、小于多少、IN)

查询时,将第2步确定的范围作为查询条件

针对查询数据量大的导致查询速度慢:

查询时,减少不需要的列,查询效率也可以得到明显提升 一次尽可能按需查询较少的数据条数 借助nosql缓存数据等来减轻mysql数据库的压力

2 准备数据

2.1 创建表

CREATE TABLE `user_operation_log`  (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `ip` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `op_data` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr4` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr6` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr7` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr8` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr9` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr10` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr11` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   `attr12` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
   PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
           

2.2 造数据脚本

采用批量插入,效率会快很多,而且每1000条数就commit,数据量太大,也会导致批量插入效率慢

less复制代码 DELIMITER ;;
 CREATE DEFINER=`root`@`%` PROCEDURE `batch_insert_log`()
 BEGIN
   DECLARE i INT DEFAULT 1;
   DECLARE userId INT DEFAULT 10000000;
  set @execSql = 'INSERT INTO `big_data`.`user_operation_log`(`user_id`, `ip`, `op_data`, `attr1`, `attr2`, `attr3`, `attr4`, `attr5`, `attr6`, `attr7`, `attr8`, `attr9`, `attr10`, `attr11`, `attr12`) VALUES';
  set @execData = '';
   WHILE i<=10000000 DO
    set @attr = "rand_string(50)";
   set @execData = concat(@execData, "(", userId + i, ", '110.20.169.111', '用户登录操作'", ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ")");
   if i % 1000 = 0
   then
      set @stmtSql = concat(@execSql, @execData,";");
     prepare stmt from @stmtSql;
     execute stmt;
     DEALLOCATE prepare stmt;
     commit;
     set @execData = "";
    else
      set @execData = concat(@execData, ",");
    end if;
   SET i=i+1;
   END WHILE;
 END
 DELIMITER ;
           
delimiter $
 create function rand_string(n INT) 
 returns varchar(255) #该函数会返回一个字符串
 begin 
 #chars_str定义一个变量 chars_str,类型是 varchar(100),默认值'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
  declare chars_str varchar(100) default
    'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
  declare return_str varchar(255) default '';
  declare i int default 0;
  while i < n do 
    set return_str =concat(return_str,substring(chars_str,floor(1+rand()*52),1));
    set i = i + 1;
    end while;
  return return_str;
 end $
           

2.3 执行存储过程函数

因为模拟数据流量是1000W,我这电脑配置不高,耗费了不少时间,应该个把小时吧

sql复制代码 SELECT count(1) FROM `user_operation_log`;
           
一千万以上的数据量,你是如何做查询的?

2.4 普通分页查询

MySQL 支持 LIMIT 语句来选取指定的条数数据, Oracle 可以使用 ROWNUM 来选取。

MySQL分页查询语法如下:

sql复制代码 SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset
           
  • 第一个参数指定第一个返回记录行的偏移量
  • 第二个参数指定返回记录行的最大数目

下面我们开始测试查询结果:

sql复制代码 SELECT * FROM `user_operation_log` LIMIT 10000, 10;
           

查询3次时间分别为:

一千万以上的数据量,你是如何做查询的?
一千万以上的数据量,你是如何做查询的?
一千万以上的数据量,你是如何做查询的?

这样看起来速度还行,不过是本地数据库,速度自然快点。

换个角度来测试

相同偏移量,不同数据量

SELECT * FROM `user_operation_log` LIMIT 10000, 10;
 SELECT * FROM `user_operation_log` LIMIT 10000, 100;
 SELECT * FROM `user_operation_log` LIMIT 10000, 1000;
 SELECT * FROM `user_operation_log` LIMIT 10000, 10000;
 SELECT * FROM `user_operation_log` LIMIT 10000, 100000;
 SELECT * FROM `user_operation_log` LIMIT 10000, 1000000;
           
一千万以上的数据量,你是如何做查询的?

从上面结果可以得出结束:数据量越大,花费时间越长(这不是废话吗?)

相同数据量,不同偏移量

sql复制代码 SELECT * FROM `user_operation_log` LIMIT 100, 100;
 SELECT * FROM `user_operation_log` LIMIT 1000, 100;
 SELECT * FROM `user_operation_log` LIMIT 10000, 100;
 SELECT * FROM `user_operation_log` LIMIT 100000, 100;
 SELECT * FROM `user_operation_log` LIMIT 1000000, 100;
           
一千万以上的数据量,你是如何做查询的?

从上面结果可以得出结束:偏移量越大,花费时间越长

3 如何优化

既然我们经过上面一番的折腾,也得出了结论,针对上面两个问题:偏移大、数据量大,我们分别着手优化

3.1 优化数据量大的问题

sql复制代码SELECT * FROM `user_operation_log` LIMIT 1, 1000000
SELECT id FROM `user_operation_log` LIMIT 1, 1000000
SELECT id, user_id, ip, op_data, attr1, attr2, attr3, attr4, attr5, attr6, attr7, attr8, attr9, attr10, attr11, attr12 FROM `user_operation_log` LIMIT 1, 1000000
           

查询结果如下:

一千万以上的数据量,你是如何做查询的?

上面模拟的是从1000W条数据表中 ,一次查询出100W条数据,看起来性能不佳,但是我们常规业务中,很少有一次性从mysql中查询出这么多条数据量的场景。可以结合nosql缓存数据等等来减轻mysql数据库的压力。

因此,针对查询数据量大的问题:

查询时,减少不需要的列,查询效率也可以得到明显提升 一次尽可能按需查询较少的数据条数 借助nosql缓存数据等来减轻mysql数据库的压力

第一条和第三条查询速度差不多,这时候你肯定会吐槽,那我还写那么多字段干啥呢,直接 * 不就完事了

注意本人的 MySQL 服务器和客户端是在同一台机器上,所以查询数据相差不多,有条件的同学可以测测客户端与MySQL分开

SELECT * 它不香吗?

一千万以上的数据量,你是如何做查询的?

在这里顺便补充一下为什么要禁止 SELECT *。难道简单无脑,它不香吗?

主要两点:

  1. 用 "SELECT * " 数据库需要解析更多的对象、字段、权限、
  2. 属性等相关内容,在 SQL 语句复杂,硬解析较多的情况下,会对数据库造成沉重的负担。
  3. 增大网络开销,* 有时会误带上如log、IconMD5之类的无用且大文本字段,数据传输size会几何增涨。特别是MySQL和应用程序不在同一台机器,这种开销非常明显。

3.2 优化偏移量大的问题

在分页查询中,当偏移量(Offset)较大时,可能会面临性能问题。这是由于数据库需要跳过大量的数据行才能获取到指定的结果集。以下是一些优化分页查询偏移量大的常见方法:

  1. 使用游标分页:传统的基于偏移量的分页查询会导致数据库扫描和跳过大量数据行,而游标分页(Cursor-based Pagination)通过使用记录指针来定位和获取数据,避免了大量的偏移操作。通过使用类似于"WHERE id > last_id"的条件,可以在每次查询中仅返回需要的数据行。
  2. 利用索引:确保分页查询的排序字段(通常是时间戳或唯一标识符)上有适当的索引。这可以减少数据库扫描的数据量,加快查询速度。
  3. 预取数据:如果分页查询需要在每次查询中返回总数(例如用于显示总页数),可以考虑在另一个单独的查询中预先获取总数,而不是在每次查询中都进行计数操作。这样可以减轻数据库的负担。
  4. 缓存结果:如果分页查询的结果集在一段时间内不会发生变化,可以考虑将结果缓存起来,以减少对数据库的频繁访问。使用适当的缓存机制,例如Redis或Memcached,可以提高查询性能。
  5. 数据预处理:如果分页查询的数据在一定时间段内不会发生变化,可以通过预先计算和存储结果集,而不是每次查询都动态生成结果。这可以提高查询的响应速度。
  6. 数据库优化:通过优化数据库的配置参数、索引、查询语句和服务器硬件等,可以提升数据库的整体性能,从而减少分页查询偏移量大的影响。

需要根据具体的业务需求和数据特征选择适当的优化策略。对于大规模数据集和偏移量较大的分页查询,考虑使用游标分页和适当的索引来减少数据库的扫描和跳过操作。同时,结合缓存和数据预处理等技术,可以进一步提高分页查询的性能。

一千万以上的数据量,你是如何做查询的?

3.2.1 采用子查询方式

我们可以先定位偏移位置的 id,然后再查询数据

SELECT id FROM `user_operation_log` LIMIT 1000000, 1;
SELECT * FROM `user_operation_log` WHERE id >= (SELECT id FROM `user_operation_log` LIMIT 1000000, 1) LIMIT 10;
           

查询结果如下:

一千万以上的数据量,你是如何做查询的?

这种查询效率不理想啊!!!奇怪,id是主键,主键索引不应当查询这么慢啊???

先EXPLAIN分析下sql语句:

sql复制代码EXPLAIN SELECT id FROM `user_operation_log` LIMIT 1000000, 1;
EXPLAIN SELECT * FROM `user_operation_log` WHERE id >= (SELECT id FROM `user_operation_log` LIMIT 1000000, 1) LIMIT 10;
           

奇怪,走了索引啊,而且是主键索引,如下

一千万以上的数据量,你是如何做查询的?
一千万以上的数据量,你是如何做查询的?

带着十万个为什么和千万个不甘心,尝试给主键再加一层唯一索引

ALTER TABLE `big_data`.`user_operation_log` 
ADD UNIQUE INDEX `idx_id`(`id`) USING BTREE;
           

由于数据量有1000W,所以,加索引需要等待一会儿,毕竟创建1000W条数据的索引,一般机器没那么快。

然后再次执行上面的查询,结果如下:

一千万以上的数据量,你是如何做查询的?

天啊,这查询效率的差距不止十倍!!!

再次EXPLAIN分析一下:

一千万以上的数据量,你是如何做查询的?
一千万以上的数据量,你是如何做查询的?

命中的索引不一样,命中唯一索引的查询,效率高出不止十倍。

结论:

对于大表查询,不要太相信主键索引能够带来多少的性能提升,老老实实根据查询字段,添加相应索引吧!!!

但是上面的方法只适用于id是递增的情况,如果id不是递增的,比如雪花算法生成的id,得按照下面的方式:

注意:

  1. 某些 mysql 版本不支持在 in 子句中使用 limit,所以采用了多个嵌套select
  2. 但这种缺点是分页查询只能放在子查询里面
SELECT * FROM `user_operation_log` WHERE id IN (SELECT t.id FROM (SELECT id FROM `user_operation_log` LIMIT 1000000, 10) AS t);
           

查询所花费时间如下:

一千万以上的数据量,你是如何做查询的?

EXPLAIN一下

vbnet复制代码EXPLAIN SELECT * FROM `user_operation_log` WHERE id IN (SELECT t.id FROM (SELECT id FROM `user_operation_log` LIMIT 1000000, 10) AS t);
           
一千万以上的数据量,你是如何做查询的?

3.2.2 采用 id 限定方式

这种方法要求更高些,id必须是连续递增(注意是连续递增,不仅仅是递增哦),而且还得计算id的范围,然后使用 between,sql如下

SELECT * FROM `user_operation_log` WHERE id between 1000000 AND 1000100 LIMIT 100;
SELECT * FROM `user_operation_log` WHERE id >= 1000000 LIMIT 100;
           
一千万以上的数据量,你是如何做查询的?

可以看出,查询效率是相当不错的

注意:这里的 LIMIT 是限制了条数,没有采用偏移量

还是EXPLAIN分析一下

EXPLAIN SELECT * FROM `user_operation_log` WHERE id between 1000000 AND 1000100 LIMIT 100;
EXPLAIN SELECT * FROM `user_operation_log` WHERE id >= 1000000 LIMIT 100;
           
一千万以上的数据量,你是如何做查询的?
一千万以上的数据量,你是如何做查询的?

因此,针对分页查询,偏移量大导致查询慢的问题:

先对查询的字段创建唯一索引 根据业务需求,先定位查询范围(对应主键id的范围,比如大于多少、小于多少、IN) 查询时,将第2步确定的范围作为查询条件