接手一个烂摊子之后:金仓数据库开发规范实战笔记

接手一个烂摊子之后:金仓数据库开发规范实战笔记

从一个凌晨三点的故障说起

去年接手一个电商中台项目,上线才两个月就开始频繁出问题。

最严重的一次是凌晨三点,订单创建接口大面积超时。排查到最后发现是一张订单表,三个月的数据量不到一千万条,但每秒钟几百个INSERT操作,加上十几个索引的维护开销,把服务器的IO完全打满了。

打开那张表的定义,我惊呆了:

  • 67个字段,其中15个是VARCHAR(500)以上
  • 13个索引,有一半从来没有被查询用过
  • 没有主键,用的一个联合唯一索引代替
  • 表的fillfactor是默认的100,意味着UPDATE会产生大量版本链

这不是个案,是开发规范缺失的典型后果。接手之后,我花了两周时间梳理了一套针对金仓数据库的开发规范,现在拿出来分享一下。

一、整体设计原则:先把大方向定下来

1.1 字符集统一

金仓支持多种字符集,但同一个实例里混用UTF8和GBK,会在跨库查询时出现乱码或转换开销。我们的规矩是:所有实例统一用UTF8。

sql 复制代码
-- 创建数据库时明确指定字符集
CREATE DATABASE order_db ENCODING = 'UTF8';

1.2 每个应用独立SCHEMA

不要把所有表都扔在public里。不同应用、不同模块用独立的SCHEMA隔离,权限管理也方便。

sql 复制代码
-- 为订单服务创建独立SCHEMA
CREATE SCHEMA order_svc AUTHORIZATION order_app;

-- 设置默认SCHEMA
SET search_path TO order_svc, public;

1.3 表的硬性约束

  • 单表列数不超过80列
  • 必须有主键或唯一约束
  • 外键必须建索引,主外键类型要一致
  • 触发器能不用就不用
sql 复制代码
-- 正确的建表示例
CREATE TABLE order_svc.t_orders (
    order_id         BIGSERIAL,
    order_no         VARCHAR(32) NOT NULL,
    user_id          INTEGER NOT NULL,
    amount           DECIMAL(12,2) NOT NULL,
    status           SMALLINT DEFAULT 0,
    created_time     TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_time     TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT pk_orders_order_id PRIMARY KEY (order_id),
    CONSTRAINT uk_orders_order_no UNIQUE (order_no)
);

1.4 大表处理策略

金仓官方建议:单表超过5000万条或100GB,就要考虑分区或归档。我们在实践中把阈值定得更保守一些------2000万条就触发评估。

sql 复制代码
-- 按月分区示例
CREATE TABLE order_svc.t_orders_202501 PARTITION OF order_svc.t_orders
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

1.5 大对象字段的处理

图片、文件不要直接存数据库。金仓虽然支持BLOB/CLOB,但大对象和业务数据混在一起,IO会成为瓶颈。

我们定的规则是:

  • 文件存OSS或NAS,数据库只存路径
  • 实在要存LOB,单独建一张表存放,通过外键关联
sql 复制代码
-- 大对象单独存储
CREATE TABLE order_svc.t_order_attachments (
    attach_id    BIGSERIAL PRIMARY KEY,
    order_id     BIGINT NOT NULL,
    file_path    VARCHAR(256) NOT NULL,
    file_size    INTEGER,
    -- 不要在大对象字段上建索引
    file_content BLOB
);

二、命名规范:让代码可读可维护

命名混乱是很多项目的通病。一套清晰的命名规范,能让新人接手时少花一半时间。

2.1 表命名

格式:TB_ + 应用名 + 模块名 + 表描述

sql 复制代码
-- 示例
TB_SHOP_ORDER      -- 订单表
TB_SHOP_PRODUCT    -- 商品表
TB_SHOP_USER       -- 用户表

2.2 索引命名

sql 复制代码
-- 普通索引:IDX_表名_字段1_字段2
CREATE INDEX idx_order_user_id ON t_orders(user_id);

-- 唯一索引:UID_表名_字段
CREATE UNIQUE INDEX uid_order_order_no ON t_orders(order_no);

-- 主键:PK_表名_主键列
ALTER TABLE t_orders ADD CONSTRAINT pk_orders_order_id PRIMARY KEY (order_id);

2.3 其他对象命名

对象类型 前缀 示例
视图 V_ v_order_summary
序列 SEQ_ seq_order_id
函数 FUNC_ func_calc_amount
存储过程 P_ p_refresh_order
临时表 TMP_ tmp_order_import_20250417_zhang

一个重要提醒:所有对象名长度不要超过30个字符。金仓对长对象名虽然支持,但会给后续维护带来麻烦。

三、字段设计:选对类型比什么都重要

3.1 类型选择的几个原则

原则一:用对类型,别用字符存数字

sql 复制代码
-- 错误:用字符存日期
create_time VARCHAR(20)  -- ❌

-- 正确:用DATE类型
create_time DATE         -- ✅

-- 错误:用字符存金额
amount VARCHAR(20)       -- ❌

-- 正确:用DECIMAL
amount DECIMAL(12,2)     -- ✅

原则二:选最小的够用类型

sql 复制代码
-- 状态字段用SMALLINT(2字节),别用INTEGER(4字节)
status SMALLINT DEFAULT 0

-- 年龄用SMALLINT就够了
age SMALLINT

-- 定长字符串用CHAR,变长用VARCHAR
country_code CHAR(2)     -- 固定2位
address VARCHAR(200)     -- 长度不固定

原则三:能用数值不用字符

数值类型比较效率比字符串高得多。这个差异在大表关联查询时特别明显。

3.2 填坑经验:fillfactor的设置

金仓有一个很实用的参数叫fillfactor,控制每个数据页的填充率。默认是100,表示写满。

对于频繁UPDATE的表,建议设为80:

sql 复制代码
CREATE TABLE order_svc.t_order_status_log (
    log_id     BIGSERIAL,
    order_id   BIGINT NOT NULL,
    old_status SMALLINT,
    new_status SMALLINT,
    change_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) WITH (fillfactor = 80);

为什么?金仓的UPDATE本质上是标记旧行+插入新行。如果页面满了,新行只能放到其他页面,导致读取时需要扫描多个页面。预留20%的空间,可以让新行留在同一个页面内(这就是HOT更新,Heap-Only Tuple),性能会好很多。

3.3 冗余字段:用空间换时间

大表关联查询代价很高。适当冗余一些字段,可以减少JOIN。

sql 复制代码
-- 订单表里冗余用户姓名,避免每次都要关联用户表
CREATE TABLE t_orders (
    order_id   BIGSERIAL,
    user_id    INTEGER NOT NULL,
    user_name  VARCHAR(64),  -- 冗余字段
    amount     DECIMAL(12,2)
);

代价是更新用户姓名时要同步更新订单表。需要在设计时权衡。

四、索引设计:少而精

4.1 索引数量的控制

金仓官方建议:单表索引不超过5个。我们内部的标准更严------核心表不超过3个。

为什么?每个索引都会增加INSERT、UPDATE、DELETE的开销。订单表每插入一条记录,要维护主键索引、唯一索引、普通索引...索引越多,写入越慢。

4.2 索引字段的选择

选择性原则:把过滤效果最好的字段放前面。

假设有两个字段:

  • status:只有3种值,每个值占30%
  • user_id:唯一值很多,每个值占0.01%

那么索引应该是(user_id, status)而不是(status, user_id)

避免冗余索引

sql 复制代码
-- 已经有联合索引
CREATE INDEX idx_user_status ON t_orders(user_id, status);

-- 这个单字段索引就是冗余的,因为联合索引已经能覆盖
CREATE INDEX idx_user_id ON t_orders(user_id);  -- ❌ 不需要

4.3 外键必须建索引

这是金仓官方特别强调的:如果子表外键没有索引,父表删除记录时会锁住子表所有记录。

sql 复制代码
-- 子表
CREATE TABLE t_order_items (
    item_id    BIGSERIAL,
    order_id   BIGINT NOT NULL,
    product_id INTEGER NOT NULL
);

-- 外键索引必须建
CREATE INDEX idx_order_items_order_id ON t_order_items(order_id);

4.4 分区表的索引策略

金仓的分区索引有个重要原则:分区索引必须包含分区列,且分区列要放在索引末尾。

sql 复制代码
-- 按月分区的订单表
CREATE TABLE t_orders (
    order_id     BIGSERIAL,
    order_no     VARCHAR(32),
    created_date DATE NOT NULL  -- 分区键
) PARTITION BY RANGE (created_date);

-- 正确的分区索引:分区键在最后
CREATE INDEX idx_orders_order_no ON t_orders(order_no, created_date);

4.5 不同索引类型的使用场景

金仓支持B-tree、Hash、GIN、GiST、BRIN等多种索引类型。日常开发中B-tree覆盖了90%的场景,但有几种情况值得留意:

BRIN索引:适合时间序列表(日志、流水),索引很小但查询效率不错。

sql 复制代码
-- 日志表用BRIN索引,几GB的数据索引可能只有几十MB
CREATE INDEX idx_log_created ON t_log USING BRIN(created_date);

GIN索引:适合数组、JSONB字段和全文检索。

sql 复制代码
-- JSONB字段的GIN索引
CREATE INDEX idx_products_attrs ON t_products USING GIN(attributes);

五、SQL编写规范

5.1 绑定变量必须用

高并发场景下,不用绑定变量会导致SQL每次都要硬解析,CPU会被占满。

sql 复制代码
-- 错误:拼接SQL
EXECUTE IMMEDIATE 'SELECT * FROM t_orders WHERE order_id = ' || v_id;

-- 正确:用绑定变量
EXECUTE IMMEDIATE 'SELECT * FROM t_orders WHERE order_id = $1' USING v_id;

5.2 避免隐式类型转换

sql 复制代码
-- 假设user_id是INTEGER类型
-- 错误:传入字符串,触发隐式转换
SELECT * FROM t_orders WHERE user_id = '123';

-- 正确:传入数值
SELECT * FROM t_orders WHERE user_id = 123;

隐式转换会让索引失效,这个坑踩一次就记住了。

5.3 SELECT只取需要的列

sql 复制代码
-- 错误:SELECT *
SELECT * FROM t_orders WHERE order_id = 12345;

-- 正确:只取需要的字段
SELECT order_no, amount, status FROM t_orders WHERE order_id = 12345;

在订单这种宽表上,SELECT *会多读大量不需要的数据,网络传输和内存占用都更大。

5.4 COUNT(*) vs COUNT(列)

sql 复制代码
-- 统计行数,用COUNT(*)
SELECT COUNT(*) FROM t_orders WHERE status = 1;

-- 统计某列非NULL值数量,用COUNT(列)
SELECT COUNT(user_id) FROM t_orders;

COUNT(*)在金仓里优化得很好,不要自己写成COUNT(1)COUNT(主键)

六、连接池管理:那些容易被忽视的坑

6.1 连接数不是越多越好

一个常见的误区:并发高,就调大max_connections

实际上,连接数超过CPU核心数的10倍,系统就会开始抖动。因为CPU大部分时间花在上下文切换上,而不是真正处理请求。

我们定的规则:每个CPU核心不超过10个连接。32核的服务器,连接数控制在300以内。

6.2 防止会话泄漏

会话泄漏是开发规范里最容易忽视的问题。异常处理不当,连接没释放,积少成多把连接池占满。

java 复制代码
// Java代码示例:必须用try-with-resources或finally释放连接
try (Connection conn = dataSource.getConnection();
     PreparedStatement stmt = conn.prepareStatement(sql)) {
    // 执行SQL
} catch (SQLException e) {
    // 记录日志,确保连接被关闭
    log.error("Database error", e);
}

6.3 登录/注销策略

不要为每个SQL请求都创建新连接。连接建立的开销很大(TCP握手、认证、分配内存)。

正确做法:使用连接池,让连接复用。

七、写在最后

这套规范推行了半年,效果很明显:P1级故障从每月3-4次降到了半年1次,新人接手项目的上手时间也从2周缩短到了3天。

规范的目的是统一认知、减少踩坑,不是给开发设障碍。每一条规则的背后,都对应着一次真实的线上故障。希望这份总结对你有帮助。

相关推荐
Stark-C2 小时前
NAS音乐必备神器,全平台音乐收割机!极空间部署『Go Music DL』
开发语言·后端·golang
Ailan_Anjuxi2 小时前
Python快速学习——第7章:选择语句
后端
用户79140679683932 小时前
分库分表策略
后端
常利兵2 小时前
大文件上传不再卡顿:Spring Boot 分片上传、断点续传与进度条实现全解析
spring boot·后端·php
用户79140679683933 小时前
MySQL的索引类型
后端
楼田莉子3 小时前
同步/异步日志系统:日志器管理器模块\全局接口\性能测试
linux·服务器·开发语言·c++·后端·设计模式
geNE GENT3 小时前
Spring Boot管理用户数据
java·spring boot·后端
怒放吧德德3 小时前
Spring Boot实战:Event事件机制解析与实战
java·spring boot·后端
梦无矶3 小时前
快速设置uv默认源为国内镜像
数据库·redis·后端·python·uv