分布式是大数据处理的万能药?

Posted Jiangxl~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式是大数据处理的万能药?相关的知识,希望对你有一定的参考价值。

使用分布式集群来处理大数据是当前的主流,将一个大任务拆分成多个子任务分布到多个节点进行处理通常能获得显著的性能提升。因此,只要发现处理能力不足就可以通过增加节点的方式进行扩容,这也是很多拥趸者最朴素的想法。以至于当我们接触一项新的大数据处理技术往往首先问的就是支不支持分布式以及能支持多大规模的集群,可见“分布式思维”已经根深蒂固。

那么分布式真是处理大数据的万能药吗?

“万能”当然不可能。没有包治百病的灵药,任何技术都有其适用场景,分布式也一样。

能否使用分布式技术解决处理能力问题,要结合任务的特点来看。如果这个任务很容易拆分,就可以使用分布式;否则如果任务比较复杂,拆分后还要相互耦合引用甚至发生大量跨节点数据传输等情况就不一定适合使用分布式了,如果强行使用效果反而更差。

具体来说,大多数交易型(OLTP)场景都较为合适,单任务涉及数据量很小但并发很多,任务很容易拆分,适合使用分布式技术提升性能(虽然会面临少量分布式事务,但目前已有成熟技术处理)。

对于分析型(OLAP)任务则要复杂一些。有些简单查询也适合分布式,比如帐户明细查询(中国近期流行的健康码查询就是此类)。这类查询的总数据量巨大,但每个帐户的数据量很少,而每个查询任务也只要本帐户的数据,并不涉及复杂计算,很类似上述OLTP场景中单任务涉及数据量小且相互无关的特点,因此很容易拆分,这时增加分布式节点就可以有效提升查询效率。在这类场景下分布式也可以称得上是灵药了。

但对于复杂一些的计算场景就未必了。比如我们常见的关联运算,在分布式环境下关联运算会有Shuffle的动作,要在节点之间交换数据,当节点数较多时数据交换造成的网络延迟就会抵消多机分摊计算带来的性能提升,再增加节点性能不仅不会提升反而可能下降。很多分布式数据库都会有节点数上限的指标,就是因为这个原因,而且这个上限还很低,通常也就几十最多上百就达到了。

更重要的是,分布式集群算力也并不能线性扩展。集群由多个物理机组成,多机通过网络进行通信。当集群中发生本机内存不足需要访问其它节点的内存时就需要通过网络,而网络只适合批量访问,但内存的使用常常是小量随机式的。通过网络进行跨节点内存访问就会导致性能下降十分明显,通常是一两个数量级的。这就需要动用数倍甚至数十倍的硬件资源才能弥补性能的缺失。使用集群虽然能提升算力,但并不能线性地扩展,在有限的节点限制下能够发挥的作用也十分有限。这时对于想要分布式发挥“无限算力”的小伙伴,分布式技术也只能遗憾了

我们实际业务中还有很多更复杂的计算场景,比如常见的跑批任务,就是每天空闲(如夜间)时间将业务数据加工成待用的结果,这类运算复杂度极高,不仅计算规则复杂并且有先后顺序,多步骤运算要按照顺序逐次完成。处理时还会涉及大量历史数据,可能要反复读取并关联,这会导致分布式技术应用困难。即使计算任务能够拆分,在数据加工的过程中经常还会生成中间结果落地以便下一步继续使用,这些临时产生的中间结果由于无法及时分布到其他节点上(临时产生的中间数据无法事先冗余),其他节点要计算就要借助网络交换数据又会大幅降低性能。在这类复杂计算场景下,别说分布式节点限制,就是想利用分布式技术都不容易,灵药就更谈不上了。所以这类复杂业务常常还是使用大型单体数据库实施,不仅成本高,容量随着任务的增多也很容易达到上限。

那么,这种场景的计算性能碰到瓶颈,如果分布式不能解决,那又能怎么办呢?

要解决问题,要先分析这类运算有什么特点,运算慢的原因到底在哪里。

其实,深入研究一下这类场景的特点就会发现,很多 “慢”运算涉及的数据量并不是很大。这类计算通常是基于以业务数据为核心的结构化数据进行的,数据总量虽然很大,但单次任务涉及的并不大,通常也就几十到几百GB,很少上TB的。比如一个典型的银行跑批场景,假设有2000万账户,每个账户每月一条汇总记录,跑批通常会使用过去一年的历史数据计算,总体算下来也不到3亿行。假设每条记录有100个统计值,每行按1K估算,物理大小也就300G左右,使用一些简单技术也能压缩到100G以内。这种数据规模单机通常就可以容纳了。

数据量并不大,那为什么会跑这么慢呢?跑批要数小时的情况比比皆是。

主要原因有两个。

一是计算复杂。数据量虽然不大,但计算过程中会反复关联,计算量上来以后性能当然就变差了。我们举个极端一点的例子,国家天文台的天体聚类计算场景就是数据量不大但计算复杂度高导致性能低下的情况。该场景共有11 张照片(数据),每张有 500 万天体,数据量总共不超过10G。现在要将位置(天文距离)邻近的天体聚合成一个再计算属性。这个任务的数据量虽然不大,但计算量非常大,和规模的平方成正比,天文距离的计算次数大约是500万 *500万 *10张=250万亿次,这真是个天文数字。这个任务用某分布式数据库动用 100 个 CPU,仅处理 50 万天体也需要 3.8 小时,处理 500 万目标规模则需要 15 天(用户期望是在数小时内处理完)。

二是单机计算性能没有被充分发挥,换句话说就是硬件资源利用率低,这跟应用的数据处理技术密切相关。我们目前处理结构化数据还主要使用SQL(数据库),这是无法发挥单机计算性能的重要原因。SQL由于缺乏一些关键的数据类型(如记录类型)和基本运算(如有序计算)导致很多高性能算法都无法描述,结果只能使用慢算法。虽然现在很多数据库在工程上有所优化,但也只能针对简单的场景,情况复杂之后数据库的优化器就会失效,解决不了根本问题。这也解释了上面天文台的例子使用SQL即使借助100CPU的集群计算时间仍然无法满足需要的原因。

事实上,如果数据处理技术能够根据实际计算场景因地制宜地使用适合的算法,就可以降低计算复杂度提升计算性能。这里的关键是,高性能算法不仅要能想出来,还要能写出来。SQL就很难实现这个目标,即使能想出来也实现不了,最后只能干瞪眼。

除了SQL,像Spark这样的新兴计算技术也同样存在性能差(资源利用率低)的问题。Spark中的 RDD 采用了immutable机制,在每个计算步骤后都会复制出新的 RDD,造成内存和 CPU 的大量占用和浪费,资源利用率很低,想要达到性能要求就需要依靠大集群大内存。

因此,想要充分利用硬件资源提升计算效率就要再选用其他技术,这就要提到SPL了。

与SQL类似,SPL也是专门面向结构化数据的计算引擎。不同的是,SPL采用了更加开放的计算体系,内部提供了很多高性能算法实现机制(以及对应的高性能存储),可以达到高效算法不仅能想出来还能实现的目标,甚至还很容易实现。这样就可以将硬件资源发挥到极致,本来要用集群的运算也可以不用集群,大集群可以改用小集群。

还是拿上面天文台的例子来说,如果一样老老实实地对比250万亿次,SPL也没法做到更快。但可以想办法优化算法,具体到这个问题时,可以利用天体距离的单调性和有序性进行粗筛,用二分法迅速把可能匹配的天体定位到很小的范围内,排除了绝大多数的比对计算;计算复杂度就可以减小到原来的1/500,再结合并行计算就可以有效提升计算效率。

前面我们说了,高性能算法不仅要能想出来,还要能实现。SPL实现这个优化后的算法要多少代码呢?一共50行!效果怎么样呢?500万规模的全量数据使用16CPU可以在4小时内完成,整体相对SQL方案可以提速几千倍。(具体案例细节可以参考: SPL 提速天体聚类任务 2000 倍

细心的小伙伴可能发现了,在这个案例中要达到用户要求的性能指标SPL使用的硬件资源很少,单机就可以满足,并不需要分布式。这就是我们主张的:先把单机性能发挥到极致,不够用再分布式

SPL还实施过很多这样单机顶级群的案例,比如在某商业银行的手机银行多并发账户查询场景中,SPL使用单台服务器就达到了原来6台ElasticSearch集群的查询效率,同时解决了实时关联的问题(案例详情: 开源 SPL 将银行手机账户查询的预先关联变成实时关联)。还有在电商漏斗计算场景中,SPL使用8CPU可以跑出29秒的结果,而同样的计算在Snowflake 的 Medium 级服务器(4节点集群)上三分钟未跑出结果(细节参考: SQL 提速:漏斗转化分析)。

除了实现单机顶级群的效果外,对于原本在单体数据库上跑得慢的任务,使用SPL充分发挥单机性能后也能提速很多倍,这样就不必再求助于分布式了。比如,在某银行的对公贷款业务计算中,原本使用AIX+DB2要计算1.5小时,改用SPL后不到10分钟就可以完成,性能提升10倍(案例详情: 开源 SPL 提速银行贷款协议跑批 10+ 倍)。还有某大型保险公司的车险跑批场景中,使用SPL替代数据库将跑批时间从原来的2个小时提升到17分钟,提速7倍(案例详情: 开源 SPL 优化保险公司跑批优从 2 小时到 17 分钟)。类似的案例还有很多,对SPL高性能计算案例及原理感兴趣的小伙伴可以参考: 快出数量级的性能是怎样炼成的

当然,这里并不是要反对分布式,而是希望不要“无脑”分布式,把单机性能充分发挥完不够用再使用分布式才是解锁大数据计算的正确姿势。

SPL也提供了完善的分布式计算功能,有相应的负载均衡和容错机制,针对不同的需求和计算场景可以使用不同的容错方案(如冗余式容错和备胎式容错)。值得一提的是,SPL集群的定位是中小规模,集群节点最好不要超过32个。由于SPL具备极高的计算性能可以有效利用硬件资源,因此在实际应用中这个集群规模已经足够用了,很多场景使用单机最多几台就都搞定了。当然,如果遇到极少数需要更大集群的应用场景就需要选用其他技术了。

总结一下,应用分布式的前提是任务易“拆”,更关键的是,先要充分发挥单机性能之后再分布式。

SPL资料

数据库索引:索引并不是万能药


几乎所有的业务项目都会涉及数据存储,虽然当前各种NoSQL和文件系统大行其道,但MySQL等关系型数据库因为满足ACID、可靠性高、对开发友好等特点,仍然最常被用于存储重要数据。在关系型数据库中,索引是优化查询性能的重要手段。

为此,我经常看到一些同学一遇到查询性能问题,就盲目要求运维或DBA给数据表相关字段创建大量索引。显然,这种想法是错误的。今天,我们就以MySQL为例来深入理解下索引的原理,以及相关误区。

InnoDB是如何存储数据的?

MySQL把数据存储和查询操作抽象成了存储引擎,不同的存储引擎,对数据的存储和读取方式各不相同。MySQL支持多种存储引擎,并且可以以表为粒度设置存储引擎。因为支持事务,我们最常使用的是InnoDB。为方便理解下面的内容,我先和你简单说说InnoDB是如何存储数据的。

虽然数据保存在磁盘中,但其处理是在内存中进行的。为了减少磁盘随机读取次数,InnoDB采用页而不是行的粒度来保存数据,即数据被分成若干页,以页为单位保存在磁盘中。InnoDB的页大小,一般是16KB。

各个数据页组成一个双向链表,每个数据页中的记录按照主键顺序组成单向链表;每一个数据页中有一个页目录,方便按照主键查询记录。数据页的结构如下:

数据库索引:索引并不是万能药_数据

 

页目录通过槽把记录分成不同的小组,每个小组有若干条记录。如图所示,记录中最前面的小方块中的数字,代表的是当前分组的记录条数,最小和最大的槽指向2个特殊的伪记录。有了槽之后,我们按照主键搜索页中记录时,就可以采用二分法快速搜索,无需从最小记录开始遍历整个页中的记录链表。

举一个例子,如果要搜索主键(PK)=15的记录:

  • 先二分得出槽中间位是(0+6)/2=3,看到其指向的记录是12<15,所以需要从#3槽后继续搜索记录;
  • 再使用二分搜索出#3槽和#6槽的中间位是(3+6)/2=4.5取整4,#4槽对应的记录是16>15,所以记录一定在#4槽中;
  • 再从#3槽指向的12号记录开始向下搜索3次,定位到15号记录。

理解了InnoDB存储数据的原理后,我们就可以继续学习MySQL索引相关的原理和坑了。

聚簇索引和二级索引

说到索引,页目录就是最简单的索引,是通过对记录进行一级分组来降低搜索的时间复杂度。但,这样能够降低的时间复杂度数量级,非常有限。当有无数个数据页来存储表数据的时候,我们就需要考虑如何建立合适的索引,才能方便定位记录所在的页。

为了解决这个问题,InnoDB引入了B+树。如下图所示,B+树是一棵倒过来的树:

数据库索引:索引并不是万能药_字段_02

 

B+树的特点包括:

  • 最底层的节点叫作叶子节点,用来存放数据;
  • 其他上层节点叫作非叶子节点,仅用来存放目录项,作为索引;
  • 非叶子节点分为不同层次,通过分层来降低每一层的搜索量;
  • 所有节点按照索引键大小排序,构成一个双向链表,加速范围查找。

因此,InnoDB使用B+树,既可以保存实际数据,也可以加速数据搜索,这就是聚簇索引。如果把上图叶子节点下面方块中的省略号看作实际数据的话,那么它就是聚簇索引的示意图。由于数据在物理上只会保存一份,所以包含实际数据的聚簇索引只能有一个

InnoDB会自动使用主键(唯一定义一条记录的单个或多个字段)作为聚簇索引的索引键(如果没有主键,就选择第一个不包含NULL值的唯一列)。上图方框中的数字代表了索引键的值,对聚簇索引而言一般就是主键。

我们再看看B+树如何实现快速查找主键。比如,我们要搜索PK=4的数据,通过根节点中的索引可以知道数据在第一个记录指向的2号页中,通过2号页的索引又可以知道数据在5号页,5号页就是实际的数据页,然后再通过二分法查找页目录马上可以找到记录的指针。

为了实现非主键字段的快速搜索,就引出了二级索引,也叫作非聚簇索引、辅助索引。二级索引,也是利用的B+树的数据结构,如下图所示:

数据库索引:索引并不是万能药_字段_03

 

这次二级索引的叶子节点中保存的不是实际数据,而是主键,获得主键值后去聚簇索引中获得数据行。这个过程就叫作回表。

举个例子,有个索引是针对用户名字段创建的,索引记录上面方块中的字母是用户名,按照顺序形成链表。如果我们要搜索用户名为b的数据,经过两次定位可以得出在#5数据页中,查出所有的主键为7和6,再拿着这两个主键继续使用聚簇索引进行两次回表得到完整数据。

考虑额外创建二级索引的代价

创建二级索引的代价,主要表现在维护代价、空间代价和回表代价三个方面。接下来,我就与你仔细分析下吧。

首先是维护代价。创建N个二级索引,就需要再创建N棵B+树,新增数据时不仅要修改聚簇索引,还需要修改这N个二级索引。

我们通过实验测试一下创建索引的代价。假设有一个person表,有主键ID,以及name、score、create_time三个字段:

CREATE TABLE `person` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`score` int(11) NOT NULL,
`create_time` timestamp NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

通过下面的存储过程循环创建10万条测试数据,我的机器的耗时是140秒(本文的例子均在MySQL 5.7.26中执行):

CREATE DEFINER=`root`@`%` PROCEDURE `insert_person`()
begin
declare c_id integer default 1;
while c_id<=100000 do
insert into person values(c_id, concat(name,c_id), c_id+100, date_sub(NOW(), interval c_id second));
set c_id=c_id+1;
end while;
end

如果再创建两个索引,一个是name和score构成的联合索引,另一个是单一列create_time的索引,那么创建10万条记录的耗时提高到154秒:

KEY `name_score` (`name`,`score`) USING BTREE,
KEY `create_time` (`create_time`) USING BTREE

这里,我再额外提一下,页中的记录都是按照索引值从小到大的顺序存放的,新增记录就需要往页中插入数据,现有的页满了就需要新创建一个页,把现有页的部分数据移过去,这就是页分裂;如果删除了许多数据使得页比较空闲,还需要进行页合并。页分裂和合并,都会有IO代价,并且可能在操作过程中产生死锁。

你可以查看这个文档,以进一步了解如何设置合理的合并阈值,来平衡页的空闲率和因为再次页分裂产生的代价。

其次是空间代价。虽然二级索引不保存原始数据,但要保存索引列的数据,所以会占用更多的空间。比如,person表创建了两个索引后,使用下面的SQL查看数据和索引占用的磁盘:

SELECT DATA_LENGTH, INDEX_LENGTH FROM information_schema.TABLES WHERE TABLE_NAME=person

结果显示,数据本身只占用了4.7M,而索引占用了8.4M。

最后是回表的代价。二级索引不保存原始数据,通过索引找到主键后需要再查询聚簇索引,才能得到我们要的数据。比如,使用SELECT * 按照name字段查询用户,使用EXPLAIN查看执行计划:

EXPLAIN SELECT * FROM person WHERE NAME=name1

执行计划如下,可以发现:

数据库索引:索引并不是万能药_字段_04

 

  • key字段代表实际走的是哪个索引,其值是name_score,说明走的是name_score这个索引。
  • type字段代表了访问表的方式,其值ref说明是二级索引等值匹配,符合我们的查询。

把SQL中的*修改为NAME和SCORE,也就是SELECT name_score联合索引包含的两列:

EXPLAIN SELECT NAME,SCORE FROM person WHERE NAME=name1

再来看看执行计划:

数据库索引:索引并不是万能药_搜索_05

 

可以看到,Extra列多了一行Using index的提示,证明这次查询直接查的是二级索引,免去了回表。

原因很简单,联合索引中其实保存了多个索引列的值,对于页中的记录先按照字段1排序,如果相同再按照字段2排序,如图所示:

数据库索引:索引并不是万能药_搜索_06

 

图中,叶子节点每一条记录的第一和第二个方块是索引列的数据,第三个方块是记录的主键。如果我们需要查询的是索引列索引或联合索引能覆盖的数据,那么查询索引本身已经“覆盖”了需要的数据,不再需要回表查询。因此,这种情况也叫作索引覆盖。我会在最后一小节介绍如何查看不同查询的成本,和你一起看看索引覆盖和索引查询后回表的代价差异。

最后,我和你总结下关于索引开销的最佳实践吧。

第一,无需一开始就建立索引,可以等到业务场景明确后,或者是数据量超过1万、查询变慢后,再针对需要查询、排序或分组的字段创建索引。创建索引后可以使用EXPLAIN命令,确认查询是否可以使用索引。我会在下一小节展开说明。

第二,尽量索引轻量级的字段,比如能索引int字段就不要索引varchar字段。索引字段也可以是部分前缀,在创建的时候指定字段索引长度。针对长文本的搜索,可以考虑使用Elasticsearch等专门用于文本搜索的索引数据库。

第三,尽量不要在SQL语句中SELECT *,而是SELECT必要的字段,甚至可以考虑使用联合索引来包含我们要搜索的字段,既能实现索引加速,又可以避免回表的开销。

不是所有针对索引列的查询都能用上索引

在上一个案例中,我创建了一个name+score的联合索引,仅搜索name时就能够用上这个联合索引。这就引出两个问题:

  • 是不是建了索引一定可以用上?
  • 怎么选择创建联合索引还是多个独立索引?

首先,我们通过几个案例来分析一下索引失效的情况。

第一,索引只能匹配列前缀。比如下面的LIKE语句,搜索name后缀为name123的用户无法走索引,执行计划的type=ALL代表了全表扫描:

EXPLAIN SELECT * FROM person WHERE NAME LIKE %name123 LIMIT 100

数据库索引:索引并不是万能药_数据_07

 

把百分号放到后面走前缀匹配,type=range表示走索引扫描,key=name_score看到实际走了name_score索引:

EXPLAIN SELECT * FROM person WHERE NAME LIKE name123% LIMIT 100

数据库索引:索引并不是万能药_搜索_08

 

原因很简单,索引B+树中行数据按照索引值排序,只能根据前缀进行比较。如果要按照后缀搜索也希望走索引的话,并且永远只是按照后缀搜索的话,可以把数据反过来存,用的时候再倒过来。

第二,条件涉及函数操作无法走索引。比如搜索条件用到了LENGTH函数,肯定无法走索引:

EXPLAIN SELECT * FROM person WHERE LENGTH(NAME)=7

数据库索引:索引并不是万能药_java_09

同样的原因,索引保存的是索引列的原始值,而不是经过函数计算后的值。如果需要针对函数调用走数据库索引的话,只能保存一份函数变换后的值,然后重新针对这个计算列做索引。

第三,联合索引只能匹配左边的列。也就是说,虽然对name和score建了联合索引,但是仅按照score列搜索无法走索引:

EXPLAIN SELECT * FROM person WHERE SCORE>45678

数据库索引:索引并不是万能药_数据_10

 

原因也很简单,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。尝试把搜索条件加入name列,可以看到走了name_score索引:

EXPLAIN SELECT * FROM person WHERE SCORE>45678 AND NAME LIKE NAME45%

需要注意的是,因为有查询优化器,所以name作为WHERE子句的第几个条件并不是很重要。

现在回到最开始的两个问题。

  • 是不是建了索引一定可以用上?并不是,只有当查询能符合索引存储的实际结构时,才能用上。这里,我只给出了三个肯定用不上索引的反例。其实,有的时候即使可以走索引,MySQL也不一定会选择使用索引。我会在下一小节展开这一点。
  • 怎么选择建联合索引还是多个独立索引?如果你的搜索条件经常会使用多个字段进行搜索,那么可以考虑针对这几个字段建联合索引;同时,针对多字段建立联合索引,使用索引覆盖的可能更大。如果只会查询单个字段,可以考虑建单独的索引,毕竟联合索引保存了不必要字段也有成本。

数据库基于成本决定是否走索引

通过前面的案例,我们可以看到,查询数据可以直接在聚簇索引上进行全表扫描,也可以走二级索引扫描后到聚簇索引回表。看到这里,你不禁要问了,MySQL到底是怎么确定走哪种方案的呢。

其实,MySQL在查询数据之前,会先对可能的方案做执行计划,然后依据成本决定走哪个执行计划。

这里的成本,包括IO成本和CPU成本:

  • IO成本,是从磁盘把数据加载到内存的成本。默认情况下,读取数据页的IO成本常数是1(也就是读取1个页成本是1)。
  • CPU成本,是检测数据是否满足条件和排序等CPU操作的成本。默认情况下,检测记录的成本是0.2。

基于此,我们分析下全表扫描的成本。

全表扫描,就是把聚簇索引中的记录依次和给定的搜索条件做比较,把符合搜索条件的记录加入结果集的过程。那么,要计算全表扫描的代价需要两个信息:

  • 聚簇索引占用的页面数,用来计算读取数据的IO成本;
  • 表中的记录数,用来计算搜索的CPU成本。

那么,MySQL是实时统计这些信息的吗?其实并不是,MySQL维护了表的统计信息,可以使用下面的命令查看:

SHOW TABLE STATUS LIKE person

输出如下:

数据库索引:索引并不是万能药_数据_11

可以看到:

  • 总行数是100086行(之前EXPLAIN时,也看到rows为100086)。你可能说,person表不是有10万行记录吗,为什么这里多了86行?其实,MySQL的统计信息是一个估算,其统计方式比较复杂我就不再展开了。但不妨碍我们根据这个值估算CPU成本,是100086*0.2=20017左右。
  • 数据长度是4734976字节。对于InnoDB来说,这就是聚簇索引占用的空间,等于聚簇索引的页面数量*每个页面的大小。InnoDB每个页面的大小是16KB,大概计算出页面数量是289,因此IO成本是289左右。

所以,全表扫描的总成本是20306左右。

接下来,我还是用person表这个例子,和你分析下MySQL如何基于成本来制定执行计划。现在,我要用下面的SQL查询name>‘name84059’ AND create_time>‘2020-01-24 05:00:00’

EXPLAIN SELECT * FROM person WHERE NAME >name84059 AND create_time>2020-01-24 05:00:00

其执行计划是全表扫描:

数据库索引:索引并不是万能药_字段_12

 

只要把create_time条件中的5点改为6点就变为走索引了,并且走的是create_time索引而不是name_score联合索引:

数据库索引:索引并不是万能药_数据_13

 

我们可以得到两个结论:

  • MySQL选择索引,并不是按照WHERE条件中列的顺序进行的;
  • 即便列有索引,甚至有多个可能的索引方案,MySQL也可能不走索引。

其原因就是,MySQL并不是猜拳决定是否走索引的,而是根据成本来判断的。虽然表的统计信息不完全准确,但足够用于策略的判断了。

不过,有时会因为统计信息的不准确或成本估算的问题,实际开销会和MySQL统计出来的差距较大,导致MySQL选择错误的索引或是直接选择走全表扫描,这个时候就需要人工干预,使用强制索引了。比如,像这样强制走name_score索引:

EXPLAIN SELECT * FROM person FORCE INDEX(name_score) WHERE NAME >name84059 AND create_time>2020-01-24 05:00:00

我们介绍了MySQL会根据成本选择执行计划,也通过EXPLAIN知道了优化器最终会选择怎样的执行计划,但MySQL如何制定执行计划始终是一个黑盒。那么,有没有什么办法可以了解各种执行计划的成本,以及MySQL做出选择的依据呢?

在MySQL 5.6及之后的版本中,我们可以使用optimizer trace功能查看优化器生成执行计划的整个过程。有了这个功能,我们不仅可以了解优化器的选择过程,更可以了解每一个执行环节的成本,然后依靠这些信息进一步优化查询。

如下代码所示,打开optimizer_trace后,再执行SQL就可以查询
information_schema.OPTIMIZER_TRACE表查看执行计划了,最后可以关闭optimizer_trace功能:

SET optimizer_trace="enabled=on";
SELECT * FROM person WHERE NAME >name84059 AND create_time>2020-01-24 05:00:00;
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";

对于按照create_time>2020-01-24 05:00:00’条件走全表扫描的SQL,我从OPTIMIZER_TRACE的执行结果中,摘出了几个重要片段来重点分析:

  • 使用name_score对name84059<name条件进行索引扫描需要扫描25362行,成本是30435,因此最终没有选择这个方案。这里的30435是查询二级索引的IO成本和CPU成本之和,再加上回表查询聚簇索引的IO成本和CPU成本之和,我就不再具体分析了:

"index": "name_score",
"ranges": [
"name84059 < name"
],
"rows": 25362,
"cost": 30435,
"chosen": false,
"cause": "cost"
,
  • 使用create_time进行索引扫描需要扫描23758行,成本是28511,同样因为成本原因没有选择这个方案:

"index": "create_time",
"ranges": [
"0x5e2a79d0 < create_time"
],
"rows": 23758,
"cost": 28511,
"chosen": false,
"cause": "cost"
  • 最终选择了全表扫描方式作为执行计划。可以看到,全表扫描100086条记录的成本是20306,和我们之前计算的一致,显然是小于其他两个方案的28511和30435:

"considered_execution_plans": [
"table": "`person`",
"best_access_path":
"considered_access_paths": [
"rows_to_scan": 100086,
"access_type": "scan",
"resulting_rows": 100086,
"cost": 20306,
"chosen": true
]
,
"rows_for_plan": 100086,
"cost_for_plan": 20306,
"chosen": true
]
,

把SQL中的create_time条件从05:00改为06:00,再次分析OPTIMIZER_TRACE可以看到,这次执行计划选择的是走create_time索引。因为是查询更晚时间的数据,走create_time索引需要扫描的行数从23758减少到了16588。这次走这个索引的成本19907小于全表扫描的20306,更小于走name_score索引的30435:


"index": "create_time",
"ranges": [
"0x5e2a87e0 < create_time"
],
"rows": 16588,
"cost": 19907,
"chosen": true

有关optimizer trace的更多信息,你可以参考MySQL的文档。

重点回顾

今天,我先和你分析了MySQL InnoDB存储引擎页、聚簇索引和二级索引的结构,然后分析了关于索引的两个误区。

第一个误区是,考虑到索引的维护代价、空间占用和查询时回表的代价,不能认为索引越多越好。索引一定是按需创建的,并且要尽可能确保足够轻量。一旦创建了多字段的联合索引,我们要考虑尽可能利用索引本身完成数据查询,减少回表的成本。

第二个误区是,不能认为建了索引就一定有效,对于后缀的匹配查询、查询中不包含联合索引的第一列、查询条件涉及函数计算等情况无法使用索引。此外,即使SQL本身符合索引的使用条件,MySQL也会通过评估各种查询方式的代价,来决定是否走索引,以及走哪个索引。

因此,在尝试通过索引进行SQL性能优化的时候,务必通过执行计划或实际的效果来确认索引是否能有效改善性能问题,否则增加了索引不但没解决性能问题,还增加了数据库增删改的负担。如果对EXPLAIN给出的执行计划有疑问的话,你还可以利用optimizer_trace查看详细的执行计划做进一步分析。

以上是关于分布式是大数据处理的万能药?的主要内容,如果未能解决你的问题,请参考以下文章

什么是大数据?

大数据基础学习

Scala为什么是大数据第一高薪语言

hadoop入门1

强-大数据第三讲

数据库索引:索引并不是万能药