命名规范
命名规范(Naming Conventions)看起来只是「风格问题」,但在 PostgreSQL 里它同时关系到标识符是否被折叠、SQL 是否需要加引号、迁移脚本能否复用、自动化巡检能否写出稳定的 SQL。本节从 PostgreSQL 的标识符大小写规则讲起,给出一套可落地的完整命名规范表,最后说明重命名对象的正确做法与依赖处理。
前置知识
阅读本节前,建议先了解:创建表(CREATE TABLE)
基础概念
为什么需要统一命名规范。 一套一致的命名规范能带来四类实在的收益:
| 收益 | 说明 |
|---|---|
| 可预期 | 看到 orders_user_id_fkey 就知道它是 orders.user_id 的外键 |
| 可维护 | 新成员无需翻文档就能猜出字段含义 |
| 可自动化 | 巡检、审计、权限脚本能按前缀写通用 SQL |
| 可移植 | 跨环境(dev/staging/prod)比对时名字一致,diff 才有意义 |
标识符大小写与引号规则。 这是 PostgreSQL 最容易被忽略、也最容易踩坑的一条规则:
未加引号的标识符会被折叠为小写;加双引号的标识符保留原样。
-- 未加引号:OrderItems 被折叠为 orderitems
CREATE TABLE OrderItems (Id int);
-- 加引号:保留大小写,表名真的是 "OrderItems"
CREATE TABLE "OrderItems" ("Id" int);用系统表验证这个差异:
SELECT relname FROM pg_class WHERE relname ILIKE 'orderitems'; relname
------------
orderitems
(1 row)两条关键结论:
| 写法 | 实际存储的名字 | 后续引用方式 |
|---|---|---|
OrderItems | orderitems(全小写) | SELECT * FROM orderitems(不区分大小写) |
"OrderItems" | OrderItems(保留) | 必须写 SELECT * FROM "OrderItems"(引号不可省) |
一旦用了引号,就永远离不开引号
"OrderItems" 之后,任何引用都必须写成 "OrderItems"。忘了引号会报 relation "orderitems" does not exist。这就是为什么社区共识是:全部用小写字母加下划线,永不使用双引号。
命名风格的选择。 PostgreSQL 生态事实上以 snake_case(小写下划线)为默认,这与 MySQL 常见的大小写混合形成对比:
| 风格 | 示例 | 适用对象 | 评价 |
|---|---|---|---|
snake_case | order_items | 表、列、索引 | 推荐,PostgreSQL 事实标准 |
PascalCase | OrderItems | 需要引号 | 不推荐,跨环境易出错 |
camelCase | orderItems | 需要引号 | 不推荐 |
kebab-case | order-items | 需要引号,且 - 是运算符 | 禁止 |
语法与用法
标识符的语法边界(来自 SQL 标准与 PostgreSQL 实现):
-- 标识符构成
第一个字符:字母(a-z, A-Z)或下划线 _
后续字符: 字母、数字、下划线、美元符号 $
-- 长度上限:NAMEDATALEN - 1 = 63 字节(默认编译值)
-- 超出部分会被静默截断
-- 三种写法
order_items -- 折叠为小写(推荐)
"order_items" -- 保留大小写(等价,但多打引号)
"OrderItems" -- 保留大小写(后续必须始终加引号)最简可运行示例——验证折叠与截断:
-- 折叠:三种写法指向同一张表
CREATE TABLE Naming_Demo (id int);
SELECT relname FROM pg_class WHERE relname = 'naming_demo'; relname
------------
naming_demo
(1 row)-- 截断:超过 63 字符会被无声截断,且 PostgreSQL 会发通知
NOTICE: identifier "a_very_long_name_that_exceeds_the_identifier_length_limit_of_63_bytes_for_sure" will be truncated to "a_very_long_name_that_exceeds_the_identifier_length_limit_of_63_b"详细说明
表名:单数还是复数
这是唯一一个没有绝对答案的争论,但必须有统一选择:
| 方案 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 复数 | orders、users | 语义直观(表是一堆行) | 不规则复数尴尬(person→people) |
| 单数 | order、user | 无复数变形问题 | 与「表即集合」的直觉略冲突 |
PostgreSQL 官方文档与主流 ORM(如 Rails、Django)习惯用复数;部分企业规范偏好单数。本节采用复数(如 orders),因为它能让 <单数表名>_id 这条外键命名规则减少歧义。
表名命名规则:
| 规则 | 示例 | 反例 |
|---|---|---|
全小写 snake_case | order_items | OrderItems、orderItems |
| 用复数 | orders | order(若你选了复数体系) |
| 避免业务前缀缩写 | customers | cst_mst_tbl |
| 避免保留字 | groups | group、order、user 等 |
| 关联表用两个单数拼接 | user_roles | users_roles、ur |
-- 推荐
CREATE TABLE order_items (...);
CREATE TABLE user_roles (...);
-- 不推荐!table 是保留字,order/user 是保留字
CREATE TABLE order (...); -- 语法错误
CREATE TABLE user (...); -- 需要引号 "user"关于保留字,可以用系统视图查证:
SELECT word, catcode, catdesc
FROM pg_get_keywords()
WHERE word IN ('order', 'user', 'table', 'group', 'select')
ORDER BY word; word | catcode | catdesc
--------+---------+------------------
group | R | reserved
order | R | reserved
select | R | reserved
table | R | reserved
user | R | reserved
(5 rows)catcode = 'R' 表示完全保留字,不能用作标识符;catcode = 'C' 是「列名可用但不能做函数/类型名」;catcode = 'U' 是「无限制」。
schema 命名
schema 是表名的命名空间,命名策略直接影响权限管理:
| 用途 | schema 名 | 说明 |
|---|---|---|
| 业务表 | 不使用(落 public)或业务名 | public 最省事,但权限粒度粗 |
| 多租户隔离 | tenant_<id> | 每租户一个 schema |
| 分层隔离 | ods / dwh / app | 数据仓库分层 |
| 扩展对象 | 独立 schema,如 extensions | 便于统一管理与权限控制 |
-- 推荐:把扩展装进独立 schema,避免污染 public
CREATE SCHEMA IF NOT EXISTS extensions;
CREATE EXTENSION IF NOT EXISTS pg_trgm SCHEMA extensions;
-- 用 search_path 控制默认可见范围
SET search_path = app, public, extensions;public 的权限默认很宽
PostgreSQL 15 之前,public schema 对所有用户默认有 CREATE 权限。生产环境应显式收紧,或把业务表放进专用 schema。
列名规范
列名是最需要规范化的部分,因为它数量最多、最容易出现同义词混乱:
| 列类型 | 规范 | 示例 |
|---|---|---|
| 主键 | id | id |
| 外键 | <单数表名>_id | user_id、order_id |
| 创建时间 | created_at | created_at timestamptz |
| 更新时间 | updated_at | updated_at timestamptz |
| 删除时间(软删) | deleted_at | deleted_at timestamptz |
| 布尔 | is_<形容词> | is_active、is_deleted |
| 数量 | <名词>_count | item_count |
| 金额 | <名词>_amount | total_amount |
| 状态 | status | status text |
| 名称 | name / <限定>_name | product_name |
| 时间点 | <动词>ed_at | published_at、expired_at |
外键命名是最有价值的一条规则,因为它同时表达指向哪张表:
CREATE TABLE order_items (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id bigint NOT NULL REFERENCES orders(id), -- 指向 orders
product_id bigint NOT NULL REFERENCES products(id), -- 指向 products
qty integer NOT NULL CHECK (qty > 0),
created_at timestamptz NOT NULL DEFAULT now()
);时间戳一律用 timestamptz
created_at / updated_at 请用 timestamptz(带时区)而非 timestamp(不带时区)。前者是绝对时间点,后者是「墙上时间」,跨时区存储会产生歧义。
约束与索引命名
显式命名约束和索引,能让迁移脚本、跨环境比对和错误定位都更容易。约定前缀如下:
| 对象 | 前缀 | 示例 |
|---|---|---|
| 主键 | pk_ | pk_orders |
| 外键 | fk_ | fk_orders_user_id |
| 唯一约束 | uq_ | uq_users_email |
| 检查约束 | ck_ | ck_orders_amount_positive |
| 排除约束 | ex_ | ex_reservations_no_overlap |
| 普通索引 | idx_ | idx_orders_user_id |
| 部分索引 | idx_ + 条件后缀 | idx_orders_active_user_id |
| 表达式索引 | idx_ + 用途 | idx_products_lower_name |
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY,
order_no text NOT NULL,
user_id bigint NOT NULL,
amount numeric(12,2) NOT NULL,
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT pk_orders PRIMARY KEY (id),
CONSTRAINT uq_orders_no UNIQUE (order_no),
CONSTRAINT ck_orders_amount_pos CHECK (amount > 0),
CONSTRAINT ck_orders_status CHECK (status IN ('pending','paid','shipped','cancelled'))
);
CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_created_at ON orders (created_at DESC);对比「不命名」的自动命名,问题立刻显现:
-- 不命名时 PostgreSQL 自动生成的名字
CREATE TABLE bad_naming (
id bigint PRIMARY KEY, -- bad_naming_pkey
email text UNIQUE, -- bad_naming_email_key
amount numeric CHECK (amount > 0), -- bad_naming_amount_check
user_id bigint REFERENCES orders(id) -- bad_naming_user_id_fkey
);
SELECT conname, contype FROM pg_constraint
WHERE conrelid = 'bad_naming'::regclass ORDER BY contype; conname | contype
------------------------+---------
bad_naming_amount_check | c
bad_naming_user_id_fkey | f
bad_naming_pkey | p
bad_naming_email_key | u
(4 rows)自动名字在跨环境比对时往往能对上,但一旦涉及「同一张表有多个 CHECK」,名字就失去语义(谁也看不出 bad_naming_amount_check 检查的是什么)。显式命名是长期收益最大的投资。
序列与视图命名
| 对象 | 规范 | 示例 |
|---|---|---|
| 序列(手动创建) | seq_<用途> | seq_order_no |
| 序列(IDENTITY 自动) | 不手动命名,跟随表 | orders_id_seq |
| 视图 | v_<用途> | v_active_users |
| 物化视图 | mv_<用途> | mv_order_report |
| 函数 | <动词>_<名词> | calculate_order_total |
| 触发器 | trg_<时机>_<表> | trg_after_update_products |
| 自定义类型 | <用途>_t 或语义名 | order_status |
-- 序列:手动创建时加 seq_ 前缀
CREATE SEQUENCE seq_order_no START WITH 1000 INCREMENT BY 1;
-- 视图:v_ 前缀
CREATE VIEW v_active_users AS
SELECT id, email, created_at FROM users WHERE deleted_at IS NULL;
-- 物化视图:mv_ 前缀
CREATE MATERIALIZED VIEW mv_daily_sales AS
SELECT created_at::date AS day, sum(amount) AS total
FROM orders GROUP BY 1;
-- 触发器:trg_ 前缀
CREATE TRIGGER trg_set_updated_at
BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION set_updated_at();避免保留字与特殊字符
先说不用引号就无法命名的标识符:
-- 错误!order / user / table 是保留字
CREATE TABLE order (id int);ERROR: syntax error at or near "order"
LINE 1: CREATE TABLE order (id int);
^-- 使用保留字必须加引号,从此离不开引号
CREATE TABLE "order" ("user" int, "select" text);因此规范上应当整体回避保留字,用同义词替代:
| 想用的名字 | 是否保留字 | 替代方案 |
|---|---|---|
order | 是(R) | orders |
user | 是(R) | users、accounts |
group | 是(R) | groups、user_groups |
table | 是(R) | tables、dining_tables |
desc | 是(R) | description |
check | 是(R) | is_checked |
value | 否(C) | 可用作列名,但不能做类型/函数名 |
text | 否(C) | 可用作列名,但极易造成混淆,建议避开 |
特殊字符方面:
-- 禁止!- 会被解析成减号运算符
-- CREATE TABLE order-items (id int);
-- 禁止!空格与中文会强制引号,且跨工具兼容差
-- CREATE TABLE "订单表" (...);
-- 允许但不推荐:$ 是合法标识符字符,但会让引用变得别扭
CREATE TABLE t$demo (id int);标识符长度限制(NAMEDATALEN 63)
PostgreSQL 的标识符上限是 NAMEDATALEN - 1,默认 NAMEDATALEN = 64,即 63 字节。超长标识符会被静默截断,这是最难排查的问题之一:
-- 两个名字前 63 字节相同的表,实际会被截断成同一个名字
CREATE TABLE this_is_a_really_long_table_name_that_will_be_truncated_aaa_111 (id int);
CREATE TABLE this_is_a_really_long_table_name_that_will_be_truncated_aaa_222 (id int);NOTICE: identifier "..." will be truncated to "this_is_a_really_long_table_name_that_will_be_truncated_aaa_1"
CREATE TABLE
ERROR: relation "this_is_a_really_long_table_name_that_will_be_truncated_aaa_1" already exists截断对约束名的影响尤其隐蔽,因为约束名是「表名 + 列名 + 后缀」拼出来的,很容易超过 63 字节:
-- 长表名 + 长列名 + 自动后缀 = 超长,被截断成难懂的名字
CREATE TABLE very_long_table_name_for_quarterly_financial_settlement_records (
very_long_descriptive_column_name_identifying_counterparty uuid
);
-- 隐藏风险:自动生成的 fkey/check 名字可能被截断后互相冲突应对策略:
| 策略 | 说明 |
|---|---|
| 主动缩短表名 | 长表名会传染到所有约束名与索引名 |
| 显式命名约束/索引 | 显式名字可控,能避免截断后的意外冲突 |
| 巡检超长标识符 | 用系统表查询长度接近 63 的对象 |
不修改 NAMEDATALEN | 改它需要重新编译,生产环境不可取 |
-- 巡检接近长度上限的标识符
SELECT c.relkind, c.relname, length(c.relname) AS name_len
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND length(c.relname) >= 50
ORDER BY name_len DESC;一套可落地的完整命名规范表
把前面的规则汇总成一张可直接用作团队规范的表格:
| 对象 | 命名模式 | 示例 |
|---|---|---|
| 数据库 | <业务>_<环境> | shop_prod |
| schema | 小写,业务或分层名 | app、ods |
| 表 | 小写复数 snake_case | order_items |
| 关联表 | 两个单数拼接 | user_roles |
| 列(主键) | id | id |
| 列(外键) | <单数表名>_id | user_id |
| 列(布尔) | is_<形容词> | is_active |
| 列(时间) | <动词>ed_at | created_at |
| 列(金额) | <名词>_amount | total_amount |
| 列(数量) | <名词>_count | item_count |
| 主键约束 | pk_<表名> | pk_orders |
| 外键约束 | fk_<表名>_<列名> | fk_orders_user_id |
| 唯一约束 | uq_<表名>_<列名> | uq_users_email |
| 检查约束 | ck_<表名>_<语义> | ck_orders_amount_pos |
| 索引 | idx_<表名>_<列名> | idx_orders_user_id |
| 唯一索引 | uq_<表名>_<列名> 或 idx_ | uq_orders_no |
| 序列 | seq_<用途> | seq_order_no |
| 视图 | v_<用途> | v_active_users |
| 物化视图 | mv_<用途> | mv_daily_sales |
| 函数 | <动词>_<名词> | calc_order_total |
| 触发器 | trg_<时机>_<表> | trg_before_update_orders |
| 自定义类型 | 语义名(不用复数) | order_status |
命名规范检查的自动化方法
规范只有能被自动检查才真正落地。核心手段是用系统表 + 正则表达式巡检:
-- 检查表名是否全小写 snake_case(允许字母、数字、下划线)
SELECT c.relname AS bad_table_name
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname = 'public'
AND c.relname !~ '^[a-z][a-z0-9_]*$'; bad_table_name
----------------
OrderItems
cst_mst_tbl
(2 rows)-- 检查索引命名是否符合前缀规范
SELECT indexrelid::regclass AS index_name, indisprimary, indisunique
FROM pg_index i
JOIN pg_class c ON c.oid = i.indexrelid
WHERE i.indrelid = 'orders'::regclass
AND c.relname !~ '^(pk_|fk_|uq_|idx_|ex_)'; index_name
------------
orders_pkey
orders_order_no_key
(2 rows)也可以把规则写成一段 SQL 生成整改清单:
-- 找出所有「自动命名」的约束(说明没显式命名)
SELECT
conrelid::regclass AS tbl,
conname,
CASE contype
WHEN 'p' THEN 'pk' WHEN 'f' THEN 'fk' WHEN 'u' THEN 'uq'
WHEN 'c' THEN 'ck' WHEN 'x' THEN 'ex' ELSE contype::text
END AS should_prefix
FROM pg_constraint
WHERE connamespace = 'public'::regnamespace
AND contype IN ('p','f','u','c','x')
AND conname !~ '^(pk_|fk_|uq_|ck_|ex_)'
ORDER BY 1, 2;重命名对象的正确做法与依赖
重命名用 ALTER ... RENAME,但不同类型的对象改名方式不同:
-- 表改名
ALTER TABLE orders RENAME TO sales_orders;
-- 列改名
ALTER TABLE sales_orders RENAME COLUMN order_no TO order_code;
-- 约束改名
ALTER TABLE sales_orders RENAME CONSTRAINT pk_orders TO pk_sales_orders;
-- 索引改名
ALTER INDEX idx_orders_user_id RENAME TO idx_sales_orders_user_id;
-- 视图改名
ALTER VIEW v_active_users RENAME TO v_enabled_users;
-- schema 改名
ALTER SCHEMA app RENAME TO app_v2;改名的依赖风险必须重视。表/列改名后,引用了它的对象会怎样?
| 引用方式 | 改名后是否自动跟进 |
|---|---|
| 视图定义中的表/列引用 | 自动跟进(存的是对象 OID) |
| 外键约束 | 自动跟进 |
| 存储过程的静态 SQL | 不跟进,会变成运行时错误 |
视图定义中的列别名(AS 别名) | 不跟进,需手工检查 |
| 应用层硬编码 SQL | 不跟进 |
| 触发器函数里的 SQL | 不跟进 |
看一个真实的陷阱:存储函数里的 SQL 是文本,改名后不会自动更新。
CREATE FUNCTION get_order_no(p_id bigint) RETURNS text AS $$
SELECT order_no FROM orders WHERE id = p_id; -- 硬编码了表名与列名
$$ LANGUAGE sql;
-- 表改名后,函数仍然引用旧名 → 运行时报错
ALTER TABLE orders RENAME TO sales_orders;
SELECT get_order_no(1);ERROR: relation "orders" does not exist
LINE 2: SELECT order_no FROM orders WHERE id = p_id;因此改名前后必须做依赖排查。用 pg_depend 找依赖对象:
-- 查看哪些对象依赖这张表的列
SELECT
dependent.relname AS dependent_object,
dependent.relkind AS kind, -- v=视图, f=函数, r=表 ...
pg_get_userbyid(dep.refobjid) AS owner
FROM pg_depend d
JOIN pg_class dependent ON dependent.oid = d.objid
JOIN pg_class referenced ON referenced.oid = d.refobjid
WHERE referenced.relname = 'orders'
AND d.deptype = 'n'
ORDER BY 1; dependent_object | kind | owner
------------------+------+-------
get_order_no | f | postgres
v_active_users | v | postgres
(2 rows)更实用的做法是用 pg_get_functiondef / pg_get_viewdef 全文检索,找出硬编码的旧名字:
-- 找出所有函数定义里还提到旧表名的对象
SELECT p.oid::regprocedure AS function_name
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public'
AND pg_get_functiondef(p.oid) ~* '\morders\M'; function_name
-------------------
get_order_no(bigint)
(1 row)另外,重命名表的列时,视图里 SELECT * 展开的列不会报错但语义可能错位;而显式列引用会自动跟进。因此还有一条配套建议:视图定义里避免 SELECT *。
实战示例
场景:对一张已经上线半年、命名混乱的 OrderItems 表做规范化整改,涉及表名、列名、约束名、索引名,并安全处理视图依赖。
第 1 步:复现混乱现状
DROP VIEW IF EXISTS v_order_items CASCADE;
DROP TABLE IF EXISTS "OrderItems" CASCADE;
CREATE TABLE "OrderItems" (
"Id" bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
"OrderID" bigint NOT NULL,
"ProductID" bigint NOT NULL,
"Qty" integer,
"Price" numeric(10,2),
CreatedAt timestamptz DEFAULT now() -- 只有这列没加引号
);
CREATE INDEX ON "OrderItems" ("OrderID"); -- 未命名索引
CREATE VIEW v_order_items AS SELECT * FROM "OrderItems";第 2 步:巡检问题清单
-- 大小写不合规的表
SELECT relname FROM pg_class
WHERE relname !~ '^[a-z][a-z0-9_]*$' AND relkind = 'r'; relname
--------------
OrderItems
(1 row)-- 未按前缀命名的索引
SELECT c.relname AS index_name, pg_get_indexdef(i.indexrelid) AS def
FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid
WHERE i.indrelid = '"OrderItems"'::regclass AND c.relname !~ '^idx_'; index_name | def
-------------------+----------------------------------------------
OrderItems_pkey | CREATE UNIQUE INDEX "OrderItems_pkey" ON "OrderItems" ("Id")
OrderItems_OrderID_idx | CREATE INDEX "OrderItems_OrderID_idx" ON "OrderItems" ("OrderID")
(2 rows)第 3 步:先处理依赖视图,再加引号改列名
由于 "OrderID" 等列被引号包裹,改名必须逐个做(RENAME 保留大小写,因此新名字也要加引号或用小写):
-- 视图先删掉(避免 SELECT * 展开口径不一的隐患),改名后重建
DROP VIEW IF EXISTS v_order_items;
-- 表名规范化
ALTER TABLE "OrderItems" RENAME TO order_items;
-- 列名规范化
ALTER TABLE order_items RENAME COLUMN "Id" TO id;
ALTER TABLE order_items RENAME COLUMN "OrderID" TO order_id;
ALTER TABLE order_items RENAME COLUMN "ProductID" TO product_id;
ALTER TABLE order_items RENAME COLUMN "Qty" TO qty;
ALTER TABLE order_items RENAME COLUMN "Price" TO unit_price;
ALTER TABLE order_items RENAME COLUMN "CreatedAt" TO created_at;ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE第 4 步:规范化约束与索引命名
-- 主键改名
ALTER TABLE order_items RENAME CONSTRAINT "OrderItems_pkey" TO pk_order_items;
-- 未命名索引改名
ALTER INDEX "OrderItems_OrderID_idx" RENAME TO idx_order_items_order_id;
-- 补上缺失的约束
ALTER TABLE order_items
ALTER COLUMN qty SET NOT NULL,
ALTER COLUMN unit_price SET NOT NULL,
ADD CONSTRAINT ck_order_items_qty_pos CHECK (qty > 0),
ADD CONSTRAINT ck_order_items_price CHECK (unit_price >= 0);
-- 补一个规范命名的唯一约束示例
ALTER TABLE order_items
ADD CONSTRAINT uq_order_items_order_product UNIQUE (order_id, product_id);ALTER TABLE
ALTER INDEX
ALTER TABLE
ALTER TABLE第 5 步:重建视图并验证
-- 重建视图时显式列出列,避免 SELECT *
CREATE VIEW v_order_items AS
SELECT id, order_id, product_id, qty, unit_price, created_at
FROM order_items;
-- 验证结构
\d order_items Table "public.order_items"
Column | Type | Nullable | Default
------------+--------------------------+----------+------------------------------
id | bigint | not null | generated always as identity
order_id | bigint | not null |
product_id | bigint | not null |
qty | integer | not null |
unit_price | numeric(10,2) | not null |
created_at | timestamp with time zone | | now()
Indexes:
"pk_order_items" PRIMARY KEY, btree (id)
"uq_order_items_order_product" UNIQUE CONSTRAINT, btree (order_id, product_id)
"idx_order_items_order_id" btree (order_id)
Check constraints:
"ck_order_items_price" CHECK (unit_price >= 0)
"ck_order_items_qty_pos" CHECK (qty > 0)第 6 步:写规范巡检脚本(可定期执行)
-- 一键巡检:列出所有不合规命名
SELECT 'table' AS obj_type, c.relname AS obj_name,
'表名应为小写 snake_case' AS rule
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r' AND n.nspname = 'public' AND c.relname !~ '^[a-z][a-z0-9_]*$'
UNION ALL
SELECT 'constraint', conname, '约束应带 pk_/fk_/uq_/ck_ 前缀'
FROM pg_constraint
WHERE connamespace = 'public'::regnamespace
AND conname !~ '^(pk_|fk_|uq_|ck_|ex_)'
UNION ALL
SELECT 'index', c.relname, '索引应带 idx_/uq_ 前缀'
FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
JOIN pg_class t ON t.oid = i.indrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = 'public' AND c.relname !~ '^(idx_|uq_|pk_|fk_|ex_)'
ORDER BY 1, 2; obj_type | obj_name | rule
----------+----------+--------------------------------
(0 rows)清理干净后返回 0 行,说明整改进度可量化。
第 7 步:清理
DROP VIEW IF EXISTS v_order_items;
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS naming_demo, bad_naming, t$demo;注意事项
错误!不要使用双引号包裹的大小写混合标识符。 一旦用了引号,后续每次引用都必须加引号,跨工具(ORM、BI、迁移脚本)极易出错。
sql-- 错误!从此所有引用都要写成 "OrderItems" CREATE TABLE "OrderItems" ("Id" int);错误!不要把保留字当标识符。
order、user、table、group、select都是完全保留字。sql-- 错误!syntax error at or near "order" CREATE TABLE order (id int);错误!不要用
-、空格、中文等特殊字符。 需要引号且兼容性差。sql-- 错误!- 会被当成减号 -- CREATE TABLE order-items (id int);注意表名过长会被静默截断。 63 字节上限,且长表名会传染到所有自动生成的约束/索引名。
sql-- 长表名 + 长列名 → 自动约束名被截断后可能冲突 CREATE TABLE very_long_table_name_... (very_long_column_name_...);错误!不要依赖「自动生成」的约束名做迁移脚本。 自动名不含语义,且表名变化时会跟着变。
sql-- 不推荐:这个约束名到底在检查什么? CONSTRAINT bad_naming_amount_check CHECK (amount > 0)注意表/列改名不会更新存储函数与视图里的硬编码 SQL。 只有通过对象 OID 建立的依赖(视图列引用、外键)会自动跟进。
sql-- 改名后该函数立即失效 CREATE FUNCTION get_order_no(bigint) ... SELECT order_no FROM orders ...; ALTER TABLE orders RENAME TO sales_orders;注意列改名对视图的隐性影响。 视图是
SELECT *时不会报错,但列别名可能与新列名不一致,需人工确认。不要频繁改名。 每次改名都应视为一次变更,需要走评审、依赖排查与回归测试。
最佳实践
全小写
snake_case,永不使用双引号。 这是 PostgreSQL 生态最低成本、最高收益的一条规则。sqlCREATE TABLE order_items (order_id bigint, product_id bigint);表名用复数,关联表用两个单数拼接。
sqlorders / order_items / user_roles外键统一叫
<单数表名>_id,并保持一致。 不要混用uid、userid、user_id。sqluser_id bigint NOT NULL REFERENCES users(id)时间戳统一
created_at/updated_at/deleted_at,且用timestamptz。sqlcreated_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz, deleted_at timestamptz布尔列统一
is_前缀,避免active/enabled/flag混用。sqlis_active boolean NOT NULL DEFAULT true所有约束与索引都显式命名,前缀
pk_/fk_/uq_/ck_/ex_/idx_。sqlCONSTRAINT pk_orders PRIMARY KEY (id), CONSTRAINT uq_orders_no UNIQUE (order_no), CONSTRAINT ck_orders_amount_pos CHECK (amount > 0)视图与物化视图加
v_/mv_前缀,把「这是定义而非数据」的信息显式化。避免保留字与歧义词,
value、text、name这类词尽量加限定(product_name)。给表和列写
COMMENT,命名无法表达的业务语义用注释补充:sqlCOMMENT ON COLUMN orders.status IS '订单状态:pending/paid/shipped/cancelled';把命名巡检写成可重复执行的 SQL 脚本,纳入 CI 或定期巡检,让规范「可度量」。
别在业务表里用业务系统前缀(如
cst_mst_tbl)。schema 已经是命名空间,前缀会浪费标识符长度并增加拼写负担。
下一节
继续学习:列设计最佳实践