草庐IT

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

Java`纯鹿人 2024-05-15 原文

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

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,数据量太大,也会导致批量插入效率慢

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

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

查询时,减少不需要的列,查询效率也可以得到明显提升 一次尽可能按需查询较少的数据条数 借助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;



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

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

有关面试官:一千万的数据,你是怎么查询的?的更多相关文章

  1. ruby - ECONNRESET (Whois::ConnectionError) - 尝试在 Ruby 中查询 Whois 时出错 - 2

    我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby-on-rails - 在 Rails 和 ActiveRecord 中查询时忽略某些字段 - 2

    我知道我可以指定某些字段来使用pluck查询数据库。ids=Item.where('due_at但是我想知道,是否有一种方法可以指定我想避免从数据库查询的某些字段。某种反拔?posts=Post.where(published:true).do_not_lookup(:enormous_field) 最佳答案 Model#attribute_names应该返回列/属性数组。您可以排除其中一些并传递给pluck或select方法。像这样:posts=Post.where(published:true).select(Post.attr

  4. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  5. ruby - Ruby 中的隐式返回值是怎么回事? - 2

    所以我开始关注ruby​​,很多东西看起来不错,但我对隐式return语句很反感。我理解默认情况下让所有内容返回self或nil但不是语句的最后一个值。对我来说,它看起来非常脆弱(尤其是)如果你正在使用一个不打算返回某些东西的方法(尤其是一个改变状态/破坏性方法的函数!),其他人可能最终依赖于一个返回对方法的目的并不重要,并且有很大的改变机会。隐式返回有什么意义?有没有办法让事情变得更简单?总是有返回以防止隐含返回被认为是好的做法吗?我是不是太担心这个了?附言当人们想要从方法中返回特定的东西时,他们是否经常使用隐式返回,这不是让你组中的其他人更容易破坏彼此的代码吗?当然,记录一切并给出

  6. ruby - 怎么来的(a_method || :other) returns :other only when assigning to a var called a_method? - 2

    给定以下方法:defsome_method:valueend以下语句按我的预期工作:some_method||:other#=>:valuex=some_method||:other#=>:value但是下面语句的行为让我感到困惑:some_method=some_method||:other#=>:other它按预期创建了一个名为some_method的局部变量,随后对some_method的调用返回该局部变量的值。但为什么它分配:other而不是:value呢?我知道这可能不是一件明智的事情,并且可以看出它可能有多么模棱两可,但我认为应该在考虑作业之前评估作业的右侧...我已经在R

  7. ruby-on-rails - 我该怎么办 :remote location validation with CarrierWave? - 2

    我在我的Rails3示例应用程序上使用CarrierWave。我想验证远程位置上传,因此当用户提交无效URL(空白或非图像)时,我不会收到标准错误异常:CarrierWave::DownloadErrorinImageController#createtryingtodownloadafilewhichisnotservedoverHTTP这是我的模型:classPaintingtrue,:length=>{:minimum=>5,:maximum=>100}validates:image,:presence=>trueend这是我的Controller:classPaintingsC

  8. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  9. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  10. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

随机推荐