首页 / PostgreSQL 入门教程 / 创建表(CREATE TABLE)基础

PostgreSQL 入门教程

创建表(CREATE TABLE)基础

本教程共 50 篇 · 第 8 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

PostgreSQLPostgreSQL 入门教程CREATE TABLE建表数据类型表约束

8. 创建表(CREATE TABLE)基础

本节目标:学完你能独立写出 CREATE TABLE 语句,建出带列、类型和约束的表,并查看它的结构。

表是存数据的地方,建表是数据库里最基本的动作。这一章我们把它拆开讲:列怎么定义、类型怎么选、约束是什么,最后用 \d 看结构。

建表的基本语法

CREATE TABLE table_name (
    column1 datatype constraints,
    column2 datatype constraints,
    ...
);

一张表由若干「列」组成,每列要说明:名字、数据类型,以及可选的约束。表名在同一模式内不能和别的表、视图等重名。

Note

如果担心表已存在会报错,可以加 IF NOT EXISTSCREATE TABLE IF NOT EXISTS 名 (...)。表已在时它会给个提示而不是报错,写初始化脚本时很实用。

列定义与类型初见

我们拿贯穿全书的 users 表来建。它包含:id、username、email、age、created_at。

CREATE TABLE users (
    id         INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    username   VARCHAR(50) NOT NULL,
    email      VARCHAR(100) NOT NULL UNIQUE,
    age        INT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

逐列解释一下用到的类型:

  • INT:整数,存 id、age 这类没有小数的数。
  • VARCHAR(50):变长字符串,括号里是最大长度。存用户名、邮箱。
  • TIMESTAMPTZ:带时区的时间戳。存创建时间。
Tip

VARCHAR(n)n 是上限不是固定占用。和 TEXT(不限长字符串)相比,VARCHAR 多了一道长度校验,其余几乎一样。新手记不住就用 TEXT,想加限制就用 VARCHAR(n)

类型这一章只初见,后面有专门章节细讲整数、字符、时间等。你现在只要知道「每列必须有个类型」即可。

类型选错的代价

类型选得不合适,后面会很麻烦。举个例:如果你把 age 设成 VARCHAR,那「年龄比较大小」就会变成字符串比较,9 会比 10 大,统计全错。所以数字就用数字类型,时间就用时间类型,别图省事全用 TEXT。

Warning

表建好之后改列类型不是不行,但要小心数据转换。比如 TEXT 转 INT,里面若有非数字内容就会报错。建表时把类型想清楚,比事后折腾省事得多。

约束初见

约束(constraint)是对列里数据的规定,用来保证数据靠谱。上面那张表已经用到了几个:

  • PRIMARY KEY(主键):唯一标识一行。一张表只能有一个主键。这里作用在 id 上,意味着每行 id 不重复。
  • NOT NULL(非空):这列不能留空。比如用户名必须有。
  • UNIQUE(唯一):这列的值在表里不能重复。比如邮箱不能两人相同。
  • DEFAULT(默认):没填时用的值。created_at 默认取 now(),即插入时的当前时间。

再看订单表 orders,它用外键关联到 users

CREATE TABLE orders (
    id         INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id    INT NOT NULL REFERENCES users(id),
    amount     NUMERIC(10,2) NOT NULL,
    status     VARCHAR(20) NOT NULL DEFAULT 'pending',
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

这里 user_id INT NOT NULL REFERENCES users(id) 是「外键」的雏形:它要求 orders.user_id 的值必须能在 users.id 里找到,保证订单一定属于某个存在的用户。外键细节后面章节会展开,这里先见个面。

Note

主键那个 GENERATED ALWAYS AS IDENTITY 负责自增:你插入时不填 id,Postgres 自动按顺序生成。这是 PostgreSQL 18 推荐写法。老的 SERIAL 写法现在只作为历史兼容保留,新代码别用了。

NUMERIC(10,2) 表示最多 10 位数字、其中 2 位是小数,正好用来存金额(如 99.99)。金额千万别用浮点类型,会有精度误差,后面类型章节细说。

给约束起个名字

约束其实都有名字。你不写,Postgres 会自动起一个(比如 users_email_key)。但自动名不好记,出了错不好定位。推荐显式命名:

CREATE TABLE users (
    id         INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    username   VARCHAR(50) NOT NULL,
    email      VARCHAR(100) NOT NULL,
    CONSTRAINT uq_users_email UNIQUE (email)
);

这样万一邮箱重复报错,错误信息里会出现 uq_users_email,一眼就知道是哪条规则。表建多之后,起名是好习惯。

建表后的提示

语句成功会显示:

CREATE TABLE

如果报错,常见原因是语法拼错、括号没配对、或类型名写错。仔细检查逗号、括号和分号。

Tip

逗号是列与列之间的分隔符,最后一行列定义后面不要加逗号,这是最常见的语法错误。报错信息会指出出错位置,照着改就行。

查看表结构

建完想确认对不对,用 psql 的 \d 看结构。

看当前库所有表:

\dt

看某张表的详细结构:

\d users

输出类似:

                       Table "public.users"
   Column   |           Type           | Collation | Nullable | Default
------------+--------------------------+-----------+----------+----------------------------------
 id         | integer                  |           | not null | generated always as identity
 username   | character varying(50)    |           | not null |
 email      | character varying(100)   |           | not null |
 age        | integer                  |           |          |
 created_at | timestamp with time zone |           | not null | now()
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "users_email_key" UNIQUE CONSTRAINT, btree (email)

你看,约束都列在底下:主键 users_pkey、邮箱唯一 users_email_key。一目了然。

Tip

想看更全的信息(比如表大小、描述),用 \d+ users\d 系列命令是排查表结构的利器,建议熟记。

一个常见坑

CREATE TABLE Users (...);
SELECT * FROM users;

Postgres 里未加引号的标识符会自动转小写,所以 Usersusers 是同一张表,这没问题。但如果你建表时用了双引号 "Users",那它就真成了大写开头,以后查也必须带双引号。所以新手建议一律用小写,别用双引号裹表名,能省很多麻烦。

Warning

表名、列名尽量用小写字母、数字、下划线。中文或大小写混用虽然支持,但容易在后续查询里踩坑。比如 "UserName"username 在 Postgres 眼里是两个不同的列。

小结

建表就是列定义 + 类型 + 约束。记住自增用 GENERATED ALWAYS AS IDENTITY、主键用 PRIMARY KEY、非空用 NOT NULL。建完用 \d 表名 检查结构。

临时表(TEMPORARY TABLE)

有些中间结果只想在本次会话用一下,不想落进正式库,可以用临时表:

CREATE TEMP TABLE tmp_stat (
    user_id INT,
    cnt     INT
);

临时表只在当前连接可见,断开后自动销毁。做复杂统计时的「中转站」特别好用,不会污染正式表空间。

Note

临时表优先于普通表被找到,即使普通库里有个同名表,会话内写的 tmp_stat 也会先命中临时那个。用完断开连接它就消失了,别指望下次还能看到。

用 LIKE 复制表结构

想建一张和现有表结构一样的空表(不要数据),用 LIKE

CREATE TABLE users_bak (LIKE users INCLUDING ALL);

INCLUDING ALL 会把列、约束、索引一起复制过来,只是没有数据。做表备份骨架、或新建一张结构相同的表时很方便。

建表前先看是否存在

前面说过 IF NOT EXISTS。完整写法配上约束长这样:

CREATE TABLE IF NOT EXISTS users (
    id      INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    username VARCHAR(50) NOT NULL
);

表已有时,它只提示 NOTICE: relation "users" already exists,不报错。写初始化脚本(可能重复跑)时必加,避免第二次跑就崩。

表建在指定模式里

结合上一章,建表时带上模式前缀就落进对应模式:

CREATE TABLE app.users (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL
);

前提是 app 模式已存在,且你有在那建表的权限。不写前缀就按 search_path 落库,默认是 public

写完建表语句后该做什么

建完表别急着插数据,先两步自检:

  1. \d 表名 看结构对不对,列类型、约束是否如预期。
  2. 想得过的,先插一两行试试约束会不会误伤正常数据,再批量导入。
Tip

我习惯建完表立刻 \d+ 表名,把输出截图或记下来,作为「设计契约」。后面代码对着这个契约写,字段名、类型对不上就一眼能发现。

下一章我们会讲怎么修改已经建好的表。