全部產品
Search
文件中心

PolarDB:偏高並發情境的實踐和最佳化

更新時間:Mar 03, 2026

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

點查是應用訪問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後,由於索引表中包含了主表上的所有列,不再需要回表,執行計畫變為索引表上的點查。

以上樣本闡述了通過索引最佳化點查效能的一般過程,其中的關鍵點是根據查詢特徵找到適合添加索引的列。