1. 程式人生 > 其它 >資料庫中介軟體 Sharding-JDBC 原始碼分析 —— SQL 解析(三)之查詢SQL

資料庫中介軟體 Sharding-JDBC 原始碼分析 —— SQL 解析(三)之查詢SQL

  • 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()

1. 概述

本文前置閱讀:

  • 《SQL 解析(一)之詞法解析》
  • 《SQL 解析(二)之SQL解析》

本文分享插入SQL解析的原始碼實現。

由於每個資料庫在遵守 SQL 語法規範的同時,又有各自獨特的語法。因此,在 Sharding-JDBC 裡每個資料庫都有自己的 SELECT 語句的解析器實現方式,當然絕大部分邏輯是相同的。本文主要分享筆者最常用的 MySQL 查詢

查詢 SQL 解析主流程如下:

// AbstractSelectParser.java
public final SelectStatement parse() {
   query();
   parseOrderBy();
   customizedSelect();
   appendDerivedColumns();
   appendDerivedOrderBy();
   return selectStatement;
}
  • #parseOrderBy() :對於 MySQL 查詢語句解析器無效果,因為已經在 #query() 方法裡面已經呼叫 #parseOrderBy(),因此圖中省略該方法。
  • #customizedSelect() :Oracle、SQLServer 查詢語句解析器重寫了該方法,對於 MySQL 查詢解析器是個空方法,進行省略。有興趣的同學可以單獨去研究研究。

查詢語句解析是增刪改查裡面最靈活也是最複雜的,希望大家有耐心看完本文。理解查詢語句解析,另外三種語句理解起來簡直是 SO EASY。騙人是小狗?。 ?如果對本文有不理解的地方,可以給我的公眾號(芋艿的後端小屋)留言,我會逐條認真耐心

回覆。騙人是小豬?。

OK,不廢話啦,開始我們這段痛並快樂的旅途。

2. SelectStatement

? 本節只介紹這些類,方便本文下節分析原始碼實現大家能知道認識它們 ?

SelectStatement,查詢語句解析結果物件。

// SelectStatement.java
public final class SelectStatement extends AbstractSQLStatement {
    /**
     * 是否行 DISTINCT / DISTINCTROW / UNION
     */
    private boolean distinct;
    /**
     * 是否查詢所有欄位,即 SELECT *
     */
    private boolean containStar;
    /**
     * 最後一個查詢項下一個 Token 的開始位置
     *
     * @see #items
     */
    private int selectListLastPosition;
    /**
     * 最後一個分組項下一個 Token 的開始位置
     */
    private int groupByLastPosition;
    /**
     * 查詢項
     */
    private final List<SelectItem> items = new LinkedList<>();
    /**
     * 分組項
     */
    private final List<OrderItem> groupByItems = new LinkedList<>();
    /**
     * 排序項
     */
    private final List<OrderItem> orderByItems = new LinkedList<>();
    /**
     * 分頁
     */
    private Limit limit;
}

我們對屬性按照型別進行歸類:

  • 特殊
    • distinct
  • 查詢欄位
    • containStar
    • items
    • selectListLastPosition
  • 分組條件
    • groupByItems
    • groupByLastPosition
  • 排序條件
    • orderByItems
  • 分頁條件
    • limit

2.1 AbstractSQLStatement

增刪改查解析結果物件的抽象父類

public abstract class AbstractSQLStatement implements SQLStatement {
    /**
     * SQL 型別
     */
    private final SQLType type;
    /**
     * 表
     */
    private final Tables tables = new Tables();
    /**
     * 過濾條件。
     * 只有對路由結果有影響的條件,才新增進陣列
     */
    private final Conditions conditions = new Conditions();
    /**
     * SQL標記物件
     */
    private final List<SQLToken> sqlTokens = new LinkedList<>();
}

2.2 SQLToken

SQLToken,SQL標記物件介面,SQL 改寫時使用到。下面都是它的實現類:

說明

GeneratedKeyToken

自增主鍵標記物件

TableToken

表標記物件

ItemsToken

選擇項標記物件

OffsetToken

分頁偏移量標記物件

OrderByToken

排序標記物件

RowCountToken

分頁長度標記物件

3. #query()

#query(),查詢 SQL 解析。

MySQL SELECT Syntax

// https://dev.mysql.com/doc/refman/5.7/en/select.html
SELECT
    [ALL | DISTINCT | DISTINCTROW ]
      [HIGH_PRIORITY]
      [STRAIGHT_JOIN]
      [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
      [SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
    select_expr [, select_expr ...]
    [FROM table_references
      [PARTITION partition_list]
    [WHERE where_condition]
    [GROUP BY {col_name | expr | position}
      [ASC | DESC], ... [WITH ROLLUP]]
    [HAVING where_condition]
    [ORDER BY {col_name | expr | position}
      [ASC | DESC], ...]
    [LIMIT {[offset,] row_count | row_count OFFSET offset}]
    [PROCEDURE procedure_name(argument_list)]
    [INTO OUTFILE 'file_name'
        [CHARACTER SET charset_name]
        export_options
      | INTO DUMPFILE 'file_name'
      | INTO var_name [, var_name]]
    [FOR UPDATE | LOCK IN SHARE MODE]]

大體流程如下:

// MySQLSelectParser.java
public void query() {
   if (getSqlParser().equalAny(DefaultKeyword.SELECT)) {
       getSqlParser().getLexer().nextToken();
       parseDistinct();
       getSqlParser().skipAll(MySQLKeyword.HIGH_PRIORITY, DefaultKeyword.STRAIGHT_JOIN, MySQLKeyword.SQL_SMALL_RESULT, MySQLKeyword.SQL_BIG_RESULT, MySQLKeyword.SQL_BUFFER_RESULT,
               MySQLKeyword.SQL_CACHE, MySQLKeyword.SQL_NO_CACHE, MySQLKeyword.SQL_CALC_FOUND_ROWS);
       parseSelectList(); // 解析 查詢欄位
       skipToFrom(); // 跳到 FROM 處
   }
   parseFrom();// 解析 表(JOIN ON / FROM 單&多表)
   parseWhere(); // 解析 WHERE 條件
   parseGroupBy(); // 解析 Group By 和 Having(目前不支援)條件
   parseOrderBy(); // 解析 Order By 條件
   parseLimit(); // 解析 分頁 Limit 條件
   // [PROCEDURE] 暫不支援
   if (getSqlParser().equalAny(DefaultKeyword.PROCEDURE)) {
       throw new SQLParsingUnsupportedException(getSqlParser().getLexer().getCurrentToken().getType());
   }
   queryRest();
}

3.1 #parseDistinct()

解析 DISTINCT、DISTINCTROW、UNION 謂語。

核心程式碼:

// AbstractSelectParser.java
protected final void parseDistinct() {
   if (sqlParser.equalAny(DefaultKeyword.DISTINCT, DefaultKeyword.DISTINCTROW, DefaultKeyword.UNION)) {
       selectStatement.setDistinct(true);
       sqlParser.getLexer().nextToken();
       if (hasDistinctOn() && sqlParser.equalAny(DefaultKeyword.ON)) { // PostgreSQL 獨有語法: DISTINCT ON
           sqlParser.getLexer().nextToken();
           sqlParser.skipParentheses();
       }
   } else if (sqlParser.equalAny(DefaultKeyword.ALL)) {
       sqlParser.getLexer().nextToken();
   }
}

此處 DISTINCT 和 DISTINCT(欄位) 不同,它是針對查詢結果做去重,即整行重複。舉個例子:

mysql> SELECT item_id, order_id FROM t_order_item;
+---------+----------+
| item_id | order_id |
+---------+----------+
| 1       | 1        |
| 1       | 1        |
+---------+----------+
2 rows in set (0.03 sec)

mysql> SELECT DISTINCT item_id, order_id FROM t_order_item;
+---------+----------+
| item_id | order_id |
+---------+----------+
| 1       | 1        |
+---------+----------+
1 rows in set (0.02 sec)

3.2 #parseSelectList()

SELECT

o.user_id

COUNT(DISTINCT i.itemid) AS itemcount

MAX(i.item_id)

FROM

SelectItem

SelectItem

SelectItem

將 SQL 查詢欄位 按照逗號( , )切割成多個選擇項( SelectItem)。核心程式碼如下:

// AbstractSelectParser.java
protected final void parseSelectList() {
   do {
       // 解析單個選擇項
       parseSelectItem();
   } while (sqlParser.skipIfEqual(Symbol.COMMA));
   // 設定 最後一個查詢項下一個 Token 的開始位置
   selectStatement.setSelectListLastPosition(sqlParser.getLexer().getCurrentToken().getEndPosition() - sqlParser.getLexer().getCurrentToken().getLiterals().length());
}

3.2.1 SelectItem 選擇項

SelectItem 介面,屬於分片上下文資訊,有 2 個實現類:

  • CommonSelectItem :通用選擇項
  • AggregationSelectItem :聚合選擇項

解析單個 SelectItem 核心程式碼:

// AbstractSelectParser.java
private void parseSelectItem() {
   // 第四種情況,SQL Server 獨有
   if (isRowNumberSelectItem()) {
       selectStatement.getItems().add(parseRowNumberSelectItem());
       return;
   }
   sqlParser.skipIfEqual(DefaultKeyword.CONNECT_BY_ROOT); // Oracle 獨有:https://docs.oracle.com/cd/B19306_01/server.102/b14200/operators004.htm
   String literals = sqlParser.getLexer().getCurrentToken().getLiterals();
   // 第一種情況,* 通用選擇項,SELECT *
   if (sqlParser.equalAny(Symbol.STAR) || Symbol.STAR.getLiterals().equals(SQLUtil.getExactlyValue(literals))) {
       sqlParser.getLexer().nextToken();
       selectStatement.getItems().add(new CommonSelectItem(Symbol.STAR.getLiterals(), sqlParser.parseAlias()));
       selectStatement.setContainStar(true);
       return;
   }
   // 第二種情況,聚合選擇項
   if (sqlParser.skipIfEqual(DefaultKeyword.MAX, DefaultKeyword.MIN, DefaultKeyword.SUM, DefaultKeyword.AVG, DefaultKeyword.COUNT)) {
       selectStatement.getItems().add(new AggregationSelectItem(AggregationType.valueOf(literals.toUpperCase()), sqlParser.skipParentheses(), sqlParser.parseAlias()));
       return;
   }
   // 第三種情況,非 * 通用選擇項
   StringBuilder expression = new StringBuilder();
   Token lastToken = null;
   while (!sqlParser.equalAny(DefaultKeyword.AS) && !sqlParser.equalAny(Symbol.COMMA) && !sqlParser.equalAny(DefaultKeyword.FROM) && !sqlParser.equalAny(Assist.END)) {
       String value = sqlParser.getLexer().getCurrentToken().getLiterals();
       int position = sqlParser.getLexer().getCurrentToken().getEndPosition() - value.length();
       expression.append(value);
       lastToken = sqlParser.getLexer().getCurrentToken();
       sqlParser.getLexer().nextToken();
       if (sqlParser.equalAny(Symbol.DOT)) {
           selectStatement.getSqlTokens().add(new TableToken(position, value));
       }
   }
   // 不帶 AS,並且有別名,並且別名不等於自己(tips:這裡重點看。判斷這麼複雜的原因:防止substring操作擷取結果錯誤)
   if (null != lastToken && Literals.IDENTIFIER == lastToken.getType()
           && !isSQLPropertyExpression(expression, lastToken) // 過濾掉,別名是自己的情況【1】(例如,SELECT u.user_id u.user_id FROM t_user)
           && !expression.toString().equals(lastToken.getLiterals())) { // 過濾掉,無別名的情況【2】(例如,SELECT user_id FROM t_user)
       selectStatement.getItems().add(
               new CommonSelectItem(SQLUtil.getExactlyValue(expression.substring(0, expression.lastIndexOf(lastToken.getLiterals()))), Optional.of(lastToken.getLiterals())));
       return;
   }
   // 帶 AS(例如,SELECT user_id AS userId) 或者 無別名(例如,SELECT user_id)
   selectStatement.getItems().add(new CommonSelectItem(SQLUtil.getExactlyValue(expression.toString()), sqlParser.parseAlias()));
}

一共分成 4 種大的情況,我們來逐條梳理:

  • 第一種: * 通用選擇項: 例如, SELECT*FROM t_user*。 為什麼要加 Symbol.STAR.getLiterals().equals(SQLUtil.getExactlyValue(literals)) 判斷呢?
SELECT `*` FROM t_user; // 也能達到查詢所有欄位的效果
  • 第二種:聚合選擇項: 例如, SELECT COUNT(user_id)FROM t_userCOUNT(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 裡出現的位置名字

public final class TableToken implements SQLToken {
    /**
     * 開始位置
     */
    private final int beginPosition;
    /**
     * 表示式
     */
    private final String originalLiterals;

    /**
     * 獲取表名稱.
     * @return 表名稱
     */
    public String getTableName() {
        return SQLUtil.getExactlyValue(originalLiterals);
    }
}

例如上文第三種情況。

3.3 #skipToFrom()

/**
* 跳到 FROM 處
*/
private void skipToFrom() {
   while (!getSqlParser().equalAny(DefaultKeyword.FROM) && !getSqlParser().equalAny(Assist.END)) {
       getSqlParser().getLexer().nextToken();
   }
}

3.4 #parseFrom()

解析表以及表連線關係。這塊相對比較複雜,請大家耐心+耐心+耐心。

MySQL JOIN Syntax

// https://dev.mysql.com/doc/refman/5.7/en/join.html
table_references:
    escaped_table_reference [, escaped_table_reference] ...

escaped_table_reference:
    table_reference
  | { OJ table_reference }

table_reference:
    table_factor
  | join_table

table_factor:
    tbl_name [PARTITION (partition_names)]
        [[AS] alias] [index_hint_list]
  | table_subquery [AS] alias
  | ( table_references )

join_table:
    table_reference [INNER | CROSS] JOIN table_factor [join_condition]
  | table_reference STRAIGHT_JOIN table_factor
  | table_reference STRAIGHT_JOIN table_factor ON conditional_expr
  | table_reference {LEFT|RIGHT} [OUTER] JOIN table_reference join_condition
  | table_reference NATURAL [{LEFT|RIGHT} [OUTER]] JOIN table_factor

join_condition:
    ON conditional_expr
  | USING (column_list)

index_hint_list:
    index_hint [, index_hint] ...

index_hint:
    USE {INDEX|KEY}
      [FOR {JOIN|ORDER BY|GROUP BY}] ([index_list])
  | IGNORE {INDEX|KEY}
      [FOR {JOIN|ORDER BY|GROUP BY}] (index_list)
  | FORCE {INDEX|KEY}
      [FOR {JOIN|ORDER BY|GROUP BY}] (index_list)

index_list:
    index_name [, index_name] ...

3.4.1 JOIN ON / FROM TABLE

先拋開子查詢的情況,只考慮如下兩種 SQL 情況。

// JOIN ON : 實際可以繼續 JOIN ON 更多表
SELECT * FROM t_order o JOIN t_order_item i ON o.order_id = i.order_id; 
// FROM 多表 :實際可以繼續 FROM 多更表
SELECT * FROM t_order o, t_order_item i

在看實現程式碼之前,先一起看下呼叫順序圖:

看懂上圖後,來繼續看下實現程式碼(?程式碼有點多,不要方!):

// AbstractSelectParser.java
/**
* 解析所有表名和表別名
*/
public final void parseFrom() {
   if (sqlParser.skipIfEqual(DefaultKeyword.FROM)) {
       parseTable();
   }
}
/**
* 解析所有表名和表別名
*/
public void parseTable() {
   // 解析子查詢
   if (sqlParser.skipIfEqual(Symbol.LEFT_PAREN)) {
       if (!selectStatement.getTables().isEmpty()) {
           throw new UnsupportedOperationException("Cannot support subquery for nested tables.");
       }
       selectStatement.setContainStar(false);
       sqlParser.skipUselessParentheses(); // 去掉子查詢左括號
       parse(); // 解析子查詢 SQL
       sqlParser.skipUselessParentheses(); // 去掉子查詢右括號
       //
       if (!selectStatement.getTables().isEmpty()) {
           return;
       }
   }
   parseTableFactor(); // 解析當前表
   parseJoinTable(); // 解析下一個表
}
/**
* 解析單個表名和表別名
*/
protected final void parseTableFactor() {
   int beginPosition = sqlParser.getLexer().getCurrentToken().getEndPosition() - sqlParser.getLexer().getCurrentToken().getLiterals().length();
   String literals = sqlParser.getLexer().getCurrentToken().getLiterals();
   sqlParser.getLexer().nextToken();
   // TODO 包含Schema解析
   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
       sqlParser.getLexer().nextToken();
       sqlParser.parseAlias();
       return;
   }
   // FIXME 根據shardingRule過濾table
   selectStatement.getSqlTokens().add(new TableToken(beginPosition, literals));
   // 表 以及 表別名
   selectStatement.getTables().add(new Table(SQLUtil.getExactlyValue(literals), sqlParser.parseAlias()));
}
/**
* 解析 Join Table 或者 FROM 下一張 Table
*/
protected void parseJoinTable() {
   if (sqlParser.skipJoin()) {
       // 這裡呼叫 parseJoinTable() 而不是 parseTableFactor() :下一個 Table 可能是子查詢
       // 例如:SELECT * FROM t_order JOIN (SELECT * FROM t_order_item JOIN t_order_other ON ) .....
       parseTable();
       if (sqlParser.skipIfEqual(DefaultKeyword.ON)) { // JOIN 表時 ON 條件
           do {
               parseTableCondition(sqlParser.getLexer().getCurrentToken().getEndPosition());
               sqlParser.accept(Symbol.EQ);
               parseTableCondition(sqlParser.getLexer().getCurrentToken().getEndPosition() - sqlParser.getLexer().getCurrentToken().getLiterals().length());
           } while (sqlParser.skipIfEqual(DefaultKeyword.AND));
       } else if (sqlParser.skipIfEqual(DefaultKeyword.USING)) { // JOIN 表時 USING 為使用兩表相同欄位相同時對 ON 的簡化。例如以下兩條 SQL 等價:
                                                                   // SELECT * FROM t_order o JOIN t_order_item i USING (order_id);
                                                                   // SELECT * FROM t_order o JOIN t_order_item i ON o.order_id = i.order_id
           sqlParser.skipParentheses();
       }
       parseJoinTable(); // 繼續遞迴
   }
}
/**
* 解析 ON 條件裡的 TableToken
*
* @param startPosition 開始位置
*/
private void parseTableCondition(final int startPosition) {
   SQLExpression sqlExpression = sqlParser.parseExpression();
   if (!(sqlExpression instanceof SQLPropertyExpression)) {
       return;
   }
   SQLPropertyExpression sqlPropertyExpression = (SQLPropertyExpression) sqlExpression;
   if (selectStatement.getTables().getTableNames().contains(SQLUtil.getExactlyValue(sqlPropertyExpression.getOwner().getName()))) {
       selectStatement.getSqlTokens().add(new TableToken(startPosition, sqlPropertyExpression.getOwner().getName()));
   }
}

3.4.2 子查詢

Sharding-JDBC 目前支援第一個包含多層級的資料子查詢。例如:

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

不支援第二個開始包含多層級的資料子查詢。例如:

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 左右量表顛倒
SELECT COUNT(*) FROM (SELECT * FROM t_order o WHERE o.id IN (SELECT id FROM t_order WHERE status = ?)) // FROM 官方不支援 SQL 舉例

使用第二個開始的子查詢會丟擲異常,程式碼如下:

// AbstractSelectParser.java#parseTable()片段
if (!selectStatement.getTables().isEmpty()) {
    throw new UnsupportedOperationException("Cannot support subquery for nested tables.");
}

使用子查詢,建議認真閱讀官方《分頁及子查詢》文件。

3.4.3 #parseJoinTable()

MySQLSelectParser 重寫了 #parseJoinTable() 方法用於解析 USE / IGNORE / FORCE index_hint。具體語法見上文 JOIN Syntax。這裡就跳過,有興趣的同學可以去看看。

3.4.4 Tables 表集合物件

屬於分片上下文資訊

// Tables.java
public final class Tables {
    private final List<Table> tables = new ArrayList<>();
}

// Table.java
public final class Table {
    /**
     * 表
     */
    private final String name;
    /**
     * 別名
     */
    private final Optional<String> alias;
}

// AbstractSelectParser.java#parseTableFactor()片段
selectStatement.getTables().add(new Table(SQLUtil.getExactlyValue(literals), sqlParser.parseAlias()));

3.5 #parseWhere()

解析 WHERE 條件。解析程式碼:《SQL 解析(二)之SQL解析》的#parseWhere()小節。

3.6 #parseGroupBy()

解析分組條件,實現上比較類似 #parseSelectList,會更加簡單一些。

3.6.1 OrderItem 排序項

屬於分片上下文資訊

public final class OrderItem {
    /**
    * 所屬表別名
    */
    private final Optional<String> owner;
    /**
    * 排序欄位
    */
    private final Optional<String> name;
    /**
    * 排序型別
    */
    private final OrderType type;
    /**
    * 按照第幾個查詢欄位排序
    * ORDER BY 數字 的 數字代表的是第幾個欄位
    */
    @Setter
    private int index = -1;
    /**
    * 欄位在查詢項({@link com.dangdang.ddframe.rdb.sharding.parsing.parser.context.selectitem.SelectItem} 的別名
    */
    @Setter
    private Optional<String> alias;
}

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

分頁物件。屬於分片上下文資訊

// Limit.java
public final class Limit {
    /**
     * 是否重寫rowCount
     * TODO 待補充:預計和記憶體分頁合併有關
     */
    private final boolean rowCountRewriteFlag;
    /**
     * offset
     */
    private LimitValue offset;
    /**
     * row
     */
    private LimitValue rowCount;
}

// LimitValue.java
public final class LimitValue {
    /**
     * 值
     * 當 value == -1 時,為佔位符
     */
    private int value;
    /**
     * 第幾個佔位符
     */
    private int index;
}

3.8.2 OffsetToken RowCountToken

  • OffsetToken:分頁偏移量標記物件
  • RowCountToken:分頁長度標記物件

只有在對應位置非佔位符才有該 SQLToken

// OffsetToken.java
public final class OffsetToken implements SQLToken {
    /**
     * SQL 所在開始位置
     */
    private final int beginPosition;
    /**
     * 偏移值
     */
    private final int offset;
}

// RowCountToken.java
public final class RowCountToken implements SQLToken {
    /**
     * SQL 所在開始位置
     */
    private final int beginPosition;
    /**
     * 行數
     */
    private final int rowCount;
}

3.9 #queryRest()

// AbstractSelectParser.java
protected void queryRest() {
   if (sqlParser.equalAny(DefaultKeyword.UNION, DefaultKeyword.EXCEPT, DefaultKeyword.INTERSECT, DefaultKeyword.MINUS)) {
       throw new SQLParsingUnsupportedException(sqlParser.getLexer().getCurrentToken().getType());
   }
}

不支援 UNION / EXCEPT / INTERSECT / MINUS ,呼叫會丟擲異常。

4. appendDerived等方法

因為 Sharding-JDBC 對錶做了分片,在 AVG , GROUP BY , ORDER BY 需要對 SQL 進行一些改寫,以達到能在記憶體裡對結果做進一步處理,例如求平均值、分組、排序等。

?:打起精神,此塊是非常有趣的。

4.1 appendAvgDerivedColumns

解決 AVG 查詢。

// AbstractSelectParser.java
/**
* 針對 AVG 聚合欄位,增加推導欄位
* AVG 改寫成 SUM + COUNT 查詢,記憶體計算出 AVG 結果。
*
* @param itemsToken 選擇項標記物件
*/
private void appendAvgDerivedColumns(final ItemsToken itemsToken) {
   int derivedColumnOffset = 0;
   for (SelectItem each : selectStatement.getItems()) {
       if (!(each instanceof AggregationSelectItem) || AggregationType.AVG != ((AggregationSelectItem) each).getType()) {
           continue;
       }
       AggregationSelectItem avgItem = (AggregationSelectItem) each;
       // COUNT 欄位
       String countAlias = String.format(DERIVED_COUNT_ALIAS, derivedColumnOffset);
       AggregationSelectItem countItem = new AggregationSelectItem(AggregationType.COUNT, avgItem.getInnerExpression(), Optional.of(countAlias));
       // SUM 欄位
       String sumAlias = String.format(DERIVED_SUM_ALIAS, derivedColumnOffset);
       AggregationSelectItem sumItem = new AggregationSelectItem(AggregationType.SUM, avgItem.getInnerExpression(), Optional.of(sumAlias));
       // AggregationSelectItem 設定
       avgItem.getDerivedAggregationSelectItems().add(countItem);
       avgItem.getDerivedAggregationSelectItems().add(sumItem);
       // TODO 將AVG列替換成常數,避免資料庫再計算無用的AVG函式
       // ItemsToken
       itemsToken.getItems().add(countItem.getExpression() + " AS " + countAlias + " ");
       itemsToken.getItems().add(sumItem.getExpression() + " AS " + sumAlias + " ");
       //
       derivedColumnOffset++;
   }
}

4.2 appendDerivedOrderColumns

解決 GROUP BY , ORDER BY。

// AbstractSelectParser.java
/**
* 針對 GROUP BY 或 ORDER BY 欄位,增加推導欄位
* 如果該欄位不在查詢欄位裡,需要額外查詢該欄位,這樣才能在記憶體裡 GROUP BY 或 ORDER BY
*
* @param itemsToken 選擇項標記物件
* @param orderItems 排序欄位
* @param aliasPattern 別名模式
*/
private void appendDerivedOrderColumns(final ItemsToken itemsToken, final List<OrderItem> orderItems, final String aliasPattern) {
   int derivedColumnOffset = 0;
   for (OrderItem each : orderItems) {
       if (!isContainsItem(each)) {
           String alias = String.format(aliasPattern, derivedColumnOffset++);
           each.setAlias(Optional.of(alias));
           itemsToken.getItems().add(each.getQualifiedName().get() + " AS " + alias + " ");
       }
   }
}
/**
* 查詢欄位是否包含排序欄位
*
* @param orderItem 排序欄位
* @return 是否
*/
private boolean isContainsItem(final OrderItem orderItem) {
   if (selectStatement.isContainStar()) { // SELECT *
       return true;
   }
   for (SelectItem each : selectStatement.getItems()) {
       if (-1 != orderItem.getIndex()) { // ORDER BY 使用數字
           return true;
       }
       if (each.getAlias().isPresent() && orderItem.getAlias().isPresent() && each.getAlias().get().equalsIgnoreCase(orderItem.getAlias().get())) { // 欄位別名比較
           return true;
       }
       if (!each.getAlias().isPresent() && orderItem.getQualifiedName().isPresent() && each.getExpression().equalsIgnoreCase(orderItem.getQualifiedName().get())) { // 欄位原名比較
           return true;
       }
   }
   return false;
}

4.3 ItemsToken

選擇項標記物件,屬於分片上下文資訊,目前有 3 個情況會建立:

  1. AVG 查詢額外 COUNT 和 SUM: #appendAvgDerivedColumns()
  2. GROUP BY 不在 查詢欄位,額外查詢該欄位 : #appendDerivedOrderColumns()
  3. ORDER BY 不在 查詢欄位,額外查詢該欄位 : #appendDerivedOrderColumns()
public final class ItemsToken implements SQLToken {
    /**
     * SQL 開始位置
     */
    private final int beginPosition;
    /**
     * 欄位名陣列
     */
    private final List<String> items = new LinkedList<>();
}

4.4 appendDerivedOrderBy()

當 SQL 有聚合條件而無排序條件,根據聚合條件進行排序。這是資料庫自己的執行規則。

mysql> SELECT order_id FROM t_order GROUP BY order_id;
+----------+
| order_id |
+----------+
| 1        |
| 2        |
| 3        |
+----------+
3 rows in set (0.05 sec)

mysql> SELECT order_id FROM t_order GROUP BY order_id DESC;
+----------+
| order_id |
+----------+
| 3        |
| 2        |
| 1        |
+----------+
3 rows in set (0.02 sec)
// AbstractSelectParser.java
/**
* 當無 Order By 條件時,使用 Group By 作為排序條件(資料庫本身規則)
*/
private void appendDerivedOrderBy() {
   if (!getSelectStatement().getGroupByItems().isEmpty() && getSelectStatement().getOrderByItems().isEmpty()) {
       getSelectStatement().getOrderByItems().addAll(getSelectStatement().getGroupByItems());
       getSelectStatement().getSqlTokens().add(new OrderByToken(getSelectStatement().getGroupByLastPosition()));
   }
}

4.3.1 OrderByToken

排序標記物件。當無 Order By 條件時,使用 Group By 作為排序條件(資料庫本身規則)。

// OrderByToken.java
public final class OrderByToken implements SQLToken {
    /**
     * SQL 所在開始位置
     */
    private final int beginPosition;
}