查看原文
其他

数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析(三)之查询SQL

2017-08-05 王文斌(芋艿) 芋道源码

🙂🙂🙂关注微信公众号:【芋艿的后端小屋】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表

  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址

  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢

  4. 新的源码解析文章实时收到通知。每周更新一篇左右

  5. 认真的源码交流微信群!


  • 1. 概述

  • 2. SelectStatement

    • 2.1 AbstractSQLStatement

    • 2.2 SQLToken

  • 3. #query()

    • 3.1 #parseDistinct()

    • 3.2 #parseSelectList()

    • 3.3 #skipToFrom()

    • 3.4 #parseFrom()

    • 3.5 #parseWhere()

    • 3.6 #parseGroupBy()

    • 3.7 #parseOrderBy()

    • 3.8 #parseLimit()

    • 3.9 #queryRest()

  • 4. appendDerived等方法

    • 4.1 appendAvgDerivedColumns

    • 4.2 appendDerivedOrderColumns

    • 4.3 ItemsToken

    • 4.4 appendDerivedOrderBy()

  • 666. 彩蛋


1. 概述

本文前置阅读:

  • 《SQL 解析(一)之词法解析》

  • 《SQL 解析(二)之SQL解析》

本文分享插入SQL解析的源码实现。

由于每个数据库在遵守 SQL 语法规范的同时,又有各自独特的语法。因此,在 Sharding-JDBC 里每个数据库都有自己的 SELECT 语句的解析器实现方式,当然绝大部分逻辑是相同的。本文主要分享笔者最常用的 MySQL 查询

查询 SQL 解析主流程如下:

  1. // AbstractSelectParser.java

  2. public final SelectStatement parse() {

  3.   query();

  4.   parseOrderBy();

  5.   customizedSelect();

  6.   appendDerivedColumns();

  7.   appendDerivedOrderBy();

  8.   return selectStatement;

  9. }

  • #parseOrderBy() :对于 MySQL 查询语句解析器无效果,因为已经在 #query() 方法里面已经调用 #parseOrderBy(),因此图中省略该方法。

  • #customizedSelect() :Oracle、SQLServer 查询语句解析器重写了该方法,对于 MySQL 查询解析器是个空方法,进行省略。有兴趣的同学可以单独去研究研究。

Sharding-JDBC 正在收集使用公司名单:传送门。
🙂 你的登记,会让更多人参与和使用 Sharding-JDBC。传送门
Sharding-JDBC 也会因此,能够覆盖更多的业务场景。传送门
登记吧,骚年!传送门

👼 查询语句解析是增删改查里面最灵活也是最复杂的,希望大家有耐心看完本文。理解查询语句解析,另外三种语句理解起来简直是 SO EASY。骗人是小狗🐶。
🙂如果对本文有不理解的地方,可以给我的公众号(芋艿的后端小屋)留言,我会逐条认真耐心回复。骗人是小猪🐷。

OK,不废话啦,开始我们这段痛并快乐的旅途。

2. SelectStatement

🙂 本节只介绍这些类,方便本文下节分析源码实现大家能知道认识它们 🙂

SelectStatement,查询语句解析结果对象。

  1. // SelectStatement.java

  2. public final class SelectStatement extends AbstractSQLStatement {

  3.    /**

  4.     * 是否行 DISTINCT / DISTINCTROW / UNION

  5.     */

  6.    private boolean distinct;

  7.    /**

  8.     * 是否查询所有字段,即 SELECT *

  9.     */

  10.    private boolean containStar;

  11.    /**

  12.     * 最后一个查询项下一个 Token 的开始位置

  13.     *

  14.     * @see #items

  15.     */

  16.    private int selectListLastPosition;

  17.    /**

  18.     * 最后一个分组项下一个 Token 的开始位置

  19.     */

  20.    private int groupByLastPosition;

  21.    /**

  22.     * 查询项

  23.     */

  24.    private final List<SelectItem> items = new LinkedList<>();

  25.    /**

  26.     * 分组项

  27.     */

  28.    private final List<OrderItem> groupByItems = new LinkedList<>();

  29.    /**

  30.     * 排序项

  31.     */

  32.    private final List<OrderItem> orderByItems = new LinkedList<>();

  33.    /**

  34.     * 分页

  35.     */

  36.    private Limit limit;

  37. }

我们对属性按照类型进行归类:

  • 特殊

    • distinct

  • 查询字段

    • containStar

    • items

    • selectListLastPosition

  • 分组条件

    • groupByItems

    • groupByLastPosition

  • 排序条件

    • orderByItems

  • 分页条件

    • limit

2.1 AbstractSQLStatement

增删改查解析结果对象的抽象父类

  1. public abstract class AbstractSQLStatement implements SQLStatement {

  2.    /**

  3.     * SQL 类型

  4.     */

  5.    private final SQLType type;

  6.    /**

  7.     * 表

  8.     */

  9.    private final Tables tables = new Tables();

  10.    /**

  11.     * 过滤条件。

  12.     * 只有对路由结果有影响的条件,才添加进数组

  13.     */

  14.    private final Conditions conditions = new Conditions();

  15.    /**

  16.     * SQL标记对象

  17.     */

  18.    private final List<SQLToken> sqlTokens = new LinkedList<>();

  19. }

2.2 SQLToken

SQLToken,SQL标记对象接口,SQL 改写时使用到。下面都是它的实现类:

说明
GeneratedKeyToken自增主键标记对象
TableToken表标记对象
ItemsToken选择项标记对象
OffsetToken分页偏移量标记对象
OrderByToken排序标记对象
RowCountToken分页长度标记对象

3. #query()

#query(),查询 SQL 解析。

MySQL SELECT Syntax

  1. // https://dev.mysql.com/doc/refman/5.7/en/select.html

  2. SELECT

  3.    [ALL | DISTINCT | DISTINCTROW ]

  4.      [HIGH_PRIORITY]

  5.      [STRAIGHT_JOIN]

  6.      [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]

  7.      [SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]

  8.    select_expr [, select_expr ...]

  9.    [FROM table_references

  10.      [PARTITION partition_list]

  11.    [WHERE where_condition]

  12.    [GROUP BY {col_name | expr | position}

  13.      [ASC | DESC], ... [WITH ROLLUP]]

  14.    [HAVING where_condition]

  15.    [ORDER BY {col_name | expr | position}

  16.      [ASC | DESC], ...]

  17.    [LIMIT {[offset,] row_count | row_count OFFSET offset}]

  18.    [PROCEDURE procedure_name(argument_list)]

  19.    [INTO OUTFILE 'file_name'

  20.        [CHARACTER SET charset_name]

  21.        export_options

  22.      | INTO DUMPFILE 'file_name'

  23.      | INTO var_name [, var_name]]

  24.    [FOR UPDATE | LOCK IN SHARE MODE]]

大体流程如下:

  1. // MySQLSelectParser.java

  2. public void query() {

  3.   if (getSqlParser().equalAny(DefaultKeyword.SELECT)) {

  4.       getSqlParser().getLexer().nextToken();

  5.       parseDistinct();

  6.       getSqlParser().skipAll(MySQLKeyword.HIGH_PRIORITY, DefaultKeyword.STRAIGHT_JOIN, MySQLKeyword.SQL_SMALL_RESULT, MySQLKeyword.SQL_BIG_RESULT, MySQLKeyword.SQL_BUFFER_RESULT,

  7.               MySQLKeyword.SQL_CACHE, MySQLKeyword.SQL_NO_CACHE, MySQLKeyword.SQL_CALC_FOUND_ROWS);

  8.       parseSelectList(); // 解析 查询字段

  9.       skipToFrom(); // 跳到 FROM 处

  10.   }

  11.   parseFrom();// 解析 表(JOIN ON / FROM 单&多表)

  12.   parseWhere(); // 解析 WHERE 条件

  13.   parseGroupBy(); // 解析 Group By 和 Having(目前不支持)条件

  14.   parseOrderBy(); // 解析 Order By 条件

  15.   parseLimit(); // 解析 分页 Limit 条件

  16.   // [PROCEDURE] 暂不支持

  17.   if (getSqlParser().equalAny(DefaultKeyword.PROCEDURE)) {

  18.       throw new SQLParsingUnsupportedException(getSqlParser().getLexer().getCurrentToken().getType());

  19.   }

  20.   queryRest();

  21. }

3.1 #parseDistinct()

解析 DISTINCT、DISTINCTROW、UNION 谓语。

核心代码:

  1. // AbstractSelectParser.java

  2. protected final void parseDistinct() {

  3.   if (sqlParser.equalAny(DefaultKeyword.DISTINCT, DefaultKeyword.DISTINCTROW, DefaultKeyword.UNION)) {

  4.       selectStatement.setDistinct(true);

  5.       sqlParser.getLexer().nextToken();

  6.       if (hasDistinctOn() && sqlParser.equalAny(DefaultKeyword.ON)) { // PostgreSQL 独有语法: DISTINCT ON

  7.           sqlParser.getLexer().nextToken();

  8.           sqlParser.skipParentheses();

  9.       }

  10.   } else if (sqlParser.equalAny(DefaultKeyword.ALL)) {

  11.       sqlParser.getLexer().nextToken();

  12.   }

  13. }

此处 DISTINCT 和 DISTINCT(字段) 不同,它是针对查询结果做去重,即整行重复。举个例子:

  1. mysql> SELECT item_id, order_id FROM t_order_item;

  2. +---------+----------+

  3. | item_id | order_id |

  4. +---------+----------+

  5. | 1       | 1        |

  6. | 1       | 1        |

  7. +---------+----------+

  8. 2 rows in set (0.03 sec)

  9. mysql> SELECT DISTINCT item_id, order_id FROM t_order_item;

  10. +---------+----------+

  11. | item_id | order_id |

  12. +---------+----------+

  13. | 1       | 1        |

  14. +---------+----------+

  15. 1 rows in set (0.02 sec)

3.2 #parseSelectList()

SELECTo.user_idCOUNT(DISTINCT i.itemid) AS itemcountMAX(i.item_id)FROM

SelectItemSelectItemSelectItem

将 SQL 查询字段 按照逗号( , )切割成多个选择项( SelectItem)。核心代码如下:

  1. // AbstractSelectParser.java

  2. protected final void parseSelectList() {

  3.   do {

  4.       // 解析单个选择项

  5.       parseSelectItem();

  6.   } while (sqlParser.skipIfEqual(Symbol.COMMA));

  7.   // 设置 最后一个查询项下一个 Token 的开始位置

  8.   selectStatement.setSelectListLastPosition(sqlParser.getLexer().getCurrentToken().getEndPosition() - sqlParser.getLexer().getCurrentToken().getLiterals().length());

  9. }

3.2.1 SelectItem 选择项

SelectItem 接口,属于分片上下文信息,有 2 个实现类:

  • CommonSelectItem :通用选择项

  • AggregationSelectItem :聚合选择项

解析单个 SelectItem 核心代码:

  1. // AbstractSelectParser.java

  2. private void parseSelectItem() {

  3.   // 第四种情况,SQL Server 独有

  4.   if (isRowNumberSelectItem()) {

  5.       selectStatement.getItems().add(parseRowNumberSelectItem());

  6.       return;

  7.   }

  8.   sqlParser.skipIfEqual(DefaultKeyword.CONNECT_BY_ROOT); // Oracle 独有:https://docs.oracle.com/cd/B19306_01/server.102/b14200/operators004.htm

  9.   String literals = sqlParser.getLexer().getCurrentToken().getLiterals();

  10.   // 第一种情况,* 通用选择项,SELECT *

  11.   if (sqlParser.equalAny(Symbol.STAR) || Symbol.STAR.getLiterals().equals(SQLUtil.getExactlyValue(literals))) {

  12.       sqlParser.getLexer().nextToken();

  13.       selectStatement.getItems().add(new CommonSelectItem(Symbol.STAR.getLiterals(), sqlParser.parseAlias()));

  14.       selectStatement.setContainStar(true);

  15.       return;

  16.   }

  17.   // 第二种情况,聚合选择项

  18.   if (sqlParser.skipIfEqual(DefaultKeyword.MAX, DefaultKeyword.MIN, DefaultKeyword.SUM, DefaultKeyword.AVG, DefaultKeyword.COUNT)) {

  19.       selectStatement.getItems().add(new AggregationSelectItem(AggregationType.valueOf(literals.toUpperCase()), sqlParser.skipParentheses(), sqlParser.parseAlias()));

  20.       return;

  21.   }

  22.   // 第三种情况,非 * 通用选择项

  23.   StringBuilder expression = new StringBuilder();

  24.   Token lastToken = null;

  25.   while (!sqlParser.equalAny(DefaultKeyword.AS) && !sqlParser.equalAny(Symbol.COMMA) && !sqlParser.equalAny(DefaultKeyword.FROM) && !sqlParser.equalAny(Assist.END)) {

  26.       String value = sqlParser.getLexer().getCurrentToken().getLiterals();

  27.       int position = sqlParser.getLexer().getCurrentToken().getEndPosition() - value.length();

  28.       expression.append(value);

  29.       lastToken = sqlParser.getLexer().getCurrentToken();

  30.       sqlParser.getLexer().nextToken();

  31.       if (sqlParser.equalAny(Symbol.DOT)) {

  32.           selectStatement.getSqlTokens().add(new TableToken(position, value));

  33.       }

  34.   }

  35.   // 不带 AS,并且有别名,并且别名不等于自己(tips:这里重点看。判断这么复杂的原因:防止substring操作截取结果错误)

  36.   if (null != lastToken && Literals.IDENTIFIER == lastToken.getType()

  37.           && !isSQLPropertyExpression(expression, lastToken) // 过滤掉,别名是自己的情况【1】(例如,SELECT u.user_id u.user_id FROM t_user)

  38.           && !expression.toString().equals(lastToken.getLiterals())) { // 过滤掉,无别名的情况【2】(例如,SELECT user_id FROM t_user)

  39.       selectStatement.getItems().add(

  40.               new CommonSelectItem(SQLUtil.getExactlyValue(expression.substring(0, expression.lastIndexOf(lastToken.getLiterals()))), Optional.of(lastToken.getLiterals())));

  41.       return;

  42.   }

  43.   // 带 AS(例如,SELECT user_id AS userId) 或者 无别名(例如,SELECT user_id)

  44.   selectStatement.getItems().add(new CommonSelectItem(SQLUtil.getExactlyValue(expression.toString()), sqlParser.parseAlias()));

  45. }

一共分成 4 种大的情况,我们来逐条梳理:

  • 第一种: * 通用选择项
    例如, SELECT*FROM t_user 的 *
    为什么要加 Symbol.STAR.getLiterals().equals(SQLUtil.getExactlyValue(literals)) 判断呢?

  1. SELECT `*` FROM t_user; // 也能达到查询所有字段的效果

  • 第二种:聚合选择项
    例如, SELECT COUNT(user_id)FROM t_user 的 COUNT(user_id)

解析结果 AggregationSelectItem:

sqlParser.skipParentheses() 解析见《SQL 解析(二)之SQL解析》的AbstractParser小节。

  • 第三种:非 * 通用选择项

例如, SELECT user_id FROM t_user

从实现上,逻辑会复杂很多。相比第一种,可以根据 * 做字段判断;相比第二种,可以使用 () 做字段判断。能够判断一个包含别名的 SelectItem 结束有 4 种 Token,根据结束方式我们分成 2 种:

  • DefaultKeyword.AS :能够接触出 SelectItem 字段,即不包含别名。例如, SELECT user_id AS uid FROM t_user,能够直接解析出 user_id

  • Symbol.COMMA / DefaultKeyword.FROM / Assist.END :包含别名。例如, SELECT user_id uid FROM t_user,解析结果为 user_id uid

基于这个在配合上面的代码注释,大家再重新理解下第三种情况的实现。

  • 第四种:SQLServer ROW_NUMBER:

ROW_NUMBER 是 SQLServer 独有的。由于本文大部分的读者使用的 MySQL / Oracle,就跳过了。有兴趣的同学可以看 SQLServerSelectParser#parseRowNumberSelectItem() 方法。

3.2.2 #parseAlias() 解析别名

解析别名,分成是否带 AS 两种情况。解析代码:《SQL 解析(二)之SQL解析》的#parseAlias()小节。

3.2.3 TableToken 表标记对象

TableToken,记录表名在 SQL 里出现的位置名字

  1. public final class TableToken implements SQLToken {

  2.    /**

  3.     * 开始位置

  4.     */

  5.    private final int beginPosition;

  6.    /**

  7.     * 表达式

  8.     */

  9.    private final String originalLiterals;

  10.    /**

  11.     * 获取表名称.

  12.     * @return 表名称

  13.     */

  14.    public String getTableName() {

  15.        return SQLUtil.getExactlyValue(originalLiterals);

  16.    }

  17. }

例如上文第三种情况。

3.3 #skipToFrom()

  1. /**

  2. * 跳到 FROM 处

  3. */

  4. private void skipToFrom() {

  5.   while (!getSqlParser().equalAny(DefaultKeyword.FROM) && !getSqlParser().equalAny(Assist.END)) {

  6.       getSqlParser().getLexer().nextToken();

  7.   }

  8. }

3.4 #parseFrom()

解析表以及表连接关系。这块相对比较复杂,请大家耐心+耐心+耐心。

MySQL JOIN Syntax

  1. // https://dev.mysql.com/doc/refman/5.7/en/join.html

  2. table_references:

  3.    escaped_table_reference [, escaped_table_reference] ...

  4. escaped_table_reference:

  5.    table_reference

  6.  | { OJ table_reference }

  7. table_reference:

  8.    table_factor

  9.  | join_table

  10. table_factor:

  11.    tbl_name [PARTITION (partition_names)]

  12.        [[AS] alias] [index_hint_list]

  13.  | table_subquery [AS] alias

  14.  | ( table_references )

  15. join_table:

  16.    table_reference [INNER | CROSS] JOIN table_factor [join_condition]

  17.  | table_reference STRAIGHT_JOIN table_factor

  18.  | table_reference STRAIGHT_JOIN table_factor ON conditional_expr

  19.  | table_reference {LEFT|RIGHT} [OUTER] JOIN table_reference join_condition

  20.  | table_reference NATURAL [{LEFT|RIGHT} [OUTER]] JOIN table_factor

  21. join_condition:

  22.    ON conditional_expr

  23.  | USING (column_list)

  24. index_hint_list:

  25.    index_hint [, index_hint] ...

  26. index_hint:

  27.    USE {INDEX|KEY}

  28.      [FOR {JOIN|ORDER BY|GROUP BY}] ([index_list])

  29.  | IGNORE {INDEX|KEY}

  30.      [FOR {JOIN|ORDER BY|GROUP BY}] (index_list)

  31.  | FORCE {INDEX|KEY}

  32.      [FOR {JOIN|ORDER BY|GROUP BY}] (index_list)

  33. index_list:

  34.    index_name [, index_name] ...

3.4.1 JOIN ON / FROM TABLE

先抛开子查询的情况,只考虑如下两种 SQL 情况。

  1. // JOIN ON : 实际可以继续 JOIN ON 更多表

  2. SELECT * FROM t_order o JOIN t_order_item i ON o.order_id = i.order_id;

  3. // FROM 多表 :实际可以继续 FROM 多更表

  4. SELECT * FROM t_order o, t_order_item i

在看实现代码之前,先一起看下调用顺序图:

看懂上图后,来继续看下实现代码(🙂代码有点多,不要方!):

  1. // AbstractSelectParser.java

  2. /**

  3. * 解析所有表名和表别名

  4. */

  5. public final void parseFrom() {

  6.   if (sqlParser.skipIfEqual(DefaultKeyword.FROM)) {

  7.       parseTable();

  8.   }

  9. }

  10. /**

  11. * 解析所有表名和表别名

  12. */

  13. public void parseTable() {

  14.   // 解析子查询

  15.   if (sqlParser.skipIfEqual(Symbol.LEFT_PAREN)) {

  16.       if (!selectStatement.getTables().isEmpty()) {

  17.           throw new UnsupportedOperationException("Cannot support subquery for nested tables.");

  18.       }

  19.       selectStatement.setContainStar(false);

  20.       sqlParser.skipUselessParentheses(); // 去掉子查询左括号

  21.       parse(); // 解析子查询 SQL

  22.       sqlParser.skipUselessParentheses(); // 去掉子查询右括号

  23.       //

  24.       if (!selectStatement.getTables().isEmpty()) {

  25.           return;

  26.       }

  27.   }

  28.   parseTableFactor(); // 解析当前表

  29.   parseJoinTable(); // 解析下一个表

  30. }

  31. /**

  32. * 解析单个表名和表别名

  33. */

  34. protected final void parseTableFactor() {

  35.   int beginPosition = sqlParser.getLexer().getCurrentToken().getEndPosition() - sqlParser.getLexer().getCurrentToken().getLiterals().length();

  36.   String literals = sqlParser.getLexer().getCurrentToken().getLiterals();

  37.   sqlParser.getLexer().nextToken();

  38.   // TODO 包含Schema解析

  39.   if (sqlParser.skipIfEqual(Symbol.DOT)) { // https://dev.mysql.com/doc/refman/5.7/en/information-schema.html :SELECT table_name, table_type, engine FROM information_schema.tables

  40.       sqlParser.getLexer().nextToken();

  41.       sqlParser.parseAlias();

  42.       return;

  43.   }

  44.   // FIXME 根据shardingRule过滤table

  45.   selectStatement.getSqlTokens().add(new TableToken(beginPosition, literals));

  46.   // 表 以及 表别名

  47.   selectStatement.getTables().add(new Table(SQLUtil.getExactlyValue(literals), sqlParser.parseAlias()));

  48. }

  49. /**

  50. * 解析 Join Table 或者 FROM 下一张 Table

  51. */

  52. protected void parseJoinTable() {

  53.   if (sqlParser.skipJoin()) {

  54.       // 这里调用 parseJoinTable() 而不是 parseTableFactor() :下一个 Table 可能是子查询

  55.       // 例如:SELECT * FROM t_order JOIN (SELECT * FROM t_order_item JOIN t_order_other ON ) .....

  56.       parseTable();

  57.       if (sqlParser.skipIfEqual(DefaultKeyword.ON)) { // JOIN 表时 ON 条件

  58.           do {

  59.               parseTableCondition(sqlParser.getLexer().getCurrentToken().getEndPosition());

  60.               sqlParser.accept(Symbol.EQ);

  61.               parseTableCondition(sqlParser.getLexer().getCurrentToken().getEndPosition() - sqlParser.getLexer().getCurrentToken().getLiterals().length());

  62.           } while (sqlParser.skipIfEqual(DefaultKeyword.AND));

  63.       } else if (sqlParser.skipIfEqual(DefaultKeyword.USING)) { // JOIN 表时 USING 为使用两表相同字段相同时对 ON 的简化。例如以下两条 SQL 等价:

  64.                                                                   // SELECT * FROM t_order o JOIN t_order_item i USING (order_id);

  65.                                                                   // SELECT * FROM t_order o JOIN t_order_item i ON o.order_id = i.order_id

  66.           sqlParser.skipParentheses();

  67.       }

  68.       parseJoinTable(); // 继续递归

  69.   }

  70. }

  71. /**

  72. * 解析 ON 条件里的 TableToken

  73. *

  74. * @param startPosition 开始位置

  75. */

  76. private void parseTableCondition(final int startPosition) {

  77.   SQLExpression sqlExpression = sqlParser.parseExpression();

  78.   if (!(sqlExpression instanceof SQLPropertyExpression)) {

  79.       return;

  80.   }

  81.   SQLPropertyExpression sqlPropertyExpression = (SQLPropertyExpression) sqlExpression;

  82.   if (selectStatement.getTables().getTableNames().contains(SQLUtil.getExactlyValue(sqlPropertyExpression.getOwner().getName()))) {

  83.       selectStatement.getSqlTokens().add(new TableToken(startPosition, sqlPropertyExpression.getOwner().getName()));

  84.   }

  85. }

OK,递归因为平时日常中写的比较少,可能理解起来可能会困难一些,努力看懂!🙂如果真的看不懂,可以加微信公众号(芋艿的后端小屋),我来帮你一起理解。

3.4.2 子查询

Sharding-JDBC 目前支持第一个包含多层级的数据子查询。例如:

  1. SELECT o3.* FROM (SELECT * FROM (SELECT * FROM t_order o) o2) o3;

  2. SELECT o3.* FROM (SELECT * FROM (SELECT * FROM t_order o) o2) o3 JOIN t_order_item i ON o3.order_id = i.order_id;

不支持第二个开始包含多层级的数据子查询。例如:

  1. SELECT o3.* FROM t_order_item i JOIN (SELECT * FROM (SELECT * FROM t_order o) o2) o3 ON o3.order_id = i.order_id; // 此条 SQL 是上面第二条 SQL 左右量表颠倒

  2. SELECT COUNT(*) FROM (SELECT * FROM t_order o WHERE o.id IN (SELECT id FROM t_order WHERE status = ?)) // FROM 官方不支持 SQL 举例

使用第二个开始的子查询会抛出异常,代码如下:

  1. // AbstractSelectParser.java#parseTable()片段

  2. if (!selectStatement.getTables().isEmpty()) {

  3.    throw new UnsupportedOperationException("Cannot support subquery for nested tables.");

  4. }

使用子查询,建议认真阅读官方《分页及子查询》文档。

3.4.3 #parseJoinTable()

MySQLSelectParser 重写了 #parseJoinTable() 方法用于解析 USE / IGNORE / FORCE index_hint。具体语法见上文 JOIN Syntax。这里就跳过,有兴趣的同学可以去看看。

3.4.4 Tables 表集合对象

属于分片上下文信息

  1. // Tables.java

  2. public final class Tables {

  3.    private final List<Table> tables = new ArrayList<>();

  4. }

  5. // Table.java

  6. public final class Table {

  7.    /**

  8.     * 表

  9.     */

  10.    private final String name;

  11.    /**

  12.     * 别名

  13.     */

  14.    private final Optional<String> alias;

  15. }

  16. // AbstractSelectParser.java#parseTableFactor()片段

  17. selectStatement.getTables().add(new Table(SQLUtil.getExactlyValue(literals), sqlParser.parseAlias()));

3.5 #parseWhere()

解析 WHERE 条件。解析代码:《SQL 解析(二)之SQL解析》的#parseWhere()小节。

3.6 #parseGroupBy()

解析分组条件,实现上比较类似 #parseSelectList,会更加简单一些。


  1. // 🙂 抱歉,微信文章篇幅长度限制,该处代码已经“屏蔽”。解决方式:

  2. 1. 点击最下方【阅读原文】

  3. 2. 访问地址:http://www.yunai.me/Sharding-JDBC/sql-parse-3/?mp


3.6.1 OrderItem 排序项

属于分片上下文信息

  1. public final class OrderItem {

  2.    /**

  3.    * 所属表别名

  4.    */

  5.    private final Optional<String> owner;

  6.    /**

  7.    * 排序字段

  8.    */

  9.    private final Optional<String> name;

  10.    /**

  11.    * 排序类型

  12.    */

  13.    private final OrderType type;

  14.    /**

  15.    * 按照第几个查询字段排序

  16.    * ORDER BY 数字 的 数字代表的是第几个字段

  17.    */

  18.    @Setter

  19.    private int index = -1;

  20.    /**

  21.    * 字段在查询项({@link com.dangdang.ddframe.rdb.sharding.parsing.parser.context.selectitem.SelectItem} 的别名

  22.    */

  23.    @Setter

  24.    private Optional<String> alias;

  25. }

3.7 #parseOrderBy()

解析排序条件。实现逻辑类似 #parseGroupBy(),这里就跳过,有兴趣的同学可以去看看。

3.8 #parseLimit()

解析分页 Limit 条件。相对简单,这里就跳过,有兴趣的同学可以去看看。注意下,分成 3 种情况:

  • LIMIT row_count

  • LIMIT offset, row_count

  • LIMIT row_count OFFSET offset

3.8.1 Limit

分页对象。属于分片上下文信息

  1. // Limit.java

  2. public final class Limit {

  3.    /**

  4.     * 是否重写rowCount

  5.     * TODO 待补充:预计和内存分页合并有关

  6.     */

  7.    private final boolean rowCountRewriteFlag;

  8.    /**

  9.     * offset

  10.     */

  11.    private LimitValue offset;

  12.    /**

  13.     * row

  14.     */

  15.    private LimitValue rowCount;

  16. }

  17. // LimitValue.java

  18. public final class LimitValue {

  19.    /**

  20.     * 值

  21.     * 当 value == -1 时,为占位符

  22.     */

  23.    private int value;

  24.    /**

  25.     * 第几个占位符

  26.     */

  27.    private int index;

  28. }

3.8.2 OffsetToken RowCountToken

  • OffsetToken:分页偏移量标记对象

  • RowCountToken:分页长度标记对象

只有在对应位置非占位符才有该 SQLToken

  1. // OffsetToken.java

  2. public final class OffsetToken implements SQLToken {

  3.    /**

  4.     * SQL 所在开始位置

  5.     */

  6.    private final int beginPosition;

  7.    /**

  8.     * 偏移值

  9.     */

  10.    private final int offset;

  11. }

  12. // RowCountToken.java

  13. public final class RowCountToken implements SQLToken {

  14.    /**

  15.     * SQL 所在开始位置

  16.     */

  17.    private final int beginPosition;

  18.    /**

  19.     * 行数

  20.     */

  21.    private final int rowCount;

  22. }

3.9 #queryRest()

  1. // AbstractSelectParser.java

  2. protected void queryRest() {

  3.   if (sqlParser.equalAny(DefaultKeyword.UNION, DefaultKeyword.EXCEPT, DefaultKeyword.INTERSECT, DefaultKeyword.MINUS)) {

  4.       throw new SQLParsingUnsupportedException(sqlParser.getLexer().getCurrentToken().getType());

  5.   }

  6. }

不支持 UNION / EXCEPT / INTERSECT / MINUS ,调用会抛出异常。

4. appendDerived等方法

因为 Sharding-JDBC 对表做了分片,在 AVG , GROUP BY , ORDER BY 需要对 SQL 进行一些改写,以达到能在内存里对结果做进一步处理,例如求平均值、分组、排序等。

😈:打起精神,此块是非常有趣的。

4.1 appendAvgDerivedColumns

解决 AVG 查询。

  1. // AbstractSelectParser.java

  2. /**

  3. * 针对 AVG 聚合字段,增加推导字段

  4. * AVG 改写成 SUM + COUNT 查询,内存计算出 AVG 结果。

  5. *

  6. * @param itemsToken 选择项标记对象

  7. */

  8. private void appendAvgDerivedColumns(final ItemsToken itemsToken) {

  9.   int derivedColumnOffset = 0;

  10.   for (SelectItem each : selectStatement.getItems()) {

  11.       if (!(each instanceof AggregationSelectItem) || AggregationType.AVG != ((AggregationSelectItem) each).getType()) {

  12.           continue;

  13.       }

  14.       AggregationSelectItem avgItem = (AggregationSelectItem) each;

  15.       // COUNT 字段

  16.       String countAlias = String.format(DERIVED_COUNT_ALIAS, derivedColumnOffset);

  17.       AggregationSelectItem countItem = new AggregationSelectItem(AggregationType.COUNT, avgItem.getInnerExpression(), Optional.of(countAlias));

  18.       // SUM 字段

  19.       String sumAlias = String.format(DERIVED_SUM_ALIAS, derivedColumnOffset);

  20.       AggregationSelectItem sumItem = new AggregationSelectItem(AggregationType.SUM, avgItem.getInnerExpression(), Optional.of(sumAlias));

  21.       // AggregationSelectItem 设置

  22.       avgItem.getDerivedAggregationSelectItems().add(countItem);

  23.       avgItem.getDerivedAggregationSelectItems().add(sumItem);

  24.       // TODO 将AVG列替换成常数,避免数据库再计算无用的AVG函数

  25.       // ItemsToken

  26.       itemsToken.getItems().add(countItem.getExpression() + " AS " + countAlias + " ");

  27.       itemsToken.getItems().add(sumItem.getExpression() + " AS " + sumAlias + " ");

  28.       //

  29.       derivedColumnOffset++;

  30.   }

  31. }

4.2 appendDerivedOrderColumns

解决 GROUP BY , ORDER BY。

  1. // AbstractSelectParser.java

  2. /**

  3. * 针对 GROUP BY 或 ORDER BY 字段,增加推导字段

  4. * 如果该字段不在查询字段里,需要额外查询该字段,这样才能在内存里 GROUP BY 或 ORDER BY

  5. *

  6. * @param itemsToken 选择项标记对象

  7. * @param orderItems 排序字段

  8. * @param aliasPattern 别名模式

  9. */

  10. private void appendDerivedOrderColumns(final ItemsToken itemsToken, final List<OrderItem> orderItems, final String aliasPattern) {

  11.   int derivedColumnOffset = 0;

  12.   for (OrderItem each : orderItems) {

  13.       if (!isContainsItem(each)) {

  14.           String alias = String.format(aliasPattern, derivedColumnOffset++);

  15.           each.setAlias(Optional.of(alias));

  16.           itemsToken.getItems().add(each.getQualifiedName().get() + " AS " + alias + " ");

  17.       }

  18.   }

  19. }

  20. /**

  21. * 查询字段是否包含排序字段

  22. *

  23. * @param orderItem 排序字段

  24. * @return 是否

  25. */

  26. private boolean isContainsItem(final OrderItem orderItem) {

  27.   if (selectStatement.isContainStar()) { // SELECT *

  28.       return true;

  29.   }

  30.   for (SelectItem each : selectStatement.getItems()) {

  31.       if (-1 != orderItem.getIndex()) { // ORDER BY 使用数字

  32.           return true;

  33.       }

  34.       if (each.getAlias().isPresent() && orderItem.getAlias().isPresent() && each.getAlias().get().equalsIgnoreCase(orderItem.getAlias().get())) { // 字段别名比较

  35.           return true;

  36.       }

  37.       if (!each.getAlias().isPresent() && orderItem.getQualifiedName().isPresent() && each.getExpression().equalsIgnoreCase(orderItem.getQualifiedName().get())) { // 字段原名比较

  38.           return true;

  39.       }

  40.   }

  41.   return false;

  42. }

4.3 ItemsToken

选择项标记对象,属于分片上下文信息,目前有 3 个情况会创建:

  1. AVG 查询额外 COUNT 和 SUM: #appendAvgDerivedColumns()

  2. GROUP BY 不在 查询字段,额外查询该字段 : #appendDerivedOrderColumns()

  3. ORDER BY 不在 查询字段,额外查询该字段 : #appendDerivedOrderColumns()

  1. public final class ItemsToken implements SQLToken {

  2.    /**

  3.     * SQL 开始位置

  4.     */

  5.    private final int beginPosition;

  6.    /**

  7.     * 字段名数组

  8.     */

  9.    private final List<String> items = new LinkedList<>();

  10. }

4.4 appendDerivedOrderBy()

当 SQL 有聚合条件而无排序条件,根据聚合条件进行排序。这是数据库自己的执行规则。

  1. mysql> SELECT order_id FROM t_order GROUP BY order_id;

  2. +----------+

  3. | order_id |

  4. +----------+

  5. | 1        |

  6. | 2        |

  7. | 3        |

  8. +----------+

  9. 3 rows in set (0.05 sec)

  10. mysql> SELECT order_id FROM t_order GROUP BY order_id DESC;

  11. +----------+

  12. | order_id |

  13. +----------+

  14. | 3        |

  15. | 2        |

  16. | 1        |

  17. +----------+

  18. 3 rows in set (0.02 sec)

  1. // AbstractSelectParser.java

  2. /**

  3. * 当无 Order By 条件时,使用 Group By 作为排序条件(数据库本身规则)

  4. */

  5. private void appendDerivedOrderBy() {

  6.   if (!getSelectStatement().getGroupByItems().isEmpty() && getSelectStatement().getOrderByItems().isEmpty()) {

  7.       getSelectStatement().getOrderByItems().addAll(getSelectStatement().getGroupByItems());

  8.       getSelectStatement().getSqlTokens().add(new OrderByToken(getSelectStatement().getGroupByLastPosition()));

  9.   }

  10. }

4.3.1 OrderByToken

排序标记对象。当无 Order By 条件时,使用 Group By 作为排序条件(数据库本身规则)。

  1. // OrderByToken.java

  2. public final class OrderByToken implements SQLToken {

  3.    /**

  4.     * SQL 所在开始位置

  5.     */

  6.    private final int beginPosition;

  7. }

666. 彩蛋

咳咳咳,确实有一些略长。但请相信,INSERT / UPDATE / DELETE 会简单很多很多。考试考的 SQL 最多的是什么?SELECT 语句呀!为啥,难呗。恩,我相信看到此处的你,一定是能看懂的,加油!

🙂如果对本文有不理解的地方,可以关注我的公众号(芋艿的后端小屋)获得微信号,我们来一场,1 对 1 的搞基吧,不不不,是交流交流。

道友,帮我分享一波怎么样?


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存