本文介紹了在應(yīng)用內(nèi)通過代碼高效抽取數(shù)據(jù)的方法。

簡(jiǎn)介

數(shù)據(jù)抽取是指通過代碼或者數(shù)據(jù)導(dǎo)出工具,從PolarDB-X中批量讀取數(shù)據(jù)的操作。主要包括以下場(chǎng)景:
  • 通過數(shù)據(jù)導(dǎo)出工具將數(shù)據(jù)全量抽取到下游系統(tǒng)。PolarDB-X支持多種數(shù)據(jù)導(dǎo)出工具,更多內(nèi)容請(qǐng)參考數(shù)據(jù)導(dǎo)入導(dǎo)出
  • 在應(yīng)用內(nèi)處理數(shù)據(jù)或者批量的將查詢結(jié)果返回給用戶瀏覽時(shí),不能依賴外部工具,必須在應(yīng)用內(nèi)通過代碼完成數(shù)據(jù)全量抽取。

本文主要介紹在應(yīng)用內(nèi)通過代碼高效抽取數(shù)據(jù)的方法,根據(jù)是否一次性讀取全量數(shù)據(jù),分為全量抽取和分頁查詢。

全量抽取場(chǎng)景

全量抽取使用的SQL通常不包含表的拆分鍵,以全表掃描的方式執(zhí)行,隨著讀取數(shù)據(jù)量的增加,數(shù)據(jù)抽取操作的執(zhí)行時(shí)間線性增長(zhǎng)。為了避免占用過多網(wǎng)絡(luò)或連接資源,可以使用HINT直接下發(fā)查詢語句,從物理分片中拉取數(shù)據(jù)。以下示例采用Java代碼編寫,完整使用方法參考如何使用HINT

public static void extractData(Connection connection, String logicalTableName, Consumer<ResultSet> consumer)
    throws SQLException {

    final String topology = "show topology from {0}";
    final String query = "/*+TDDL:NODE({0})*/select * from {1}";

    try (final Statement statement = connection.createStatement()) {
        final Map<String, List<String>> partitionTableMap = new LinkedHashMap<>();
        // Get partition id and physical table name of given logical table
        try (final ResultSet rs = statement.executeQuery(MessageFormat.format(topology, logicalTableName))) {
            while (rs.next()) {
                partitionTableMap.computeIfAbsent(rs.getString(2), (k) -> new ArrayList<>()).add(rs.getString(3));
            }
        }
        // Serially extract data from each partition
        for (Map.Entry<String, List<String>> entry : partitionTableMap.entrySet()) {
            for (String tableName : entry.getValue()) {
                try (final ResultSet rs = statement
                    .executeQuery(MessageFormat.format(query, entry.getKey(), tableName))) {
                    // Consume data
                    consumer.accept(rs);
                }
            }
        }
    }
}

分頁查詢場(chǎng)景

向用戶展示列表信息時(shí),需要分頁來提高頁面的加載效率,避免返回過多冗余信息,用于處理分頁顯示需求的查詢,稱為分頁查詢。關(guān)系型數(shù)據(jù)庫沒有直接提供分段返回表中數(shù)據(jù)的能力,高效的實(shí)現(xiàn)分頁查詢,還需要結(jié)合數(shù)據(jù)庫本身的特點(diǎn)來設(shè)計(jì)查詢語句。

以MySQL為例,分頁查詢最直觀的實(shí)現(xiàn)方法,是使用limit offset,pageSize來實(shí)現(xiàn),例如如下查詢:

select * from t_order where user_id = xxx order by gmt_create, id limit offset, pageSize

因?yàn)間mt_create可能重復(fù),所以order by時(shí)應(yīng)加上id,保證結(jié)果順序的確定性。

說明 該方案在表規(guī)模較小的時(shí)候,能夠正常運(yùn)行。當(dāng)t_order表增長(zhǎng)到十萬級(jí),隨著頁數(shù)增加,執(zhí)行速度明顯變慢,可能降到幾十毫秒的量級(jí),如果數(shù)據(jù)量增長(zhǎng)到百萬級(jí),則耗時(shí)達(dá)到秒級(jí),數(shù)據(jù)量繼續(xù)增長(zhǎng),耗時(shí)最終會(huì)變得不可接受。
問題分析

假設(shè)我們?cè)趗ser_id,gmt_create上創(chuàng)建了局部索引,由于只有user_id上的條件,每次需要掃描的總數(shù)據(jù)量為offset + pageSize ,隨著offset的增大逐漸接近全表掃描,導(dǎo)致耗時(shí)增加。并且在分布式數(shù)據(jù)庫中,全表排序的吞吐無法通過增加DN數(shù)量來提高。

改進(jìn)方案1

每次獲取下一頁記錄時(shí),指定從上次結(jié)束的位置繼續(xù)往后取,這樣不需要設(shè)置offset ,能夠避免出現(xiàn)全表掃描的情況。如下為一個(gè)按ID進(jìn)行分頁查詢的例子:

select * from t_order where id > lastMaxId order by id limit pageSize

第一次查詢不指定條件,后續(xù)查詢則傳入前一次查詢的最大id,在執(zhí)行時(shí),數(shù)據(jù)庫首先在索引上定位到lastMaxId的位置,然后連續(xù)返回pageSize條記錄即可,非常高效。

說明 當(dāng)ID為主鍵或者唯一鍵時(shí),改進(jìn)方案1可以達(dá)到分頁查詢的效果,也有不錯(cuò)的性能。但缺點(diǎn)也比較明顯,當(dāng)ID上有重復(fù)值時(shí),可能會(huì)漏掉部分記錄。
改進(jìn)方案2

MySQL支持通過 Row Constructor Expression實(shí)現(xiàn)多列比較的語義(PolarDB-X同樣支持)。

(c2,c3) > (1,1) 
等價(jià)于 
c2 > 1 OR ((c2 = 1) AND (c3 > 1))

因此,可以用下面的方法實(shí)現(xiàn)分頁查詢:

select * from t_order 
where user_id = xxx and (gmt_create, id) > (lastMaxGmtCreate, lastMaxId)
order by user_id, gmt_create, id limit pageSize

第一次查詢不指定條件,后續(xù)查詢則傳入前一次查詢的最大gmt_create和id,通過Row Constructor Expression正確處理gmt_create存在重復(fù)的情況。

說明 示例中,為了提高查詢性能,在user_id和gmt_create上建立了聯(lián)合索引,并在order by中加入user_id提示優(yōu)化器可以通過索引來消除排序。由于Row Constructor Expression包含null值會(huì)導(dǎo)致表達(dá)式求值結(jié)果為null,當(dāng)存在null值時(shí)需要使用OR表達(dá)式。PolarDB-X目前只在Row Constructor Expression僅包含拆分鍵時(shí)才將其用于分區(qū)裁剪,其他場(chǎng)景同樣需要使用OR表達(dá)式。

結(jié)合上述分析,給出一個(gè)PolarDB-X上分頁查詢的最佳實(shí)踐:

-- lastMaxGmtCreate is not null 
select * from t_order 
where user_id = xxx 
and (
      (gmt_create > lastMaxGmtCreate) 
      or ((gmt_create = lastMaxGmtCreate) and (id > lastMaxId))
    )
order by user_id, gmt_create, id limit pageSize

-- lastMaxGmtCreate is null
select * from t_order 
where user_id = xxx 
and (
      (gmt_create is not null)
      or (gmt_create is null and id > lastMaxId)
    )
order by user_id, gmt_create, id limit pageSize