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

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步确认的规模作为查询条件