一、openGauss大表优化的核心痛点

做过数据库开发的人都知道,当一张表的数据量超过千万甚至上亿级别时,会出现各种让人头疼的问题:查询慢到要等几分钟,更新数据时锁表卡业务,备份恢复要花大半天,甚至连统计信息更新都能拖垮整个库。openGauss作为一款主流的国产关系型数据库,虽然性能本身不错,但面对超大表时也不能“裸奔”,必须针对性优化。这一章我们就先搞懂大表优化的核心目标:不是让单条数据查询快上天,而是让整个系统的稳定性、可维护性、整体性能都达标。

二、分区表设计:大表拆分的核心方案

很多人第一次接触大表时,第一个想法就是“把表拆成小表”,但直接手动拆成多张独立表会带来很多麻烦:比如查询时要拼多张表的结果,业务代码要改,数据一致性难保证。而分区表就是数据库帮你自动完成“拆表”的工具,你只需要提前定义好拆分规则,数据库会把数据按规则分到不同的分区里,对外还是一张完整的表,业务代码几乎不用改。

2.1 分区表的核心类型

openGauss支持几种常用的分区类型,我们用最常见的“订单表”来举例,比如一张存所有用户订单的表,数据量已经到了1亿条,每天新增10万条,我们可以根据不同的需求选不同的分区方式:

2.1.1 范围分区:按时间拆分最常用

范围分区是最适合按时间维度拆分的场景,比如按订单的创建时间,把每个月的订单分到一个分区里。比如2023年1月的订单存到p_202301分区,2023年2月的存到p_202302,以此类推。这种方式的好处是,当你查询最近一个月的订单时,数据库只会扫p_202405这个分区,不用扫全表,速度会快很多。

我们来写一个完整的创建范围分区表的示例,技术栈统一用openGauss:

-- 技术栈:openGauss 5.0
-- 创建范围分区表:订单表
CREATE TABLE t_order (
    order_id BIGINT PRIMARY KEY, -- 订单ID,主键
    user_id BIGINT, -- 用户ID
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间,默认当前时间
    amount NUMERIC(10,2), -- 订单金额
    status CHAR(1) -- 订单状态:1=已支付,2=已发货,3=已完成
) PARTITION BY RANGE (create_time) ( -- 按创建时间做范围分区
    -- 第一个分区:2023年全年的数据
    PARTITION p_2023 VALUES LESS THAN ('2024-01-01 00:00:00'),
    -- 第二个分区:2024年1月的数据
    PARTITION p_202401 VALUES LESS THAN ('2024-02-01 00:00:00'),
    -- 第三个分区:2024年2月的数据
    PARTITION p_202402 VALUES LESS THAN ('2024-03-01 00:00:00'),
    -- 第四个分区:2024年3月的数据
    PARTITION p_202403 VALUES LESS THAN ('2024-04-01 00:00:00'),
    -- 第五个分区:2024年4月的数据
    PARTITION p_202404 VALUES LESS THAN ('2024-05-01 00:00:00'),
    -- 第六个分区:2024年5月及以后的数据(默认分区)
    PARTITION p_202405 VALUES LESS THAN (MAXVALUE)
);

创建完之后,我们可以给p_202405这个分区加一个新的子分区,比如2024年6月的,这样数据会自动分到新分区里:

-- 技术栈:openGauss 5.0
-- 给p_202405分区添加2024年6月的子分区
ALTER TABLE t_order SPLIT PARTITION p_202405 AT ('2024-06-01 00:00:00') INTO (
    PARTITION p_202406, -- 拆分出2024年6月的分区
    PARTITION p_202405 -- 剩下的还是p_202405(2024年6月及以后)
);

2.1.2 列表分区:按枚举值拆分

如果你的表是按状态、地区这种固定值拆分的,比如订单的状态只有几种,或者用户的地区是固定的几个,就可以用列表分区。比如把所有已完成的订单存到一个分区,已取消的存到另一个分区,这样查询已完成的订单时只会扫对应的分区。

2.1.3 哈希分区:均匀拆分

如果你的表没有明显的时间或枚举维度,比如用户表,你想把数据均匀分到几个分区里,就可以用哈希分区。openGauss会根据你指定的字段(比如user_id)做哈希计算,把数据均匀分配到各个分区,避免某个分区数据太多。

2.2 分区表的优缺点

优点很明显:查询时只扫需要的分区,速度快;备份时可以只备份某个分区,不用备份全表;删除旧数据时直接删分区,比delete快很多,还不会产生垃圾数据。但也有缺点:分区数不能太多,openGauss默认一个表最多支持1024个分区,太多了会影响数据库的元数据管理性能;分区键必须选对,如果选的字段不对,反而会导致查询时扫很多分区,效果适得其反;有些复杂的SQL(比如跨分区的聚合)性能可能不如小表。

2.3 分区表设计的注意事项

第一,分区键一定要选常用的查询条件,比如你经常按创建时间查订单,就选create_time做分区键,如果你经常按user_id查,就选user_id。第二,分区大小要合适,不要太小也不要太大,一般每个分区的大小控制在1G到10G之间比较好,太小了分区数太多,太大了和没分区差不多。第三,要提前规划分区的生命周期,比如历史数据超过3年的可以归档,所以分区要按时间划分,方便后续归档。

三、数据归档:历史数据的合理管理

大表优化不能只盯着当前的数据,历史数据也是个大问题。比如一张订单表,2020年的订单现在几乎不会被查询,但还是占着存储空间,影响备份和查询速度。数据归档就是把这些不常用的历史数据从主表移到归档表,既不影响业务查询,又能释放主表的空间。

3.1 归档的核心思路

归档的核心是“冷热分离”:把数据分成热数据(经常查询的,比如最近3个月的订单)和冷数据(不经常查询的,比如超过3年的订单),热数据放在性能好的存储(比如SSD),冷数据放在便宜的存储(比如机械硬盘),甚至可以存到对象存储里。

我们还是用订单表来举例,假设我们要把2021年的订单归档到归档表t_order_archive,步骤是这样的: 第一步,创建归档表,结构和主表完全一样:

-- 技术栈:openGauss 5.0
-- 创建归档表,结构和主表t_order完全一致
CREATE TABLE t_order_archive (
    order_id BIGINT PRIMARY KEY,
    user_id BIGINT,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    amount NUMERIC(10,2),
    status CHAR(1)
);

第二步,把主表中2021年的订单移到归档表,因为主表是分区表,2021年的订单都在p_2021这个分区里,所以可以直接把分区交换到归档表,这种方式比insert再delete快很多,因为分区交换是元数据操作,几乎不涉及数据移动:

-- 技术栈:openGauss 5.0
-- 把主表的p_2021分区交换到归档表
ALTER TABLE t_order EXCHANGE PARTITION p_2021 WITH TABLE t_order_archive;

第三步,给归档表做备份,比如备份到对象存储,然后可以把归档表放到便宜的存储上,甚至如果很久都不会查,可以压缩存储:

-- 技术栈:openGauss 5.0
-- 给归档表做压缩,节省空间
ALTER TABLE t_order_archive COMPRESS;

3.2 归档的优缺点

优点:主表的数据量变小,查询速度变快;存储成本降低,冷数据用便宜的存储;备份和恢复的时间变短,因为主表数据少了。缺点:归档后如果要查历史数据,需要切换到归档表,业务代码可能要改;归档过程如果操作不当,可能会导致数据不一致;如果归档的策略不合理,可能会导致经常要查的历史数据被归档,反而影响业务。

3.3 归档的注意事项

第一,归档前一定要做备份,防止数据丢失。第二,归档的时间窗口要选在业务低峰期,比如凌晨,避免影响业务。第三,要制定归档的触发规则,比如每月初自动归档上个月的历史数据,不用手动操作。第四,归档表要保留足够的索引,方便偶尔查询历史数据时速度不会太慢。

四、大表查询性能提升:除了分区和归档,还要做这些

分区表和归档是大表优化的基础,但有时候还是会遇到查询慢的问题,这时候还要做一些针对性的优化。

4.1 合理创建索引

很多人以为索引越多越好,其实不然,索引会占用存储空间,而且更新数据时(比如insert、update、delete)会同时更新索引,导致更新速度变慢。所以索引要按需创建:经常作为查询条件的字段才建索引,比如订单表的order_id、user_id、create_time,这些字段经常用来查,所以可以建索引;而订单的备注字段几乎不会用来查,就不用建索引。

另外,openGauss支持分区索引,就是给每个分区单独建索引,查询时只会用到对应分区的索引,比全局索引性能更好。比如给t_order表的每个分区的create_time字段建分区索引:

-- 技术栈:openGauss 5.0
-- 创建分区索引,每个分区单独建索引
CREATE INDEX idx_order_create_time ON t_order (create_time) LOCAL;

4.2 优化SQL语句

很多时候查询慢不是表的问题,是SQL写得不好。比如查询订单表时,不要用select ,只查需要的字段;不要用select count() from t_order where create_time < '2023-01-01',如果是分区表,直接查p_2023这个分区的count会更快;尽量避免跨分区的复杂查询,比如跨10个分区做聚合,性能会比较差。

比如一个不好的SQL:

-- 技术栈:openGauss 5.0
-- 不好的SQL:查2023年所有已完成订单的总金额,扫全表
SELECT SUM(amount) FROM t_order WHERE create_time BETWEEN '2023-01-01' AND '2024-01-01' AND status = '3';

优化后的SQL,因为2023年的订单都在p_2023分区里,直接指定分区查询:

-- 技术栈:openGauss 5.0
-- 优化后的SQL:只扫p_2023分区,速度快很多
SELECT SUM(amount) FROM t_order PARTITION (p_2023) WHERE status = '3';

4.3 统计信息更新

openGauss的查询优化器会根据表的统计信息(比如数据量、字段的分布情况)来选择最优的执行计划,如果统计信息过时,优化器可能会选错执行计划,导致查询变慢。所以要定期更新大表的统计信息,比如每天凌晨更新一次:

-- 技术栈:openGauss 5.0
-- 更新t_order表的统计信息
ANALYZE t_order;

五、应用场景分析

大表优化的方案不是万能的,适合的场景才是最好的。分区表适合数据量超过千万级、按时间或固定维度查询频繁的表,比如订单表、日志表、交易记录表;数据归档适合历史数据占比超过30%、历史数据查询频率低的场景,比如电商的历史订单、运营商的历史通话记录;大表查询性能提升适合所有大表场景,是基础优化手段。

六、文章总结

openGauss大表优化是一个系统性的工作,不能只靠某一种方案。首先用分区表把大表拆成小的逻辑分区,让查询、备份、维护都更方便;然后用数据归档把历史冷数据移到便宜的存储,释放主表的空间;最后通过合理建索引、优化SQL、更新统计信息来进一步提升查询性能。整个过程要注意业务的影响,选对合适的方案,定期维护,才能保证数据库的稳定和高效。