PostgreSQL TOAST机制详解:大字段存储、压缩与性能调优实践
1. 从一次“物理备份差点翻车”说起先说个我早年的真实经历。当时接手一套业务系统里面有一张核心流水表每条记录都存了一份完整的请求响应快照字段类型是text。单条数据不大平均也就几十KB但表总行数到了上亿整张表物理体积轻松突破几百GB。最要命的是每次跑全量备份耗时从原来的半小时一路涨到四五个小时存储空间也吃紧。我一开始以为是单纯的数据量大直到某天翻pg_relation_size发现一个诡异现象主表大小只有几GB但旁边一张名字里带pg_toast_的附属表体积居然是主表的几十倍。那时候我才认认真真去研究 PostgreSQL 的大字段存储机制——也就是标题里写的 TOASTThe Oversized-Attribute Storage Technique超尺寸属性存储技术。搞懂它之后很多之前“说不清为什么”的现象都通了为什么主表那么小为什么某些查询快、某些查询慢为什么压缩字段有时反而拖后腿这篇就把我踩过的坑和梳理过的原理一起讲清楚适合正在做 PostgreSQL 运维、表结构设计或者想深入理解数据库存储层的朋友。提示这篇文章涉及的命令都是基于 PostgreSQL 常用版本具体函数名和系统视图在各版本间差异很小可以放心直接用。2. 为什么需要TOAST8KB块和2KB行的硬约束2.1 PostgreSQL的存储模型决定了天花板要理解 TOAST先得理解 PostgreSQL 底层的存储规则。PostgreSQL 的数据文件是按“块block”组织的默认一个块是 8KB这个值由编译参数BLCKSZ决定绝大多数发行版都是这个值。表里的每一行数据最终要落到这些 8KB 的块里而且一条记录不能跨块存放TOAST 是唯一的例外机制后面会讲。但“不能跨块”不等于“最大行就是满打满算 8KB”。每条记录在块里还要额外携带行头信息比如事务ID、行指针、NULL 位图等等这部分固定开销通常有个几十字节。再加上块本身还有页头、行指针数组需要占用。所以严格算下来普通堆表中一行数据可用空间大概在 1.9KB 左右。也就是说如果你有一行数据超过这个大小直接塞进普通堆表块里就是不可能的。你可以做个小实验验证一下建一张只有int列的表去查pg_column_size看一行要占多少字节然后把一列改成text并塞入一个较大的字符串再对比系统表中的信息。实测下来单行可用空间很低超过某个阈值就会被自动处理这个阈值不是 2KB 整而是BLCKSZ / 4再减去一部分固定开销大约在 1960 字节上下。2.2 为什么 mobile开发里常见的JSON、日志、图片字段最容易触发业务开发中真正会触发 TOAST 的几乎都是“大字段”典型的有这几种text和varchar超长文本、JSON 字符串、日志全文。bytea图片二进制、文件内容、加密数据。jsonb和json嵌套复杂的接口返回值、请求响应快照。自定义复合类型或数组类型元素特别多时也会变大。这些字段的共同特点是本身可变长而且可能很大。小数值类型的int、bigint完全不存在这个问题因为它们的长度上限远低于 TOAST 阈值PostgreSQL 也会直接给它们标记为PLAIN存储策略不做行外处理。2.3 TOAST的全称和设计思路TOAST 的全称是The Oversized-Attribute Storage Technique设计核心就是一句话把大字段从主表行里挪出去必要时先压缩然后在主表中只保留一个指针。这样主表的一行数据仍然可以保持很小不会突破块的限制而大字段本体被拆成若干分片放在一个隐藏的附属表里。关键点在于这个机制对应用完全透明。你插入一个几十MB的text字段SQL 不需要做任何特殊处理你查询这张表时PostgreSQL 会在需要时自动把 TOAST 表里的分片拼回来解压后返回给你。应用层面根本感知不到底层经历了“压缩→行外→分片→重组→解压”这一整套流程。这也是 TOAST 最精髓的地方能用底层机制解决问题就不给应用增加负担。2.4 TOAST是“表”还是“机制”严格说TOAST 并不是一张固定的表而是一套存储行为。每张需要支持大字段的表在首次出现超阈值数据时PostgreSQL 会在pg_class.reltoastrelid里登记一个关联附件表的 OID这个附件表的名字就是pg_toast_主表OID。主表与 TOAST 表通过“隐式JOIN”关联但对用户来说平时查询是看不到也不需要关心这张表存在的。你可以在系统视图里找到这些信息SELECT relname, reltoastrelid FROM pg_class WHERE relname user_events;如果reltoastrelid不为0就说明这张表有 TOAST 表。直接查这张附属表的大小SELECT pg_relation_size(reltoastrelid) AS toast_size_bytes FROM pg_class WHERE relname user_events;我在实际排查时经常会接到一个需求“为什么我的表主文件只有 500MB备份出来却要 5GB” 十有八九就是 TOAST 表在作祟用上面这个查询一秒钟就能定位。3. TOAST完整流程压缩、行外存储和分片重组3.1 触发阈值到底是怎么算出来的TOAST 的触发阈值并非拍脑袋定的而是从块大小推导来的。默认块大小是 8KBPostgreSQL 的设计目标是让主表行的最大长度不超过块大小的四分之一也就是约 2KB这样每块至少能放下四行有代表性的数据。然后减去行头开销和块内管理开销最后得到的值就是触发阈值。你可以通过编译参数BLCKSZ直接决定块大小。如果BLCKSZ是 16KB那么 TOAST 阈值大约就是 4KB 左右。这个参数在编译时固定后续无法修改所以平时我们说的“2KB左右”指的是默认 8KB 块下的情况。如果一行数据小于这个阈值PostgreSQL 直接用普通存储不启用 TOAST一旦超过TOAST 流程就开始介入。3.2 先压缩还是先存出去策略排序问题TOAST 的存储流程有固定的优先级如果选择了压缩策略先尝试压缩压缩之后如果仍然大于阈值再进行行外存储。这个顺序很关键因为压缩可以大幅缩减体积很多时候压缩完就直接缩到阈值以内根本不需要动用行外存储。举个例子一段 10KB 的重复性 JSON 文本用内置压缩算法通常能压到 1KB 以下压缩后直接留在主表行里不占用 TOAST 表空间读取时也更快。但如果字段本身是不可压缩的比如已经加密的数据、压缩过的图片JPEG、PNG、随机数生成的二进制串压缩后体积基本不变这时候就会触发行外存储。数据本体被挪到 TOAST 表主表行内只留一个约 18 字节的指针用来指向TOAST表数据的定位信息。你可以查一个字段压缩前的实际体积和压缩后的实际体积来验证SELECT pg_column_size(body) AS raw_size, pg_column_compression(body) AS compression FROM user_events LIMIT 1;pg_column_compression会告诉你这个字段实际用的是哪种压缩算法。如果返回空说明没有压缩生效可能被EXTERNAL策略绕过了也可能字段本身已压缩。3.3 行外存储的chunk分片机制当字段被决定行外存储后PostgreSQL 会把大字段切成一片片的小块写入 TOAST 表。TOAST 表的结构可以用这个查询看到SELECT attname, atttypid::regtype FROM pg_attribute WHERE attrelid pg_toast.pg_toast_主表OID::regclass ORDER BY attnum;TOAST 表通常包含三个核心列chunk_id关联回主表行内指针的标识符。chunk_seq分片的顺序号0、1、2……用于拼回原始数据时排序。chunk_data分片的本体内容。默认情况下每个分片大约是 2KB也就是和 TOAST 阈值差不多大。把一个大字段切成多个 2KB 的分片每条记录都指向同一个chunk_id查询时按chunk_seq依次读回来最后重组。这里有一个值得留意的特点TOAST 表不是“一个字段对应一行”而是“一个字段的多片分别占多行”。所以如果某个字段特别大比如一个 50MB 的bytea它在 TOAST 表里可能有上万行分片。这类表的行数会非常夸张整体体积也惊人。3.4 四种存储策略PLAIN / EXTENDED / EXTERNAL / MAINTOAST 的行为通过存储策略来控制每种列都可以单独设置。策略一共有四种策略名压缩行外存储适用场景PLAIN禁止禁止短数值、定长类型不参与TOASTEXTENDED允许允许默认策略先压缩再行外EXTERNAL禁止允许数据本身已压缩绕过CPU压缩开销MAIN允许尽量留在主表优先放主表压缩后仍超阈值才行外默认情况下支持TOAST的字段基本都是EXTENDED。但实践中我经常会把一些特殊字段改成EXTERNAL。比如某个字段存的是已经压缩过的数据JSON 序列化时如果已经整体 gzip 过或者存储的是图片缩略图再让 PostgreSQL 压缩一遍就是纯浪费 CPU。设成EXTERNAL后数据会直接走到行外存储不做压缩。修改策略的命令很简单ALTER TABLE user_events ALTER COLUMN body SET STORAGE EXTERNAL;3.5 主表只留指针对查询性能的双刃剑TOAST 最大的收益是让主表行变小每一页能放更多行全表扫描时 IO 次数大幅减少。但代价是访问大字段本体时要额外去 TOAST 表读取多一次随机IO读取多个分片还要处理重组。好在 PostgreSQL 做了一个优化只在真正需要某个大字段列时才去读TOAST表。如果查询只取id和created_at完全不会碰 TOAST 表。这就是“宽表但是查询快”的原因。反过来如果应用层写了个SELECT *把大字段也带回来那 TOAST 表的读取就躲不掉了查询自然慢下来。我排查慢查询时经常遇到这类情况业务方反馈“查询很慢”执行计划一跑扫描倒是走索引了但每条记录返回前都要去 TOAST 表把几十KB的字段捞出来解压事务耗时自然低不了。这种问题的解法通常不是加索引而是把大字段拆分到独立表或者改成按需查询。4. 压缩算法pglz与LZ4的选择4.1 默认压缩算法pglz是怎么工作的PostgreSQL 最早只支持一种内置压缩算法叫pglz。这个名字大多数人没听说过因为它不是独立的第三方库而是 PostgreSQL 内部实现的一套基于 LZ 家族的压缩方案专门为“短数据块、低延迟”场景优化。pglz 的压缩率在同级别算法里不算顶尖但它的设计目标是**“尽量快”**。TOAST 场景有一个特点数据往往是分散的小分片每个分片只有 2KB 左右压缩和解压的单元很小。如果算法本身太重解压时间甚至可能超过读取时间那就得不偿失了。pglz 在这个场景下表现均衡一直沿用到今天。4.2 LZ4进场从PG14开始的选项从 PostgreSQL 14 开始官方把 LZ4 引入为可选的 TOAST 压缩算法。LZ4 在压缩率上通常略低于 pglz但压缩和解压速度有压倒性优势尤其适合高并发、大字段读取极其频繁的业务。选择算法不是改一个全局参数那么简单因为pglz和lz4并存时必须保证数据可互操作——也就是旧数据不管用什么算法存的查询时都能正确解压。PostgreSQL 在存储分片时会额外记录压缩算法标识解压时根据标识选择对应算法。默认压缩算法由default_toast_compression参数控制SHOW default_toast_compression;默认是pglz。你可以在建表时为特定字段指定lz4CREATE TABLE event_snapshot ( id bigint PRIMARY KEY, body text COMPRESSION lz4 );也可以建表后再改ALTER TABLE event_snapshot ALTER COLUMN body SET COMPRESSION lz4;这里要特别提醒一句这个修改只影响后续新写入的数据已存在的行仍然沿用旧压缩算法。如果想让全表数据都切到新算法得重写表或逐步迁移不能指望一条 DDL 就完成全量转换。4.3 怎么确认数据实际用了哪种压缩想知道一个字段实际压缩成什么样两个函数就够了SELECT pg_column_size(body) AS raw_compressed_size, pg_column_compression(body) AS used_algorithm FROM event_snapshot WHERE id 123;pg_column_compression返回pglz或lz4如果返回空NULL说明这条记录没有应用压缩。什么情况下会NULL比如字段策略是EXTERNAL或者数据在写入时确定不需要压缩。我遇到过一种情况字段设了lz4但查了很多行pg_column_compression全是 NULL。原因就是数据量太小不超过 TOAST 阈值根本没有触发 TOAST。字段设了压缩算法不表示每条记录都压缩只有真正触发 TOAST 流程的才有机会分析压缩率。4.4 pglz和lz4怎么选几个实测方向如果一定要给个选择思路我可以分享下我的做法字段很大且读取频繁比如每次都要捞 JSON 回应用户优先考虑lz4读延迟会直观下降。字段偏文本且重复度高压缩率更重要就继续用pglz它对文本类数据压得更狠。字段内容本身已经是压缩格式gzip 过的文件、图片无论哪种算法都压不动直接设EXTERNAL别让数据库白费 CPU。实际压测时可以同时建两张结构一样的表一个用 pglz、一个用 lz4插入相同的数据比较pg_column_size和查询耗时。数据库内部原理是一回事真实环境的数据特征往往差异很大实测最可靠。5. 存储策略实操什么时候该换EXTERNAL或MAIN5.1 EXTERNAL给“已经压缩过的数据”松绑前面提到EXTERNAL是最容易被忽略但实用价值很高的策略。它禁止压缩但允许行外存储。适合两种典型场景数据量很大且结构性很强比如 JSONB 文档、log 原文虽然可以压缩但你的查询需要频繁读取其中一段压缩反而增加每次读取的解压开销。字段压缩收益极小比如加密数据、随机串、图片音频二进制。举个例子某个表存了加密后的用户敏感字段长度非常稳定比如固定 4KB而且内容随机分布。用EXTENDED策略时PostgreSQL 每写一条就要尝试压缩但压缩率接近1:1纯纯的 CPU 浪费。改成EXTERNAL之后数据直接行外存储省掉压缩环节写性能提升明显。ALTER TABLE user_secrets ALTER COLUMN encrypted_blob SET STORAGE EXTERNAL;5.2 MAIN尽量留在主表的取舍MAIN策略的语义是优先把数据留在主表只有当压缩后仍然超过阈值时才行外存储。听起来很“友好”但要注意MAIN 是允许压缩的只是不允许“先存出去”。在某些访问模式下一个坑很容易踩假设某字段设为MAIN而且数据能压到阈值以内那么它确实一直留在主表里。但如果这个字段体积较大、压缩率又不高每次行外存储的判定都会比对阈值边缘情况下可能产生大量主表行块利用率下降后续更新还会引发行迁移。所以我的建议是非必要别优先用 MAIN。默认EXTENDED已经权衡得很好你只需要在“是否能压缩”这个维度上做微调也就是换成EXTERNAL或保持EXTENDED绝大多数场景就够了。5.3 如何验证你的策略切换生效了改完存储策略最直观的验证就是看主表和 TOAST 表体积变化。插入一批相同的数据分别统计SELECT pg_relation_size(user_events) AS main_size, pg_total_relation_size(user_events) AS total_size;pg_total_relation_size包含主表、TOAST 表和索引。对比main_size和total_size的差值就能看出 TOAST 表占了多少。策略切换后同一批数据产生的total_size可能有明显不同这正是你要关注的变化。另一个实用操作是直接查看 TOAST 表里的分片信息SELECT chunk_id, count(*) AS chunk_count, sum(octet_length(chunk_data)) AS data_total_bytes FROM pg_toast.pg_toast_主表OID GROUP BY chunk_id ORDER BY chunk_count DESC LIMIT 5;如果某些chunk_id对应的分片特别多说明有一个超大字段正在占用大量 TOAST 空间如果整体分片行数不多说明大部分数据在压缩后已经留在主表里了。6. 实操记录一张定义混乱的表如何完成TOAST瘦身6.1 一张“问题表”的体检过程我遇到过一个真实案例这张表叫order_snapshot表结构大概如下CREATE TABLE order_snapshot ( id bigint PRIMARY KEY, order_id bigint, status smallint, payload text );业务方反馈两个问题一是表体积无限膨胀两周就翻一倍二是包含payload的查询耗时严重不稳定。我做了三个步骤第一查整体体积构成SELECT pg_relation_size(order_snapshot) AS main_bytes, pg_total_relation_size(order_snapshot) AS total_bytes;结果主表只有 1.2GBtotal_bytes却到了 9.8GB。也就是说接近 90% 的空间全在 TOAST 表里。第二看payload字段的实际大小分布SELECT count(*) AS total_rows, count(*) FILTER (WHERE pg_column_size(payload) 1960) AS toast_rows, avg(pg_column_size(payload))::bigint AS avg_payload_size FROM order_snapshot;发现 95% 的行payload都在几KB到几十KB之间平均 8KB 左右。这意味着几乎每行都触发了 TOAST 流程。第三看压缩情况SELECT pg_column_compression(payload) AS algo, count(*) FROM order_snapshot GROUP BY 1;结果很有意思大部分行的算法是pglz但还有一部分行算法为 NULL说明这些行写入时因为数据不足阈值或策略问题没有压缩。再查一下pg_attribute的attstorage字段才知道payload的存储策略是默认的EXTENDED但实际压出来的体积仍然很大。6.2 问题根因频繁UPDATE导致TOAST表膨胀进一步深挖后发现问题核心不在存储策略而在业务模式。这张表的payload会随着订单状态变化被反复更新每次更新都会生成新版本的整行数据。TOAST 表里的旧分片并不会立即被清理而是要等 VACUUM 回收。如果更新频率远超 VACUUM 频率TOAST 表就疯狂膨胀。解决办法分两步第一步把payload从EXTENDED改成EXTERNAL。因为订单快照里的 JSON 内容通常是序列化后的结果内部已经有大量重复字段压缩率不高但每次解压还带来额外 CPU。改成EXTERNAL后至少省掉了压缩环节的 CPU 开销。第二步调快 VACUUM。对这张频繁更新的表我把autovacuum_vacuum_scale_factor调小同时单独设置表的fillfactor降低到 60减少更新时的行迁移概率。修改后TOAST 表体积的增长趋势明显放缓。这里有朋友可能会问直接VACUUM FULL不行吗VACUUM FULL会把整张表和 TOAST 表重建一次确实能把体积压下来但代价是长时间锁表在线业务根本接受不了。所以线上大表优先靠pg_repack或逐步迁移而不是VACUUM FULL。6.3 验证结果主表与TOAST表的体积变化优化后一周我再查体积构成主表 1.2GB 仍然是 1.2GB因为payload走行外存储后主表行大小稳定在阈值以内。TOAST 表的体积增速从“每天约 700MB”降到“每天约 150MB”。查询耗时从原来的平均 800ms 降到 300ms 左右CPU 使用率也下降了不少。这个案例让我印象特别深因为它说明了一个问题TOAST 不是单纯“省空间”的机制更像是一套存储策略的平衡器。不同业务模式下它的表现差异很大必须结合具体的写入模式、压缩收益、读取频率来做调整。7. 监控TOAST从系统表读到有用信息7.1 核心系统视图与字段速查如果你想系统性地监控 TOAST下面几个查询可以长期保留。查询每张表的TOAST大小SELECT n.nspname AS schema_name, c.relname AS table_name, pg_relation_size(c.reltoastrelid) AS toast_size_bytes, pg_total_relation_size(c.oid) AS total_size_bytes FROM pg_class c JOIN pg_namespace n ON n.oid c.relnamespace WHERE c.reltoastrelid 0 ORDER BY toast_size_bytes DESC LIMIT 20;查询某列的实际存储策略SELECT a.attname, a.attstorage, CASE a.attstorage WHEN p THEN PLAIN WHEN e THEN EXTENDED WHEN x THEN EXTERNAL WHEN m THEN MAIN END AS storage_strategy FROM pg_attribute a WHERE a.attrelid order_snapshot::regclass AND a.attnum 0 AND NOT a.attisdropped;attstorage字段是理解列存储策略的直接入口p/e/x/m分别对应四种策略。7.2 观察VACUUM与TOAST表回收TOAST 表的大小和 VACUUM 行为强相关。如果发现 TOAST 表一直很大可以用SELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum FROM pg_stat_user_tables WHERE relname order_snapshot;n_dead_tup如果长期居高不下说明 VACUUM 没能及时回收旧版本TOAST 表膨胀只是连锁反应之一。这时候要先解决 VACUUM 效率问题再谈 TOAST 优化。7.3 什么时候该考虑“把大字段移出主表”TOAST 虽然能处理大字段但不代表你可以无限制地在表里堆大字段。如果一个字段非常大且高频读取它始终会对查询造成影响。我常用的一个判断标准是如果单个字段平均大小超过主表一行其他字段总和的几十倍且它被查询的频率又很高就该考虑拆表。拆表有两种典型方案垂直拆分把大字段单独放到一张附属表通过主键关联。查询时按需 JOIN。分区或归档把旧数据按时间迁移到冷表减少在线表体积。TOAST 是底层机制拆表是业务层设计两者互相配合才能在大字段场景下把性能做到最好。8. 避坑指南与实战经验总结8.1 踩过的大坑EXTERNAL设了但不生效有次我在某张表上设置了EXTERNAL但过了一段时间发现数据仍然被压缩了。排查下来发现原因很直接那张表当时还没有任何数据我先做了ALTER TABLE改策略后来导数据的时候用的是INSERT INTO ... SELECT而源表里的一些列带有显式的压缩属性导入时把压缩属性也带过去了覆盖了目标列的EXTERNAL设置。所以给列设置压缩策略时要确认你的数据导入链路有没有隐式覆盖。最稳妥的做法是导入后再查一次pg_attribute.attstorage和数据实际压缩状态SELECT pg_column_compression(payload), count(*) FROM order_snapshot GROUP BY 1;如果还有大量非 NULL 结果说明策略没生效需要重新处理。8.2 关于压缩率的两个常见误解误解一压缩率高就一定好。实际上压缩率提升意味着读取时解压开销更高对于高频小范围读取的场景宁可压缩率低一点也要解压更快。误解二所有text字段都适合压缩。如果内容本身是已加密的、已压缩的或者随机生成的压缩只会消耗 CPU不产生任何收益。这类字段正确策略是EXTERNAL。8.3 块大小不是你想调就能调BLCKSZ在编译时确定线上环境想改块大小基本等于要重新编译一套 PostgreSQL。默认8KB是绝大多数场景下的最优选择别为了“大字段能存更多”去单独调大块大小收益有限反而可能影响缓存命中率、TOAST 阈值、页内行数等多个指标。我没有见过哪个生产环境因为块大小调大而获得质的提升倒是见过不少因为编译参数改完忘了维护成本增加而头疼的团队。8.4 更新频繁的大字段避免行迁移与TOAST碎片如果一个大字段所在的表更新非常频繁可以考虑降低fillfactor。fillfactor表示数据页的填充率默认是100%也就是新行尽可能把页面填满。但更新一行数据时如果页内空间不足PostgreSQL 会把新版本放到其他页导致行迁移间接增加 TOAST 访问和碎片。把fillfactor调低比如 70 或 80可以留出足够空间给 HOT 更新Heap-Only Tuple减少行迁移。ALTER TABLE order_snapshot SET (fillfactor 70);这个操作不用重写数据只影响后续插入和更新行为算是低成本高收益的调优手段。8.5 查询时如何避免“误触”TOAST解压记住一个原则不要轻易 SELECT *。SELECT *会把所有大字段列一并带回哪怕你只需要其中一个小字段。只要返回结果包含 TOAST 列数据库就得去 TOAST 表读取分片并解压。我发现很多慢查询问题都出在代码里随手写的SELECT *上。把字段列表写明确是避免 TOAST 性能问题最简单、也最容易被忽略的手段。8.6 备份和迁移时TOAST表带来的额外开销TOAST 表在备份和逻辑导出时都算在内。pg_dump导出大表时TOAST 表内容会占据极大比例。如果一次性导出一张包含巨量 TOAST 数据的表可能几十分钟都跑不完。遇到这种情况可以考虑分批导出或者用COPY按条件分段处理。物理备份比如pg_basebackup则没有这个问题因为它直接复制整个数据目录TOAST 表和主表一视同仁。差别在于物理备份期间如果写入频繁TOAST 表大量膨胀备份出来的体积也会异常大。所以备份前观察 TOAST 表大小变化能帮你判断是不是该做一次清理了。9. 写在最后理解TOAST不是终点我个人在实际排查和调优中体会到TOAST 机制的价值不只是让你知道“大字段被压缩了”更重要的是让你建立起一套判断存储问题的框架看到主表很小、总表很大先想 TOAST看到大字段查询慢先想是不是每次都在解压看到表体积持续增长先想更新模式和 VACUUM 回收节奏。这些判断链条一旦建立起来很多数据库怪现象就不再神秘了。最后再分享一个小技巧每次调整完存储策略或压缩算法记得留一份前后对比记录至少包含主表大小、TOAST 表大小、平均查询耗时、CPU 使用率这几项指标。数据库调优效果好不好不能靠感觉得靠这些数字说话。我见过太多“感觉变快了”但一查监控指标几乎没有变化的案例指标才是唯一的裁判。