動態

詳情 返回 返回

GreatSQL CTE 查詢報告臨時表找不到問題解析 - 動態 詳情

GreatSQL CTE 查詢報告臨時表找不到問題解析

一、問題發現

在客户現場的一次問題報告中發現某個帶有CTE語句進行查詢的時候,把tmp_table_size設置為最小1024,數據量少的時候卻報錯臨時表找不到。注意:該問題在最新的MySQL版本中仍存在。

看下面例子:

1、準備表

CREATE TABLE t7 (
  ID int NOT NULL AUTO_INCREMENT,
  NAME varchar(128) COLLATE utf8mb4_bin DEFAULT NULL,
  PARENTNODEID int DEFAULT NULL,
  STATE int DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

greatsql> INSERT INTO t7 VALUES (-1,'萬里',0,1);

2、tmp_table_size為默認值場合

如下所示用默認tmp_table_size並且進行CTE派生表查詢,可以發現結果正常顯示一條數據,符合預期。

-- tmp_table_size參數為默認值的情況
greatsql> SHOW variables LIKE '%tmp_table_size%';
+----------------+----------+
| Variable_name  | Value    |
+----------------+----------+
| tmp_table_size | 16777216 |
+----------------+----------+

greatsql> WITH RECURSIVE cte AS(   (select id    from t7 where id= -1)   UNION   (select b.id     from t7 b inner join cte on cte.id=b.parentNodeid and b.state =1) ) select * from cte;      
+------+
| id   |
+------+
|   -1 |
+------+

3、tmp_table_size修改小的場合

如下所示設置最小的tmp_table_size並且進行CTE派生表查詢,可以發現報錯了,問題復現。

greatsql> WITH RECURSIVE cte AS(   (select id    from t7 where id= -1)   UNION   (select b.id     from t7 b inner join cte on cte.id=b.parentNodeid and b.state =1) ) select * from cte;      
ERROR 1813 (HY000): Tablespace '`tmp`.`#sqle7cc2_8_4`' exists.

二、問題調查過程

查詢帶有CTE派生表的時候內部會創建臨時表用於保存臨時數據,因此先看一下下面的運行流程圖:

image.png

上面的流程圖可以簡化歸納總結為以下:

1、創建表A,創建表B ==>先賦值tmp table引擎
2、創建表A引擎 ==> 創建表A表tmp table引擎,發現內存不夠,改為innodb引擎並創建表空間(見圖上紅字部分),但是表B還是用tmp table引擎
3、打開表A,打開表B ==>表A打開成功,打開表B失敗,因為在kv_store列表找不到這個臨時表名
4、插入表A,插入表B
因此可以看出問題出在第二步,表A改變了引擎但是沒有對應改變表B的引擎,接着表B打開的時候列表找不到對應的名字報錯。
三、問題解決

結合上面分析,可以發現應該在第二步場景下,在表A的引擎改變的時候同步改變表B的引擎,因此對函數create_tmp_table_with_fallback修改如下:

static bool resolve_cte_common_table_expr(TABLE *wtable,
                                          Table_ref *wtable_list) {
  assert(wtable_list);
  Derived_refs_iterator ref_it(wtable_list);

  if (wtable_list) {
    Common_table_expr *cte = wtable_list->common_table_expr();
    if (cte) {
      int i = 0, found = -1;
      TABLE *t;
      while ((t = ref_it.get_next())) {
        if (t == wtable) {
          found = i;
          break;
        }
        ++i;
      }
      assert(found >= 0);
      if (found > 0)
        // 'wtable' is at position 'found', move it to 0 to convert it first
        std::swap(cte->tmp_tables[0], cte->tmp_tables[found]);
      ref_it.rewind();
    }
  }
  TABLE new_table, *table = nullptr;
  bool error = false;
  TABLE_SHARE *share = wtable->s;
  uint count = 0;
  // 遍歷所有相關表並改變引擎
  while (true) {
    HA_CREATE_INFO create_info;
    table = ref_it.get_next();
    if (table == nullptr) break;

    table->file = get_new_handler(
        table->s, false, share->alloc_for_tmp_file_handler, innodb_hton);
    // for CTE's cloned table,it doesn't need to do
    // innobase_basic_ddl::create_impl again.
    if (count == 0) {
      error = table->file->create(share->table_name.str, table, &create_info,
                                  nullptr);
      if (error) return true;
    }
    count++;
  }
  return false;
}

static bool create_tmp_table_with_fallback(THD *thd, TABLE *table) {
  int error =
      table->file->create(share->table_name.str, table, &create_info, nullptr);
  if (error == HA_ERR_RECORD_FILE_FULL &&
      table->s->db_type() == temptable_hton) {
    Table_ref *const wtable_list = table->pos_in_table_list;
    // for CTE table,it should set all tables to innodb_hton.
    // 這裏增加一個CTE判斷,如果是CTE就對所有相關表做操作,改變所有表的引擎。
    if (wtable_list && resolve_cte_common_table_expr(table, wtable_list))
      return true;
    else {
      table->file = get_new_handler(
          table->s, false, share->alloc_for_tmp_file_handler, innodb_hton);
      error = table->file->create(share->table_name.str, table, &create_info,
                                  nullptr);
    }
  }
}

接着執行上面的查詢,發現可以查出結果了。

greatsql> SET tmp_table_size=1024;
Query OK, 0 rows affected (0.00 sec)

greatsql> WITH RECURSIVE cte AS(   (select id    from t7 where id= -1)   UNION   (select b.id     from t7 b inner join cte on cte.id=b.parentNodeid and b.state =1) ) select * from cte;      
+------+
| id   |
+------+
|   -1 |
+------+
1 row in set (0.10 sec)
四、問題總結

通過以上分析我們可以發現,帶有CTE派生表的查詢會在內部創建臨時表用於儲存中間數據,根據tmp_table_size值設置會影響臨時表存放地方,如果tmp_table_size設置小了那麼一開始就會從內存表改為創建落盤表,但是CTE內部涉及好幾層迭代器,這時候每一層臨時表的引擎都需要改,而導致報錯的代碼只改了其中一層的臨時表,沒有把別的引擎一起改了,最後導致問題。

這個問題涉及了2個不同的迭代器的強相關表,理論上應該一起操作引擎,但是有時候代碼處理一張表卻漏了處理另一張,這就會導致問題。

建議:在沒修復該Bug前,可以先適當調大tmp_table_size參數值。

Add a new 評論

Some HTML is okay.