PostgreSQL 完全指南 / 06 - 数据类型详解
第 06 章 · 数据类型详解
PostgreSQL 拥有所有关系型数据库中最丰富的类型系统。本章深入讲解各类数据类型的选择、用法和注意事项。
6.1 数值类型
| 类型 | 存储 | 范围 | 适用场景 |
|---|---|---|---|
SMALLINT | 2B | -32768 ~ 32767 | 状态码、年份 |
INTEGER / INT | 4B | -2^31 ~ 2^31-1 | 一般整数 |
BIGINT | 8B | -2^63 ~ 2^63-1 | 大 ID、计数器 |
NUMERIC(p,s) | 变长 | 任意精度 | 金额、精算 |
REAL | 4B | 6 位精度 | 科学计算 |
DOUBLE PRECISION | 8B | 15 位精度 | 浮点计算 |
SMALLSERIAL | 2B | 自增 | 小表自增 |
SERIAL | 4B | 自增 | 中表自增 |
BIGSERIAL | 8B | 自增 | 大表自增 |
整数类型选择
-- 状态字段用 SMALLINT
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
status SMALLINT NOT NULL DEFAULT 0 -- 0:待支付 1:已支付 2:已发货 3:已完成
);
-- 不要用 INTEGER 存状态(浪费空间)
-- 不要用 TEXT 存状态(性能差、无法做数值比较)
精确数值 NUMERIC
-- NUMERIC(精度, 小数位)
-- 金额必须用 NUMERIC,不要用 FLOAT!
CREATE TABLE financial (
id SERIAL PRIMARY KEY,
amount NUMERIC(15, 2), -- 最大 999,999,999,999.99
rate NUMERIC(8, 6), -- 最大 99.999999
quantity NUMERIC(10, 0) -- 整数,但不受 INTEGER 范围限制
);
-- FLOAT 的精度问题(为什么不用 FLOAT 存金额)
SELECT 0.1::FLOAT + 0.2::FLOAT; -- 0.30000000000000004 ❌
SELECT 0.1::NUMERIC + 0.2::NUMERIC; -- 0.3 ✅
⚠️ 注意事项:涉及金额、财务数据,必须使用 NUMERIC。FLOAT 有精度损失,会导致金额不一致。
SERIAL vs IDENTITY
-- 方式 1:SERIAL(传统方式)
CREATE TABLE t1 (id SERIAL PRIMARY KEY);
-- 等价于创建序列 + 设置默认值
-- 问题:可以直接 INSERT 指定 id 值,绕过序列
-- 方式 2:GENERATED ALWAYS AS IDENTITY(SQL 标准,推荐)
CREATE TABLE t2 (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY);
-- 不允许手动指定 id 值(除非使用 OVERRIDING SYSTEM VALUE)
INSERT INTO t2 (id) VALUES (1); -- ❌ ERROR: cannot insert into column "id"
INSERT INTO t2 OVERRIDING SYSTEM VALUE VALUES (1); -- ✅ 强制插入
-- 方式 3:GENERATED BY DEFAULT AS IDENTITY
CREATE TABLE t3 (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY);
-- 允许手动指定,不指定时自动生成
-- 迁移建议:新表用 IDENTITY,旧表逐步迁移
💡 技巧:BIGINT GENERATED ALWAYS AS IDENTITY 是 2024 年以后新表主键的最佳实践。
6.2 字符串类型
| 类型 | 存储 | 说明 |
|---|---|---|
VARCHAR(n) | 变长 | 有最大长度限制 |
CHAR(n) | 定长 | 不足补空格(几乎不用) |
TEXT | 变长 | 无长度限制,与 VARCHAR 性能相同 |
"char" | 1B | 单字节内部类型(不用) |
-- 推荐:直接用 TEXT
CREATE TABLE articles (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title TEXT NOT NULL, -- 不需要 VARCHAR(200)
content TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL -- 约束放在 UNIQUE 上,不在类型上
);
-- 如果需要长度约束,用 CHECK
ALTER TABLE articles ADD CONSTRAINT chk_title_len
CHECK (length(title) BETWEEN 1 AND 200);
字符串函数
SELECT
length('Hello 你好') AS len, -- 8(字符数)
bit_length('Hello') AS bits, -- 40(位数)
upper('hello') AS caps, -- HELLO
lower('HELLO') AS low, -- hello
initcap('hello world') AS title, -- Hello World
trim(' hello ') AS trimmed, -- hello
ltrim(' hello') AS left_trimmed, -- hello
rtrim('hello ') AS right_trimmed, -- hello
concat('Hello', ' ', 'World') AS joined, -- Hello World
'Hello' || ' ' || 'World' AS concat2, -- Hello World
left('Hello World', 5) AS left5, -- Hello
right('Hello World', 5) AS right5, -- World
substring('Hello World' from 1 for 5) AS sub, -- Hello
replace('Hello World', 'World', 'PG') AS replaced, -- Hello PG
split_part('a,b,c', ',', 2) AS split, -- b
repeat('ab', 3) AS repeated, -- ababab
reverse('Hello') AS reversed; -- olleH
6.3 布尔类型
CREATE TABLE flags (
id SERIAL PRIMARY KEY,
is_active BOOLEAN DEFAULT TRUE,
is_deleted BOOLEAN DEFAULT FALSE
);
-- 布尔字面量
SELECT TRUE, FALSE, NULL::BOOLEAN;
-- TRUE 值: TRUE, 't', 'true', 'y', 'yes', 'on', '1'
-- FALSE 值: FALSE, 'f', 'false', 'n', 'no', 'off', '0'
-- 布尔聚合
SELECT bool_and(is_active) FROM users; -- 所有都是 TRUE 则 TRUE
SELECT bool_or(is_active) FROM users; -- 任一为 TRUE 则 TRUE
SELECT every(is_active) FROM users; -- bool_and 的别名
6.4 日期和时间类型
| 类型 | 存储 | 说明 |
|---|---|---|
DATE | 4B | 仅日期 |
TIME | 8B | 仅时间(无时区) |
TIMETZ / TIME WITH TIME ZONE | 12B | 仅时间(有时区,不推荐) |
TIMESTAMP | 8B | 日期时间(无时区) |
TIMESTAMPTZ / TIMESTAMP WITH TIME ZONE | 8B | 日期时间(有时区,推荐) |
INTERVAL | 16B | 时间间隔 |
时区问题
-- TIMESTAMPTZ 存储的是 UTC 时间,显示时根据会话时区转换
SET timezone = 'Asia/Shanghai';
SELECT '2024-01-15 14:30:00+08'::TIMESTAMPTZ;
-- 存储: UTC 2024-01-15 06:30:00
-- 显示: 2024-01-15 14:30:00+08
SET timezone = 'America/New_York';
SELECT '2024-01-15 14:30:00+08'::TIMESTAMPTZ;
-- 显示: 2024-01-15 01:30:00-05
⚠️ 注意事项:永远使用 TIMESTAMPTZ。TIMESTAMP WITHOUT TIME ZONE 会导致时区混乱。
日期/时间函数
SELECT
now(), -- 当前时间(带时区)
current_date, -- 当前日期
current_time, -- 当前时间(带时区)
clock_timestamp(), -- 实时时钟(每行不同)
date_trunc('day', now()), -- 截断到天
date_trunc('month', now()), -- 截断到月
date_trunc('year', now()), -- 截断到年
extract(YEAR FROM now()), -- 提取年
extract(MONTH FROM now()), -- 提取月
extract(DOW FROM now()), -- 星期几(0=周日)
extract(EPOCH FROM now()), -- Unix 时间戳
age('2024-01-15'::DATE, '1990-06-01'::DATE), -- 年龄间隔
to_char(now(), 'YYYY-MM-DD HH24:MI:SS'), -- 格式化
to_timestamp(1700000000), -- Unix 时间戳转时间
'2024-01-15'::DATE + INTERVAL '30 days', -- 日期加减
'2024-12-31'::DATE - '2024-01-01'::DATE; -- 日期差(天数)
时区转换
-- 查看当前时区
SHOW timezone;
-- 设置时区
SET timezone = 'Asia/Shanghai';
-- 转换时区
SELECT
now() AT TIME ZONE 'UTC' AS utc_time,
now() AT TIME ZONE 'Asia/Shanghai' AS shanghai_time,
now() AT TIME ZONE 'America/New_York' AS ny_time;
-- 从无时区转换为带时区
SELECT '2024-01-15 14:30:00'::TIMESTAMP AT TIME ZONE 'Asia/Shanghai';
6.5 数组类型(Array)
PostgreSQL 原生支持数组类型,这是其独特优势之一。
-- 创建数组列
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
tags TEXT[] DEFAULT '{}', -- 文本数组
scores INTEGER[], -- 整数数组
prices NUMERIC(10,2)[] -- 数值数组
);
-- 插入数组
INSERT INTO products (name, tags, scores) VALUES
('Laptop', ARRAY['electronics', 'computers'], ARRAY[85, 90, 78]),
('Phone', '{"mobile", "electronics"}', '{95, 88, 92}'), -- 字面量语法
('Book', '{"education", "science"}', '{80, 85}');
-- 数组索引访问(1-based)
SELECT name, tags[1] AS first_tag, scores[2] AS second_score FROM products;
-- 数组操作
SELECT
array_length(tags, 1) AS tag_count, -- 数组长度
array_append(tags, 'new') AS appended, -- 追加元素
array_remove(tags, 'electronics') AS removed, -- 移除元素
array_cat(ARRAY[1,2], ARRAY[3,4]) AS concatenated, -- 拼接
array_position(tags, 'electronics') AS pos, -- 元素位置
unnest(tags) AS tag -- 展开为行
FROM products;
-- 数组查询
-- @> 包含
SELECT * FROM products WHERE tags @> ARRAY['electronics'];
-- <@ 被包含
SELECT * FROM products WHERE ARRAY['electronics'] <@ tags;
-- && 重叠
SELECT * FROM products WHERE tags && ARRAY['electronics', 'books'];
-- ANY
SELECT * FROM products WHERE 'electronics' = ANY(tags);
-- 数组索引
CREATE INDEX idx_products_tags ON products USING GIN (tags);
⚠️ 注意事项:数组类型虽然方便,但不要过度使用。如果元素需要独立查询、关联或有复杂属性,应该用关联表。数组适合简单的标签、分类等场景。
6.6 Range 类型
PostgreSQL 原生支持范围类型,非常适合表示区间。
内置 Range 类型
| 类型 | 基础类型 | 用途 |
|---|---|---|
INT4RANGE | INTEGER | 整数范围 |
INT8RANGE | BIGINT | 大整数范围 |
NUMRANGE | NUMERIC | 数值范围 |
TSRANGE | TIMESTAMP | 时间范围(无时区) |
TSTZRANGE | TIMESTAMPTZ | 时间范围(有时区) |
DATERANGE | DATE | 日期范围 |
-- Range 字面量
SELECT '[2024-01-01, 2024-12-31]'::DATERANGE; -- 闭区间
SELECT '[2024-01-01, 2024-12-31)'::DATERANGE; -- 左闭右开
SELECT '(0, 100]'::INT4RANGE; -- 左开右闭
-- 创建使用 Range 的表
CREATE TABLE reservations (
id SERIAL PRIMARY KEY,
room_id INT NOT NULL,
during TSTZRANGE NOT NULL,
EXCLUDE USING GIST (room_id WITH =, during WITH &&) -- 排他约束!
);
INSERT INTO reservations (room_id, during) VALUES
(1, '[2024-03-15 14:00, 2024-03-15 18:00)'),
(1, '[2024-03-16 09:00, 2024-03-16 12:00)');
-- 尝试插入重叠会失败(排他约束)
INSERT INTO reservations (room_id, during) VALUES
(1, '[2024-03-15 16:00, 2024-03-15 20:00)');
-- ERROR: conflicting key value violates exclusion constraint
-- Range 操作
SELECT
lower(during) AS start_time,
upper(during) AS end_time,
isempty(during) AS is_empty,
lower_inc(during) AS lower_inclusive,
upper_inc(during) AS upper_inclusive
FROM reservations;
-- Range 包含/重叠判断
SELECT * FROM reservations
WHERE during @> '2024-03-15 15:00'::TIMESTAMPTZ; -- 包含某时刻
-- 时间范围生成
SELECT * FROM reservations
WHERE during && '[2024-03-15, 2024-03-17]'::DATERANGE; -- 重叠
💡 技巧:EXCLUDE USING GIST 约束是 Range 类型的杀手级特性——可以在数据库层面保证时间/空间不重叠,比应用层校验更可靠。
6.7 自定义类型
-- 复合类型
CREATE TYPE address AS (
street TEXT,
city TEXT,
state TEXT,
zip_code VARCHAR(10)
);
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
billing_address address,
shipping_address address
);
-- 使用复合类型
INSERT INTO customers (name, billing_address) VALUES
('Alice', ROW('123 Main St', 'Shanghai', 'SH', '200000'));
-- 访问复合类型字段
SELECT
name,
(billing_address).city AS city,
(billing_address).zip_code AS zip
FROM customers;
-- 枚举类型
CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral', 'angry');
CREATE TABLE person (
id SERIAL PRIMARY KEY,
name TEXT,
current_mood mood DEFAULT 'neutral'
);
INSERT INTO person (name, current_mood) VALUES ('Bob', 'happy');
-- 向枚举添加新值
ALTER TYPE mood ADD VALUE 'excited' BEFORE 'neutral';
-- 域类型(基于现有类型添加约束)
CREATE DOMAIN positive_int AS INTEGER CHECK (VALUE > 0);
CREATE DOMAIN email_address AS VARCHAR(255)
CHECK (VALUE ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
email email_address NOT NULL,
login_count positive_int DEFAULT 1
);
6.8 网络地址类型
| 类型 | 说明 | 示例 |
|---|---|---|
CIDR | IP 地址/子网 | '192.168.1.0/24' |
INET | IP 地址 | '192.168.1.100' |
MACADDR | MAC 地址 | '08:00:2b:01:02:03' |
CREATE TABLE access_logs (
id BIGSERIAL PRIMARY KEY,
ip INET NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO access_logs (ip) VALUES
('192.168.1.100'), ('10.0.0.1'), ('::1');
-- 子网查询
SELECT * FROM access_logs WHERE ip <<= '192.168.1.0/24';
6.9 UUID 类型
-- 需要扩展生成 UUID
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY, -- UUID v4
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com') RETURNING id;
-- 返回类似: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
| 方式 | 说明 | 特点 |
|---|---|---|
gen_random_uuid() | UUID v4(随机) | PG 13+ 内置(pgcrypto) |
uuid_generate_v4() | UUID v4(uuid-ossp 扩展) | 需要安装扩展 |
uuid_generate_v1() | UUID v1(时间戳+MAC) | 可排序但暴露 MAC |
💡 技巧:UUID v7(PG 18 预计支持)结合了 UUID 的全局唯一性和时间有序性,是未来的推荐方案。
业务场景
| 场景 | 推荐类型 |
|---|---|
| 主键 | BIGINT GENERATED ALWAYS AS IDENTITY 或 UUID |
| 金额 | NUMERIC(15,2) |
| 时间戳 | TIMESTAMPTZ |
| 标签 | TEXT[] + GIN 索引 |
| 时间段 | TSTZRANGE + 排他约束 |
| IP 地址 | INET / CIDR |
| 状态机 | SMALLINT 或 ENUM |
| 文档字段 | JSONB |