日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

從存儲引擎讀取一條記錄之后,對 Or 連接的 N 個 Where 條件(N >= 2)調用 Item->val_bool(),只要其中一個返回值等于True,記錄就匹配 Or 連接的 N 個 Where 條件。

我們來聊聊 MySQL 是怎么判斷一條記錄是否匹配 where 條件的。

本文內容基于 MySQL 8.0.32 源碼。

正文

準備工作

創建測試表:

CREATE TABLE `t1` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `str1` varchar(255) DEFAULT '',
  `i1` int DEFAULT '0',
  `i2` int DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

插入測試數據:

INSERT INTO t1(str1, i1, i2) VALUES
('s1', NULL, NULL),
('s2', 20, NULL),
('s3', 30, 31),
('s4', 40, 41),
('s5', 50, 51),
('s6', 60, 61),
('s7', 70, 71),
('s8', 80, 81);

示例 SQL:

select * from t1
where i2 > 20 and (i1 = 50 or i1 = 80)

 

整體介紹

在源碼中,where 條件會形成樹狀結構,示例 SQL 的 where 條件結構如下:

注意:這里的樹狀結構不是數據結構中的樹。

圖片

我們可以從圖中得到以下信息:

  • Item_cond_and 代表 where 條件中的 and,連接 Item_func_gt 和 Item_cond_or。
  • Item_func_gt 代表 i2 > 20,其中 Item_field 包含 Field_long,代表 i2 字段,Item_int 代表整數 20。
  • Item_cond_or 代表 where 條件中的 or,連接兩個 Item_func_eq。
  • 第 1 個 Item_func_eq 代表 i1 = 50,其中 Item_field 包含 Field_long,代表 i1 字段,Item_int 代表整數 50。
  • 第 2 個 Item_func_eq 代表 i1 = 80,其中 Item_field 包含 Field_long,代表 i1 字段,Item_int 代表整數 80。

接下來,我們結合堆棧來看看 where 條件的實現流程:

| > mysql_execute_command(THD*, bool) sql/sql_parse.cc:4688
| + > Sql_cmd_dml::execute(THD*) sql/sql_select.cc:578
| + - > Sql_cmd_dml::execute_inner(THD*) sql/sql_select.cc:778
| + - x > Query_expression::execute(THD*) sql/sql_union.cc:1823
| + - x = > Query_expression::ExecuteIteratorQuery(THD*) sql/sql_union.cc:1770
| + - x = | > FilterIterator::Read() sql/iterators/composite_iterators.cc:79
| + - x = | + > Item_cond_and::val_int() sql/item_cmpfunc.cc:5973
| + - x = | + - > // 第 1 個 Item::val_bool()
| + - x = | + - > // 代表 i2 > 20
| + - x = | + - > Item::val_bool() sql/item.cc:218
| + - x = | + - x > Item_func_gt::val_int() sql/item_cmpfunc.cc:2686
| + - x = | + - x = > Arg_comparator::compare() sql/item_cmpfunc.h:210
| + - x = | + - x = | > Arg_comparator::compare_int_signed() sql/item_cmpfunc.cc:1826
| + - x = | + - x = | + > Item_field::val_int() sql/item.cc:3013
| + - x = | + - x = | + - > Field_long::val_int() const sql/field.cc:3763 // i2
| + - x = | + - x = | + > Item_int::val_int() sql/item.h:4934 // 20
| + - x = | + - > // 第 2 個 Item::val_bool()
| + - x = | + - > // 代表 i1 = 50 or i1 = 80
| + - x = | + - > Item::val_bool() sql/item.cc:218
| + - x = | + - x > Item_cond_or::val_int() sql/item_cmpfunc.cc:6017
| + - x = | + - x = > // 第 3 個 Item::val_bool()
| + - x = | + - x = > // 代表 i1 = 50
| + - x = | + - x = > Item::val_bool() sql/item.cc:218
| + - x = | + - x = | > Item_func_eq::val_int() sql/item_cmpfunc.cc:2493
| + - x = | + - x = | + > Arg_comparator::compare() sql/item_cmpfunc.h:210
| + - x = | + - x = | + - > Arg_comparator::compare_int_signed() sql/item_cmpfunc.cc:1826
| + - x = | + - x = | + - x > Item_field::val_int() sql/item.cc:3013
| + - x = | + - x = | + - x = > Field_long::val_int() const sql/field.cc:3763 // i1
| + - x = | + - x = | + - x > Item_int::val_int() sql/item.h:4934 // 50
| + - x = | + - x = > // 第 4 個 Item::val_bool()
| + - x = | + - x = > // 代表 i1 = 80
| + - x = | + - x = > Item::val_bool() sql/item.cc:218
| + - x = | + - x = | > Item_func_eq::val_int() sql/item_cmpfunc.cc:2493
| + - x = | + - x = | + > Arg_comparator::compare() sql/item_cmpfunc.h:210
| + - x = | + - x = | + - > Arg_comparator::compare_int_signed() sql/item_cmpfunc.cc:1826
| + - x = | + - x = | + - x > Item_field::val_int() sql/item.cc:3013
| + - x = | + - x = | + - x = > Field_long::val_int() const sql/field.cc:3763 // i1
| + - x = | + - x = | + - x > Item_int::val_int() sql/item.h:4934 // 80

FilterIterator::Read() 從存儲引擎讀取一條記錄,Item_cond_and::val_int() 判斷該記錄是否匹配 where 條件。

從堆棧中可以看到,Item_cond_and::val_int() 的下一層有兩個 Item::val_bool():

  • 第 1 個 Item::val_bool() 代表 i2 > 20,經過多級調用 Arg_comparator::compare_int_signed() 判斷記錄的 i2 字段值是否大于 20。
  • 第 2 個 Item::val_bool() 代表 i1 = 50 or i1 = 80。
  • 第 2 個 Item::val_bool() 是復合條件,它的下層還嵌套了第 3、4 個 Item::val_bool():
  • 第 3 個 Item::val_bool() 代表 i1 = 50,經過多級調用 Arg_comparator::compare_int_signed() 判斷記錄的 i1 字段值是否等于 50。
  • 第 4 個 Item::val_bool() 代表 i1 = 80,經過多級調用 Arg_comparator::compare_int_signed() 方法判斷記錄的 i1 字段值是否等于 80。

第 3、4 個 Item::val_bool() 中只要有一個返回 true,第 2 個 Item::val_bool() 就會返回 true,表示記錄匹配 i1 = 50 or i1 = 80。

第 1、2 個 Item::val_bool() 必須都返回 true,Item_cond_and::val_int() 才會返回 1,表示記錄匹配示例 SQL 的 where 條件。

源碼分析

ExecuteIteratorQuery()

// sql/sql_union.cc
bool Query_expression::ExecuteIteratorQuery(THD *thd) {
  ...
  {
    ...
    for (;;) {
      // 從存儲引擎讀取一條記錄
      int error = m_root_iterator->Read();
      DBUG_EXECUTE_IF("bug13822652_1", thd->killed = THD::KILL_QUERY;);

      // 讀取出錯,直接返回
      if (error > 0 || thd->is_error())  // Fatal error
        return true;
      // error < 0
      // 表示已經讀完了所有符合條件的記錄
      // 查詢結束
      else if (error < 0)
        break;
      // SQL 被客戶端干掉了
      else if (thd->killed)  // Aborted by user
      {
        thd->send_kill_message();
        return true;
      }
      ...
      // 發送數據給客戶端
      if (query_result->send_data(thd, *fields)) {
        return true;
      }
      ...
    }
  }
  ...
}

這個方法是 select 語句的入口,屬于重量級方法,在源碼分析的第 1 篇文章《帶你讀 MySQL 源碼:limit, offset》中也介紹過,但是,本文示例 SQL 的執行計劃和之前不一樣,這里有必要再介紹下。

m_root_iterator->Read() 從存儲引擎讀取一條記錄,對于示例 SQL 來說,m_root_iterator 是 FilterIterator 迭代器對象,實際執行的方法是 FilterIterator::Read()。

FilterIterator::Read()

int FilterIterator::Read() {
  for (;;) {
    int err = m_source->Read();
    if (err != 0) return err;

    bool matched = m_condition->val_int();

    if (thd()->killed) {
      thd()->send_kill_message();
      return 1;
    }

    /* check for errors evaluating the condition */
    if (thd()->is_error()) return 1;

    if (!matched) {
      m_source->UnlockRow();
      continue;
    }

    // Successful row.
    return 0;
  }
}

上面是 FilterIterator::Read() 方法的全部代碼,代碼量比較少,主要邏輯如下:

m_source->Read() 方法從存儲引擎讀取一條記錄,因為示例 SQL 中 t1 表的訪問方式為全表掃描,所以 m_source 是 TableScanIterator 迭代器對象。

通過 explain 可以確認示例 SQL 中 t1 表的訪問方式為全表掃描(type = ALL):

explain select * from t1
where i2 > 20 and (i1 = 50 or i1 = 80)G

***************************[ 1. row ]***************************
id            | 1
select_type   | SIMPLE
table         | t1
partitions    | <null>
type          | ALL
possible_keys | <null>
key           | <null>
key_len       | <null>
ref           | <null>
rows          | 8
filtered      | 12.5
Extra         | Using where

m_source->Read() 從存儲引擎讀取一條記錄之后,m_condition->val_int() 會判斷這條記錄是否匹配 where 條件。

m_condition 代表 SQL 的 where 條件,對于示例 SQL 來說,它是 Item_cond_and 對象。

m_condition->val_int() 實際執行的方法是 Item_cond_and::val_int(),這就是判斷記錄是否匹配示例 SQL where 條件的入口。

compare_int_signed()

// sql/item_cmpfunc.cc
int Arg_comparator::compare_int_signed() {
  // 獲取 where 條件操作符左邊的值
  // 例如:i2 > 20
  // 獲取當前讀取記錄的 i2 字段值
  longlong val1 = (*left)->val_int();
  if (current_thd->is_error()) return 0;
  // where 條件操作符左邊的值不為 NULL
  // 才進入 if 分支
  if (!(*left)->null_value) {
    // 獲取 where 條件操作符右邊的值
    // 例如:i2 > 20
    // val2 的值就等于 20
    longlong val2 = (*right)->val_int();
    if (current_thd->is_error()) return 0;
    // where 條件操作符右邊的值不為 NULL
    // 才進入 if 分支
    if (!(*right)->null_value) {
      // 到這里,where 條件操作符左右兩邊的值都不為 NULL
      // 把 where 條件的 null_value 設置為 false
      if (set_null) owner->null_value = false;
      // 接下來 3 行代碼
      // 比較 where 條件操作符左右兩邊的值的大小
      if (val1 < val2) return -1;
      if (val1 == val2) return 0;
      return 1;
    }
  }
  // 如果執行到下面這行代碼
  // 說明 where 條件操作符左右兩邊的值
  // 至少有一個是 NULL
  // 把 where 條件的 null_value 設置為 true
  if (set_null) owner->null_value = true;
  return -1;
}

我們以 id = 2、3 的兩條記錄和示例 SQL 的 where 條件 i2 > 20 為例介紹 compare_int_signed() 的邏輯:

圖片

對于 where 條件 i2 > 20,longlong val1 = (*left)->val_int() 中的 *left 表示 i2 字段。

讀取 id = 2 的記錄:

i2 字段值為 NULL,if (!(*left)->null_value) 條件不成立,執行流程直接來到 if (set_null) owner->null_value = true,把 where 條件的 null_value 設置為 true,表示對于當前讀取的記錄,where 條件包含 NULL 值。

然后,return -1,compare_int_signed() 方法執行結束。

讀取 id = 3 記錄:

i2 字段值為 31(即 val1 = 31),if (!(*left)->null_value) 條件成立,執行流程進入該 if 分支。

對于 where 條件 i2 > 20,longlong val2 = (*right)->val_int() 中的 *right 表示大于號右邊的 20(即 val2 = 20),if (!(*right)->null_value) 條件成立,進入該 if 分支:

if (set_null) owner->null_value = false,把 where 條件的 null_value 設置為 false,表示對于當前讀取的記錄,where 條件不包含 NULL 值。

  • if (val1 < val2),val1 = 31 大于 val2 = 20,if 條件不成立。
  • if (val1 == val2),val1 = 31 大于 val2 20,if 條件不成立。
  • return 1,因為 val1 = 31 大于 val2 = 20,返回 1,表示當前讀取的記錄匹配 where 條件 i2 > 20。

Arg_comparator::compare()

// sql/item_cmpfunc.h
inline int compare() { return (this->*func)(); }

Arg_comparator::compare() 只有一行代碼,就是調用 *func 方法,比較兩個值的大小。

func 屬性保存了用于比較兩個值大小的方法的地址,在 Arg_comparator::set_cmp_func(...) 中賦值。

對于示例 SQL 來說,where 條件中的 i1、i2 字段類型都是 int,func 屬性保存的是用于比較兩個整數大小的 Arg_comparator::compare_int_signed() 方法的地址。(this->*func)() 調用的方法就是 Arg_comparator::compare_int_signed()。

Item_func_gt::val_int()

// sql/item_cmpfunc.cc
longlong Item_func_gt::val_int() {
  assert(fixed == 1);
  int value = cmp.compare();
  return value > 0 ? 1 : 0;
}

這里調用的 cmp.compare() 就是上一小節介紹的 Arg_comparator::compare() 方法。

對于示例 SQL 來說,Arg_comparator::compare() 會調用 Arg_comparator::compare_int_signed() 方法,返回值只有 3 種:

  • -1:表示 where 條件操作符左邊的值小于右邊的值。
  • 0:表示 where 條件操作符左邊的值等于右邊的值。
  • 1:表示 where 條件操作符左邊的值大于右邊的值。

我們以 id = 3 的記錄和示例 SQL 的 where 條件 i2 > 20 為例,介紹 Item_func_gt::val_int() 的邏輯:

圖片

i2 字段值為 31,對 where 條件 i2 > 20 調用 cmp.compare(),得到的返回值為 1(即 value = 1)。

value > 0 ? 1 : 0 表達式的值為 1,這就是 Item_func_ge::val_int() 的返回值,表示 id = 3 的記錄匹配 where 條件 i2 > 20。

Item_cond_and::val_int()

// sql/item_cmpfunc.cc
longlong Item_cond_and::val_int() {
  assert(fixed == 1);
  // and 連接的 N 個 where 條件都保存到 list 中
  // 根據 list 構造迭代器
  List_iterator_fast<Item> li(list);
  Item *item;
  null_value = false;
  // 迭代 where 條件
  while ((item = li++)) {
    if (!item->val_bool()) {
      if (ignore_unknown() || !(null_value = item->null_value))
        return 0;  // return false
    }
    if (current_thd->is_error()) return error_int();
  }
  return null_value ? 0 : 1;
}

Item_cond_and::val_int() 的邏輯:

  • 判斷當前讀取的記錄是否匹配 Item_cond_and 對象所代表的 and 連接的 N 個 where 條件(N >= 2)。
  • 如果對每個條件調用 item->val_bool() 的返回值都是 true,說明記錄匹配 and 連接的 N 個 where 條件。
  • 如果對某一個或多個條件調用 item->val_bool() 的返回值是 false,就說明記錄不匹配 and 連接的 N 個 where 條件。

由于 if (ignore_unknown() || !(null_value = item->null_value)) 中的 ignore_unknown() 用于控制 where 條件中包含 NULL 值時怎么處理,我們需要展開介紹 Item_cond_and::val_int() 的代碼。

想要深入了解 Item_cond_and::val_int() 代碼細節的讀者朋友,可以做個心理建設:內容有點長(但不會太長)。

首先,我們來看一下 null_value = false:

null_value 的初始值被設置為 false,表示 and 連接的 N 個 where 條件中,還沒出現哪個 where 條件包含 NULL 值的情況(畢竟還啥都沒干)。

null_value 比較重要,它有可能最終決定 Item_cond_and::val_int() 的返回值(后面會介紹)。

然后,再來看看 while 循環的邏輯,這塊內容會有一點點多:

while 循環迭代 and 連接的 N 個 where 條件。

每迭代一個 where 條件,都調用 item->val_bool() 方法,判斷當前讀取的記錄是否匹配該條件。

如果 val_bool() 的返回值是 true,說明記錄匹配該條件,進入下一輪循環,迭代下一個 where 條件(如果有的話)。

if (current_thd->is_error()),這行代碼表示執行過程中出現了錯誤,我們先忽略它。

如果 val_bool() 的返回值是 false,說明記錄不匹配該條件。

接下來是進入下一輪循環,還是執行 return 0 結束 Item_cond_and::val_int() 方法,就要由 if (ignore_unknown() || !(null_value = item->null_value)) 決定了。

展開介紹 if (ignore_unknown() || ...) 之前,先來看看 ignore_unknown() 的定義:

class Item_cond : public Item_bool_func {
  ...
  /// Treat UNKNOWN result like FALSE 
  /// because callers see no difference
  bool ignore_unknown() const { return abort_on_null; }
  ...
}

從代碼注釋可以看到,ignore_unknown() 用于決定是否把 UNKNOWN 當作 FALSE 處理。

那么,什么是 UNKNOWN?

在 MySQL 中,NULL 會被特殊對待。NULL 和任何值(包含 NULL 本身)通過關系操作符(=、>、<、...)比較,得到的結果都是 NULL,這個結果就被認為是 UNKNOWN。

如果想知道某個值是否為 NULL,只能使用 IS NULL、IS NOT NULL 進行判斷。

說完了 ignore_unknown(),我們回到 if (ignore_unknown() || !(null_value = item->null_value)),它包含兩個表達式:

  • ignore_unknown()
  • !(null_value = item->null_value))

如果 ignore_unknown() 的返回值為 true,if 條件成立,執行流程就會進入 if 分支,執行 return 0,Item_cond_and::val_int() 方法的執行流程就此結束,表示當前讀取的記錄不匹配 and 連接的 N 個 where 條件。

如果 ignore_unknown() 的返回值為 false,那么還需要再判斷 !(null_value = item->null_value)) 的值是 true 還是 false。

我們先分解一下 !(null_value = item->null_value)),其中包含 2 個步驟:

  • null_value = item->null_value
  • !null_value

如果 item->null_value 的值為 false,賦值給 null_value 之后,!null_value 的值為 true,if 條件成立,執行流程就會進入 if (ignore_unknown() || ...) 分支,執行 return 0,Item_cond_and::val_int() 方法的執行流程就此結束,表示當前讀取的記錄不匹配 and 連接的 N 個 where 條件。

item->null_value = false,表示對于當前讀取的記錄,where 條件不包含 NULL 值。

如果 item->null_value 的值為 true,賦值給 null_value 之后,!null_value 的值為 false,即 !(null_value = item->null_value)) 的值為 false,if 條件不成立,執行流程不會進入 if (ignore_unknown() || ...) 分支,也就不會執行 return 0 了,接下來就會進入下一輪循環,迭代下一個 where 條件(如果有的話)。

item->null_value = true,表示對于當前讀取的記錄,where 條件包含 NULL 值。

最后,再來看看 return null_value ? 0 : 1:

while 循環迭代完 and 連接的 N 個 where 條件之前,如果 Item_cond_and::val_int() 方法的執行流程都沒有被 while 代碼塊中包含的 return 0 提前結束,執行流程就會來到 return null_value ? 0 : 1。

有兩種場景會導致這種情況的出現:

場景 1:

while 循環迭代 and 連接的 N 個 where 條件的過程中,對每個條件調用 item->val_bool() 的返回值都是 true。

此時,null_value 屬性的值為 false,null_value ? 0 : 1 表達式的值為 1,說明當前讀取的記錄匹配 and 連接的 N 個 where 條件。

場景 2:

while 循環迭代 and 連接的 N 個 where 條件的過程中,某個條件同時滿足以下 4 個要求:

調用 item->val_bool() 的返回值是 false,說明當前讀取的記錄不匹配該條件。

ignore_unknown() 的返回值也是 false,表示包含 NULL 值的 where 條件的比較結果(UNKNOWN)不按 false 處理,而是要等到 while 循環結束之后,根據 null_value 屬性的值(true 或 false)算總帳。

這是由 Item_cond_and 對象控制的行為,而不是 and 連接的某個 where 條件控制的行為。

!(null_value = item->null_value)) 表達式的值為 false,說明該條件包含 NULL 值,那么它就是 ignore_unknown() = false 時需要等到 while 循環結束之后,根據 null_value 屬性的值算總帳的條件。

該條件之后的其它 where 條件,不會導致 while 循環被提前中止(這樣執行流程才能來到 return null_value ? 0 : 1)。

此時,null_value 屬性的值為 true,null_value ? 0 : 1 表達式的值為 0,說明當前讀取的記錄不匹配 and 連接的 N 個 where 條件。

Item_func_eq::val_int()

// sql/item_cmpfunc.cc
longlong Item_func_eq::val_int() {
  assert(fixed == 1);
  int value = cmp.compare();
  return value == 0 ? 1 : 0;
}

這里調用的 cmp.compare() 就是前面介紹的 Arg_comparator::compare() 方法。

對于示例 SQL 來說,Arg_comparator::compare() 調用的是 Arg_comparator::compare_int_signed() 方法,返回值只有 3 種:

  • -1:表示 where 條件操作符左邊的值小于右邊的值。
  • 0:表示 where 條件操作符左邊的值等于右邊的值。
  • 1:表示 where 條件操作符左邊的值大于右邊的值。

我們以 id = 5 的記錄和示例 SQL 的 where 條件 i1 = 50 為例,介紹 Item_func_eq::val_int() 的邏輯:

圖片

i1 字段值為 50,對 where 條件 i1 = 50 調用 cmp.compare(),得到的返回值為 0(即 value = 0)。

value == 0 ? 1 : 0 表達式的值為 1,這就是 Item_func_eq::val_int() 的返回值,表示 id = 5 的記錄匹配 where 條件 i1 = 50。

Item_cond_or::val_int()

// sql/item_cmpfunc.cc
longlong Item_cond_or::val_int() {
  assert(fixed == 1);
  List_iterator_fast<Item> li(list);
  Item *item;
  null_value = false;
  while ((item = li++)) {
    if (item->val_bool()) {
      null_value = false;
      return 1;
    }
    if (item->null_value) null_value = true;
    ...
  }
  return 0;
}

我們以 id = 8 的記錄和示例 SQL 的 where 條件 i1 = 50 or i1 = 80 為例,介紹 Item_cond_or::val_int() 的邏輯:

圖片

Item_cond_or 對象的 list 屬性包含 2 個條件:i1 = 50、i1 = 80,List_iterator_fastli(list) 根據 list 構造一個迭代器。

對于 id = 8 的記錄,i1 字段值為 80,while 循環每次迭代一個 where 條件:

第 1 次迭代,對 where 條件 i1 = 50 調用 item->val_bool(),返回值為 false,不進入 if (item->val_bool()) 分支。

if (item->null_value) 條件不成立,不執行 null_value = true。

第 2 次迭代,對 where 條件 i1 = 80 調用 item->val_bool(),返回值為 true,進入 if (item->val_bool()) 分支。

設置 Item_cond_or 對象的 null_value 屬性值為 false,表示 Item_cond_or 所代表的 or 連接的 where 條件(i1 = 50、i1 = 80)都不包含 NULL 值。

return 1,這就是 Item_cond_or::val_int() 的返回值,表示 id = 8 的記錄匹配 where 條件 i1 = 50 or i1 = 80。

總結

本文介紹了 SQL 的 where 條件中包含 and、or 的實現邏輯:

從存儲引擎讀取一條記錄之后,對 and 連接的 N 個 where 條件(N >= 2)調用 item->val_bool() 的返回值必須全部等于 true,記錄才匹配 and 連接的 N 個 where 條件。

Item_cond_and::val_int() 的代碼不多,但是這個方法中調用了 ignore_known() 用于控制怎么處理 where 條件包含 NULL 值的場景,代碼細節并不太好理解,所以花了比較長的篇幅介紹 Item_cond_and::val_int() 方法的邏輯,需要多花點時間去理解其中的邏輯。

從存儲引擎讀取一條記錄之后,對 or 連接的 N 個 where 條件(N >= 2)調用 item->val_bool(),只要其中一個返回值等于 true,記錄就匹配 or 連接的 N 個 where 條件。

本文轉載自微信公眾號「一樹一溪」

分享到:
標簽:MySQL
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定