本文介紹了如何判斷查詢語句是否為點查,以及如何將查詢優化為點查

點查是應用訪問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。

如何識別點查

如上所述,分布式數據庫中的點查,是指掃描少量分片和數據的查詢。通過查看執行計劃,可以確認一個查詢語句掃描的分片數,更多執行計劃介紹請參見調優原理和執行計劃。以下為一個點查的示例:
  1. 執行以下語句,查看執行計劃:
    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` = ?)") |
    +------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  2. 執行以下語句,匯總展示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 10select * from t1 order by c1 limit 10

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

  1. 創建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;
  2. 查看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` = ?)") |
    +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  3. customer表上只有主鍵索引,因此雖然c_phone指定了等值條件,依然需要掃描全部分片,此處可以通過添加GSI來優化。
    create global index g_i_phone on customer(c_phone) dbpartition by hash(c_phone);
  4. 添加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階段無法確定)。

  5. 使用聚簇索引代替GSI。
    drop index g_i_phone on customer;
    create clustered index g_i_phone on customer(c_phone) dbpartition by hash(c_phone);
  6. 創建聚簇索引后,查看執行計劃。
    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,詳情請參考智能索引推薦