工程师在使用 shardingsphere jdbc 时,都是面向逻辑库逻辑表 编写 SQL 的 ,然而这些 SQL 并不能直接在真实的数据库中执行。

路由引擎返回路由结果后,改写引擎会将逻辑 SQL 改写为在真实数据库中可以正确执行的 SQL 。

img

1. 标识符改写

标识符改写最核心的是:表名称改写。表名称改写是指将找到逻辑表在原始SQL中的位置,并将其改写为真实表。

从一个最简单的例子开始,若逻辑 SQL 为:

1
SELECT order_id FROM t_order WHERE order_id = 1;

假设该 SQL 配置分片键 order_id ,并且 order_id=1 的情况,将路由至分片表 1。

那么改写之后的 SQL 应该为:

1
SELECT order_id FROM t_order_1 WHERE order_id = 1;

需要注意的是:表名称修改并不是简单的字符串查找和替换

比如下面的场景,就无法仅仅通过字符串的查找替换来正确的改写 SQL 了:

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

正确改写的 SQL 应该是:

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

而非:

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

我们再来看一个更加复杂的例子,

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

上面的 SQL 将表名作为字段的标识符,因此在 SQL 改写时需要一并修改:

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

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

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

SQL 改写则仅需要改写表名称就可以了:

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

2. 补列

补列通常用于查询语句中,分两种场景:

1、分组和排序

ShardingSphere 需要在结果归并时获取相应数据,但该数据并未能通过查询的SQL 返回 。

查询 SQL 语句中,若包含分组或者排序 , 当结果归并时,需要根据GROUP BY和ORDER BY的字段项进行分组和排序,但如果原始 SQL 的选择项中若并未包含分组项或排序项,则需要对原始SQL进行改写。

先看一下原始 SQL 中带有结果归并所需信息的场景:

1
SELECT order_id, user_id FROM t_order ORDER BY user_id;

由于使用 user_id 进行排序,在结果归并中需要能够获取到 user_id 的数据,而上面的SQL是能够获取到user_id数据的,因此无需补列。

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

1
SELECT order_id FROM t_order ORDER BY user_id;

由于原始 SQL 中并不包含需要在结果归并中需要获取的user_id,因此需要对 SQL 进行补列改写。补列之后的 SQL 是:

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

值得一提的是,补列只会补充缺失的列,不会全部补充,而且,在SELECT语句中包含*的SQL,也会根据表的元数据信息选择性补列。

下面是一个较为复杂的 SQL 补列场景:

1
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是:

1
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;

2、AVG 聚合函数

分布式的场景中,使用 avg1 + avg2 + avg3 / 3计算平均值并不正确,需要改写为 (sum1 + sum2 + sum3) / (count1 + count2 + count3)。

这就需要将包含 AVG 的SQL 改写为 SUM 和 COUNT ,并在结果归并时重新计算平均值。

例如以下 SQL :

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

需要改写为如下 SQL ,才能够通过结果归并正确的计算平均值。

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

3、自增主键补列

执行 INSERT 的 SQL 语句时,如果使用数据库自增主键,是无需写入主键字段的。

但数据库的自增主键是无法满足分布式场景下的主键唯一的,因此 ShardingSphere 提供了分布式自增主键的生成策略,并且可以通过补列,让使用方无需改动现有代码,即可将分布式自增主键透明的替换数据库现有的自增主键。

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

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

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

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

如果 INSERT 的 SQL 中并未包含表的列名称,ShardingSphere 也可以根据判断参数个数以及表元信息中的列数量对比,并自动生成自增主键。

例如,原始的 SQL 为:

1
INSERT INTO t_order VALUES (10, 1);

改写的 SQL 将只在主键所在的列顺序处增加自增主键即可:

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

3. 分页修正

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

举例说明,若 SQL 为:

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

下图展示了不进行 SQL 的改写的分页执行结果:

img

通过图中所示,想要取得两个表中共同的按照分数排序的第 2 条和第 3 条数据,应该是 95 和 90。

由于执行的 SQL 只能从每个表中获取第 2 条和第 3 条数据,即从 t_score_0 表中获取的是 90 和 80;从 t_score_1 表中获取的是 85 和 75。

因此进行结果归并时,只能从获取的 90,80,85 和 75 之中进行归并,那么结果归并无论怎么实现,都不可能获得正确的结果。

正确的做法是将分页条件改写为 LIMIT 0, 3,取出所有前两页数据,再结合排序条件计算出正确的数据。

下图展示了进行 SQL 改写之后的分页执行结果。

img

越获取偏移量位置靠后数据,使用LIMIT分页方式的效率就越低。 有很多方法可以避免使用LIMIT进行分页。比如构建行记录数量与行偏移量的二级索引,或使用上次分页数据结尾ID作为下次查询条件的分页方式等。

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

4. 批量拆分

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

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

假设数据库仍然是按照 order_id 的奇偶值分为两片的,仅将这条 SQL 中的表名进行修改,然后发送至数据库完成 SQL 的执行,则两个分片都会写入相同的记录。虽然只有符合分片查询条件的数据才能够被查询语句取出,但存在冗余数据的实现方案并不合理。因此需要将 SQL 改写为:

1
2
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:

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

改写为:

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

可以进一步提升查询性能。ShardingSphere 暂时还未实现此改写策略,目前的改写结果是:

1
2
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 的执行结果是正确的,但并未达到最优的查询效率。

5. 总结

这篇文章,我们简单介绍了重写引擎的设计思路。

1、标识符改写

核心是表名称改写,并不是通过简单的字符串查询和替换来执行的。

2、补列

补列通常用于查询语句中,分三种场景:

  1. 分组和排序
  2. AVG 聚合函数
  3. 自增主键补列

3、分页修正

查询分页语句中,分布式场景下,为了保证结果正确,需要对分页参数进行修改。

4、 批量拆分

用批量插入的 SQL 时,如果插入的数据是跨分片的,那么需要对 SQL 进行改写来防止将多余的数据写入到数据库中。


本站由 卡卡龙 使用 Stellar 1.27.0 主题创建

本站访问量 次. 本文阅读量 次.