Skip to content

命名规范 ​

命名规范(Naming Conventions)看起来只是「风格问题」,但在 PostgreSQL 里它同时关系到标识符是否被折叠、SQL 是否需要加引号、迁移脚本能否复用、自动化巡检能否写出稳定的 SQL。本节从 PostgreSQL 的标识符大小写规则讲起,给出一套可落地的完整命名规范表,最后说明重命名对象的正确做法与依赖处理。

前置知识

阅读本节前,建议先了解:创建表(CREATE TABLE)

基础概念 ​

为什么需要统一命名规范。 一套一致的命名规范能带来四类实在的收益:

收益说明
可预期看到 orders_user_id_fkey 就知道它是 orders.user_id 的外键
可维护新成员无需翻文档就能猜出字段含义
可自动化巡检、审计、权限脚本能按前缀写通用 SQL
可移植跨环境(dev/staging/prod)比对时名字一致,diff 才有意义

标识符大小写与引号规则。 这是 PostgreSQL 最容易被忽略、也最容易踩坑的一条规则:

未加引号的标识符会被折叠为小写;加双引号的标识符保留原样。

sql
-- 未加引号:OrderItems 被折叠为 orderitems
CREATE TABLE OrderItems (Id int);

-- 加引号:保留大小写,表名真的是 "OrderItems"
CREATE TABLE "OrderItems" ("Id" int);

用系统表验证这个差异:

sql
SELECT relname FROM pg_class WHERE relname ILIKE 'orderitems';
text
  relname
------------
 orderitems
(1 row)

两条关键结论:

写法实际存储的名字后续引用方式
OrderItemsorderitems(全小写)SELECT * FROM orderitems(不区分大小写)
"OrderItems"OrderItems(保留)必须写 SELECT * FROM "OrderItems"(引号不可省)

一旦用了引号,就永远离不开引号

"OrderItems" 之后,任何引用都必须写成 "OrderItems"。忘了引号会报 relation "orderitems" does not exist。这就是为什么社区共识是:全部用小写字母加下划线,永不使用双引号。

命名风格的选择。 PostgreSQL 生态事实上以 snake_case(小写下划线)为默认,这与 MySQL 常见的大小写混合形成对比:

风格示例适用对象评价
snake_caseorder_items表、列、索引推荐,PostgreSQL 事实标准
PascalCaseOrderItems需要引号不推荐,跨环境易出错
camelCaseorderItems需要引号不推荐
kebab-caseorder-items需要引号,且 - 是运算符禁止

语法与用法 ​

标识符的语法边界(来自 SQL 标准与 PostgreSQL 实现):

text
-- 标识符构成
第一个字符:字母(a-z, A-Z)或下划线 _
后续字符:  字母、数字、下划线、美元符号 $

-- 长度上限:NAMEDATALEN - 1 = 63 字节(默认编译值)
-- 超出部分会被静默截断

-- 三种写法
order_items          -- 折叠为小写(推荐)
"order_items"        -- 保留大小写(等价,但多打引号)
"OrderItems"         -- 保留大小写(后续必须始终加引号)

最简可运行示例——验证折叠与截断:

sql
-- 折叠:三种写法指向同一张表
CREATE TABLE Naming_Demo (id int);

SELECT relname FROM pg_class WHERE relname = 'naming_demo';
text
  relname
------------
 naming_demo
(1 row)
text
-- 截断:超过 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_caseorder_itemsOrderItems、orderItems
用复数ordersorder(若你选了复数体系)
避免业务前缀缩写customerscst_mst_tbl
避免保留字groupsgroup、order、user 等
关联表用两个单数拼接user_rolesusers_roles、ur
sql
-- 推荐
CREATE TABLE order_items (...);
CREATE TABLE user_roles (...);

-- 不推荐!table 是保留字,order/user 是保留字
CREATE TABLE order (...);   -- 语法错误
CREATE TABLE user (...);    -- 需要引号 "user"

关于保留字,可以用系统视图查证:

sql
SELECT word, catcode, catdesc
FROM pg_get_keywords()
WHERE word IN ('order', 'user', 'table', 'group', 'select')
ORDER BY word;
text
  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便于统一管理与权限控制
sql
-- 推荐:把扩展装进独立 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。

列名规范 ​

列名是最需要规范化的部分,因为它数量最多、最容易出现同义词混乱:

列类型规范示例
主键idid
外键<单数表名>_iduser_id、order_id
创建时间created_atcreated_at timestamptz
更新时间updated_atupdated_at timestamptz
删除时间(软删)deleted_atdeleted_at timestamptz
布尔is_<形容词>is_active、is_deleted
数量<名词>_countitem_count
金额<名词>_amounttotal_amount
状态statusstatus text
名称name / <限定>_nameproduct_name
时间点<动词>ed_atpublished_at、expired_at

外键命名是最有价值的一条规则,因为它同时表达指向哪张表:

sql
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
sql
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);

对比「不命名」的自动命名,问题立刻显现:

sql
-- 不命名时 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;
text
        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
sql
-- 序列:手动创建时加 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();

避免保留字与特殊字符 ​

先说不用引号就无法命名的标识符:

sql
-- 错误!order / user / table 是保留字
CREATE TABLE order (id int);
text
ERROR:  syntax error at or near "order"
LINE 1: CREATE TABLE order (id int);
                     ^
sql
-- 使用保留字必须加引号,从此离不开引号
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)可用作列名,但极易造成混淆,建议避开

特殊字符方面:

sql
-- 禁止!- 会被解析成减号运算符
-- CREATE TABLE order-items (id int);

-- 禁止!空格与中文会强制引号,且跨工具兼容差
-- CREATE TABLE "订单表" (...);

-- 允许但不推荐:$ 是合法标识符字符,但会让引用变得别扭
CREATE TABLE t$demo (id int);

标识符长度限制(NAMEDATALEN 63) ​

PostgreSQL 的标识符上限是 NAMEDATALEN - 1,默认 NAMEDATALEN = 64,即 63 字节。超长标识符会被静默截断,这是最难排查的问题之一:

sql
-- 两个名字前 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);
text
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 字节:

sql
-- 长表名 + 长列名 + 自动后缀 = 超长,被截断成难懂的名字
CREATE TABLE very_long_table_name_for_quarterly_financial_settlement_records (
  very_long_descriptive_column_name_identifying_counterparty uuid
);
-- 隐藏风险:自动生成的 fkey/check 名字可能被截断后互相冲突

应对策略:

策略说明
主动缩短表名长表名会传染到所有约束名与索引名
显式命名约束/索引显式名字可控,能避免截断后的意外冲突
巡检超长标识符用系统表查询长度接近 63 的对象
不修改 NAMEDATALEN改它需要重新编译,生产环境不可取
sql
-- 巡检接近长度上限的标识符
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_caseorder_items
关联表两个单数拼接user_roles
列(主键)idid
列(外键)<单数表名>_iduser_id
列(布尔)is_<形容词>is_active
列(时间)<动词>ed_atcreated_at
列(金额)<名词>_amounttotal_amount
列(数量)<名词>_countitem_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

命名规范检查的自动化方法 ​

规范只有能被自动检查才真正落地。核心手段是用系统表 + 正则表达式巡检:

sql
-- 检查表名是否全小写 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_]*$';
text
 bad_table_name
----------------
 OrderItems
 cst_mst_tbl
(2 rows)
sql
-- 检查索引命名是否符合前缀规范
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_)';
text
 index_name
------------
 orders_pkey
 orders_order_no_key
(2 rows)

也可以把规则写成一段 SQL 生成整改清单:

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,但不同类型的对象改名方式不同:

sql
-- 表改名
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 是文本,改名后不会自动更新。

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);
text
ERROR:  relation "orders" does not exist
LINE 2:   SELECT order_no FROM orders WHERE id = p_id;

因此改名前后必须做依赖排查。用 pg_depend 找依赖对象:

sql
-- 查看哪些对象依赖这张表的列
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;
text
 dependent_object | kind | owner
------------------+------+-------
 get_order_no     | f    | postgres
 v_active_users   | v    | postgres
(2 rows)

更实用的做法是用 pg_get_functiondef / pg_get_viewdef 全文检索,找出硬编码的旧名字:

sql
-- 找出所有函数定义里还提到旧表名的对象
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';
text
   function_name
-------------------
 get_order_no(bigint)
(1 row)

另外,重命名表的列时,视图里 SELECT * 展开的列不会报错但语义可能错位;而显式列引用会自动跟进。因此还有一条配套建议:视图定义里避免 SELECT *。

实战示例 ​

场景:对一张已经上线半年、命名混乱的 OrderItems 表做规范化整改,涉及表名、列名、约束名、索引名,并安全处理视图依赖。

第 1 步:复现混乱现状

sql
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 步:巡检问题清单

sql
-- 大小写不合规的表
SELECT relname FROM pg_class
WHERE relname !~ '^[a-z][a-z0-9_]*$' AND relkind = 'r';
text
   relname
--------------
 OrderItems
(1 row)
sql
-- 未按前缀命名的索引
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_';
text
    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 保留大小写,因此新名字也要加引号或用小写):

sql
-- 视图先删掉(避免 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;
text
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE
ALTER TABLE

第 4 步:规范化约束与索引命名

sql
-- 主键改名
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);
text
ALTER TABLE
ALTER INDEX
ALTER TABLE
ALTER TABLE

第 5 步:重建视图并验证

sql
-- 重建视图时显式列出列,避免 SELECT *
CREATE VIEW v_order_items AS
SELECT id, order_id, product_id, qty, unit_price, created_at
FROM order_items;

-- 验证结构
\d order_items
text
                         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 步:写规范巡检脚本(可定期执行)

sql
-- 一键巡检:列出所有不合规命名
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;
text
 obj_type | obj_name |              rule
----------+----------+--------------------------------
(0 rows)

清理干净后返回 0 行,说明整改进度可量化。

第 7 步:清理

sql
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 生态最低成本、最高收益的一条规则。

    sql
    CREATE TABLE order_items (order_id bigint, product_id bigint);
  • 表名用复数,关联表用两个单数拼接。

    sql
    orders / order_items / user_roles
  • 外键统一叫 <单数表名>_id,并保持一致。 不要混用 uid、userid、user_id。

    sql
    user_id bigint NOT NULL REFERENCES users(id)
  • 时间戳统一 created_at / updated_at / deleted_at,且用 timestamptz。

    sql
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz,
    deleted_at timestamptz
  • 布尔列统一 is_ 前缀,避免 active/enabled/flag 混用。

    sql
    is_active boolean NOT NULL DEFAULT true
  • 所有约束与索引都显式命名,前缀 pk_/fk_/uq_/ck_/ex_/idx_。

    sql
    CONSTRAINT 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,命名无法表达的业务语义用注释补充:

    sql
    COMMENT ON COLUMN orders.status IS '订单状态:pending/paid/shipped/cancelled';
  • 把命名巡检写成可重复执行的 SQL 脚本,纳入 CI 或定期巡检,让规范「可度量」。

  • 别在业务表里用业务系统前缀(如 cst_mst_tbl)。schema 已经是命名空间,前缀会浪费标识符长度并增加拼写负担。

下一节

继续学习:列设计最佳实践

参考链接 ​