# 列类型与约束

> 在实际的表定义中组合数据类型、默认值和约束。

---

LLMS 索引： [llms.txt](/llms.txt)

---

实际的表会为每一列选择一个类型，通常还会添加*约束*——数据库对每一行强制执行的规则。最常见的约束包括 `NOT NULL`（列必须有值）、`DEFAULT`（未提供值时使用该值）、`CHECK`（必须为真的布尔表达式）、`UNIQUE`、`PRIMARY KEY` 和 `REFERENCES`（外键）。每种约束都会单独讲解；这里把其中几种组合到一个贴近实际的表中。

## 本章要点 {#learn}

- 在一个 `CREATE TABLE` 中组合多种类型和约束
- 内联 `DEFAULT` 和 `NOT NULL`
- 用于业务规则的 `CHECK` 约束
- 使用 `timestamptz`，并以 `current_timestamp` 作为默认值
- 在系统目录中检查约束

## 示例 {#example}

```sql {num="17.1" caption="列类型与约束"}
DROP TABLE IF EXISTS products;

CREATE TABLE products (
  id          integer PRIMARY KEY,
  name        text NOT NULL,
  price       numeric(10, 2) NOT NULL CHECK (price >= 0),
  in_stock    boolean NOT NULL DEFAULT true,
  category    text DEFAULT 'misc',
  created_at  timestamptz NOT NULL DEFAULT current_timestamp,
  CONSTRAINT name_not_blank CHECK (length(trim(name)) > 0)
);

INSERT INTO products (id, name, price)              VALUES (1, 'Widget',    19.99);
INSERT INTO products (id, name, price, in_stock)    VALUES (2, 'Gadget',    49.99, false);
INSERT INTO products (id, name, price, category)    VALUES (3, 'Sprocket',  4.95, 'parts');

SELECT * FROM products ORDER BY id;

-- Inspect the constraints on the table
SELECT conname, pg_get_constraintdef(oid) AS def
FROM pg_constraint
WHERE conrelid = 'products'::regclass
ORDER BY conname;
```

每个约束都是数据库对你的数据作出的一项承诺。`NOT NULL` 表示该值始终存在。`DEFAULT` 会在 `INSERT` 省略该列时提供一个值。`CHECK (expr)` 会拒绝 `expr` 为假的任何行（NULL 值会通过）。`PRIMARY KEY` 意味着 `NOT NULL` 和 `UNIQUE`，并会创建索引。

约束可以是*内联*的（位于列类型之后，如上），也可以是*表级*的（位于所有列之后）。复合的 `PRIMARY KEY`、`UNIQUE` 和外键约束必须使用表级语法。`CHECK` 在两种位置都可以引用多个列；列约束和表约束都可以用 `CONSTRAINT name` 命名，以便生成更清晰的错误信息。

`timestamptz NOT NULL DEFAULT current_timestamp` 是最常见的列定义形式之一——行的创建时间存储为 UTC，永不为空，并自动设置。

## 运行示例 {#run}

运行方式：

```text
$ psql -f source/column-types-and-constraints.sql postgres
DROP TABLE
CREATE TABLE
INSERT 0 1
INSERT 0 1
INSERT 0 1
 id |   name   | price | in_stock | category |          created_at
----+----------+-------+----------+----------+-------------------------------
  1 | Widget   | 19.99 | t        | misc     | 2024-xx-xx xx:xx:xx.xxxxxx+00
  2 | Gadget   | 49.99 | f        | misc     | 2024-xx-xx xx:xx:xx.xxxxxx+00
  3 | Sprocket |  4.95 | t        | parts    | 2024-xx-xx xx:xx:xx.xxxxxx+00
(3 rows)
 ...
```

## 常见陷阱 {#pitfalls}

- 忘记在本不应为空的列上添加 `NOT NULL`——以后添加需要先回填数据，而且在大表上可能需要长时间持有锁。
- `CHECK` 在表达式为 `NULL` 时接受该行。若要拒绝 NULL 值，请将 `NOT NULL` 与 `CHECK` 结合使用。
- 将 `now()` 用作 `DEFAULT`：它确实有效，但在长事务中，每个默认值都是事务开始时的时间。通常这没有问题；如果确实需要每一行的挂钟时间，请使用 `clock_timestamp()`。
- 过去，在超大表上为新列添加 `DEFAULT` *和* `NOT NULL` 会重写整个表；现代 PostgreSQL（11+）在默认值为常量时会避免这种重写，但易变的默认值仍会触发表重写。

> [!TIP] 提示
> 为约束命名。PostgreSQL 生成的默认约束名虽然实用，却晦涩难懂。在生产环境的错误信息中出现 `products_price_nonneg` 这样的名称，会有很大帮助。

> [!NOTE] 动手练习
> 尝试执行 `INSERT INTO products (id, name, price) VALUES (4, '   ', 5);`，观察 `name_not_blank` 检查约束拒绝该插入。再尝试执行 `INSERT INTO products (id, name, price) VALUES (5, 'Negative', -1);`，看看价格检查如何拒绝该行。然后在表创建之后添加一个约束：`ALTER TABLE products ADD CONSTRAINT category_valid CHECK (category IN ('misc','parts','tools'));`。

## 下载 {#downloads}

SQL 文件：[column-types-and-constraints.sql](/examples/source/column-types-and-constraints.sql) · [全部示例文件](/examples/examples.zip) · [准备工作](/#prerequisites)
