乐趣区

关于数据库:面试官一千万的数据你是怎么查询的

面试官:一千万的数据,你是怎么查问的?

1 先给论断

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

  • 针对偏移量大导致查问速度慢:
  1. 先对查问的字段创立惟一索引
  2. 依据业务需要,先定位查问范畴(对应主键 id 的范畴,比方大于多少、小于多少、IN)
  3. 查问时,将第 2 步确定的范畴作为查问条件
  • 针对查问数据量大的导致查问速度慢:
  1. 查问时,缩小不须要的列,查问效率也能够失去显著晋升
  2. 一次尽可能按需查问较少的数据条数
  3. 借助 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,数据量太大,也会导致批量插入效率慢

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,我这电脑配置不高,消耗了不少工夫,应该个把小时吧

SELECT count(1) FROM `user_operation_log`;

2.4 一般分页查问

MySQL 反对 LIMIT 语句来选取指定的条数数据,Oracle 能够应用 ROWNUM 来选取。

MySQL 分页查问语法如下:

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

上面咱们开始测试查问后果:

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;

从下面后果能够得出完结:数据量越大,破费工夫越长(这不是废话吗?😂)

雷同数据量,不同偏移量

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 优化数据量大的问题

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 数据库的压力。

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

  1. 查问时,缩小不须要的列,查问效率也能够失去显著晋升
  2. 一次尽可能按需查问较少的数据条数
  3. 借助 nosql 缓存数据等来加重 mysql 数据库的压力

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

留神自己的 MySQL 服务器和客户端是在_同一台机器_上,所以查问数据相差不多,有条件的同学能够测测客户端与 MySQL 离开

SELECT * 它不香吗?

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

次要两点:

  1. 用 “SELECT * ” 数据库须要解析更多的对象、字段、权限、属性等相干内容,在 SQL 语句简单,硬解析较多的状况下,会对数据库造成惨重的累赘。
  2. 增大网络开销,* 有时会误带上如 log、IconMD5 之类的无用且大文本字段,数据传输 size 会几何增涨。特地是 MySQL 和应用程序不在同一台机器,这种开销非常明显。

3.2 优化偏移量大的问题

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 语句:

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 一下

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;

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

  1. 先对查问的字段创立惟一索引
  2. 依据业务需要,先定位查问范畴(对应主键 id 的范畴,比方大于多少、小于多少、IN)
  3. 查问时,将第 2 步确定的范畴作为查问条件

本文由 mdnice 多平台公布

退出移动版