第四章 OB SQL调优
4.1 SQL 调优方法
4.1.1 OceanBase 调优的特殊性
OceanBase 作为关系型数据库,其调优的方法与思路与传统数据库有很多相似的地方,但由于其自身特点主要有以下 2个不同之处:
4.1.1.1 LSM-tree存储引擎对 SQL 执行的影响
LSM-Tree 有以下特点
- 数据分为静态数据(SSTable)和动态数据(MemTable)两部分
- 当数据合并后,SQL的执行效率往往都有明显的提升
- buffer表是指那些被用户当做业务过程中的临时存储的数据表
- 内存中“标记删除”(比如快速“写入-修改-删除”的数据)
- 当有大量删除操作后立即访问被删除的数据范围,仍然有可能遇到由于访问标记删除节点而导致的执行变慢的问题
OceanBase 的存储引擎采用了两层LSM-tree 的架构,数据分为静态数据(SSTable)和动态数据(MemTable)两部分。
- 针对写请求,用户的修改按照 BTree 的方式写入内存中的 MemTable,并定期通过合并过程合入到存储在磁盘上的 SSTable 中;
- 针对读请求,存储引擎需要读取 MemTable、SSTable 两部分数据,合成最终的行。这一特点决定了OceanBase 在基表访问路径的代价模型上与传统数据库有较大的差异。例如,MemTable 为空(全部数据在 SSTable 中)时,存储引擎的执行效率比 MemTable 中有数据的场景的执行效率要高很多。因此,当数据合并后,SQL 的执行效率往往都有明显的提升。
- “buffer”表是指那些被用户当做业务过程中的临时存储的数据表,这些表的访问模型往往是:写入-修改-删除,且整个过程周期很短,往往在几分钟或几个小时之内。由于OceanBase 所有的DML 操作都是“逻辑”的,数据被删除时,并不做原地修改,而只是在内存中做“标记”删除。因此,即使用户的行被删除了,访问对应的“删除”标记也需要花费一定的时间。对此, OceanBase 通过 row purge,在 BTree 中标记一个范围段的删除标记以加速数据的访问。row purge 的过程是异步的,因此,当有大量删除操作后立即访问被删除的数据范围,仍然有可能遇到由于访问标记删除节点而导致的执行变慢的问题。
4.1.1.1 分布式架构对 SQL 执行的影响
传统数据库和OceanBase分布式的集群架构差异主要体现在存储层
- 传统的 share-disk 架构:执行计划并不区分数据所在的物理节点,所有的数据访问都可以认为是“本地”的
- 分布式 share-nothing 架构:不同的数据被存储在不同的节点上,SQL 执行计划的生成必须考虑到数据的实际物理分布,并可能因此表现出不同的性能特征。
分布式执行场景举例
- 当连接两张表时,如果两个表的数据分布在不同的物理节点上,则必然涉及节点之间的数据传输,执行计划也变为分布式执行计划,相比两表数据分布在同一个节点上的场景,执行代价必然有所增加。
- 数据切主,由于数据的主可以在分布在不同节点上的副本之间进行切换,有可能出现之前的本地执行计划(所访问的数据在本机)变为远程执行计划或者分布式执行计划的问题,也可能增加一定的执行时间。这一问题在用户开启轮转合并功能之后尤其明显(在数据重新选主后,客户端路由可能由于数据物理位置信息刷新不及时而不准确)。
4.1.2 SQL 性能问题来源
参考自:SQL 调优常见问题:https://www.oceanbase.com/docs/oceanbase-database/oceanbase-database/V2.2.76/faq-about-sql-tuning
4.1.2.1 用户 SQL 写法未遵循 OceanBase 数据库开发规范
用户 SQL 的写法对 SQL 的执行性能有决定性的作用。在使用过程中,用户应尽量遵循 OceanBase 数据库开发规范的要求。
4.1.2.2 代价模型缺陷导致的执行计划选择错误
OceanBase 数据库内建的代价模型是服务器的固有逻辑,最佳的执行计划依赖此代价模型。因此,一旦出现由代价模型导致的计划选择错误,用户只能通过执行计划绑定来确保选择“正确”的执行计划。
4.1.2.3 数据统计信息不准确
查询优化过程依赖数据统计信息的准确性,OceanBase 数据库的优化器默认会在数据合并过程中收集一些统计信息,当用对数据进行了大量修改时,可能会导致统计信息落后于真实数据的特征,用户可以通过发起每日合并,主动更新统计信息。
除了优化器收集的统计信息以外,优化器还会根据查询条件对存储层进行采样,用以后续的优化选择。OceanBase 数据库目前仅支持对本地存储进行采样,对于数据分区在远程节点上的情况,只能使用默认收集的统计信息进行代价估计,可能会引入代价偏差。
4.1.2.4 数据库物理设计降低查询性能
查询的性能很大程度上取决于数据库的物理设计,包括所访问对象的 schema 信息等。例如,对于二级索引,如果所需的投影列没有包括在索引列之中,则需要使用回表的机制访问主表,查询的代价会增加很多。此时,可以考虑将用户的投影列加入到索引列中,构成所谓的“覆盖索引”,避免回表访问。
4.1.2.5 系统负载影响单条 SQL 的响应时间
系统的整体负载除了会影响系统的整体吞吐量,也会引起单条 SQL 的响应时间变化。OceanBase 数据库的 SQL 引擎采用队列模型,针对用户请求,如果可用线程全部被占用,则新的请求需要在请求队列中排队,直到某个线程完成当前请求。请求在队列中的排队时间可以在 (g)v$sql_audit 中看到。
4.1.2.6 客户端路由与服务器之间出现路由反馈逻辑错误
OBProxy 的一个主要功能是将 SQL 查询路由到恰当的服务器节点。具体来说,如果用户查询没有指定使用弱一致性读属性,Proxy 需要将其路由到所涉及的表(或具体分区)的主节点上,以避免服务器节点之前的二次转发;否则,Proxy 会根据预先设置好的规则将其转发到恰当的节点。
由于 Proxy 与服务器之间采用松耦合的方式,Proxy 上缓存的数据物理分布信息刷新可能不及时,导致错误的路由选择。可能导致路由信息变化的场景有:
- 网络不稳导致服务器间重新选主
- 由服务器上下线、轮转合并等导致的重新选主
- 负载均衡导致重新选主
当在 SQL audit 或执行计划缓存中发现有大量远程执行时,需要考虑是否与上述场景吻合。客户端与服务器之间有路由反馈逻辑,一旦发生错误,客户端会主动刷新数据物理分布信息,随后路由的选择也将恢复正常。
4.1.3 针对单条 SQL 执行的性能调优
针对SQL 执行时间的性能调优是最常见的性能调优,关注的问题是某一条或某一类SQL 的执行时间或者执行资源的消耗(如内存、磁盘IO 等)。单条 SQL 的性能调优往往与该SQL 的执行计划相关,因此,执行计划的分析是该调优场景的最重要的手段。一般来说,应该首先通过静态分析SQL 的执行计划来找到可能的调优点。
排除优化器自身的bug 原因,为了使某些SQL 的执行时间或资源消耗符合预期,一般需要用户对数据库的设置做相应的修改,常见的手段包括:
- 修改系统配置项或系统变量
- 对数据schema 进行修改,包括创建数据分区、创建二级索引等
- 修改用户SQL,包括对SQL做等价改写、增加hint 等
- 调整并行查询的并行度
一条SQL从发送到数据开始执行,到返回结果给用户,会经历队列等待、plan cache 查询、计划优化(如果计划缓存不命中)、计划执行、返回结果等过程。当发现SQL的响应时间增加时,第一步应该明确具体是哪部分的耗时增加,此时采用的一般手段包括SQL Trace、查询SQL Audit 表、查看慢查询日志等。只有明确了具体耗时在哪里,才能有针对性的进一步分析问题的根源。
针对单条SQL 的执行计划性能调优又可以分为单表访问和多表访问两种场景。
4.1.3.1 单表访问场景
对单表访问SQL 来说,需要重点关注的问题包括:
- 访问路径是否开启索引扫描
访问路径的分析是单表查询的最重要的问题之一,对于使用主表扫描的访问路径来说,执行时间一般与需要扫描的数据量(范围)成正比。一般来说,可以使用 explain extended 命令,将表扫描的范围段展示出来。对于有合适索引的查询,使用索引可以大大减小数据的访问量,因此对于使用主表扫描的查询,要分析没有选中索引扫描的原因,例如是由于不存在可用的索引,还是索引扫描范围过大以至于代价过高。
- 是否存在排序或聚合
操作排序或聚合往往都是比较耗时的操作。优化器为了尽可能的降低执行时间,在有合适索引可用时,考虑直接使用索引的顺序以避免额外的排序操作,同理,用户也可以根据经常需要排序的列,创建合适的索引,以避免不必要的排序操作。
- 分区裁剪是否正确
分区裁剪是分区表优化的重要手段,一般来说,只要用户提供了合适的分区条件,优化器会自动跳过无需访问的分区。
- 是否需要调整查询的并行度
提高查询的并行度可以使用更多资源的代价获取单条SQL 查询的性能提升,当查询牵涉到的数据量较大,分区数目较多时,可以通过提高并行度的方式加快执行时间。
4.1.3.2 多表访问场景
针对多表访问的SQL 不仅要关注单表的 SQL 调优的重点问题,还需要关注多
表间的连接问题,需要分析的点包括:
- 连接顺序
- 连接算法
- 跨机或并行连接的数据再分布方式
- 查询改写
4.1.4 针对吞吐量的性能优化
针对吞吐量的性能调优主要是考虑在一定资源CPU, IO, 网络等 情况下,能够将数据库系统处理请求量最大化,我们在新业务上线以及各种大促活动前往往需要进行吞吐量评估及吞吐量的性能调优。吞吐量性能调优可考虑以下几个方面:
- 优化慢 SQL
目标:减少单个慢SQL的资源消耗,提升整体处理能力。
大量慢SQL 请求会消耗大量的资源,导致整体吞吐量上不去,可按如下步骤处理:
-
- 通过 OCP 的 TOP SQL 功能或利用 Plan Cache 视图 查询耗时为TOP N 的 SQL 。
- 找到具体的慢 SQL 可根据前面说的 针对单条 SQL 进行性能调优 。
- 均衡 SQL 的请求流量资源
目标:避免单机资源过载,充分利用集群所有节点。
在多机环境下,我们需要尽量将所有机器资源都能使用到,因此需要考虑流量是否均衡,可以 通过 sql_audit 查看 SQL 请求是否均衡 ,影响均衡的因素主要有:
- ob_read_consistency 如何设置
- Primary Zone 如何设置
- proxy 或 java 客户端路由策略相关设置
- 业务热点查询分区是否均衡
- 均衡子计划的 RPC 请求流量资源
目标:分布式执行计划中,避免子任务(RPC请求)倾斜。
大量分布式计划时一般都设置了弱读,资源消耗主要在子计划的 RPC 请求上,在 SQL 请求均衡的情况下,通过 sql_audit 可查看子计划 RPC 请求是否均衡,影响这些子计划请求是否均匀的主要因素有:
- OBServer 内部路由策略相关设置
- 业务热点查询的分区是否均衡
4.2 使用分区表提升 SQL 性能
分区是物理数据库设计技术,它的操作对象是表。实现分区的表,我们称之为分区表,表分布在多个分区上。创建分区的目的是为了在特定的SQL操作中减少数据读写的总量以减少响应时间,
如下图所示,一张表被划分成了 5 个分区,分布在 2 台机器上:
上图分区表的每个分区还能按照一定的规则再拆分成多个分区,这种分区表叫做二级分区表。
数据表中每一行中用于计算这一行属于哪一个分区的列的集合叫做分区键,分区键必须是主键或唯一键的子集。由分区键构成的用于计算这一行属于哪一个分区的表达式叫做分区表达式。
OceanBase 分区表有以下特点
- 可多机扩展• 自动负载均衡、自动容灾
- 对业务透明,可以取代“分库分表”方案
- 支持分区间并行
- 单表分区个数最大8192
- 单机Partition支持上限: 8万(推荐不超过3万)
OceanBase 分区表支持分区能力
- 分为一级分区和二级分区
- OB MySQL 模式支持的一级分区类型有:HASH, KEY, LIST,LIST COLUMNS, RANGE,RANGE COLOMNS,生成列分区,组合分区
- OB Oracle 模式支持 RANGE, LIST, HASH, 组合分区 ?
- 二级分区相当于在一级分区的基础上,又从第二个维度进行了拆分
完整的分区类型说明参见官网文档《分区策略》:https://www.oceanbase.com/docs/oceanbase-database/oceanbase-database/V2.2.76/Partitioning-policy-2
重点
- HASH/LIST/RANGE 分区分区表达式的结果必须是 int 类型,不支持向量
- HASH 分区通常能消除热点查询
- KEY/LIST COLUMNS/RANGE COLUMNS 分区不要求是int类型,可以是任意类型,不支持表达式,支持向量
- KEY 用户通常没有办法自己通过简单的计算来得知某一行属于哪个分区
- KEY 分区不写分区键表示使用主键列
- RANGE 分区是按用户指定的表达式范围将每一条记录划分到不同分区,支持 ADD DROP 分区
- 生成列分区是指将生成列作为分区键进行分区,该功能能够更好的满足期望将某些字段进行一定处理后作为分区键的需求(比如提取一个字段的一部分,作为分区键)
思考:使用了分区之后,一些TP查询是快了,以下场景呢?
- 跨多节点的数据写入
- 跨多节点的聚合计算
如何解决呢?
4.3 使用索引提升 SQL 性能
4.3.1 相关概念
4.3.1.1 路径选择 Access Path Selection
路径选择的概念、方式和考虑因素
主索引是表的主键,二级索引可以根据你自己需要用到表的任何字段的组合来创建。
系统自动选择,先是基于规则模型,如果规则不确定的话,使用代价模型 OceanBase 的规则体系分为前置规则(正向规则)和 Skyline 剪枝规则(反向规则)。
- 正向规则(前置规则)直接决定了一个查询选取什么样的路径,是一个强匹配的规则体系。
- 逆向规则(Skyline剪枝规则)会两两比较两个路径,如果一个路径在一些定义的维度上优于另外一个路径,那么这个路径会被剪掉(Query range, 序,是否需要回表),最后没有被剪掉的路径会进行代价比较,选出最优的路径。
优化器会优先使用前置规则,如果前置规则不能得到一个确定最优的路径,那么优化器会进一步通过Skyline剪枝规则剪掉一些路径,最后代价模型会在没有被剪掉的索引中选择代价最低的路径。
访问路径相关概念:https://www.oceanbase.com/docs/oceanbase-database/oceanbase-database/V2.2.76/basic-concepts-4
4.3.1.2 索引回表
索引回表示意
比如上图 insert、update、delete 的例子,需要基线数据和内存数据 fuse(合并在一起),索引表和物理表(主表)是同构的,回表场景可能是
- 索引表没有cover,就要回表
- Row purge(回表)表示这个delete的操作,内存中删除这个操作链
- Row key 表示根据主键去回表
Oracle 内部存储的索引表是堆表(根据row id),所以比 OceanBase 回表效率要高一些(row_id可以拿到主表的偏移量)。OB现在是拿到主键的 row key 的值,回到主表中,二分查找。
4.3.1.3 路径选择的访问策略
- 目前仅支持 B+ 索引
- 两种访问
- get:索引键全部等值覆盖
- scan:返回有序数据
- 字符串条件:‘T%’( ‘%T%’, ‘%T’无法利用索引)
- 扫描顺序由优化器智能决定
4.3.1.4 Interesting Order
ORDER BY 如果恰好命中索引,可以避免排序。
【例】
说明
- Name里面t1代表主表查询,有括号的话代表索引查询因为主键/索引已经排序了,索引的序就是order需要的序(主键/索引已经有序了)
- 如果order by c2就不是interesting order了,这个时候如果有需求的话,就再建一个索引表(基于c2)
4.3.1.5 逆序索引扫描
逆序索引扫描执行计划样例
4.3.2 创建高效索引
4.3.2.1 索引创建的原则和机制
创建高效索引的一般原则
- 索引表与普通数据表一样都是实体表,在数据表进行更新的时候会先更新索引表然后再更新数据表
- 索引要全部包含所查询的列:包含的列越全越好,这样可以尽可能的减少回表的行数
- 等值条件永远放在最前面
- 过滤与排序数据量大的放前面
可以对一张表的单列或多列创建索引来提高表查询速度。创建合适的索引,能够减少对磁盘的读写。
OceanBase 索引生效机制
- 建表的时候创建,立即生效
- 建表后再创建索引,是同步生效,表中数据量大时需要等待一段时间
4.3.2.2 等值索引匹配
等值索引条件的先后顺序不影响索引效果
如 where A = ? and B = ? 和 where B = ? and A = ? 效果相同,
从索引能效来看
【Where A =? And B=? and C=?】>【Where A=? and B=? 】> 【Where A=? and C=?】
4.3.2.3 范围索引匹配
范围索引遇到第一个范围查询字段后,后续的字段不参与索引过滤(不走索引)
常见的范围查询有: 大于、小于、大于等于、小于等于、between…and 、in(?,?)
如【where A > ? and B > ? and C < ?】只能走A字段的索引
4.3.2.4 等值和范围索引共同匹配
遇到第一个范围查询字段后,后续的字段不参与索引过滤(不走索引)
从索引能效看
- 【where A = ? and B = ? and C > ?】>【where A = ? and B > ? and C > ?】
- 【where A = ? and B > ? and C = ?】=【where A = ? and B > ? and C > ?】
4.4 连接顺序和性能
不同的连接顺序对执行效率影响极大
- 目前只考虑左深树(某些特定场景除外)
- 搜索空间
- 对内存占用更友好
- 连接顺序的选择是一个动态规划的过程
- 可通过hint指定连接顺序
- 存在显式连接条件的连接优先于笛卡尔积连接
左深树、右深树、多枝树 比较
4.5 局部索引与全局索引
4.5.1 基本概念
4.5.1.1 主表
指使用CREATE TABLE语句创建的表对象。也是索引对象所依赖的表(即CREATE INDEX语句中ON子句所指定的表)。
4.5.1.2 主键
OceanBase 的每一张表都有主键,并在内部以主键为序组织数据。如果在创建用户表时不显式指定主键,系统会自动为表生成隐藏主键,隐藏主键不可被查询。
主键可以是单一字段或多个字段的组合,组合主键的字段数不能超过 64。主键一旦指定,不可更改。当用作分区表时,主键必须覆盖所有的分区列。
4.5.1.3 索引(索引表)
指使用CREATE INDEX语句创建的索引对象。有时为了便于大家理解,也会把索引对象类比为一个表对象,即索引表。
4.5.1.4 外键
像传统的关系数据库一样,OceanBase 支持外键功能,这是OceanBase 相对于其他大多数分布式数据库系统而言,在完整性约束方面的重大优势。
4.5.1.5 非分区表中主表和索引的关系
传统的“非”分区表中,主表和索引的对应关系:
主表的所有数据都保存在一个完整的数据结构中,主表上的每一个索引也对应一个完整的数据结构(比如最常见的B+ Tree),主表的数据结构和索引的数据结构之间是一对一的关系。
4.5.2 分区表的索引
当分区表出现之后,情况发生了变化:主表的数据按照分区键(Partitioning Key)的值被分成了多个分区,每个分区都是独立的数据结构,分区之间的数据没有交集。这样一来,索引所依赖的单一数据结构不复存在,那索引需要如何应对呢?
这就引入了“局部索引”和“全局索引”两个概念。
4.5.2.1 局部索引
局部索引又名分区索引,创建索引的分区关键字是LOCAL,分区键等同于表的分区键,分区数等同于表的分区数,总之,局部索引的分区机制和表的分区机制一样。
分区表的局部索引和非分区表的索引类似,索引的数据结构还是和主表的数据结构保持一对一的关系,但由于主表已经做了分区,主表的“每一个分区”都会有自己单独的索引数据结构。局部索引的结构如下图所示:
- 对每一个局部索引数据结构来说,里面的键(Key)只映射到自己分区中的主表数据,不会映射到其它分区中的主表,因此这种索引被称为局部索引。
- 从另一个角度来看,这种模式下索引的数据结构也做了分区处理,因此有时也被称为本地分区索引(Local Partitioned Index)。
4.5.2.2 全局索引
全局索引的创建规则是在索引属性中指定GLOBAL关键字,与局部索引相比,全局索引最大的特点是全局索引的分区规则跟表分区是相互独立的,全局索引允许指定自己的分区规则和分区个数,不一定需要跟表分区规则保持一致。
分区表的全局索引不再和主表的分区保持一对一的关系,而是将所有主表分区的数据合成一个整体来建立全局索引。更进一步,全局索引可以定义自己独立的数据分布模式,既可以选择非分区模式也可以选择分区模式:
- 全局非分区索引(Global Non-Partitioned Index)
- 全局分区索引(Global Partitioned Index)
4.5.2.3 全局非分区索引
全局索引中的一个键可能会映射到多个主表分区中的数据(当索引键有重复值时)。在上图中,虽然 employee 表按照 emp_id 做了分区,但是创建在 dept_id 上的全局索引并没有分区,因此同一个键值可能会映射到多个分区中。
4.5.2.4 全局分区索引
全局索引的分区键一定是索引键本身
索引数据按照指定的方式做分区处理,比如做哈希(Hash)分区或者范围(Range)分区,将索引数据分散到不同的分区中。但索引的分区模式是完全独立的,和主表的分区没有任何关系,因此对于每个索引分区来说,里面的某一
个键都可能映射到不同的主表分区(当索引键有重复值时),索引分区和主表分区之间是“多对多”的对应关系。
全局索引的分区键一定是索引键本身,在查询到索引键值后可以利用索引表中存储的主键信息计算出主表的分区位置,进而对主表也能进行快速的分区定位,避免扫描主表的所有分区。
由于全局索引的分区模式和主表的分区模式完全没有关系,看上去全局索引更像是另一张独立的表,因此也会将全局索引叫做索引表,理解起来会更容易一些(和主表相对应)。
这里特别说一点:“非”分区表也可以创建全局分区索引。但如果主表没有分区的必要,通常来说索引也就没有必要分区了。
4.5.3 局部索引和全局索引比较
局部索引的问题
- 局部索引在“索引键没有包含主表所有的分区键字段”的情况下,此时索引键值对应的索引数据在所有分区中都可能存在。如下图,employee 按照 emp_id 做了分区,但同时想利用局部索引建立关于 emp_name 的唯一约束是无法实现的。
- 由于某索引键值在所有分区的局部索引上都可能存在,索引扫描必须在所有的分区上都做一遍,以免造成数据遗漏。这会导致索引扫描效率低下,并且会在全局范围内造成CPU和IO资源的浪费。
针对这个问题,有些数据库在分区表中会增加限制,要求主键和唯一索引的定义中必须包含主表所有的分区键字段。有了这个限制,索引中的某一个键所对应的索引数据只可能存在于一个分区中,因此只要在每一个分区内保证唯一性
约束,即可在全表范围内保证唯一性约束。这个限制虽然解决了数据库的难题,却大大增加了开发人员的烦恼,因为它会导致主键和唯一索引的可选范围大大缩小(只能是主表分区键的“超集”),很多业务需求因此无法满足。
全局索引的分区键一定是索引键的前缀,所以:
全局非分区索引:
- 此时索引的结构和“非分区”表没有区别,只有一个完整的索引树,自然保证唯一性。
- 并且只有一个完整的索引树,自然没有多分区扫描的问题
全局分区索引:
- 数据只可能落在一个固定的索引分区中,因此每一个索引分区内保证唯一性约束,就能在全表范围内保证唯一性约束。
- 全局索引能保证某一个索引键的数据只落在一个固定的索引分区中,所以无论是针对固定键值的索引扫描,还是针对一个键值范围的索引扫描,都可以直接定位出需要扫描的一个或者几个分区。
从使用者的角度来看,分区表的全局索引和非分区表的索引已经没有太大区别,除了一点:全局索引需要定义索引的分区模式。这里虽然也可以使用全局“非分区”索引,但是引进全局索引的目的就是针对数据量较大的分区表,对应的索引数据量往往也是非常大的,因此还是推荐使用全局分区索引,不但可以突破非分区索引面临的单点容量限制,在并发较大的情况下也能获得更好的性能。
当然,全局索引也面临一些问题,主要是架构复杂,实现难度大,以及由此引发的一些相关问题,比如当索引数据和对应的主表数据位于不同的机器时,在事务内会面临数据一致性和性能方面的挑战。但考虑到全局索引给数据库使用
者带来的巨大便利,付出一点代价也是值得的。
4.5.4 分布式场景下索引面临的挑战
关于全局索引的实现原理,以及全局索引和本地索引的比较,前面都已经有过详细的描述,这是业界实现全局索引的基本思路,也是OceanBase中实现全局索引的基本思路。但是,前面我们更多的是讨论索引数据结构和主表数据结构的映射关系(一对一、一对多、多对多等),而忽略了另外一个很重要的因素要,那就是数据的物理分布。
对于传统的单点数据库来说,每个数据结构之下都是“本地可访问”的存储层,比如一个本地数据库表空间(里面包含若干本地文件或者设备),无论有多少个主表分区或者索引分区,数据库总是可以通过本地机器访问到,即Share-Everything的架构。但是对于OceanBase这样的分布式数据库来说,每一个主表分区和索引分区都可能分布在不同的机器上,比如“N个主表分区+M个全局索引分区”这种多对多的复杂情况,可能是(N+M)台物理机器上形成的(N*M)种索引键到主表分区的映射关系,这会带来诸多挑战:
- 如果一个事务中要修改的N条记录分别位于N台不同的机器上,而它们对应的全局索引数据又位于另外M台不同的机器上,如何在这(N+M)台机器间保证主表数据和索引数据的同步更新?
- 还是上面所说的主表数据和索引数据分布在不同机器的场景,在保证数据一致性的前提下,如何进一步缩短跨机器访问的时间,进一步提高查询效率?
- 如何保证全局索引数据在全局范围内的读写一致性?
- 一台机器上更新的索引数据,如何确保一定能被其它机器上后续的访问者读到?
可以说,如果不能解决上述问题,全局索引在分布式数据库中就失去了存在的基础。因此,虽然业内有很多分布式数据库,但全局索引功能却并不是一个标配,很多大家所熟悉的分布式数据库产品(如 MongoDB Cassandra Hbase 等)并不提供全局索引功能,只有少数产品才能提供,比如 Google F1/Spanner 和CouchBase ,这也从一个侧面印证了在分布式数据库中实现全局索引的难度。下面我就简单给大家介绍一下, OceanBase 数据库是如何跨越上述的一个个难关,在 2.0 版本中实现了全局索引的功能。
首先来看第一个问题,如何保证主表数据和索引数据的跨机器同步更新。这个问题的本质,其实是分布式数据库中“分布式事务一致性”问题,主表数据和索引数据是同一个事务中的两部分数据,当它们分布在不同的机器上时,分布式事务需要保证事务的原子性( Atomicity ):两部分数据要么全部更新成功,要么全部失败,不会因事务异常而导致主表数据和索引数据的不同步。 OceanBase 数据库在几年前的 1.0 版本中就提供了分布式事务功能,利用 Paxos 协议和经过改良的“两阶段提交( Two phase Commit )”方法,能在跨机器的分布式事务内保证ACID ,即使事务的参与者发生异常(如机器宕机),也能确保分布式事务的完整性,避免了传统两阶段提交方法会导致的“事务部分未决( In doubtTransaction )”问题,因此 OceanBase 完全可以保证跨机器事务中主表数据和索引数据的同步。
解决了分布式事务的一致性问题,我们来关注一下第二个问题,就是索引数据和主表数据分跨机器访问时的效率问题。这里面临的最大挑战就是分布式事务的网络延迟和多次日志落盘,而这种物理开销是没有办法完全消除的,为此Google 在 F1 的论文中明确说明不推荐太多的全局索引,并且应尽量避免对有全局索引的表做大事务访问。那 OceanBase 是如何应对这个问题的呢?前面提到过,OceanBase 使用了经过改良的两阶段提交方法,这种改良不仅体现在保证数据的一致性上,也体现在性能上:和传统的两阶段提交相比, OceanBase 的两阶段提交过程中会有更少的网络交互次数以及更少的写日志次数,而这些恰恰都是分布式事务中最耗时的操作。有了这样的优化,在很大程度上缩短了分布式事务的处理时间,提高了全局索引的处理效率。
最后一个问题,则是分布式数据库中全局(跨多台机器)的读写一致性问题。对 OceanBase 这样实现了快照隔离级别( Snapshot Isolation )和多版本并发控制MVCC )的分布式数据库来说,如何在机器间有时钟差异的情况下,仍能维持时间戳(即版本号)在全局范围内的前后一致性,这是一个很重要的问题。在OceanBase 数据库的 2.0 版本中,我们提供了“全局一致性快照”功能,并在此基础上实现了全局范围内的快照隔离级别和多版本并发控制,这样就能保证全局范围内前后事务的读写一致性,满足了全局索引的要求。
4.6 Hint
4.6.1 Oceanbase Hint 概念和用法
Hint 基础
- 基于代价的优化器,与 Oracle 的 Hint 类似
- 如果使用 MySQL 的客户端执行带 Hint 的 SQL 语句,需要使用 -c 选项登陆, 否则 MySQL客户端会将Hint作为注释从用户SQL中去除,导致系统无法收到用户Hint
- 如果 Server 端不认识 SQL 语句中的 Hint,会忽略而不报错
- Hint 只影响数据库优化器生成计划的逻辑,而不影响 SQL 语句本身的语义
OceanBase Hint 用法
- 支持不带参数,如 /*+ FUNC */
- 支持带参数,如 /*+ FUNC(param) */
- 多个hint可以写到同一个注释中,用逗号分隔,如/*+ FUNC1, FUNC2(param) */
- SELECT语句的hint必须近接在关键字SELECT之后,其他词之前。如:SELECT /*+ FUNC */ …
- UPDATE, DELETE 语句的 hint 必须紧接在关键字 UPDATE,DELETE 之后
OceanBase HINT 行为理念
Hint是为了告诉优化器考虑hint中的方式, 其它数据库的行为更像贪心算法,不会考虑全部可能的路径最优,hint的指定的方式就是为了告诉数据库加入到它的考虑范围。
OB优化器更像是动态规划,已经考虑了所有可能,因此hint告诉数据库加入到考虑范围就没有什么意义。基于这种情况,OB 的 hint更多是告诉优化器按照指定行为做。
与MySQL,Oracle不一致的地方
- MySQL 5.6版本index hint如果Index不存在会报错;OB中如果Hint中index不存在,则Hint不生效,但是SQL语句也不会报错;这一点MySQL会在后面也改成不报错的方式
- Oracle leading hint,出现不存在的表时候hint是否生效会做推算,导致行为不确定,部分情况有效,部分情况全部无效。OB中如果Hint中表名不存在,则Hint不生效,但是SQL语句也不会报错。
- 健壮性更好,减少因Hint不正确而报错的情况。
4.6.2 OceanBase Hint 举例说明
几个样例
- /*+READ_CONSISTENCY(STRONG)*/
- /*+READ_CONSISTENCY(WEAK)*/
- /*+query_timeout(100000000)*/ 单位微秒
- /*+USE_MERGE(表名1 表名2)*/
- /*+INDEX(表名索引名) */
- SELECT /*+ INDEX(t1 i1) , INDEX(t2 i2)*/ from t1, t2 WHERE 1.c1=t2.c1;
- /*+PARALLEL(N)*/
- 指定语句级别的并发度
- 当该hint指定时,会忽略系统变量 ob_stmt_parallel_degree 的设置
- /*+ leading(table_name_list)*/
- 指定表的连接顺序
- 如果发现 hint 指定的 table_name不存在,leading hint失效
- 如果发现 hint 中存在重复 table,leading hint 失效
- 在一个多表关联的查询中,该Hint指定 由哪个表作为驱动表,告诉优化器首先要访问哪个表上的数据
说明
- READ_CONSISTENCY 表明是强一致性读还是弱一致性读,如果SQL语句中不指定,默认值根据系统变量ob_read_consistency的值决定。FROZEN表示读最近一次冻结点的数据,冻结版本号由系统自动选择。
- query_timeout 设定 Server 端执行语句的超时时间,超时以后 Server 端中断执行并返回超时错误码。
- Merge Join 是先将关联表的关联列各自做排序,然后从各自的排序表中抽取数据,到另一个排序表中做匹配,因为merge join需要做更多的排序,所以消耗的资源更多。
- INDEX和 Oracle 类似,设定对于指定表的查询强制走索引名,如果索引不存在或者不可用,也不报错。
- 关于 PARALLEL,满足以下条件之一的 SELECT 语句,才会并发
- 包含聚合函数
- 包含Group by
- 包含order by
- 不包含limit
- leading 指定表的连接顺序
OceanBase 常用 Hint 参考文档:https://www.oceanbase.com/docs/oceanbase-database/oceanbase/V2.2.77/common-sql-hints-for-oceanbase
4.7 SQL 执行性能监控
4.7.1 SQL 监控系统视图
4.7.1.1 (g)v$sql_audit
是全局 SQL 审计表,可以用来查看每次请求客户端来源,执行 Server 信息,执行状态信息,等待事件及执行各阶段耗时等。
相关系统参数
alter system set enable_sql_audit = true/false;
alter system set sql_audit_memory_limit = '3G';
淘汰机制概述
- 目的:防止
sql_audit
记录占用过多内存或存储空间,影响系统稳定性。
- 检测频率:后台任务每 1秒 检查一次是否需要触发淘汰。
- 触发依据:
-
- 内存使用量 达到阈值。
- 记录条数 达到阈值。
触发淘汰的时机
(1) 基于内存的触发条件
sql_audit
可用的最大内存上限(avail_mem_limit
)由以下两者较小值决定:
OBServer 可用内存 × 10%
- 手动配置的
sql_audit_memory_limit
触发淘汰的内存阈值:
范围 | 触发淘汰的阈值 | 说明 |
64MB ≤ < 100MB |
| 接近上限时预留 20MB 缓冲。 |
100MB ≤ < 5GB |
(80%) | 使用 80% 时触发,避免突增。 |
≥ 5GB |
| 大内存环境下预留 1GB 缓冲。 |
(2) 基于记录数的触发条件
- 触发阈值:当
sql_audit
记录数超过 900万条 时触发淘汰。
停止淘汰的标准
(1) 内存触发淘汰后的停止条件
范围 | 停止淘汰的阈值 | 说明 |
64MB ≤ < 100MB |
| 释放到比触发点更低 20MB。 |
100MB ≤ < 5GB |
(60%) | 释放到 60% 以下,留足空间。 |
≥ 5GB |
| 大内存下预留 2GB 安全空间。 |
(2) 记录数触发淘汰后的停止条件
- 停止阈值:淘汰至 800万条记录 时停止(比触发点少 100万条)。
4.7.1.2 SQL Trace
SQL Trace 能够交互式的提供上一次执行的SQL 请求执行过程信息及各阶段的耗时。
SQL Trace 开关
SQL Trace 功能默认时关闭的,可通过session 变量来控制其关闭和打开。
set ob_enable_trace_log = 0/1;
Show Trace
当SQL Trace 功能打开后,执行需要诊断的SQL, 然后通过show trace 能够查看该SQL 执行的信息。
这些执行信息以表格方式输出,每列说明如下:
注意:系统参数 enable_sql_audit 打开时,用户租户下的 ob_enable_trace_log 才会起作用。
4.7.2 Plan Cache 视图
几个视图
- (g)v$plan_cache_stat : 记录每个计划缓存的状态,每个计划缓存在该视图中有一条记录;
- (g)v$plan_cache_plan_stat :记录计划缓存中所有plan 的具体信息及每个计划总的执行统计信息, 每个plan 在该视图中一条记录;
- (g)v$plan_cache_plan_explain : 记录某条SQL 在计划缓存中的执行计划。