本文介紹了如何判斷查詢語句是否為點查,以及如何將查詢最佳化為點查。
點查是應用訪問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後,由於索引表中包含了主表上的所有列,不再需要回表,執行計畫變為索引表上的點查。
以上樣本闡述了通過索引最佳化點查效能的一般過程,其中的關鍵點是根據查詢特徵找到適合添加索引的列。