做DBA这些年,性能问题见了不少,有一种问题最让人头疼:你翻遍AWR、盯了半天活动会话,发现罪魁祸首居然是Oracle数据字典里那几个不起眼的内部基表。CDEF$和CON$就是这类角色。它们平时藏在数据字典深处,几乎没人关心,可一旦约束变更频繁、数据字典查询量暴涨,它们就能把系统拖得步履蹒跚。最近处理某业务系统的卡顿问题,最终定位到这两个表上的竞争,顺手把相关的原理和排查过程整理一下,给遇到类似问题的朋友一个参考。
1. 先认识CDEF$和CON$:Oracle约束元数据的源头
1.1 数据字典里的“约束档案柜”
Oracle数据库的元数据不是凭空存在的,而是以普通表的形式,存放在SYSTEM表空间的一组内部基表中。这些基表平时被隐藏得很深,普通用户连查询权限都没有,但执行DDL、查询数据字典视图时,Oracle内部会不断读写它们。
CDEF$和CON$就是这组内部基表里专门负责约束信息的两张表。约束是什么?主键、唯一键、外键、检查约束、非空约束,都属于约束。只要你在数据库里创建过表、加过主键,Oracle就需要把这条约束的元数据记录下来。记录到哪里?一部分进CON$,一部分进CDEF$,还有一部分进CCOL$等关联表。
很多DBA对这两张表不熟,是因为日常工作主要和DBA_CONSTRAINTS、USER_CONSTRAINTS这些视图打交道。但视图只是一层“窗口”,窗口背后的数据源正是这些基表。当你在数据库里查询一个表有哪些约束时,Oracle会自动把视图SQL重写成对基表的访问,这个过程对你是透明的,但它的消耗会真实地体现在数据库性能数据中。
1.2 两个基表的分工:一张“总台账”和一张“明细卡片”
要理解性能问题,先要理解数据结构。CON$可以理解成约束的“总台账”,一条约束在CON$里对应一条记录。它保存了约束的编号、所属用户、约束名称、约束类型等基本信息。比如你创建了一个主键约束“PK_EMP_ID”,CON$里就会多一行,记下约束名为PK_EMP_ID,类型是主键,属主是哪个USER#。
CDEF$则是同一份约束的“明细卡片”,它和CON$通过约束编号CON#关联,保存了约束的扩展属性,比如是否启用、是否处于验证状态、是否可延迟、检查约束的具体条件文本等。一个约束在CON$里有一条总台账,在CDEF$里也会有一条明细记录。
从数据查询的角度看,两张表要连接起来才能看到一个约束的完整信息。这也是为什么如果它们上的执行计划或竞争出现问题,数据字典查询会整体变慢。除了这两张表,约束列信息还存放在CCOL$中,它记录了约束涉及了哪些列,但本文主要聚焦在CDEF$和CON$上。
1.3 从SQL视角看:查询约束时背后发生了什么
拿一个最常见的操作举例:
SELECT constraint_name, constraint_type, status FROM user_constraints WHERE table_name = 'EMPLOYEES';你以为这只是读一张视图,其实Oracle的递归SQL会访问USER$、CON$、CDEF$、CCOL$等基表。具体逻辑大体是:先通过USER#定位当前用户,再通过OBJ#找到EMPLOYEES表对应的对象,然后从CON$里找到该对象上所有约束,再从CDEF$里取出状态等字段。
这个过程在约束很少的库上几乎无感,但如果一个Schema里有几百万条约束,或者正在批量创建约束,递归SQL可能变成性能瓶颈。所以我一直建议DBA不要忽略那些看起来简单的数据字典查询,它的背后可能是多张基表的大连接。
理解了这个底层结构,后面再看性能问题就清晰多了。所有和约束有关的活动,最终都会汇聚在这两张基表上,就像所有车辆都要经过同一座桥。
2. 为什么看起来不相干,却会和性能问题挂钩
2.1 所有约束变更都绕不开这两个表
Oracle里创建、删除、启用、禁用约束,本质上都是在这两张基表上执行DML操作。
- CREATE TABLE语句里带上了CONSTRAINT关键字,那么建表时就会地插入CON$和CDEF$的记录。
- ALTER TABLE ADD CONSTRAINT,同理,新增一条约束记录。
- ALTER TABLE DROP CONSTRAINT,会从这两张表删除对应记录。
- ALTER TABLE DISABLE CONSTRAINT,不会删除记录,但会把CDEF$里的ENABLED状态位改掉。
- DROP TABLE,会把该表上的所有约束记录一并删除。
这些操作平时单独看非常轻,一次插入一条记录而已。但数据库是共享资源,当大量会话同时做类似操作时,它们会去修改同一张基表,甚至是同一个Block上的数据。这时候就会产生行锁竞争、Buffer忙等待、Latch竞争,然后以等待事件的形式出现在AWR报告里。
在很多具体案例里,系统并不是因为业务SQL写得差,而是因为某个自动化工具在后台批量建表、批量加约束,把数据字典基表“打”成了热块。性能问题的根源,往往不在业务侧,而在这些基础操作。
2.2 高频DDL场景:大家都挤在同一扇门里
我遇到过一个典型场景:某公司的数据中台系统,每天凌晨会有大量作业并发建表,每个作业创建几十张表,每张表平均有十来个主键、外键、检查约束。作业之间还会重启,失败后反复重试。
一开始系统只是慢,后来直接出现大量“row cache lock”等待,所有会话都堵在字典缓存上。排查时发现,活动会话里的SQL几乎全是递归DDL,内容就是往CON$和CDEF$里插入数据。为什么慢?因为大家都往同一个高频Block上插入,每次插入都要拿Latch、排队、等Commit,互相拖累。
更麻烦的是RAC环境。两个节点的会话如果同时修改同一个Buffer,会产生跨节点的Buffer竞争,等待事件可能还会带上gc buffer busy。这种情况下,问题会被进一步放大,看起来像节点间通信出了问题,实际源头还是数据字典基表上的并发DDL太密集。
2.3 大规模数据加载/结构迁移:递归SQL成了“隐形杀手”
数据导入、克隆Schema、应用发版、数据库升级等场景,也会大量触发约束元数据操作。比如用数据泵导入一批表,数据泵工具在导入时会重建约束。把导入的并行度调得过高,所有并行进程都会同时去操作CON$/CDEF$。这时候你看到的可能是大量INSERT INTO CON$、INSERT INTO CDEF$这样的递归SQL。
有些DBA遇到这类情况,总是怀疑数据泵参数配得不对,或者怀疑表空间I/O不行。但实际上,如果I/O和等待都集中在数据字典基表上,就需要意识到,这是约束元数据写入的排队问题。并行度不是越高越好,尤其是涉及到系统基表的DDL密集型作业,并行度过高反而会加剧竞争。
在大规模Schema迁移时,我建议先评估源端的约束总量:约束少,并行度可以高一些;约束多,并行度要保守一些。最好在测试环境模拟一次,看看等待事件会不会集中到row cache lock上。
2.4 基表上的执行计划坑:统计信息缺失与全表扫描
CDEF$和CON$也是表,也归优化器管。在正常情况下,Oracle对系统基表的访问采用内部优化过的固定执行计划,用CON#或OBJ#快速定位记录。但有些场景下,基表统计信息不准确,优化器就会算出全表扫描加嵌套循环的烂计划。
我见过一个案例:升级后某系统查询DBA_CONSTRAINTS非常慢,一条原本毫秒级的语句,执行成了几十秒。把执行计划抓出来一看,CDEF$被全表扫描,过滤条件是CON#等于某个值。理论上CON#是主键,应该走索引或直接定位,但优化器因为缺少统计信息,估算出CDEF$只有几十行,所以选择了全表扫描。
问题在于这个库的CDEF$里有上百万行,全表扫描一触发,Buffer Gets暴涨,直接拖垮Buffer Cache。这种问题用“优化SQL”的思路怎么都绕不过去,因为SQL是递归的、你改不了,只能从基表访问路径上想办法。
关于统计信息,这里必须提醒一句:不要轻易对SYS模式的内部表收集统计信息。Oracle对数据字典基表有自己的管理方式,乱收集可能导致更多计划异常。如果确实怀疑统计信息问题,先在测试库验证,或者在Oracle支持指导下操作。
3. 如何一步步定位是它们在“捣乱”
3.1 先看AWR:从等待事件和SQL统计里找线索
排查任何性能问题,我习惯先拿一个高峰期或问题时段的AWR报告,看Top 10等待事件。如果CDEF$/CON$有问题,AWR里往往会出现以下等待事件:
- row cache lock:字典缓存上的锁竞争,常见于并发DDL、序列、约束操作。
- buffer busy waits:多个会话争抢同一个数据块,对应基表上的热块。
- enq: TX - row lock contention:行锁竞争,通常是并发修改同一行约束记录。
- latch: row cache objects:访问字典缓存时的Latch争用。
只看等待事件还不够,要看SQL统计。如果SQL ordered by Gets里大量出现SQL_TEXT包含“CON$”“CDEF$”的递归语句,那基本可以锁定方向了。递归SQL平时很少出现在TopSQL里,一旦它们上榜,说明数据字典内部操作已经成了系统的主要负担。
3.2 借助动态性能视图锁定递归SQL和具体会话
拿到AWR线索后,下一步是实时或从历史会话中抓取具体SQL。可以这样查:
SELECT sql_id, sql_text, executions, buffer_gets, cpu_time FROM v$sql WHERE sql_text LIKE '%CDEF$%' ORDER BY buffer_gets DESC;重点看两类语句:一类是以INSERT、DELETE、UPDATE开头的DML,另一类是SELECT开头的递归查询。前者通常来自DDL,后者通常来自数据字典视图查询。
如果问题还在发生,可以直接查V$ACTIVE_SESSION_HISTORY或DBA_HIST_ACTIVE_SESS_HISTORY:
SELECT sample_time, session_id, sql_id, event, wait_class, blocking_session FROM v$active_session_history WHERE event LIKE '%row cache lock%' ORDER BY sample_time DESC;通过ASH的SQL_ID,可以反查出对应的语句文本,确认是不是在操作CON$和CDEF$。
3.3 现场取证:抓当时的Session、锁和Latch
问题发生时,如果条件允许,直接抓现场。下面这几个手段比较管用:
- 查询V$SESSION,找等待事件为row cache lock或buffer busy waits的会话,记录SQL_ID和调用的对象。
- 查询V$LOCKED_OBJECT,看是否有会话持有CON$、CDEF$上的行锁。这需要以SYS或合适权限查看。
- 使用如下SQL看Buffer上有多少会话在等待:
SELECT obj.owner, obj.object_name, w.session_id, w.wait_event_text FROM v$wait_chains w, dba_objects obj WHERE w.in_wait = 'WAITING' AND w.object_id = obj.object_id;当然,V$WAIT_CHAINS在一些版本中不直接给出对象,可能需要结合P1/P2参数反查文件号、块号,再映射到对象。这个流程虽然麻烦,但定位结果非常准确。
还有一种经典方法,就是对某个DDL会话做10046事件跟踪,打开递归SQL输出:
ALTER SESSION SET events '10046 trace name context forever, level 12';跟踪文件里会完整显示递归的INSERT INTO CON$、CDEF$语句,以及它们消耗的时间和执行计划。通过这种方式,能很清楚地看到一次ALTER TABLE ADD CONSTRAINT到底做了多少内部操作。
3.4 一个虚拟案例:看着CDEF$/CON$上的竞争怎么出现
为了更直观,我描述一个模拟场景。某核心系统在晚上20点出现会话堆积,AWR显示Top事件为row cache lock和enq: TX - row lock contention。
排查时从DBA_HIST_ACTIVE_SESS_HISTORY中,抓到了几十个阻塞型会话的SQL_ID,指向同一类语句。利用V$SQL查看SQL_TEXT,内容大致是:
text:INSERT INTO CON$ (CON#,OWNER#,NAME,TYPE#...)
另一个SQL_ID对应的是:
text:INSERT INTO CDEF$ (CON#,OBJ#,ENABLED,CONSISTENT,VALIDATED...)
结合时间点,发现是一批大数据平台的调度作业正在并发创建维度表和事实表,每个作业有20个并发线程。每个线程建表后会再加主键、外键和检查约束,相当于在短时间内往这两个基表插入几十万条记录。这些插入集中在同一个数据块附近,于是所有线程互相排队。
定位到根因后,把并发从20降到4,并在每个DDL之间加了极小的间隔,问题立刻缓解。后续把批量建表脚本优化为“建表和加约束合并到同一条DDL里”,从根源上减少了字典基表的访问次数。
4. 优化与预防:别让字典基表成为性能瓶颈
4.1 约束设计层面的“减肥”
很多人以为性能问题是DBA的锅,其实约束本身的设计也很重要。每一条约束都会在CON$和CDEF$里增加记录,约束越多,字典基表越大,相关操作越容易产生性能开销。
我见过一些系统,因为建表工具配置不当,每张表生成了一堆SYS_C开头的系统命名约束,其中还有大量重复的检查约束。业务上,一个列已经有非空约束了,又额外加一个CHECK(XX IS NOT NULL);或者约束条件互相重叠,完全没有必要。
优化方向不是让你去掉必要的数据完整性约束,而是合并、简化。比如:
- 对于必须保证非空的列,直接用NOT NULL约束,不要再用冗余CHECK约束。
- 对于状态列,能用CHECK约束限定取值范围,就不要同时加多个单值约束。
- 约束如果可以放在表级一次声明,就不要在列级重复声明。
约束数量减少了,字典基表上的操作自然减少。这个道理很简单,但很多项目在最初建表时根本不会想那么远。
4.2 DDL操作节奏控制与拆解
如果业务上确实需要大量建表、加约束,就要控制DDL节奏。我总结了几条可落地的经验:
- 尽量把约束定义直接写在CREATE TABLE语句里,这是最高效的。Oracle建表时一次性创建表和约束,比建完表再逐个ALTER TABLE ADD CONSTRAINT少很多递归调用。
- 若要批量执行DDL,控制并发度在2到4个DBE之间,不要动辄开到十几个线程。
- 每个DDL语句之间,可以加上短暂的sleep(比如0.5秒到1秒),让字典基表的热度降下来。
- 在RAC环境中,尽量让不同节点的批量作业错开执行时间,避免跨节点争用同一数据块。
- 对于超大的导入作业,不要用INSERT触发器或嵌套DDL,尽量保证约束创建过程平滑。
有时候运维人员为了缩短总耗时,拼命加大并行度,结果等待事件比执行时间还长,总耗时反而更差。性能优化不是一味追求“快”,而是找到合适的并发度。
4.3 字典基表维护建议
CDEF$和CON$位于SYSTEM表空间。很多时候SYSTEM表空间的I/O能力被人忽略,实际上它承载了数据字典的大量读写。如果SYSTEM表空间在慢盘上,字典基表的竞争会更明显。有条件的情况下,保证SYSTEM表空间所在存储的性能,是基础性工作。
其次,不要对这两张内部表做任何手工DML操作。我见过有人试图通过DELETE FROM CDEF$来删除所谓的“无效约束记录”,结果导致字典不一致,数据库只能做恢复。这是绝对的高危操作。
如果确认是执行计划问题,可以考虑对特定系统基表收集统计信息。但要遵循两条原则:先备份、测试环境验证;最好在Oracle官方支持指导下进行。例如:
EXEC DBMS_STATS.GATHER_TABLE_STATS(USERNAME => 'SYS', TABNAME => 'CDEF$');这条命令可能在特定版本下有用,但可能影响其他递归SQL。我在生产环境从来不会上来就收集SYS表统计信息,除非已经通过10046和99事件确认了计划问题的确来自统计信息缺失。
4.4 监控与巡检清单
要把问题挡在发生之前,建议把下面这些检查项加入日常巡检:
- 定期检查AWR中row cache lock和buffer busy waits趋势。
- 查询V$ROW_CACHE,关注dc_constraints、dc_objects的Gets和Misses,对比历史基线。
- 监控CON$和CDEF$的行数变化,可以这样查:
SELECT (SELECT COUNT(*) FROM sys.con$) con_cnt, (SELECT COUNT(*) FROM sys.cdef$) cdef_cnt FROM dual;正常情况下,行数应该缓慢增长。如果某个时间段内行数突然暴增,那大概率有批量DDL作业在集中创建约束。
- 对生产环境的高频DDL作业进行审核,记录每个作业峰值会话数和约束影响量。
- 建立DDL操作申报机制,避免随意在业务高峰期执行批量建表命令。
我把这些内容整理成一张表格,也方便大家直接拿去对照:
| 监控对象 | 关键指标 | 异常信号 | 建议动作 |
|---|---|---|---|
| AWR等待事件 | Row Cache Lock | 连续多个报告上榜 | 检查并发DDL和递归SQL |
| V$SQL | SQL_TEXT含CDEF$/CON$ | 出现在Top SQL | 抓取SQL_ID和来源 |
| V$ROW_CACHE | DC_CONSTRAINTS Miss | Miss率持续上升 | 评估约束维护频率 |
| DBA_OBJECTS | 当前对象DDL会话 | 长时间占用 | 分析锁和会话状态 |
| CON$/CDEF$数量 | COUNT(*) | 非预期突增 | 定位新增约束来源 |
5. 常见问题排查实录与避坑心得
5.1 问题类型速查表
下面这张表汇总了我遇到过的几类典型问题,给大家参考:
| 症状 | 可能原因 | 核心定位手段 | 解决方法 |
|---|---|---|---|
| 数据库偶发停顿,AWR出现Row Cache Lock | 并发DDL,字典缓存争用 | 查看ASH中事件、SQL_ID | 降低DDL并发,错峰执行 |
| 大量Buffer Busy Waits,面向系统基表 | 约束记录热块争用 | 反查块号、对象映射 | 合并DDL,减少递归DML |
| 查询DBA_CONSTRAINTS非常慢 | CDEF$/CON$执行计划差 | EXPLAIN PLAN、10046 | 在指导下修正统计信息 |
| 数据泵导入期间严重阻塞 | 并行进程争用字典基表 | SQL_TEXT含INSERT INTO CDEF$ | 降低并行度,拆分导入批次 |
| DROP大表/分区时长时间等待 | 删除约束记录引发行锁 | V$SESSION阻塞链 | 先离线禁用约束,分批操作 |
如果你遇到的问题和上面某一行吻合,可以直接按对应方向排查。但要注意,一个复杂问题可能同时涉及多个因素,AWR、ASH、V$SQL、10046这些工具要组合使用,才不会有盲区。
5.2 我的几点实操心得
这类问题经历多了,我有几个体会非常深。
第一,不要一看到CON$和CDEF$就觉得是系统坏了,它们只是“受害者”。真正的原因是业务侧产生了大量约束变更。这时候应该去查是谁在DDL,而不是盯着基表想办法。我之前有过一次弯路,花了两个小时研究CDEF$的访问路径,最后才发现是一套自动化压测脚本在疯狂建删表。找到源头,问题自然就解决了。
第二,10046追踪是分析这类问题的终极武器。普通视图查不到递归SQL的细节,但10046能完整看到一条ALTER TABLE背后发起了多少次对CON$、CDEF$的DML,以及每次消耗了多长事件。有了这个证据,说服应用团队优化DDL脚本才有力度。
第三,要注意数据字典缓存的特殊性。即使你优化了SQL,字典缓存竞争也可能依旧存在。因为DDL本身会持有字典锁和缓存锁,这是Oracle内部机制决定的。你能做的是减少同时发生的DDL,而不是试图绕过缓存。
第四,如果是在RAC环境,看到gc buffer busy与数据字典基表相关,往往不是网络问题。先确认所有节点上是否有相同业务的并发DDL。跨节点对同一个数据块的争用,本质上和单实例的Buffer Busy一样,只是表现更隐蔽。
最后分享一个小技巧:在分析V$SQL时,建议过滤掉普通的业务SQL,只保留递归SQL:
SELECT sql_id, substr(sql_text,1,100) sql_text, executions, buffer_gets FROM v$sql WHERE sql_text LIKE 'INSERT INTO CON$%' OR sql_text LIKE 'INSERT INTO CDEF$%' OR sql_text LIKE '%CDEF$%' ORDER BY buffer_gets DESC;很多时候,你根本不需要分析完整执行计划,只要列出来就能看出是哪个模块在制造压力。把递归SQL和业务时间点对上,基本就能定位到具体作业。
CDEF$和CON$这两个基表,平时安静地躺在数据字典里,但它们承载了Oracle所有约束的生命周期。性能问题一旦和它们挂钩,往往意味着整个系统的DDL活动已经超过了数据库内部能平滑处理的阈值。与其等它爆发,不如提前做好监控和节奏控制。这样等下次遇到奇怪卡顿时,你也能第一时间辨别:这次到底是不是CDEF$和CON$在背后“捣乱”。