乐趣区

关于数据库:ShardingSphere-4x-数据分片内核剖析之改写引擎

工程师面向逻辑库与逻辑表书写的 SQL,并不可能间接在实在的数据库中执行,SQL 改写用于将逻辑 SQL 改写为在实在数据库中能够正确执行的 SQL。
它包含正确性改写和优化改写两局部。

正确性改写

在蕴含分表的场景中,须要将分表配置中的逻辑表名称改写为路由之后所获取的实在表名称。仅分库则不须要表名称的改写。除此之外,还包含补列和分页信息修改等内容。

标识符改写

须要改写的标识符包含表名称、索引名称以及 Schema 名称。

表名称改写是指将找到逻辑表在原始 SQL 中的地位,并将其改写为实在表的过程。表名称改写是一个典型的须要对 SQL 进行解析的场景。
从一个最简略的例子开始,若逻辑 SQL 为:

SELECT order_id FROM t_order WHERE order_id=1;

假如该 SQL 配置分片键 order_id,并且 order_id= 1 的状况,将路由至分片表 1。那么改写之后的 SQL 应该为:

SELECT order_id FROM t_order_1 WHERE order_id=1;

在这种最简略的 SQL 场景中,是否将 SQL 解析为形象语法树仿佛无关紧要,只有通过字符串查找和替换就能够达到 SQL 改写的成果。
然而上面的场景,就无奈仅仅通过字符串的查找替换来正确的改写 SQL 了:

SELECT order_id FROM t_order WHERE order_id=1 AND remarks='t_order xxx';

正确改写的 SQL 应该是:

SELECT order_id FROM t_order_1 WHERE order_id=1 AND remarks='t_order xxx';

而非:

SELECT order_id FROM t_order_1 WHERE order_id=1 AND remarks='t_order_1 xxx';

因为表名之外可能含有表名称的相似字符,因而不能通过简略的字符串替换的形式去改写 SQL。

上面再来看一个更加简单的 SQL 改写场景:

SELECT t_order.order_id FROM t_order WHERE t_order.order_id=1 AND remarks='t_order xxx';

下面的 SQL 将表名作为字段的标识符,因而在 SQL 改写时须要一并批改:

SELECT t_order_1.order_id FROM t_order_1 WHERE t_order_1.order_id=1 AND remarks='t_order xxx';

而如果 SQL 中定义了表的别名,则无需连同别名一起批改,即便别名与表名雷同亦是如此。例如:

SELECT t_order.order_id FROM t_order AS t_order WHERE t_order.order_id=1 AND remarks='t_order xxx';

SQL 改写则仅须要改写表名称就能够了:

SELECT t_order.order_id FROM t_order_1 AS t_order WHERE t_order.order_id=1 AND remarks='t_order xxx';

索引名称是另一个有可能改写的标识符。
在某些数据库中(如 MySQL、SQLServer),索引是以表为维度创立的,在不同的表中的索引是能够重名的;
而在另外的一些数据库中(如 PostgreSQL、Oracle),索引是以数据库为维度创立的,即便是作用在不同表上的索引,它们也要求其名称的唯一性。

在 ShardingSphere 中,治理 Schema 的形式与治理表一模一样,它采纳逻辑 Schema 去治理一组数据源。
因而,ShardingSphere 须要将用户在 SQL 中书写的逻辑 Schema 替换为实在的数据库 Schema。

ShardingSphere 目前还不反对在 DQL 和 DML 语句中应用 Schema。
它目前仅反对在数据库治理语句中应用 Schema,例如:

SHOW COLUMNS FROM t_order FROM order_ds;

Schema 的改写指的是将逻辑 Schema 采纳单播路由的形式,改写为随机查找到的一个正确的实在 Schema。

补列

须要在查问语句中补列通常由两种状况导致。
第一种状况是 ShardingSphere 须要在后果归并时获取相应数据,但该数据并未能通过查问的 SQL 返回。
这种状况次要是针对 GROUP BY 和 ORDER BY。后果归并时,须要依据 GROUP BYORDER BY的字段项进行分组和排序,但如果原始 SQL 的选择项中若并未蕴含分组项或排序项,则须要对原始 SQL 进行改写。
先看一下原始 SQL 中带有后果归并所需信息的场景:

SELECT order_id, user_id FROM t_order ORDER BY user_id;

因为应用 user_id 进行排序,在后果归并中须要可能获取到 user_id 的数据,而下面的 SQL 是可能获取到 user_id 数据的,因而无需补列。

如果选择项中不蕴含后果归并时所需的列,则须要进行补列,如以下 SQL:

SELECT order_id FROM t_order ORDER BY user_id;

因为原始 SQL 中并不蕴含须要在后果归并中须要获取的 user_id,因而须要对 SQL 进行补列改写。补列之后的 SQL 是:

SELECT order_id, user_id AS ORDER_BY_DERIVED_0 FROM t_order ORDER BY user_id;

值得一提的是,补列只会补充缺失的列,不会全副补充,而且,在 SELECT 语句中蕴含 * 的 SQL,也会依据表的元数据信息选择性补列。上面是一个较为简单的 SQL 补列场景:

SELECT o.* FROM t_order o, t_order_item i WHERE o.order_id=i.order_id ORDER BY user_id, order_item_id;

咱们假如只有 t_order_item 表中蕴含 order_item_id 列,那么依据表的元数据信息可知,在后果归并时,排序项中的 user_id 是存在于 t_order 表中的,无需补列;order_item_id 并不在 t_order 中,因而须要补列。
补列之后的 SQL 是:

SELECT o.*, order_item_id AS ORDER_BY_DERIVED_0 FROM t_order o, t_order_item i WHERE o.order_id=i.order_id ORDER BY user_id, order_item_id;

补列的另一种状况是应用 AVG 聚合函数。在分布式的场景中,应用 avg1 + avg2 + avg3 / 3 计算平均值并不正确,须要改写为 (sum1 + sum2 + sum3) / (count1 + count2 + count3)。
这就须要将蕴含 AVG 的 SQL 改写为 SUM 和 COUNT,并在后果归并时从新计算平均值。例如以下 SQL:

SELECT AVG(price) FROM t_order WHERE user_id=1;

须要改写为:

SELECT COUNT(price) AS AVG_DERIVED_COUNT_0, SUM(price) AS AVG_DERIVED_SUM_0 FROM t_order WHERE user_id=1;

而后才可能通过后果归并正确的计算平均值。

最初一种补列是在执行 INSERT 的 SQL 语句时,如果应用数据库自增主键,是无需写入主键字段的。
但数据库的自增主键是无奈满足分布式场景下的主键惟一的,因而 ShardingSphere 提供了分布式自增主键的生成策略,并且能够通过补列,让应用方无需改变现有代码,即可将分布式自增主键通明的替换数据库现有的自增主键。
分布式自增主键的生成策略将在下文中详述,这里只论述与 SQL 改写相干的内容。
举例说明,假如表 t_order 的主键是 order_id,原始的 SQL 为:

INSERT INTO t_order (`field1`, `field2`) VALUES (10, 1);

能够看到,上述 SQL 中并未蕴含自增主键,是须要数据库自行填充的。ShardingSphere 配置自增主键后,SQL 将改写为:

INSERT INTO t_order (`field1`, `field2`, order_id) VALUES (10, 1, xxxxx);

改写后的 SQL 将在 INSERT FIELD 和 INSERT VALUE 的最初局部减少主键列名称以及主动生成的自增主键值。上述 SQL 中的 xxxxx 示意主动生成的自增主键值。

如果 INSERT 的 SQL 中并未蕴含表的列名称,ShardingSphere 也能够依据判断参数个数以及表元信息中的列数量比照,并主动生成自增主键。例如,原始的 SQL 为:

INSERT INTO t_order VALUES (10, 1);

改写的 SQL 将只在主键所在的列程序处减少自增主键即可:

INSERT INTO t_order VALUES (xxxxx, 10, 1);

自增主键补列时,如果应用占位符的形式书写 SQL,则只须要改写参数列表即可,无需改写 SQL 自身。

分页修改

从多个数据库获取分页数据与单数据库的场景是不同的。
假如每 10 条数据为一页,取第 2 页数据。在分片环境下获取 LIMIT 10, 10,归并之后再依据排序条件取出前 10 条数据是不正确的。
举例说明,若 SQL 为:

SELECT score FROM t_score ORDER BY score DESC LIMIT 1, 2;

下图展现了不进行 SQL 的改写的分页执行后果。

通过图中所示,想要获得两个表中独特的依照分数排序的第 2 条和第 3 条数据,应该是 9590
因为执行的 SQL 只能从每个表中获取第 2 条和第 3 条数据,即从 t_score_0 表中获取的是 9080;从 t_score_0 表中获取的是 8575
因而进行后果归并时,只能从获取的 90808575之中进行归并,那么后果归并无论怎么实现,都不可能取得正确的后果。

正确的做法是将分页条件改写为 LIMIT 0, 3,取出所有前两页数据,再联合排序条件计算出正确的数据。
下图展现了进行 SQL 改写之后的分页执行后果。

越获取偏移量地位靠后数据,应用 LIMIT 分页形式的效率就越低。
有很多办法能够防止应用 LIMIT 进行分页。比方构建行记录数量与行偏移量的二级索引,或应用上次分页数据结尾 ID 作为下次查问条件的分页形式等。

分页信息修改时,如果应用占位符的形式书写 SQL,则只须要改写参数列表即可,无需改写 SQL 自身。

批量拆分

在应用批量插入的 SQL 时,如果插入的数据是跨分片的,那么须要对 SQL 进行改写来避免将多余的数据写入到数据库中。
插入操作与查问操作的不同之处在于,查问语句中即应用了不存在于以后分片的分片键,也不会对数据产生影响;而插入操作则必须将多余的分片键删除。
举例说明,如下 SQL:

INSERT INTO t_order (order_id, xxx) VALUES (1, 'xxx'), (2, 'xxx'), (3, 'xxx');

假如数据库依然是依照 order_id 的奇偶值分为两片的,仅将这条 SQL 中的表名进行批改,而后发送至数据库实现 SQL 的执行,则两个分片都会写入雷同的记录。
尽管只有合乎分片查问条件的数据才可能被查问语句取出,但存在冗余数据的实现计划并不合理。因而须要将 SQL 改写为:

INSERT INTO t_order_0 (order_id, xxx) VALUES (2, 'xxx');
INSERT INTO t_order_1 (order_id, xxx) VALUES (1, 'xxx'), (3, 'xxx');

应用 IN 的查问与批量插入的状况类似,不过 IN 操作并不会导致数据查问后果谬误。通过对 IN 查问的改写,能够进一步的晋升查问性能。如以下 SQL:

SELECT * FROM t_order WHERE order_id IN (1, 2, 3);

改写为:

SELECT * FROM t_order_0 WHERE order_id IN (2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 3);

能够进一步的晋升查问性能。ShardingSphere 临时还未实现此改写策略,目前的改写后果是:

SELECT * FROM t_order_0 WHERE order_id IN (1, 2, 3);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2, 3);

尽管 SQL 的执行后果是正确的,但并未达到最优的查问效率。

优化改写

优化改写的目标是在不影响查问正确性的状况下,对性能进行晋升的无效伎俩。它分为单节点优化和流式归并优化。

单节点优化

路由至单节点的 SQL,则无需优化改写。
当取得一次查问的路由后果后,如果是路由至惟一的数据节点,则无需波及到后果归并。因而补列和分页信息等改写都没有必要进行。
尤其是分页信息的改写,无需将数据从第 1 条开始取,大量的升高了对数据库的压力,并且节俭了网络带宽的无谓耗费。

流式归并优化

它仅为蕴含 GROUP BY 的 SQL 减少 ORDER BY 以及和分组项雷同的排序项和排序程序,用于将内存归并转化为流式归并。
在后果归并的局部中,将对流式归并和内存归并进行具体阐明。

改写引擎的整体构造划分如下图所示。

退出移动版