selectlab.prove('a million float8 0.1s sum to 100000.00000133288',(selectsum(0.1::float8)::textfromgenerate_series(1,1000000))='100000.00000133288');selectlab.prove('a million numeric 0.1s sum to exactly 100000.0',(selectsum(0.1::numeric)::textfromgenerate_series(1,1000000))='100000.0');
检查约束有名字,可以在不触碰列类型的情况下替换,可以先以 not valid 添加、之后再验证而不持有长时间锁(添加 CHECK NOT VALID,再验证它),还可以表达长度无法表达的事情,比如“不能为空”。varchar(255) 是带有 MySQL 理由的 MySQL 习惯(索引前缀限制),而这里不存在这种限制。
selectlab.prove('the case-insensitive unique constraint rejects USER1@EXAMPLE.COM',lab.try($$insertintousers_civalues(1000001,2,'USER1@EXAMPLE.COM')$$)like'23505:%users_ci_account_id_email_key%');selectlab.prove('but it is a sequential scan of all 834 pages','Seq Scan'=any(lab.nodes($$selectcount(*)fromusers_ciwhereemaillike'USER4242@%'$$))andlab.buffers($$selectcount(*)fromusers_ciwhereemaillike'USER4242@%'$$)>=834);selectlab.prove('regular expressions fail: nondeterministic collations are not supported',lab.try($$selectcount(*)fromusers_ciwhereemail~'x'$$)like'0A000: nondeterministic collations are not supported%');
在新会话中,name = 'Zebra' 什么也不返回,name >= 'Tag1' and name < 'Tag2' 的计数为 0;关闭索引扫描后,则分别找到 Zebra 和 1,111 行。然后:
insert into tags values ('Zebra');
INSERT 0 1
reindex index tags_pkey;
ERROR: could not create unique index "tags_pkey"
DETAIL: Key (name)=(Zebra) is duplicated.
动手验证 · lab 11-types
-- in the lab's own throwaway clone, after the pg_collation update and a reconnect
selectlab.prove('after the change, the index finds no Zebra',(selectcount(*)fromtagswherename='Zebra')=0);selectlab.prove('with index scans disabled, the same queries find Zebra and 1,111 rows',(selectcount(*)fromtagswherename='Zebra')=1and(selectcount(*)fromtagswherename>='Tag1'andname<'Tag2')=1111);selectlab.prove('the primary key accepts a second Zebra',lab.try($$insertintotagsvalues('Zebra')$$)='ok');selectlab.prove('reindex fails: could not create unique index "tags_pkey"',lab.try($$reindexindextags_pkey$$)like'23505: could not create unique index "tags_pkey"%');
collname | recorded | actual
----------+----------+--------
app_en | 2.28 | 2.41
把任何不匹配都当作“在信任之前先重建”。修复重复值,在该排序规则下对每个文本索引执行 reindex(并发执行),用 amcheck 检查(它会按今天的规则验证每个索引的顺序),然后用 alter collation ... refresh version 记录新版本。
只有重建索引后才能刷新版本
refresh version 什么也修复不了,它只会让警告消失。把操作系统升级、基础镜像更新和新副本镜像都当成排序规则事件;或者使用 builtin 提供者,从此不再操心。
深入阅读:警告、暴露的索引和 amcheck
使用过时排序规则的会话会得到:
WARNING: collation "app_en" has version mismatch
DETAIL: The collation in the database was created using version 2.28,
but the operating system provides version 2.41.
动手验证 · lab 11-types
selectlab.prove('the version check finds app_en recorded at 2.28 against the library''s 2.41',(select(collversion,pg_collation_actual_version(oid))=('2.28','2.41')frompg_collationwherecollversion<>pg_collation_actual_version(oid)andcollname='app_en'));
WARNING: database "w_edges" has a collation version mismatch
DETAIL: The database was created using collation version 2.31,
but the operating system provides version 2.41.
HINT: Rebuild all objects in this database that use the default collation
and run ALTER DATABASE w_edges REFRESH COLLATION VERSION,
or build PostgreSQL with the right library version.
执行 alter database w_edges refresh collation version 后返回 NOTICE: changing version from 2.31 to 2.41,除此之外什么也没有修复。先按提示重建,再刷新。
动手验证 · lab 11-types
selectlab.prove('the recorded collation version (2.31) no longer matches the library',(selectdatcollversion<>pg_database_collation_actual_version(oid)frompg_databasewheredatname=current_database()));selectlab.prove('REFRESH COLLATION VERSION only rewrites the recorded number',(selectdatcollversion=pg_database_collation_actual_version(oid)frompg_databasewheredatname=current_database()));
1 day 保持挂钟时间;24 hours 增加 86,400 秒。这只对命名的会话时区有效,对固定偏移量无效。普通 timestamp 对第一个结果会给出 02:30,而这个时间在纽约根本不存在。
动手验证 · lab 11-types
selectlab.prove('timestamp records the two inserts four hours apart; timestamptz under a second apart',(selectmax(ts)-min(ts)betweeninterval'4 hours'andinterval'4 hours 1 second'andmax(tstz)-min(tstz)<interval'1 second'fromtz_demo));settimezone='America/New_York';selectlab.prove('across the 2026-03-08 DST jump: +1 hour is 03:30, +1 day is 01:30 next day, +24 hours is 02:30',('2026-03-08 01:30'::timestamptz+interval'1 hour')::text='2026-03-08 03:30:00-04'and('2026-03-08 01:30'::timestamptz+interval'1 day')::text='2026-03-09 01:30:00-04'and('2026-03-08 01:30'::timestamptz+interval'24 hours')::text='2026-03-09 02:30:00-04');selectlab.prove('a plain timestamp returns 02:30, a time that never existed in New York',('2026-03-08 01:30'::timestamp+interval'1 hour')::text='2026-03-08 02:30:00');
begin;selectpg_sleep(0.05)isnullasslept\gsetselectlab.prove('inside a transaction now() stays at its start; clock_timestamp() moves on',clock_timestamp()-now()>=interval'50 milliseconds');commit;
date 用来表示真正的日期,比如生日,而不是“创建于何时”这种你只是在展示时转换为日期的时间点。查询时间范围时使用半开区间:>= '2026-10-01' and < '2026-11-01'(日期范围是半开的),并记住 '2026-01-31'::date + interval '1 month' 是 2 月 28 日。
在增加一个布尔值前,先问问自己是否将来会想知道它何时变成了真。archived_at timestamptz 用 is not null 回答“它已归档吗?”,还能回答“何时归档”,代价是多 7 个字节。对于审计关心的状态(已验证、已归档、已取消),时间戳更好。
要把列限制在一组值中,我们的默认选择是 text 加检查约束,比如运行示例中的 check (plan in ('free', 'team', 'enterprise'))。每个工具都理解它,而且可以在线替换。如果值本身还携带数据,就使用查找表。
枚举很难收回
枚举占 4 字节,并按声明顺序排序:这对于真正稳定且有序的集合(比如日志严重性)很合适。但有两个陷阱。没有 drop value,所以要废弃一个值,就得创建新类型并重写表。而且,在事务中添加的值要等事务提交后才能使用;这会破坏那些在同一事务中添加值并填充行的迁移工具(ERROR: unsafe use of new value "audit" of enum type event_kind)。
用 uuid 存储 UUID
uuid 占 16 字节;作为 text 要占 40 字节,每个索引也会随之变大(慎选键,少后悔对此有测量)。
所有了解自身领域的类型也是如此。inet 和 cidr 会验证地址,并用一个操作符回答“这个 IP 是否在那个网络中”。范围类型(tstzrange、daterange)让“不能有两个预订重叠”成为一个排他约束。而 create domain email_address as text check (...) 则把反复出现的规则只写一次。
数组适合随行生灭的列表
数组适合属于一行的小列表:标签、通知渠道。当元素其实是伪装成列表的行时,数组就不合适:它们引用其他表(元素上没有外键),你一次只更新一个元素(每次更新都会重写整行),或者列表会无限增长。项目上的用户 ID 数组,其实就是一张尚未创建的成员关系表。
selectlab.prove('events.payload values take 63 to 69 bytes each',(selectmin(pg_column_size(payload))>=63andmax(pg_column_size(payload))<=69fromevents));selectlab.prove('the same facts as typed columns would take about a fifth of the space',(selectavg(pg_column_size(payload))/avg((octet_length(payload->>'status')+1)+4+(octet_length(payload->>'region')+1))from(selectpayloadfromeventstablesamplesystem(5)repeatable(42))e)between5and6);
ERROR: indexes on virtual generated columns are not supported
CREATE INDEX
表达式必须是不可变的:不能使用 now(),也不能引用其他表。
动手验证 · lab 11-types
selectlab.prove('in PostgreSQL 18 the default is virtual (v); stored is s',(selectarray_agg(attgenerated::textorderbyattnum)frompg_attributewhereattrelid='g_demo'::regclassandattgenerated<>'')=array['v','s']);selectlab.prove('indexes on virtual generated columns are not supported; on stored ones they are',lab.try($$createindexong_demo(total_cents)$$)='0A000: indexes on virtual generated columns are not supported'andlab.try($$createindexong_demo(total_stored)$$)='ok');