如何优雅地遍历大表

233 阅读2分钟

场景

工作中我们经常遇到遍历表的场景,查询表数据,处理表数据,当表的数据量多的时候,如何保证性能呢?

我们假设表结构如下:

-- 源表结构示例
CREATE TABLE source_table (
  id INT AUTO_INCREMENT PRIMARY KEY, 
  data VARCHAR(255),
  create_date DATE,
  processed TINYINT DEFAULT 0  -- 可选:标记是否已处理
);

-- 确保排序字段有索引
CREATE INDEX idx_id ON source_table(id);

使用传统的游标式分页,LIMIT offset, size,在大数据量下,效率极低,每次都需要扫描前 N 行。

直接全表查询更不可行,大概率查询超时,或者直接把内存打爆。

解决方案

确定一个递增的字段,就可以使用游标式分页查询的方式,伪代码如下:

-- 每次循环执行以下查询(应用程序中通过变量传递last_id)
SELECT id, data 
FROM source_table 
WHERE id > @last_id 
ORDER BY id 
LIMIT 1000;

这里不一定要是主键 id,只要能保证字段单调递增就行。如果数据允许的话,可以创建覆盖索引(id, data),避免回表查询。

如果想进一步提升性能,可以使用多线程并发处理。这里有个注意点,并发处理的时候,不是并发查 last_id,而是在内存中并发处理数据。流程如下:

相比串行处理,并发的优势在于虽然多了一次回表查询,但是极大提升了处理效率,因为大部分时候,数据处理才是最耗时的。伪代码如下:

List<CompletableFuture<Integer>> futures = Lists.newArrayList();
Long lastId = -1L;
do {
    futures.add(batchJob(maxTaskFlowId, PAGE_SIZE);
    lastId = queryLastId(lastId, PAGE_SIZE);
} while (lastId != null);

public CompletableFuture<Integer> batchJob(Long lastId, int pageSize) {
        return CompletableFuture.supplyAsync(() -> {
            List<Data> datas = queryData(lastId, pageSize);
            //deal data
        }, executorService);
}

有个风险点是因为同步查完 last_id,就丢到线程池阻塞队列中去了,在极端情况下,可能会导致阻塞队列很长,但是数据上我们只是存了 id 值,并没有存具体的数据,所以不会占用太多内存。如果确实有这方面的担忧,那就设置下阻塞队列为数组,更新下线程池拒绝策略。

总结

这里是基于单机的做法,如果系统支持分布式调度的话,那方案就不一样了。我们可以将每个分片作为一个单独的任务,根据调度策略分发到其他机器上执行。在确定技术方案的时候没有什么最优的方案,充分利用当前系统能力,能解决问题、不留坑的就是好方案。