本文介紹了如何判斷查詢語句是否為點查,以及如何將查詢優化為點查。
點查是應用訪問OLTP數據庫的一種常見方式,特點是返回結果前只掃描表中的少量數據,在淘寶上查看訂單或者商品信息對應到數據庫上的操作就是點查。PolarDB-X對點查的響應時間(Response Time, RT)和資源占用做了較多優化,能夠支持較高的吞吐,適合高并發讀取場景使用。
什么是點查
顧名思義,點查是只掃描少量數據的查詢。注意這里說的是掃描少量數據而不是返回少量數據,比如select * from t1 order by c1 limit 1
雖然只返回了一條數據,但如果c1上沒有索引,需要先掃描t1上所有數據排序后才能返回結果,不符合點查的定義。
單機數據庫中,最常見的點查是按照主鍵(Primary Key,PK)查詢數據,通過掃描主鍵索引快速得到結果,平均只需要掃描log(n)(n為整張表的行數)條記錄。如果通過其他條件查詢,可以增加局部二級索引(Local Secondary Index,LSI),首先掃描局部二級索引得到主鍵,然后回表查出完整記錄。特殊場景下,如果局部二級索引中包含了查詢涉及的所有列,則回表的步驟也可以省略。
PolarDB-X是一個分布式數據庫,為了將數據分散到不同數據節點(Data Node,DN)上,引入了分區表的概念,預先將數據切分成多個分區,然后建立分區和DN的映射,其中切分數據需要選取一個或多個列作為切分維度,這些列因此被稱為分區鍵。分布式數據庫中,查詢性能除了與掃描的數據量線性相關,還與掃描的分片數量正相關,因此點查的定義還需要加上掃描少量分區。
PolarDB-X具備透明分布式能力,默認使用主鍵作為分區鍵,按照主鍵查詢時首先定位到數據所在的分區,然后通過分區上的主鍵索引得到結果,性能最高。如果通過其他條件查詢,可以增加全局二級索引(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` = ?)") | +------------------------------------------------------------------------------------------------------------------------------------------------------------------+
- 執行以下語句,匯總展示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查詢等。能夠優化為點查的語句可以概括為以下兩類:
- 固定范圍掃描的查詢:條件中包含等值條件(或可以簡化為等值條件),小范圍BETWEEN AND條件 ,參數數量固定的IN條件的查詢;
- 結果行數固定的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
。
對于這兩類查詢,優化的思路是添加合適的索引,將全表掃描轉化為索引掃描,示例如下:
- 創建customer表。
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;
- 查看
select * from customer where c_phone = "11";
語句的執行計劃。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);
- 添加GSI后,查看執行計劃。
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階段無法確定)。
- 使用聚簇索引代替GSI。
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,詳情請參考智能索引推薦。