很多 DBA 每天都和数据字典打交道但对SYS.CDEF$和SYS.CON$这两张基表的印象往往只停留在名字上。今天想聊的就是它们约束元数据到底怎么存放、什么时候会变成性能瓶颈以及真遇到row cache lock、递归 SQL 暴涨、大批量 DELETE 卡死时该怎么一步步排查和落地优化。这些内容适用面很广——无论是维护传统 OLTP 生产库还是在做数据迁移、DDL 变更、多租户环境运维只要你的库里约束多了、外键多了这几张内部表迟早会出现在等待事件或 AWR 报告里。我最早注意到这两张表是有一次在客户现场排查一个“每天固定时间点 DELETE 变慢”的问题。应用层 TOP SQL 并不复杂但 v$session 里全是TM锁和异常多的 recursive calls辅助诊断里清清楚楚能看到递归 SQL 在反复碰SYS.CDEF$。那时我才意识到约束检查背后这些“看不见的账本”才是很多性能问题的真正源头。1. CON$ 和 CDEF$ 到底是什么1.1 约束背后的“总账本”你在 Oracle 里建一张表再加上主键、唯一约束、外键、CHECK 约束用户看到的都是《数据字典视图》里的整洁条目比如DBA_CONSTRAINTS、DBA_CONS_COLUMNS。但这些视图只是外壳数据真正落在底层一张张以$结尾的内部基表上。约束信息最核心的两张就是CON$和CDEF$。CON$可以理解成“约束主表”它的每一行代表一个约束这个约束叫什么名字、属于哪个用户、作用在哪个表、约束类型是什么、当前是启用还是禁用、是验证过还是未验证、是否可延迟都在这里记录。而CDEF$更贴近字面意思——column definition它记录的是“约束和列的对应明细”。一个主键如果建在两列上那么在CDEF$里就会体现为这个约束下的两行明细通过列序号关联回表的列定义。这个设计思路其实和大多数业务系统的主子表结构没什么区别主表CON$放约束的“头信息”明细表CDEF$放约束涉及的“列清单”。Oracle 内部在做约束检查、DDL 变更、递归 SQL 优化时都要反复读取和关联这两张表。它们的行数与活跃度直接决定了你数据库里约束维护的成本。1.2 一张表、两本账字段与职责CON$的表结构在不同版本里有一些细微调整但核心字段的逻辑很稳定。大方向包括约束编号CON#、约束名称NAME、所属用户编号OWNER#、基础对象编号OBJ#、约束类型标志位、启用状态、延迟状态、验证状态等。换句话说DBA_CONSTRAINTS视图里你看到的CONSTRAINT_TYPE、STATUS、VALIDATED、DEFERRABLE都可以追溯到CON$上的字段。CDEF$则更像约束和列之间的“映射表”。它的关键字段包括约束编号CON#、基础对象编号OBJ#、列编号COL#、列位置POS#等。当一个约束涉及多个列时CDEF$会有多条记录POS#标明它在约束中的顺序。比较特别的是CDEF$里也会保存启用、验证等状态信息因为 Oracle 在运行期做约束检查时并不总需要回到CON$拿全部信息很多场景下读取CDEF$已经足够。有一点必须提醒不要试图手工去改这两张表。它们属于 Oracle 内部递归事务保护的对象任何直接 DML 都可能在毫无预兆的情况下损坏数据字典导致实例起不来或者大量对象报 ORA-00600。正确姿势永远是通过ALTER TABLE ... ADD CONSTRAINT / MODIFY CONSTRAINT这类 DDL 去操作底层由 Oracle 自己维护。1.3 字典基表如何参与日常 SQL很多人好奇普通 SQL 到底什么时候会碰CON$和CDEF$其实只要你执行一条涉及约束的 DMLOracle 的优化器就可能启动一段递归调用。比如往子表插入一行数据外键约束要求父表主键必须存在这时候数据库就会检查约束定义“这个约束的类型是什么关联的是哪一列是否需要做引用校验”这些信息如果已经在共享池或数据字典缓存row cache里那就直接在内存中拿一旦缓存没有命中或者内存压力过大Oracle 就会走底层基表去读取也就是要访问CON$和CDEF$。生产环境里如果约束数量非常多、缓存频繁被挤掉这些“隐形 SQL”会成倍增加最终反映到 AWR 里就是一条条以SYS_...开头的递归 SQL或大量的 latch 等待。所以这两张基表并不是只在 DBA 查数据字典时才存在的静态数据。它们和普通业务表一样会被高并发地读取、在 DDL 中被修改、随回收站对象一起保留甚至还会因为统计信息不准影响内部执行计划。认清这一点后面再分析性能问题思路就会清晰很多。2. 性能问题的内核约束校验为何会拖垮并发2.1 每次 DML 都要翻一次“家谱”约束校验的递归 SQL很多时候看起来体积很小但频率极高。尤其是外键约束它的检查不是“可选项”而是事务提交路径上的硬环节。你更新一行父表主键数据库要确认没有子行引用你删除一行父表数据数据库要确认子表不存在孤儿记录你插入一行子表数据数据库要确认父表对应记录存在。这些动作每次都要从缓存或者字典基表中取得约束元数据。我经常用一个生活化类比和朋友解释约束检查好比家庭登记台账。你每次办理业务DML办事员都要翻家庭关系账本CON$/CDEF$确认这个人的配偶、父母、子女关系成立。如果账本太大、关联关系太复杂、查账本的人又特别多那么整个办事大厅就会排队。数据库里的表现就是 latch 竞争、递归调用增多、CPU 虽然不高但事务提交非常慢。在极端情况下一张表上有几十个外键每个外键关联的都是大表那么一条简单的UPDATE就可能触发几十条递归 SQL。如果这些递归 SQL 又要反复访问CDEF$去获取列映射开销就会被放大。这也是我建议开发者在核心交易表上谨慎堆外键的原因不是说不要用而是要有意识地控制约束数量和检查成本。2.2 外键无索引性能恶化的放大器提到外键就绕不开“外键列必须建索引”这条运维铁律。它的核心原因不是为了让子表查询更快而是为了让父表删除、更新主键时能快速定位子表记录。先设想一个场景订单表有 1000 万行customer_id上有外键但没索引业务要删除父表里一批客户数据。Oracle 为了确认没有未完成的订单引用这些客户就必须在子表订单表上检查对应customer_id是否存在。如果没有索引数据库只能对订单表做全表扫描逐行确认是否引用。这个扫描过程不是简单的只读操作它还需要锁定匹配行——因为你不知道后续会不会再更新这些行外键关系必须保持一致性。于是一条本来很简单的删除操作变成了对千万级表的全表扫描加隐性锁范围扩大整个表的 DML 并发都会受到影响。等这段 SQL 真正结束你可能才在 AWR 里看到 CON$/CDEF$ 相关的递归调用记录。更隐蔽的是即便父表删除的数据量很小只要子表这个外键列没有索引数据库每删除一行父表记录都可能要重新访问一次约束定义和列映射从而频繁触碰CDEF$相关字典信息。诊断时AWR 的 Top SQL 可能并不显示业务 SQL而是显示大量的递归 SQL、latch: row cache objects或enq: TM - contention这时候往“外键”方向排查往往是正确路径。2.3 DDL 的字典锁与 row cache lock外键问题之外另一类高频性能问题来自 DDL 和字典缓存的互相挤压。当你执行ALTER TABLE ... ADD CONSTRAINT、DROP CONSTRAINT、ENABLE CONSTRAINT这类操作时Oracle 需要修改CON$和CDEF$里的记录同时更新数据字典缓存。这个修改过程是串行化的。可以这么理解字典缓存就像一个小黑板所有会话写黑板时都要先拿粉笔、写内容、擦掉再让下一个人写。如果在此之前有一大堆业务会话正在读取同一块区域那么 DDL 会话就要排队等粉笔反过来如果 DDL 会话长时间占着粉笔不放手其他会话就会等在row cache lock等待事件上。实际运维中我见过不少“卡死”案例发起端是一条增加约束的 DDL但最终受害者却是大量夜间批处理 SQL。批处理对子表持续 INSERT需要做外键校验结果 DDL 正锁着相关字典缓存条目两边互不相让。这种场景从现场看就是大量会话无限期挂在row cache lock上而持有者往往是一条看起来和业务毫无关系的 DDL。处理原则也很明确DDL 尽量放到维护窗口批处理前先梳理约束变更计划不要在大并发 DML 持续期间做字典结构变更。2.4 回收站里的“幽灵约束”还有一个经常被忽视的点回收站。从 10g 开始DROP TABLE默认不是物理删除而是把对象放进回收站对象名变成 BIN$ 开头的内部名。很多人以为表被 DROP 后它的约束元数据也马上消失了其实不然——只要回收站里的对象还没被PURGE它在CON$、CDEF$、OBJ$等字典基表里的记录就依然存在。我曾经帮一个客户排查“AWR 里数据字典基表扫描量异常大”的问题发现DBA_RECYCLEBIN里有几千张历史表对应的约束行全部残留在CDEF$中。每次某些系统任务扫描字典信息时都要在这些“幽灵约束”上做判断白白浪费 I/O 和 CPU。而且回收站对象越多字典表行数越多后续升级脚本、DBMS_STATS.GATHER_DICTIONARY_STATS等操作也会越慢。处理方式不算复杂定期清理回收站尤其在项目迁移、灰度发布频繁的环境里PURGE DBA_RECYCLEBIN;之后CON$和CDEF$的行数会有肉眼可见的下降。这一招对“没有明显热点 SQL 但字典相关等待偏高”的案例特别有效。3. 问题排查实录3.1 现场症状从等待事件和递归 SQL 开始假设你现在接手一套系统线上反馈“订单批量删除任务从半小时变成三小时”AWR 里没有严重的磁盘 I/OCPU 也不高但出现了两类等待enq: TM - contention和一部分row cache lock。这时候我会按几个步骤查。第一步看v$session和v$active_session_history确认阻塞和被阻塞的会话关系。如果大量会话都在等同一个锁找到持有者看它正在执行的 SQL 是什么。第二步看业务 SQL 的执行计划重点观察有没有对子表的全表扫描尤其当子表特别大、where 条件里又是外键列时基本可以怀疑外键索引缺失。第三步抓一段 10046 trace确认递归 SQL 里是否高频出现SYS.CDEF$、SYS.CON$相关的字典读取。抓 trace 的命令很简单-- 在问题会话执行 alter session set events 10046 trace name context forever, level 8; -- 固定一下等待时间再关闭 alter session set events 10046 trace name context off;然后去 trace 文件所在目录grep 一下关键表名grep -E CDEF\$|CON\$ 会话_SQL_trace.trc看到大量以字典基表访问为主的递归 SQL基本可以确认约束元数据被频繁加载要么是外键检查太重要么是缓存被频繁淘汰要么是字典缓冲本身配置或统计信息有问题。3.2 数据体检CDEF$ 和 CON$ 的规模排查这类问题第一件事不是看 SQL而是看字典账本到底膨胀到了什么程度。以 SYS 身份执行下面几条查询能快速评估规模select count(*) from sys.cdef$; select count(*) from sys.con$; select count(*) from dba_recyclebin; select owner, substr(object_name,1,30) object_name, type from dba_recyclebin;如果CDEF$和CON$的行数异常大比如几万甚至几十万行而库里真正活跃的表和约束数量并不多那就说明大量元数据来自历史残留对象。再进一步可以按对象维度去看哪个表占用了最多的 CDEF$ 行select obj#, count(*) cnt from sys.cdef$ group by obj# order by cnt desc fetch first 20 rows only;这里得到OBJ#后再关联DBA_OBJECTS就能还原出是哪个表的约束。通过这种“从字典基表到业务对象”的追溯方式能精准定位“约束大户”。当然不同版本之间的CDEF$和CON$结构可能略有差异。你在自己的环境里执行上述查询前最好先desc sys.cdef$和desc sys.con$确认字段存在。生产库上不要轻易对字典基表做全表扫建议放在低峰期或者先通过数据字典视图估算规模再决定是否执行。3.3 锁定元凶外键与约束的映射如果分析结论指向“外键检查太慢”那就需要把系统中所有外键和子表索引情况梳理一遍。下面是一个简化版的外键缺索引检测脚本适合大多数常规库select c.owner, c.table_name, c.constraint_name, c.r_owner, c.r_constraint_name from dba_constraints c where c.constraint_type R and c.status ENABLED and not exists ( select 1 from dba_ind_columns ic where ic.table_owner c.owner and ic.table_name c.table_name and ic.column_position 1 and ic.column_name in ( select cc.column_name from dba_cons_columns cc where cc.owner c.owner and cc.constraint_name c.constraint_name and cc.position 1 ) );这套脚本的核心逻辑是找出所有启用的外键约束然后检查对应子表上是否存在以该外键第一列为前导列的索引。需要说明的是它只检查了首列对于复合外键如果索引首列正确但后续列顺序不同也可能漏检或误报。所以在生产库使用前我建议再结合人工核对重点外键。找到问题外键后建索引通常能直接改善父表删除、更新时的子表检查路径create index idx_orders_customer_id on orders(customer_id) online;注意ONLINE选项可以降低建索引期间的 DML 阻塞但也不是完全没有影响建议放在维护窗口或者低峰期执行。建索引前先确认表数据量、表空间剩余空间避免因为排序空间不足导致索引创建失败。3.4 优化手段索引、清理、字典统计解决外键缺索引只是第一步。如果字典表本身已经膨胀或者统计信息不准确还需要同时做几件事。第一件是清理回收站。在确认没有业务需要从回收站恢复对象后执行PURGE DBA_RECYCLEBIN;。这个操作会真正删除回收站中的对象及其元数据让CON$、CDEF$的“幽灵记录”被释放。注意回收站很大的时候这个操作本身也会有压力所以同样要放到维护窗口。第二件是收集数据字典统计信息。Oracle 优化器在生成递归 SQL 的执行计划时也会参考CON$、CDEF$这些基表的行数和分布。如果它们长期没有统计信息或者统计信息严重过期递归查询就可能产生效率很低的嵌套循环或笛卡尔连接。常规做法是exec dbms_stats.gather_dictionary_stats;对于超大字典基表这条命令也许要跑一段时间。你可以观察它是否在等待什么正常情况下它在读取和分析 SYS 下的对象。如果业务库的字典对象实在太多可以分成几次处理但不要跳过——统计信息不准带来的成本往往比收集它花费的时间还要高得多。第三件是规划约束本身。对数据迁移、初始化导入这类场景可以考虑在操作前先禁用外键alter table orders disable constraint fk_orders_customer; -- 批量插入或清理完成后 alter table orders enable novalidate constraint fk_orders_customer;注意ENABLE NOVALIDATE只是跳过存量数据校验不等于数据一定正确。迁移完成后我仍然建议专门跑一次孤儿数据检查确认没有脏数据再考虑做一次ENABLE VALIDATE让约束真正完整生效。这个方法在大批量 DELETE、INSERT 场景下能明显减少对CON$/CDEF$的递归访问。4. 高频问题与避坑清单4.1 高频问题速查表现象常见原因优先处理方向大批量 DELETE 父表变慢等待 TM 锁子表外键列缺少索引全表扫描锁定检查外键索引并创建高并发 INSERT/UPDATE 时 recursive calls 高外键校验频繁访问字典缓存缓存淘汰压力大优化业务分批提交考虑临时禁用约束或约束数量瘦身会话大量挂在row cache lockDDL 正在修改CON$/CDEF$与 DML 争用字典缓存找持有者并停止/错峰 DDL维护窗口执行latch: row cache objects持续偏高共享池或字典缓存压力大递归 SQL 频繁检查共享池配置收集字典统计信息清理回收站CDEF$行数和业务对象数量明显不匹配回收站残留对象、历史约束未清理PURGE DBA_RECYCLEBIN定期巡检DDL 添加约束非常慢大表全表校验 字典登记双重开销分批次处理评估ENABLE NOVALIDATE方案后再验证存量这张表是基于我遇到过的真实场景总结出来的不一定覆盖所有情况但可以作为排查方向参考。实际工作中症状往往重叠出现比如既有外键缺索引又有回收站膨胀需要先抓主要矛盾再逐个处理。4.2 避坑清单与个人体会先说几条我踩过坑之后特别想强调的注意事项。第一条永远不要绕开 DDL 直接操作CON$和CDEF$。哪怕是看起来人畜无害的UPDATE SYS.CON$ SET ...也可能绕过 Oracle 的递归保护破坏字典一致性最终只能靠备份恢复。数据字典是数据库的命根子不是拿来“调优”的。第二条DROP TABLE时如果不打算从回收站恢复就顺手带上PURGE。我见过太多测试库、临时库一年下来回收站几千张表字典元数据成倍增长性能就是这个过程里一点点被拖垮的。虽然PURGE不能完全替代定期清理但至少能减少新生残留。第三条外键索引不是可选项是必选项。即使业务查询不通过外键列过滤数据外键索引的主要作用也是保护父表 DML 效率而不是查询效率。判断标准很简单凡是启用的外键子表对应列必须有合适索引这是硬指标。第四条收集字典统计信息不能只在出问题时才做。数据字典也在不断变化表的增删、约束的启停、回收站的清理都影响CON$和CDEF$的数据分布。建议在定期维护窗口里执行DBMS_STATS.GATHER_DICTIONARY_STATS并把结果记录到巡检清单里。第五条遇到性能问题时先把“业务 SQL 本身”和“约束校验引发的递归 SQL”分开看。很多情况下Top SQL 只是表象实际瓶颈在后台那些访问字典基表的隐形操作上。训练自己在看到大量 recursive calls 时条件反射去查外键状态这比死磕一条 SQL 的执行计划有用得多。最后再分享一个小技巧。在做大版本升级或者大范围字典变更前我会先记录CON$、CDEF$、OBJ$的当前行数并和半年前的数据对比。如果行数增长异常说明库里积压了大量回收站对象或历史残留元数据先做一次彻底清理再进入升级流程往往能显著缩短字典升级阶段的时间也能降低升级过程中出现 ORA-00600 之类异常的概率。这套“先体检账本再动大手术”的思路虽然不是银弹但确实帮我在几次关键运维窗口里避免了大麻烦。