天天看點

最佳實踐—偏高并發場景的實踐和優化

什麼是點查

顧名思義,“點查”是指隻掃描少量資料的查詢。注意這裡說的是“掃描少量資料”而不是“傳回少量資料”,比如

select * from t1 order by c1 limit 1

雖然隻傳回了一條資料,但如果c1上沒有索引,需要先掃描t1上所有資料排序後才能傳回結果,不符合“點查”的定義。

單機資料庫中,最常見的點查是按照主鍵(Primary Key, PK)查詢資料,通過掃描主鍵索引快速得到結果,平均隻需要掃描logn條記錄。如果通過其他條件查詢,可以增加局部二級索引(Local Secondary Index,LSI),首先掃描局部二級索引得到主鍵,然後回表查出完整記錄。特殊場景下,如果局部二級索引中包含了查詢涉及的所有列,則回表的步驟也可以省略。

PolarDB-X是一個分布式資料庫,為了将資料分散到不同資料節點(Data Node,DN)上,引入了分區表的概念,預先将資料切分成多個分區,然後建立分區和DN的映射,其中切分資料需要選取一個或多個列作為切分次元,這些列是以被稱為“分區鍵”。分布式資料庫中,查詢性能除了與掃描的資料量線性相關,還與掃描的分片數量正相關,是以“點查”的定義還需要加上“掃描少量分區”。

PolarDB-X具備透明分布式能力,預設使用主鍵作為分區鍵,按照PK查詢時首先定位到資料所在的分區,然後通過分區上的主鍵索引得到結果,性能最高。如果通過其他條件查詢,可以增加

全局二級索引

(Global Secondary Index,GSI)。使用GSI優化查詢的原理與LSI相同,首先查到主鍵然後回表獲得完整記錄,主要差別在于GSI本身也是一張分區表,資料與主表儲存在不同DN上,回表操作大機率需要經過網絡,回表代價高于單機資料庫。是以,PolarDB-X支援建立聚簇索引來消除回表,達到與主鍵查詢相同的性能。

注意事項

LSI和GSI本質上是以額外存儲空間和寫入開銷為代價,換取查詢性能的方案,使用時需要謹慎評估對寫入性能的影響。索引表與主表的資料分布不同,為了保證GSI的資料與主表強一緻,所有涉及GSI的寫入操作都預設被包裝在分布式事務中。相比沒有GSI的場景,寫入RT會增加2~3倍,同時由于索引表和主表混合并行寫入,高并發寫入場景下産生分布式死鎖的機率會增加。綜上所述,建議每張邏輯表上建立不超過3個GSI。

如何識别點查

如上所述,分布式資料庫中的點查,是指掃描少量分片和資料的查詢。通過檢視執行計劃,可以确認一個查詢語句掃描的分片數,更多執行計劃介紹請參見

執行計劃介紹

。以下為一個點查的示例:

> explain select c_custkey, c_name, c_address from customer where c_custkey = 42;
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| LOGICAL EXECUTIONPLAN                                                                                                                                            |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| LogicalView(tables="TEST1_000002_GROUP.customer_IVgG_10", sql="SELECT `c_custkey`, `c_name`, `c_address` FROM `customer` AS `customer` WHERE (`c_custkey` = ?)") |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+      

EXPLAIN EXECUTE用于彙總展示DN上的執行計劃,由此可以判斷查詢在DN上是否命中正确的索引。DN節點基于MySQL實作,執行計劃與MySQL相同,更多介紹參考

MySQL 官方文檔

。以下展示一個點查的示例:

> explain execute select c_custkey, c_name, c_address from customer where c_custkey = 42;
+----+-------------+----------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+
| id | select_type | table    | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra           |
+----+-------------+----------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+
| 1  | SIMPLE      | customer | NULL       | const | PRIMARY       | PRIMARY | 4       | const | 1    | 100      | Using pk access |
+----+-------------+----------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+      

如何将查詢優化為點查

不是所有查詢語句都可以優化為點查,例如沒有任何條件的資料抽取查詢

select * from t1

,不合理的分頁查詢

select * from t1 where c1 = 1 limit 100000, 10

,參數數量随着業務增長而增長的IN查詢等。能夠優化為點查的語句可以概括為以下兩類:

  1. 固定範圍掃描的查詢:條件中包含等值條件(或可以簡化為等值條件),小範圍BETWEEN AND條件 ,參數數量固定的IN條件的查詢;
  2. 結果行數固定的TopN查詢:例如

    select * from t1 where c1 > 42 limit 10 和 select * from t1 order by c1 limit 10

    select * from t1 order by c1 limit 10

對于這兩類查詢,優化的思路是添加合适的索引,将全表掃描轉化為索引掃描,示例如下:

CREATE TABLE `customer` (
  `c_custkey` int(11) NOT NULL,
  `c_name` varchar(25) NOT NULL,
  `c_address` varchar(40) NOT NULL,
  `c_nationkey` int(11) NOT NULL,
  `c_phone` varchar(15) NOT NULL,
  `c_acctbal` decimal(15,2) NOT NULL,
  `c_mktsegment` varchar(10) NOT NULL,
  `c_comment` varchar(117) NOT NULL,
  PRIMARY KEY (`c_custkey`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 dbpartition by hash(`c_custkey`) tbpartition by hash(`c_custkey`) tbpartitions 4;
> explain select * from customer where c_phone = "11";
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| LOGICAL EXECUTIONPLAN                                                                                                                                                                                                                            |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Gather(concurrent=true)                                                                                                                                                                                                                          |
|   LogicalView(tables="[000000-000003].customer_[00-15]", shardCount=16, sql="SELECT `c_custkey`, `c_name`, `c_address`, `c_nationkey`, `c_phone`, `c_acctbal`, `c_mktsegment`, `c_comment` FROM `customer` AS `customer` WHERE (`c_phone` = ?)") |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+      

customer表上隻有主鍵索引,是以雖然c_phone指定了等值條件,依然需要掃描全部分片,可以通過添加GSI來優化。

> create global index g_i_phone on customer(c_phone) dbpartition by hash(c_phone);
> explain select * from customer where c_phone = "11";
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| LOGICAL EXECUTIONPLAN                                                                                                                                                                                                                                                  |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Project(c_custkey="c_custkey", c_name="c_name", c_address="c_address", c_nationkey="c_nationkey", c_phone="c_phone", c_acctbal="c_acctbal", c_mktsegment="c_mktsegment", c_comment="c_comment")                                                                        |
|   BKAJoin(condition="c_custkey = c_custkey", type="inner")                                                                                                                                                                                                             |
|     IndexScan(tables="TEST1_000000_GROUP.g_i_phone_2CSp", sql="SELECT `c_custkey`, `c_phone` FROM `g_i_phone` AS `g_i_phone` WHERE (`c_phone` = ?)")                                                                                                                   |
|     Gather(concurrent=true)                                                                                                                                                                                                                                            |
|       LogicalView(tables="[000000-000003].customer_[00-15]", shardCount=16, sql="SELECT `c_custkey`, `c_name`, `c_address`, `c_nationkey`, `c_acctbal`, `c_mktsegment`, `c_comment` FROM `customer` AS `customer` WHERE ((`c_phone` = ?) AND (`c_custkey` IN (...)))") |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+      

添加GSI後,查詢變為索引表上的點查加回表,回表操作隻通路一個分片(執行計劃中回表顯示為主表上的全表掃描,這是因為确定需要掃描的主表分片依賴索引表的查詢結果,explain階段無法确定)。

> drop index g_i_phone on customer;
> create clustered index g_i_phone on customer(c_phone) dbpartition by hash(c_phone);
> explain select * from customer where c_phone = "11";
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| LOGICAL EXECUTIONPLAN                                                                                                                                                                                                            |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| IndexScan(tables="TEST1_000000_GROUP.g_i_phone_fHmZ", sql="SELECT `c_custkey`, `c_name`, `c_address`, `c_nationkey`, `c_phone`, `c_acctbal`, `c_mktsegment`, `c_comment` FROM `g_i_phone` AS `g_i_phone` WHERE (`c_phone` = ?)") |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+      

使用聚簇索引代替GSI後,由于索引表中包含了主表上的所有列,不再需要回表,執行計劃變為索引表上的點查。

以上示例闡述了通過索引優化點查性能的一般過程,其中的關鍵點是根據查詢特征找到适合添加索引的列。對于包含多個條件比較複雜的查詢,可以通過PolarDB-X内置的索引推薦功能來找到合适的LSI和GSI,詳情請參考

智能索引推薦