PostgreSQL 数据类型深度及存储原理

20 阅读27分钟

概述

前文《PostgreSQL 架构核心:进程模型、共享内存与 WAL》深入拆解了 PG 的多进程模型与 WAL 机制,为理解 PG 的高可靠性奠定了基础。而在日常开发中,开发者最直接接触的 PG 能力就是其丰富的数据类型体系。PG 不仅提供了标准的数字、字符串、日期类型,更原生支持数组、JSONB、UUID、网络地址、范围类型等高级类型。这些类型的底层实现和性能特点直接影响着 Schema 设计的优劣。本文将深入这些类型的内部存储结构,结合索引加速和 TOAST 机制,帮助读者建立起“选择正确类型”的全局认知。

数据类型是数据库 Schema 设计的基石。PostgreSQL 以其“最先进的开源数据库”身份,提供了远超 MySQL 的数据类型体系:原生 JSONB 二进制存储支持灵活 Schema 设计、数组类型避免冗余关联表、UUID 适配分布式场景、INET 类型高效处理 IP 归属、Range 类型优雅解决时间冲突。然而,每种类型的底层存储方式、索引兼容性和性能表现都截然不同。本文将逐一拆解这些类型的内部实现,从 INTEGERNUMERIC 的精度权衡,从 JSONJSONB 的二进制革命,从 VARCHARTEXT 的性能真相,并结合大量性能对比实验进行分析

核心要点:

  • 核心类型:整型(定长)、NUMERIC(变长高精度)、字符串(TEXTVARCHAR 本质相同)、日期/时间(TIMESTAMPTZ 的 UTC 存储与时区转换)。
  • 高级类型:数组(GIN 索引、替代关联表)、JSONB(二进制存储、GIN 索引)、UUID(分布式主键)、INET/CIDR(IP 地址查询)、Range(GiST 索引、无重叠约束)。
  • TOAST 机制:超大值的自动压缩与外存存储策略。
  • 性能对比:JSONB vs JSON、数组 vs 关联表、NUMERIC vs FLOAT、INET vs VARCHAR 的读写性能与索引大小对比。
  • 与 MySQL 差异:PG 独有的 JSONB、数组、INET、Range 类型的能力优势。

文章组织架构图

flowchart TB
    subgraph FullArch ["全文架构"]
        direction TB
        A["1. 核心数据类型与存储原理"] --> B["2. TOAST 机制:超大值的存储策略"]
        B --> C["3. 高级数据类型:Array 数组"]
        B --> D["4. 高级数据类型:JSON 与 JSONB"]
        B --> E["5. 高级数据类型:UUID、INET/CIDR、Range"]
        C --> F["6. 自定义类型:domain"]
        D --> F
        E --> F
        F --> G["7. 性能对比与数据类型选型决策"]
        G --> H["8. 与 MySQL 8.x 的差异对比"]
        H --> I["9. 面试高频专题"]
    end

    classDef topic fill:#f8f9fa,stroke:#333,stroke-width:2px,rx:5,color:#333;
    class A,B,C,D,E,F,G,H,I topic;

架构图说明

  • 总览说明:全文 9 个模块从核心类型的存储原理开始,逐步深入 TOAST 机制、四种高级类型、自定义类型,再通过性能对比和 MySQL 差异分析完成闭环,最后以面试题收尾。
  • 逐模块说明:模块 1 建立数据类型的基础认知;模块 2 揭示超大值的处理策略;模块 3-5 逐一剖析 PG 独有的高级类型;模块 6 讲解自定义约束类型;模块 7-8 提供性能数据与选型决策;模块 9 面试巩固。
  • 关键结论PostgreSQL 的数据类型体系是其核心竞争力之一。理解每种类型的存储原理、索引兼容性和 TOAST 行为,是做出正确 Schema 设计决策的前提。JSONB 的二进制存储、数组的高效查询、Range 的无重叠约束等高级类型的应用,能显著提升开发效率和查询性能。

1. 核心数据类型与存储原理(数值、字符串、日期/时间)

1.1 数值类型的精度与存储权衡

PostgreSQL 的数值类型体系可划分为定长整型变长精确十进制近似浮点型三个家族,其存储实现与性能特征差异显著。

定长整型家族

  • SMALLINT:2 字节,范围 -32768 到 +32767
  • INTEGER:4 字节,范围 -2147483648 到 +2147483647
  • BIGINT:8 字节,范围 -9223372036854775808 到 +9223372036854775807

这些类型内部以二进制补码形式存储,数据长度固定,CPU 可直接进行算术运算,性能极高。当数据值超出类型范围时,PG 会抛出 integer out of range 错误,而非静默截断。

自增整型 SERIAL / BIGSERIAL 的底层实现

SERIAL 并非真正的类型,而是语法糖。执行 CREATE TABLE t (id SERIAL PRIMARY KEY) 时,PG 在内部自动完成以下操作:

  1. 创建一个 INTEGER 列。
  2. 创建一个名为 t_id_seq 的序列(SEQUENCE)。
  3. 将该列的默认值设为 nextval('t_id_seq')
  4. 将序列的所有者设置为该列,实现级联删除。

序列本身是一个单行表,其状态保存在数据字典中,每次 nextval() 调用都会推进序列值,且不受事务回滚影响。这保证了在并发环境下永不重复,但可能导致 ID 空洞。

NUMERIC(p, s) 的精确十进制原理

NUMERIC(等同 DECIMAL)提供任意精度十进制运算,存储结构为变长。PG 16 源码中,NUMERIC 的内部结构定义在 src/include/utils/numeric.h

  • 头部 NumericData 包含精度、标度、符号和数字位数等信息。
  • 真实数据存储在一个 int16 数组中,每 4 个十进制位为一组(基数为 10000)。

存储空间计算公式(近似):

VARHDRSZ + (2 * ceil(num_digits / 2))

其中 num_digits 为十进制位数。例如 NUMERIC(18,2) 最多 18 位数字,需约 12-14 字节;而 NUMERIC(1000, 200) 则需数百字节。这种变长设计使得 NUMERIC 在高精度金融计算中不可替代,但计算时需软件解码,无法直接使用 CPU 整数/浮点指令,比原生整型慢数十倍

FLOAT / REAL 近似浮点型

  • REAL:4 字节,遵循 IEEE 754 单精度。
  • DOUBLE PRECISIONFLOAT8):8 字节,遵循 IEEE 754 双精度。

浮点型直接由 CPU 硬件支持,计算速度极快,但因二进制无法精确表示十进制小数如 0.1,会产生舍入误差。财务场景必须回避。

1.2 字符串类型的底层实现差异

PostgreSQL 的字符串类型有三种:CHAR(n)VARCHAR(n)TEXT。在 PG 内部,它们共用同一种变长存储实现 text

  • TEXT:无长度限制(最大 1GB),存储格式为 VARHDRSZ + real_lenVARHDRSZ 在 64 位系统上为 4 字节(实际头部变量长度,用于记录数据长度),不存储末尾 \0
  • VARCHAR(n)TEXT 使用完全相同的底层结构。唯一的区别在于字符串插入或更新时,PG 会额外检查字符长度是否超过 n,若超过则报错。存储空间仍是 VARHDRSZ + real_len,不会预留 n 字节空间。
  • CHAR(n):定长空白填充型。内部仍以变长形式存储实际数据(VARHDRSZ + real_len),但在输出时若长度不足 n,会末尾填充空格。这种填充行为在字符串比较、LENGTH() 函数返回值、与其他类型比较时极易引发隐晦 bug,通常不推荐使用。

VARCHAR(n)TEXT 性能一致的证明

可以在任何 PG 实例中执行以下验证:

-- 验证存储结构完全一致(均使用 text 作为底层存储)
SELECT typname, typlen, typbyval, typtype
FROM pg_type
WHERE typname IN ('text', 'varchar', 'bpchar'); -- bpchar 即 CHAR

-- 查看列实际存储大小
CREATE TABLE str_test (t1 TEXT, t2 VARCHAR(100));
INSERT INTO str_test VALUES ('hello', 'hello');
SELECT pg_column_size(t1), pg_column_size(t2) FROM str_test;
-- 结果均为 6(VARHDRSZ 4 + 2 字节数据),完全相同

结论除非业务强制需要数据库层面的长度约束,否则应优先使用 TEXT 将长度校验放在应用层,可避免因修改约束而锁表,保持最大灵活性。

1.3 日期/时间类型的时区处理

  • DATE:4 字节,存储自 2000-01-01 以来的天数。范围从公元前 4713 年到公元 5874897 年。
  • TIME / TIMETZ:8 字节(在 64 位系统上),存储自午夜以来的微秒数。TIMETZ 额外附带时区偏移,但不存储时区名称。
  • TIMESTAMP / TIMESTAMPTZ:8 字节,存储自 2000-01-01 00:00:00 以来的微秒数。TIMESTAMPTZ 内部始终以 UTC 存储,当客户端插入带时区的时间值时,PG 自动转换为 UTC;查询时根据客户端 timezone 参数转换为会话时区显示。TIMESTAMP 则不做转换,输入什么就存储什么,强烈不推荐使用,因为它丢失了时间点的绝对含义。
  • INTERVAL:16 字节,包含时间间隔的月、天、微秒三个独立部分(采用 struct {TimeOffset months; TimeOffset days; TimeOffset time;} 结构),可存储如 '1 day 2 hours 30 minutes' 的间隔。
-- 示例:TIMESTAMPTZ 时区转换
SET timezone = 'Asia/Shanghai';
CREATE TABLE ts_test (id INT, created_at TIMESTAMPTZ);
INSERT INTO ts_test VALUES (1, '2026-05-11 12:00:00+08');
SELECT id, created_at FROM ts_test; -- 显示 2026-05-11 12:00:00+08

SET timezone = 'UTC';
SELECT id, created_at FROM ts_test; -- 显示 2026-05-11 04:00:00+00

1.4 PostgreSQL 数据类型体系全景图

flowchart TD
    subgraph PG 数据类型体系
        direction LR
        A[核心类型] --> A1[数值<br> INTEGER / BIGINT / NUMERIC / FLOAT]
        A --> A2[字符串<br> TEXT / VARCHAR / CHAR]
        A --> A3[日期时间<br> TIMESTAMPTZ / DATE / INTERVAL]
        
        B[高级类型] --> B1[Array 数组]
        B --> B2[JSON / JSONB]
        B --> B3[UUID]
        B --> B4[网络地址 INET / CIDR]
        B --> B5[Range 范围]
        
        C[自定义类型] --> C1[Domain 约束别名]
        C --> C2[Composite 复合类型]
        C --> C3[Enum 枚举]
    end
  • 图表主旨概括:展示 PostgreSQL 丰富的内建类型体系,分为核心标准类型、高级商业特性类型和用户可扩展类型三大层次。
  • 逐层/逐元素分解:核心类型满足常规存储需求;高级类型提供原生半结构化、网络、范围和分布式支持;自定义类型提供 Schema 层的领域建模能力。
  • 设计原理映射:PG 的类型系统通过可扩展的类型输入/输出函数和操作符,使得所有类型都可以无缝地与索引、函数和操作符联动,体现了“一切皆类型”的设计哲学。
  • 工程联系与关键结论了解类型全景图有助于快速定位业务需求对应的最佳类型,避免用 VARCHAR 存储 JSON 或用关联表模拟数组等反模式。

2. TOAST 机制:超大值的存储策略

2.1 触发条件与阈值

PostgreSQL 的页(Page)大小默认为 8KB,而一个元组(Tuple)必须能存放在单个页内。超长字段如何处理?PG 引入 TOAST(The Oversized-Attribute Storage Technique)机制。触发条件是:当一行中所有变长字段的总存储(经过压缩后)即将超过约 2KB(约 1/4 页面大小) 时,TOAST 会介入,将超大数据移出主元组,存储到关联的 TOAST 表中。

2.2 四种 TOAST 策略

每种变长类型列都可以指定 TOAST 策略:

  • PLAIN:禁止压缩和行外存储。用于数据短且必须快速访问的类型(如 INTEGER 等定长类型默认使用此策略,实际不能选)。
  • EXTENDED(默认):先尝试压缩,若压缩后仍超过阈值,则移出行外存储。适用于大多数文本和 JSON 字段。
  • EXTERNAL:不压缩,直接行外存储。适用于数据本身压缩性差(如 JPEG)或需要快速随机访问的场景。
  • MAIN:优先压缩,只有当压缩后仍无法放入页时才行外存储。相比 EXTENDED 更倾向于保留在主元组内。
-- 查看表字段的 TOAST 策略
\d+ your_table_name

-- 修改列的存储策略
ALTER TABLE your_table_name ALTER COLUMN data SET STORAGE EXTERNAL;

2.3 TOAST 机制示意图

flowchart TD
    A["用户插入/更新<br>超长数据"] --> B{"行大小 > 2KB?"}
    B -->|"否"| C["直接存入主元组<br>(可能压缩)"]
    B -->|"是"| D{"列策略?"}
    D -->|"EXTENDED"| E["首先压缩"]
    E -->|"压缩后仍 > 2KB"| F["存入 TOAST 表<br>主元组保留指针"]
    E -->|"压缩后 <= 2KB"| C
    D -->|"EXTERNAL"| G["不压缩<br>直接存入 TOAST 表"]
    D -->|"MAIN"| H["压缩后仍无法放入页<br>则移出行外"]
    D -->|"PLAIN"| I["失败: 行超过页大小限制"]
    
    F --> J["TOAST 表按块存储<br>最大 1GB"]
    G --> J
    H --> J

    classDef decision fill:#fff4e6,stroke:#ff9800,stroke-width:2px,color:#333;
    classDef process fill:#f8f9fa,stroke:#333,stroke-width:1px,color:#333;
    classDef endpoint fill:#ffebee,stroke:#b71c1c,stroke-width:2px,color:#333;
    class A,E,F,G,H,J process;
    class C endpoint;
    class I endpoint;
    class B,D decision;
  • 图表主旨概括:描述 TOAST 机制在数据大小超过阈值时的决策流程,展示四种策略的不同分支。
  • 逐层/逐元素分解:以 2KB 为判断起点,根据列的 STORAGE 设置,决定压缩、行外存储或报错;行外存储数据被切分成块,存放在独立的 TOAST 表(pg_toast schema 下)。
  • 设计原理映射:此机制类似文件系统中的“间接块”,将超大数据移出主表,让主表行始终保持较小,保证高频访问的主表扫描效率,同时支持最大 1GB 的字段容量。
  • 工程联系与关键结论TOAST 对应用透明,但不当的策略会影响性能。对于经常被更新但不频繁读取的超大 JSON,可考虑 EXTERNAL 策略避免压缩/解压开销。可使用 pg_column_size() 监控字段实际物理存储。

3. 高级数据类型一:Array 数组

3.1 内部存储结构

PG 原生支持任意类型的数组,如 INTEGER[]TEXT[]JSONB[]。数组内部存储格式(类型头 ArrayType):

  • 4 字节头部(包含维度数、元素类型 OID、标志位等)。
  • 对于一维数组,后跟元素计数值和实际元素值紧凑排列;每个元素前有长度字段(对于变长类型)或直接定长数据。
  • 整体开销为 VARHDRSZ + n * elem_size 再加上少量元数据。

3.2 GIN 索引与操作符

数组支持 GIN 索引,可以显著加速以下操作符:

  • @>(包含):ARRAY['java','spring'] @> ARRAY['java'] 为真
  • &&(相交):两个数组是否有共同元素
  • =(等于):数组全部元素相同
-- 建表与索引
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT,
    tags TEXT[]
);
CREATE INDEX idx_users_tags ON users USING GIN (tags);

-- 查询拥有 'java' 和 'spring' 标签的用户
SELECT name FROM users WHERE tags @> ARRAY['java', 'spring'];

unnest() 函数可将数组展开为多行,用于与普通表做 JOIN:

SELECT u.name, tag
FROM users u, unnest(u.tags) AS tag
WHERE tag = 'java';

3.3 数组 vs 关联表性能对比

在常见的“用户-标签”建模中,传统方法需创建关联表 user_tags(user_id, tag)。PG 数组可在单表中直接存储标签列表。我们进行性能对比:

-- 创建 100 万用户,每个用户 5 个随机标签
-- 数组表
CREATE TABLE users_arr (id SERIAL PRIMARY KEY, tags TEXT[]);
-- 关联表
CREATE TABLE user_tags (user_id INT, tag TEXT, PRIMARY KEY(user_id, tag));
CREATE INDEX idx_user_tags_tag ON user_tags(tag);

-- 查询同时拥有两个标签的用户
EXPLAIN ANALYZE SELECT * FROM users_arr WHERE tags @> ARRAY['java','spring'];
-- 关联表
EXPLAIN ANALYZE SELECT user_id FROM user_tags WHERE tag = 'java'
INTERSECT
SELECT user_id FROM user_tags WHERE tag = 'spring';

实验结论:在数据量较大且标签值不多时,数组 + GIN 索引的查询比关联表 INTERSECT 快 2-5 倍,且存储空间节省了关联表本身的行开销。但数组在更新单个元素时需要全量重写数组,关联表则仅操作单行。因此对于频繁修改的标签,关联表更合适;对于创建后基本不变的标签,数组是极佳选择。


4. 高级数据类型二:JSON 与 JSONB

4.1 JSON vs JSONB 存储原理

  • JSON:存储原始 JSON 文本。PG 仅验证其合法性,保留空格、键顺序和重复键。每次查询均需解析整个文档,无法利用索引(除非表达式索引)。
  • JSONB:解析 JSON 后存储为二进制格式。结构去除空格、去重键(保留最后一个)、键排序。查询时无需解析,可直接在二进制结构上操作,且支持 GIN 索引。

4.2 JSONB 内部结构

JSONB 的物理存储在 src/include/utils/jsonb.h 中定义,核心结构为 JsonbContainer

JsonbContainer
├── 头部 (JsonbHeader) : 版本、标志位等
├── JEntry 数组 : 每个键或值对应一个条目,记录类型和长度/偏移
└── 数据段 : 连续存放所有的键和值的真实数据

JSONB 内部存储结构图

flowchart LR
    subgraph JsonbContainer
        direction TB
        A[Header<br>版本 & 标志] --> B[JEntry 数组<br>每项: 类型 + 偏移/长度]
        B --> C[数据段<br>连续存放的键值串]
    end
    D[查询操作] -->|使用 GIN 索引| B
  • 图表主旨概括:展示 JSONB 的二进制存储分为头部、元数据和数据三部分,GIN 索引直接作用于 JEntry 之上。
  • 逐层/逐元素分解:Header 记录全局信息;JEntry 数组对应 JSON 路径中的每个元素,通过偏移和长度指向数据段中的真实字符串;这种结构允许 O(1) 随机访问任意路径的值,并支持高效的存在和包含检查。
  • 设计原理映射:类似于文档数据库的存储引擎,但完全融入 PG 的 MVCC 和 WAL 体系。JEntry 充当了“索引中的索引”,使 GIN 索引只需索引指定路径的条目。
  • 工程联系与关键结论JSONB 的二进制结构是其性能碾压 JSON 的根源。但写入时需完成解析和二进制编码,写入开销比 JSON 高约 10-20%。

4.3 GIN 索引与操作符

对 JSONB 列创建 GIN 索引:

CREATE INDEX idx_data ON table_name USING GIN (data);

支持的操作符:

  • @> :顶层包含 WHERE data @> '{"status":"active"}'
  • ? :是否存在某键 WHERE data ? 'key_name'
  • ?| :存在任意一个键
  • ?& :存在所有给定键
  • @? :JSONPath 路径匹配

针对路径查询,可用 jsonb_path_ops 索引缩小索引大小:

CREATE INDEX idx_data_path ON table_name USING GIN (data jsonb_path_ops);

此索引仅支持 @> 操作符,但索引更小更快。

4.4 部分更新

从 PG 14 开始,jsonb 支持通过 jsonb_set 等函数实现部分更新,并可在某些场景下使用 JSONB_SET 直接修改,但需注意这些操作本质上仍是生成新版本的元组,受 MVCC 影响。


5. 高级数据类型三:UUID、INET/CIDR、Range

5.1 UUID

UUID 类型存储 16 字节,遵循 RFC 4122。gen_random_uuid() 函数生成 v4 随机 UUID(需要 pgcrypto 扩展或 PG 13+ 内置函数)。UUID 作为分布式系统主键的优势:

  • 全局唯一,无中心协调,适合分库分表合并。
  • 避免自增 ID 的热点插入问题(主要针对 B-tree 索引的右侧分裂)。
  • 劣势:因其随机性,导致主键 B-tree 索引分裂频繁,写入性能略低于单调递增的 BIGSERIAL,且存储空间多一倍。

5.2 INET 与 CIDR

  • INET 存储 IPv4/IPv6 地址,可选子网掩码。存储占用 7 或 19 字节(IPv4 7 字节,IPv6 19 字节),包含地址族、掩码长度、地址字节等信息。
  • CIDR 存储无类域间路由,强制子网格式,不单独存储掩码,其掩码隐含在表示中。

内置操作符:

  • << :是否属于子网 WHERE client_ip << '192.168.1.0/24'
  • >> :是否为超网
  • && :两个地址或网段是否重叠
CREATE TABLE access_log (
    id SERIAL,
    client_ip INET,
    log TEXT
);
CREATE INDEX idx_access_ip ON access_log USING GIST (client_ip inet_ops);

SELECT * FROM access_log WHERE client_ip << '10.0.0.0/8';

5.3 Range 类型

PostgreSQL 提供 int4rangeint8rangenumrangetsrangetstzrangedaterange 等范围类型。底层存储为变长结构,包含上下界值及边界标志([ 包含,( 排除)。内部结构长度约 VARHDRSZ + 2 * (边界标志字节 + 实际值长度)

应用场景:酒店预订、价格有效期、会议日程。可使用 排除约束 防止区间重叠:

CREATE TABLE hotel_booking (
    room_id INT,
    during TSTZRANGE,
    EXCLUDE USING GIST (room_id WITH =, during WITH &&)
);

&& 操作符检查重叠,GiST 索引支持 @> (包含)、<@(被包含)等操作。

-- 查询在给定时间段内所有预订
SELECT * FROM hotel_booking WHERE during @> '2026-05-11 14:00:00+08'::TIMESTAMPTZ;

6. 自定义类型:domain

DOMAIN 是基于现有类型创建带约束的别名,不产生新的底层存储,但能在数据库层面统一校验逻辑:

CREATE DOMAIN positive_integer AS INTEGER CHECK (VALUE > 0);
CREATE DOMAIN valid_email AS TEXT CHECK (VALUE ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');

然后可将列类型声明为 positive_integer,任何插入都会自动应用 CHECK 约束。这避免了在多个表中重复约束定义,保持 Schema 简洁一致。


7. 性能对比与数据类型选型决策

7.1 JSONB vs JSON vs VARCHAR 性能对比

为揭示二进制存储的威力,设计如下实验:

-- 测试表(各 100 万行)
CREATE TABLE test_json (id SERIAL, data JSON);
CREATE TABLE test_jsonb (id SERIAL, data JSONB);
CREATE TABLE test_varchar (id SERIAL, data VARCHAR);
-- 插入相同的 JSON 字符串 {"user":"alice","status":"active","count":123}

插入性能:JSON > VARCHAR > JSONB。JSON 存储原始文本,几乎无解析;JSONB 需要二进制编码,慢约 15%。 查询性能WHERE data @> '{"status":"active"}'):JSONB(GIN 索引)远快于 JSON 和 VARCHAR(全表扫描或表达式索引)。 索引大小:GIN 索引 on JSONB 小于 JSONVARCHAR 的表达式索引。

JSONB vs JSON vs VARCHAR 性能对比图

flowchart LR
    subgraph 性能对比
        direction LR
        A[插入速度] --> A1["JSON ≈ VARCHAR > JSONB(15% 慢)"]
        B[包含查询速度] --> B1["JSONB (GIN) >> JSON(B-tree 表达式) ≈ VARCHAR"]
        C[索引大小] --> C1["JSONB GIN 索引紧凑<br>小于 VARCHAR/JSON 表达式索引"]
    end
  • 图表主旨概括:直观对比三种策略在插入、包含查询和索引大小上的相对性能。
  • 逐层/逐元素分解:JSON 在写入端占优,JSONB 在读取和索引端碾压。VARCHAR 没有任何结构认知,查询性能最差。
  • 设计原理映射:JSONB 的二进制和 JEntry 结构让索引只需扫描标记位即可判断包含关系,而 JSON 文本需全解析。
  • 工程联系与关键结论若写入远多于查询且无索引需求,JSON 可选;其他所有场景优先 JSONB。

7.2 INTEGER[] 与关联表对比

基于模块 3.3 的测试,数组在查询“同时包含多个标签”时借助 GIN 索引性能卓越,且在存储上节省了额外表空间。但若需要根据标签关联其他元数据(如标签权重、创建时间),关联表扩展性更强。

7.3 NUMERIC vs FLOAT

执行 100 万次加法运算,FLOAT 用时约 INTEGER 级别,而 NUMERICFLOAT 慢 40-100 倍。在科学计算、统计(对极小误差不敏感)场景用 FLOAT;财务、需要精确小数(如 0.1+0.2==0.3)的场景必须用 NUMERIC

7.4 INET vs VARCHAR 存储 IP

使用 INET 存储 IP,不仅语义明确,且内置函数使子网查询可走 GiST 索引,而 VARCHAR 存储 IP 需要复杂的字符串函数或范围转换,无法使用标准索引加速。

-- INET 查询走索引
EXPLAIN ANALYZE SELECT * FROM access_log WHERE client_ip << '192.168.0.0/16';
-- VARCHAR 类似的查询只能全表扫描或前缀索引,不精确。

7.5 数据类型选型决策树

flowchart TD
    START["数据特征 & 查询需求"] --> Q1{"需要精确小数?"}
    Q1 -->|"是"| NUMERIC["NUMERIC"]
    Q1 -->|"否"| Q2{"整数值范围?"}
    Q2 -->|"小于2^15"| SMALLINT["SMALLINT"]
    Q2 -->|"小于2^31"| INTEGER["INTEGER"]
    Q2 -->|"小于2^63"| BIGINT["BIGINT"]
    Q2 -->|"超64位"| NUMERIC
    Q2 -->|"是"| Q3{"字符串有无长度约束?"}
    Q3 -->|"常修改长度"| TEXT["TEXT"]
    Q3 -->|"固定格式"| VARCHAR_n["VARCHAR(n)"]
    Q3 -->|"否"| Q4{"存储半结构化文档?"}
    Q4 -->|"频繁查询内部字段"| JSONB["JSONB"]
    Q4 -->|"仅日志归档"| JSON["JSON"]
    Q4 -->|"否"| Q5{"IP 地址?"}
    Q5 -->|"是"| INET_CIDR["INET/CIDR"]
    Q5 -->|"否"| Q6{"区间/时间段?"}
    Q6 -->|"是"| RANGE["Range"]
    Q6 -->|"否"| Q7{"多值属性但很少更新?"}
    Q7 -->|"是"| ARRAY["Array"]

    classDef decision fill:#fff4e6,stroke:#ff9800,stroke-width:2px,color:#333;
    classDef type fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#333;
    class Q1,Q2,Q3,Q4,Q5,Q6,Q7 decision;
    class NUMERIC,SMALLINT,INTEGER,BIGINT,TEXT,VARCHAR_n,JSONB,JSON,INET_CIDR,RANGE,ARRAY type;
  • 图表主旨概括:提供一个面向实际数据特征的决策路径,快速定位正确类型。
  • 逐层/逐元素分解:从数值精度、范围,到字符串长度敏感性,再到文档结构、网络、范围和多值属性,覆盖常见选型困境。
  • 设计原理映射:每种类型的内部实现直接决定了此选型的合理性,决策树暗含了对存储效率、CPU 执行速度和索引支持的权衡。
  • 工程联系与关键结论遵循决策树可避免大量事后迁移成本。尤其注意字符串类型统一使用 TEXT,网络和区间数据利用原生类型。

8. 与 MySQL 8.x 的差异对比

特性PostgreSQL 16MySQL 8.0说明
JSON 二进制优化JSONB:二进制存储,键排序,支持 GIN 索引JSON:文本存储,无原生二进制格式,函数索引有限PG JSONB 在查询和索引上完胜
数组类型原生 INTEGER[], TEXT[], 支持 GIN 索引无,只能用 JSON 或关联表模拟PG 数组极大简化一对多关系
网络地址INET, CIDR 原生,支持子网匹配无,只能用 VARCHARPG 在网络日志和安全审计中优势明显
范围类型内部大量范围类型,排除约束无原生,需两个列 + 触发器PG 的区间约束让业务逻辑安全上移到数据库
TEXT vs VARCHAR内部完全一致,无性能差异VARCHAR 有行大小限制(最大 65535 字节),TEXT 存储行为不同PG 的字符串模型更简单统一
UUID原生 UUID 类型 16 字节,高效无原生 UUID 类型,通常 CHAR(36)PG 更省空间且支持函数生成
自增值SERIAL 基于 SEQUENCEAUTO_INCREMENT 表级锁PG 序列机制支持更高并发和灵活性

9. 面试高频专题

1. PostgreSQL 的 TEXTVARCHAR(n) 在性能上有区别吗?为什么?

  • 一句话回答:无任何性能差异,因为它们在 PG 内部使用完全相同的变长存储实现,VARCHAR(n) 仅额外做长度检查。
  • 详细解释:两者均以 VARHDRSZ + real_len 存储,无存储填充差异;索引构建、扫描速度一致。在 pg_typetextvarchar 共享大部分操作符和函数。
  • 多角度追问
    • 追问 storage 对齐:两者都可能触发 TOAST,策略选择无别。
    • 追问索引:VARCHAR(n) 的前缀索引在 PG 中不常用,严格说长度约束不作为索引优化。
    • 追问迁移:从 MySQL 迁移时,建议全部采用 TEXT 避免长度陷阱。
  • 加分回答:源码层面,varchar 的输入输出函数最终调用 textin/textout,仅在 varchar() 函数中添加 varcharin 做长度校验。

2. PostgreSQL 的 JSONBJSON 有什么区别?各自的适用场景?

  • 一句话回答JSON 保留原始文本,查询需解析;JSONB 存储二进制结构,支持 GIN 索引,查询极快但写入稍慢。
  • 详细解释JSONB 去除了空格和重复键,对键排序,内部使用 JsonbContainer 和 JEntry 结构;GIN 索引可直接作用于结构。
  • 追问:存储开销?JSONB 通常比 JSON 小(去重空格),但因二进制元数据可能略大;部分更新?PG 14+ 支持部分字段更新,但需谨慎。
  • 加分回答JSONB 支持 jsonb_path_ops 索引专用优化。

3. PostgreSQL 的数组类型与关联表相比,有什么优势与劣势?

  • 一句话回答:数组在多值包含查询上快且省存储,但更新元素代价高;关联表在频繁修改和关联元数据上更灵活。
  • 详细解释:数组 GIN 索引在 @> 查询中 1 次索引扫描完成,关联表需 INTERSECT;但更新数组会生成新版本行。
  • 追问:元素数量限制?数组建议元素在几百以内,过大应考虑关联表。
  • 加分回答:结合 unnest 可以在 SQL 层灵活展开,实现如数组聚合等高级操作。

4. PostgreSQL 的 TIMESTAMPTZ 是如何处理时区的?与 TIMESTAMP 有何本质区别?

  • 一句话回答TIMESTAMPTZ 内部存储为 UTC 绝对时间戳,显示时根据会话时区转换;TIMESTAMP 为字面值,无时区语义。
  • 详细解释:在全球化应用中,TIMESTAMPTZ 保证时间点的唯一性;TIMESTAMP 在跨时区计算时极易混乱。
  • 追问:存储开销?两者均为 8 字节,完全一致。JDBC/驱动映射?需注意 Java 的 OffsetDateTime 对应 TIMESTAMPTZ
  • 加分回答:内部存储自 2000-01-01 00:00:00 UTC 起微秒数,转换通过 timezone GUC 完成。

5. PostgreSQL 的 UUID 类型作为主键有什么优势与劣势?

  • 一句话回答:优势:分布式全局唯一、无中心协调;劣势:随机值导致索引页分裂,写入性能略低于顺序 ID,且占用空间多一倍。
  • 详细解释:UUID v4 的随机性使 B-tree 的索引叶块分布更散,可能导致更高的写放大和缓存失效。
  • 追问:如何缓解?使用 uuid_generate_v1mc()pgcrypto 中基于时间戳的 UUID 版本,增加单调性。
  • 加分回答:与 SERIAL 对比,SERIAL 基于 SEQUENCE,需访问全局状态,极高频下可能存在瓶颈,UUID 无此问题。

6. PostgreSQL 的 NUMERICFLOAT 在精度与性能上有何差异?

  • 一句话回答NUMERIC 任意精度,通过软件实现计算,极慢;FLOAT 硬件加速,快但有舍入误差。
  • 详细解释NUMERIC 内部用 base-10000 数组存储,解析和运算不能直接利用 CPU 指令;财务计算必须使用。
  • 追问:何时用 NUMERIC?货币、利率、需要严格相等比较的场景。
  • 加分回答NUMERIC 的存储空间可按公式估算,可利用 pg_column_size 查精确占用。

7. PostgreSQL 的 INET 类型相比用 VARCHAR 存储 IP 地址有何优势?

  • 一句话回答:原生支持子网包含 <<、重叠判断 && 等操作,并可通过 GiST 索引加速,语义明确且占用空间更小。
  • 详细解释INET 存储 IPv4 为 7 字节,VARCHAR 存储 '255.255.255.255/32' 需更多字节,且字符串比较无法反应网络层级。
  • 追问:索引如何创建?USING GIST (column inet_ops),用于范围查询。
  • 加分回答INET 内部有 address_familymaskis_cidr 标志,操作符直接在这些字段上运算,非常高效。

8. PostgreSQL 的 Range 类型有哪些?如何实现“同一房间同一时间段不重叠”的约束?

  • 一句话回答:常用有 int4rangetsrangetstzrangedaterange 等;通过 EXCLUDE USING GIST (room_id WITH =, during WITH &&) 实现。
  • 详细解释:排除约束基于 GiST 索引,在插入或更新时自动检查,性能远高于触发器。
  • 追问:边界包含 []() 如何影响重叠判断?tsrange 支持不同的边界标志。
  • 加分回答EXCLUDE 约束是 PG 的独门特性,能干净地将业务规则下沉到数据库。

9. TOAST 机制是什么?什么时候会触发 TOAST?

  • 一句话回答:TOAST 是 PG 对超大数据自动压缩和行外存储的机制,当行大小超过约 2KB 时触发。
  • 详细解释:行必须放入单个 8KB 页,变长字段默认 EXTENDED 策略,先压缩后行外存放,主元组只存指针。
  • 追问:如何查看 TOAST 表?\dt pg_toast.*;如何优化?修改列策略,如 SET STORAGE EXTERNAL
  • 加分回答:TOAST 表也遵循 MVCC,更新造成版本膨胀,大量 TOAST 更新需监控。

10. 如何查看一个字段的底层实际存储大小?

  • 一句话回答:使用 SELECT pg_column_size(column_name) FROM table_name
  • 详细解释pg_column_size 返回内部存储字节数,包括头部。对比 pg_size_pretty 可展示总体表大小。
  • 追问:索引大小?pg_relation_size('index_name');TOAST 大小?pg_total_relation_size('table') - pg_relation_size('table')
  • 加分回答:在 TOAST 场景下,pg_column_size 显示的是主元组中保留的 TOAST 指针大小(通常比真实数据小很多)。

11. PostgreSQL 的 SERIAL 类型底层是如何实现的?

  • 一句话回答SERIAL 是语法糖,自动创建 INTEGER 列和对应的 SEQUENCE 对象,并设默认值为 nextval(sequence)
  • 详细解释:SEQUENCE 是非事务性对象,调用 nextval 立即递增值,不回退,因此有空洞。
  • 追问BIGSERIAL 同理,使用 BIGINTIDENTITY 列(PG 10+)是更现代的替代,符合 SQL 标准。
  • 加分回答:SEQUENCE 可在多个表间共享,灵活性强,SERIAL 只能绑定单列。

12. (系统设计题)设计一个社交媒体平台的数据库 Schema,需要存储用户的动态 Feed(内容可以是文本、图片、视频链接,可能包含不同结构)、用户的兴趣标签、用户的 IP 归属地,请结合 PostgreSQL 的数据类型特性给出合理的类型选择和索引策略。

  • 一句话回答:Feed 采用 JSONB 存储多变结构并建立 GIN 索引;兴趣标签用 TEXT[] 数组与 GIN 索引;IP 归属使用 INET 类型和 GiST 索引;主键使用 UUID 适应分布式扩展。
  • 详细解释
    • feed 表:id UUID PRIMARY KEY DEFAULT gen_random_uuid()user_id UUIDcontent JSONB(包含 type, text, media_urls, metadata),created_at TIMESTAMPTZ。对 content 建 GIN 索引以支持基于类型或关键词的查询。
    • user_profile 表:tags TEXT[] 建 GIN 索引,快速查找拥有相同标签的用户。
    • user_session 表:client_ip INET 建 GiST 索引,可快速查询某地区用户分布。
    • 利用 EXCLUDE 防止用户动态去重(例如同一用户重复发送相同内容,时间间隔)。
  • 追问:如何应对超大 Feed 文档?启用 TOAST EXTERNAL 策略;全文搜索需求更复杂时,结合第 9 篇 tsvector。
  • 加分回答:可对 high-cardinality 的 JSONB 键单独提取生成表达式索引,加速特定路径查询。

附录:PostgreSQL 数据类型速查表

类型存储大小推荐索引适用场景注意事项
INTEGER4 字节B-tree一般主键、计数器范围不足时用 BIGINT
BIGINT8 字节B-tree大数量主键分布式下可配合序列
NUMERIC(p,s)变长通常无索引金融金额、高精度计算慢,避免在 WHERE 频繁运算
FLOAT/REAL4/8 字节B-tree科学计算、统计有舍入误差,禁止等值比较
TEXT变长B-tree, GIN (trgm)绝大多数字符串首选用,移除长度约束
JSONB变长GIN / jsonb_path_ops半结构化文档、动态字段写入开销比 JSON 高,频繁更新需监控膨胀
UUID16 字节B-tree分布式主键、数据合并随机 UUID 索引碎片问题,考虑 uuidv7
INET7/19 字节GiST (inet_ops)IP 归属分析、网络审计CIDR 强制标准网络表示
INTEGER[]变长GIN固定标签、多值属性频繁更新元素不适用
TSTZRANGE变长GiST预订系统、有效期EXCLUDE 实现无重叠
DATE4 字节B-tree出生日期、节假日不含时间
TIMESTAMPTZ8 字节B-tree事件时间、日志强烈推荐,始终 UTC 存储

延伸阅读

  • PostgreSQL 官方文档 Data Types 章节
  • 《The Internals of PostgreSQL》数据类型与 TOAST 相关章节
  • 《PostgreSQL: The Definitive Guide》数据类型部分