一次从11g平滑升级到12c的数据库迁移做完了,业务上新当天,应用连上数据库就报ORA-00959: tablespace does not exist。查了几轮才发现,应用连的是容器数据库下的某个PDB,而DBA同事习惯性地用老方法、连到了旧文档里写的实例上,把业务表空间建到了整个CDB的根容器里,PDB里根本看不见。这类问题在第一次接触Oracle 12c CDB架构时太容易踩了,因为表空间管理的“单位”发生了变化:不再是同一个实例下的所有用户共享一套表空间,而是每个容器各管各的表空间。这篇文章就把我在CDB环境里折腾表空间的管理经验完整梳理一遍,从连接、建表空间、扩容告警到UNDO与临时表空间的坑,给后面接手的同学省点时间。
1. 从11g跨到12c第一课:表空间不再属于实例,而是属于容器
1.1 一次典型的“沿用老经验”事故
先说那个比较常见的翻车现场。某个项目使用的数据库从11g升级到12c,当时数据库按多租户架构建成了CDB,里面有一个业务PDB。负责迁移的同事沿用了11g时代的管理习惯,用sqlplus / as sysdba登录后,直接执行了很长一串创建表空间脚本,表空间和数据文件都建在了CDB的根容器(CDB$ROOT)里。
结果是什么?业务库的表空间看起来建好了,DBA_TABLESPACES查出来是存在的,但应用连接的PDB里始终报表空间不存在。当时排查了很久:用户权限没问题,服务名没问题,表空间明明存在,为什么应用说没有?
问题就出在“容器”上。CDB$ROOT里创建的表空间对PDB完全不可见。这在Non-CDB时代是不可能出现的情况,因为那时候一个实例对应一个数据库,不存在“表空间归属”的歧义。到了12c,如果不先确认自己当前在哪个容器里就执行DDL,很容易把对象建错地方,而且错得非常隐蔽。
1.2 CDB、PDB与表空间的归属关系
Oracle 12c引入多租户架构之后,一个实例可以承载一个CDB(容器数据库),CDB里又包含:
CDB$ROOT:根容器,存放Oracle系统元数据和公共对象;PDB$SEED:种子容器,专门用来创建新PDB的模板,平时不能被修改;- 一个或多个PDB:用户真正的业务容器,业务数据、用户、业务表空间都放在这里。
用生活化的类比:CDB就像一个小区,CDB$ROOT是小区物业办公室,PDB$SEED是毛坯样板间,而PDB是业主自己装修的套间。每个套间都有自己的水电表(SYSTEM表空间)、配电箱(TEMP表空间)、管道(UNDO表空间),物业办公室里的设备跟业主套间里的设备互不通用。
所以,在12c里管理表空间,第一反应不该是“我要在实例上创建表空间”,而应该是“我当前在哪个容器,我要给哪个容器创建表空间”。这也是整个CDB表空间管理和老版本最大的思维差异。
有一点要特别提醒:虽然12c在创建数据库时可以选择Non-CDB模式,但从12.2开始默认就是CDB,很多安装向导也默认走了多租户。如果你不主动确认容器关系,极大概率会踩到我上面说的坑。后面所有命令和排查逻辑,都必须围绕容器上下文来展开。
2. 连接哪个库先搞清楚:SERVICE_NAME、CONTAINER_ID与三条关键视图
2.1 一个连接错位导致的“表空间消失”
再说第二个高频问题:觉得“表空间怎么莫名其妙不见了”。其实不是不见了,是连接错了容器。
在CDB架构下,CONN / AS SYSDBA默认登录的是CDB$ROOT。如果你在这个会话里执行:
SELECT tablespace_name FROM dba_tablespaces;看到的是根容器下的表空间,这里面基本只有SYSTEM、SYSAUX、UNDOTBS1、TEMP等系统表空间,看不到任何PDB里的业务表空间。
而应用之所以能找到自己的业务表空间,是因为它通过类似pdb1.example.com这样的服务名连接到了具体的PDB。这时候再用PDB里的会话执行同样的SQL,才能看到属于这个PDB的表空间。
我遇到过不止一次:有人在根容器里用DBA_DATA_FILES视图查数据文件,发现某个业务表空间没有数据文件,于是断定“数据文件丢了”。实际情况是该表空间属于某个PDB,在CDB$ROOT里本来就不该查得到。
2.2 DBA_TABLESPACES、CDB_TABLESPACES怎么配合
正确做法是区分两类视图:
DBA_TABLESPACES、DBA_DATA_FILES:只查当前容器内的表空间和数据文件。连接的是PDB就查PDB的,连接的是根容器就查根容器的。CDB_TABLESPACES、CDB_DATA_FILES:在根容器下查看整个CDB内所有容器的表空间和数据文件,每一行还带一个CON_ID字段,用于标识属于哪个容器。
举个例子,在根容器里执行:
SELECT con_id, tablespace_name, contents, bigfile FROM cdb_tablespaces ORDER BY con_id, tablespace_name;CON_ID的含义一般是:1代表CDB$ROOT,2代表PDB$SEED,其余数字对应各个PDB。这样一条SQL就能把整个CDB下的表空间清单拉出来,比挨个PDB登录去查高效得多,也能避免“建错容器”这种问题。
如果再结合CDB_DATA_FILES看数据文件:
SELECT con_id, tablespace_name, file_name, bytes/1024/1024/1024 AS size_gb FROM cdb_data_files ORDER BY con_id, tablespace_name;就能非常清晰地看到每个表空间的数据文件在哪个路径、属于哪个容器。遇到“表空间不存在”“文件找不到”类的报错,先跑这条SQL定位归属,能解决一大半的困惑。
2.3 切换容器的正确姿势:ALTER SESSION SET CONTAINER和SERVICE_NAME的关系
管理表空间时,很多人习惯用sqlplus / as sysdba这把“万能钥匙”,但在CDB里这把钥匙只能打开物业办公室的门。要进入具体的PDB,有两条路:
通过连接字符串直接连到PDB的服务名,例如:
CONN sys@pdb1_service AS SYSDBA然后这个会话里操作的就是PDB。
先连到根容器,再切换容器:
CONN / AS SYSDBA ALTER SESSION SET CONTAINER = pdb1;这种方式适合已经在根容器里并且拥有常用权限(比如被赋予了PDB_DBA角色)的场景。
切换之后,建议立刻做一次确认:
SHOW CON_NAME; SELECT SYS_CONTEXT('USERENV', 'CON_NAME') AS current_container FROM dual;这两条命令输出的就是当前会话所在容器名称,一旦养成“先看容器再动手”的习惯,表空间管理就基本不会出现“改错地方”的低级错误。这也是我在团队里反复强调的规范:任何关于表空间的DDL执行前,第一件事必须是确认CON_NAME,而不是直接敲CREATE TABLESPACE。
3. 建表空间实操:从CREATE TABLESPACE到OMF文件命名
3.1 创建之前的三个确认
真正开始建表空间之前,有三个问题一定要心里有数:
- 当前会话在哪个容器?这决定表空间建到哪儿;
- 这个容器里是否已有同名表空间?CDB内不同PDB之间允许同名表空间,但同一个PDB内不能重名;
- 数据文件路径怎么写?是用Oracle管理的OMF方式自动生成文件名,还是手工指定绝对路径。
这三点看似基础,却直接决定了后续维护的复杂度。很多人在“建表空间”时栽跟头,不是因为不会写CREATE TABLESPACE语法,而是因为没有回答好这三个问题。
第一个问题前面已经讲了,第二个问题比较直接,第三个问题值得多说两句:在12c的CDB环境里,PDB的数据文件路径习惯上会放到按PDB名字区分的目录下,比如/u01/app/oracle/oradata/ORCL/PDB1/,这样既方便识别归属,也方便做备份和恢复。如果你启用了OMF(Oracle Managed Files),只需要设置好DB_CREATE_FILE_DEST,连文件名都不用操心,Oracle会自动生成类似/u01/app/oracle/oradata/ORCL/PDB1/datafile/o1_mf_ts_app_xxxx.dbf的名字。
3.2 完整命令与参数批注
在PDB已经切换到目标容器之后,一个典型的业务表空间创建语句长这样:
CREATE TABLESPACE ts_app DATAFILE '/u01/app/oracle/oradata/ORCL/PDB1/ts_app01.dbf' SIZE 10G AUTOEXTEND ON NEXT 1G MAXSIZE 32G EXTENT MANAGEMENT LOCAL AUTOALLOCATE SEGMENT SPACE MANAGEMENT AUTO;逐段拆解一下:
SIZE 10G:初始数据文件大小,尽量一次性规划得合理。过小会导致后面频繁扩容,过大又占用磁盘空间。AUTOEXTEND ON NEXT 1G MAXSIZE 32G:开启自动扩展,每次自动增加1G,上限32G。这里要注意,MAXSIZE是针对单个数据文件的最大上限,不是表空间的硬性总大小;如果表空间需要更大容量,可以继续添加多个数据文件。EXTENT MANAGEMENT LOCAL AUTOALLOCATE:本地管理表空间、区大小由Oracle自动分配。这是从9i开始就一直推荐的方式,省去手动管理PCTINCREASE、FREELISTS这些老参数。SEGMENT SPACE MANAGEMENT AUTO:段空间自动管理,可以避免大量自由列表争用问题,也是默认推荐。
如果是bigfile表空间,写法略有不同:
CREATE BIGFILE TABLESPACE ts_big DATAFILE '/u01/app/oracle/oradata/ORCL/PDB1/ts_big01.dbf' SIZE 32G AUTOEXTEND ON NEXT 1G MAXSIZE 64G EXTENT MANAGEMENT LOCAL AUTO SEGMENT SPACE MANAGEMENT AUTO;BIGFILE表空间只有一个数据文件,但这个数据文件理论上可以非常大(在8K块大小的库中可高达32T级别),适合大批量顺序写入的业务,也减少了文件数量维护成本。缺点是扩容粒度比较粗,一旦磁盘不够灵活加第二个文件。
3.3 OMF与绝对路径:怎么选
我个人的建议是:能用OMF就用OMF,尤其在容器多、PDB多的环境里,OMF能帮你省掉很多命名上的麻烦。
启用OMF后,创建表空间可以极简:
CREATE TABLESPACE ts_app;Oracle会根据DB_CREATE_FILE_DEST参数自动决定数据文件位置和文件名。这样做的好处是:不用关心每个文件的全路径;PDB被plug/unplug时,文件路径信息更容易被自动处理。坏处是:如果你对文件系统布局有强制规范(比如审计要求、存储分层),全自动管理会让审查比较麻烦。
如果必须手工指定路径,我建议在文件路径里带上容器名和表空间名的前缀,例如:
CREATE TABLESPACE ts_app_pdb1 DATAFILE '/u01/app/oracle/oradata/ORCL/PDB1/ts_app_pdb1_01.dbf' SIZE 10G;这样在操作系统层看到文件,也能快速对应上属于哪个PDB、哪个表空间,备查和迁移都会省心很多。
3.4 常见报错与排查
创建表空间常见的报错集中在以下两类:
第一类:ORA-65040: operation not allowed from inside a PDB。这个报错说明你正处在PDB里,却尝试执行了只能在根容器执行的操作,方向反了。反过来,在根容器下直接创建业务表空间不会报错,但结果往往不是你要的。
第二类:ORA-01537: cannot add file ... - file already part of database。多半是文件路径写重复了,或者之前建表空间失败但文件已经存在于磁盘上。处理办法:确认文件确实没有被任何表空间引用,删除残留文件后再执行。
第三类:路径不存在或者权限不够。比如ORA-01276: cannot add files,通常是因为目录不存在或者Oracle进程没有目录写权限。12c的OMF模式下,目录如果没有提前创建,Oracle不一定会自动创建,所以最好先把目录准备好并赋予正确的属主和权限。
排查这类问题,我通常先看alert日志和DBA_DATA_FILES中实际记录到的文件路径,再结合操作系统层面ls -ld确认权限,基本能快速定位。
4. 容量管理的实战链路:监控SQL、告警分析、扩容和回收
4.1 一条跑遍CDB的表空间使用率查询
表空间使用率告警,是DBA最日常的运维工作。但CDB环境下常用的老脚本有个问题:只查DBA_DATA_FILES和DBA_FREE_SPACE,结果只反映当前容器。如果CDB里有5个PDB,就要登5遍、跑5次脚本,效率太低。
我一般备用一条基于CDB视图的汇总查询,一条SQL扫完所有容器:
SELECT c.name AS container_name, t.name AS tablespace_name, t.contents AS type, d.file_name, ROUND(d.bytes / 1024 / 1024 / 1024, 2) AS allocated_gb, ROUND((d.bytes - NVL(f.bytes, 0)) / 1024 / 1024 / 1024, 2) AS used_gb, ROUND((d.bytes - NVL(f.bytes, 0)) / NULLIF(d.bytes, 0) * 100, 1) AS used_pct, d.autoextensible, ROUND(d.maxbytes / 1024 / 1024 / 1024, 2) AS max_gb FROM cdb_data_files d JOIN cdb_tablespaces t ON d.tablespace_name = t.tablespace_name AND d.con_id = t.con_id JOIN v$containers c ON d.con_id = c.con_id LEFT JOIN ( SELECT con_id, file_id, SUM(bytes) AS bytes FROM cdb_free_space GROUP BY con_id, file_id ) f ON d.con_id = f.con_id AND d.file_id = f.file_id ORDER BY c.name, t.name, d.file_name;执行的结果会列出每个容器、每个表空间的分配大小、已用大小、使用率、是否自动扩展、最大上限。哪个PDB的哪个表空间快满了,一眼就能看到。
这条SQL特别适合定时巡检,也适合在接到告警时先快速判断:是真的数据文件满了,还是因为MAXSIZE设太小被顶住了,还是磁盘物理空间不足。
4.2 告警常见三种情况
接到表空间使用率告警,不要急着加数据文件,先分清楚是哪种情况:
- 使用率超过90%,但文件仍可自动扩展:这种最省事,通常只是增长较快,需要评估要不要调高
MAXSIZE,或者加新的数据文件。 - 使用率接近100%,且
MAXSIZE已经触顶:说明单文件上限被卡住了,需要执行ALTER DATABASE DATAFILE ... RESIZE或者ALTER TABLESPACE ... ADD DATAFILE。 - 使用率不高,但磁盘分区满了:这是CDB环境里容易被忽略的一点。多个PDB可能共享同一个挂载点,某一个PDB临时表空间暴涨,可能把整个共享盘打满,进而影响所有PDB。收告警时我习惯同步执行一次
df -h,把文件系统和数据库视图放在一起看,避免只盯着表空间百分比却忽略了宿主文件系统容量。
4.3 应急扩容操作示例
确认需要扩容后,常见操作按照场景区分:
如果只是想放宽单个数据文件的上限:
ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/ORCL/PDB1/ts_app01.dbf' AUTOEXTEND ON NEXT 1G MAXSIZE 64G;或直接调整现有文件大小(注意该文件当前使用量必须小于目标大小):
ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/ORCL/PDB1/ts_app01.dbf' RESIZE 40G;如果需要增加一个全新的数据文件,注意切换到正确的容器再执行:
ALTER TABLESPACE ts_app ADD DATAFILE '/u01/app/oracle/oradata/ORCL/PDB1/ts_app02.dbf' SIZE 20G AUTOEXTEND ON NEXT 1G MAXSIZE 64G;这里有一个很实际的经验:扩容前先查V$SESSION里有没有长时间占用的会话,做过大事务或大量排序的业务。因为ALTER DATABASE DATAFILE ... RESIZE这类操作虽然本身很快,但如果在业务高峰执行,可能遇到文件系统锁、空间分配不足等问题。最好放在低峰期操作,并且操作完立刻跑一次使用率SQL确认新容量已经生效。
4.4 缩容和回收碎片的边界
删了很多数据后,表空间使用率下降,但文件大小没变,这是正常现象:数据文件不会因为删了数据而自动收缩。回收空间有两类手段,边界要搞清楚:
第一类是合并碎片,命令简单但效果有限:
ALTER TABLESPACE ts_app COALESCE;COALESCE只能合并相邻的空闲区,对本地管理表空间来说作用已经很小,因为区分配本身就是动态管理。它不会释放任何数据文件。
第二类是真正收缩数据文件大小。12c里可以对表空间执行SHRINK SPACE:
ALTER TABLESPACE ts_app SHRINK SPACE KEEP 10G;KEEP参数表示保留目标大小。执行时Oracle会尝试把空闲区回收并压缩文件。这个方法不见得总是有效:如果某个段(比如一个大表或大索引)占据了文件的后半部分,而段本身无法移动,收缩就会中途停下,或者收缩效果很有限。真正彻底的场景还是重建表空间:新建一个表空间,把数据搬过去,切换业务,再删旧表空间。对在线业务来说,这个过程往往需要停机窗口,不是一句SQL能搞定的。
所以我对缩容的总体心得是:规划大于事后治理。建表空间时宁可把初始文件设大一点、自动扩展策略设合理一点,也不要靠事后收缩来硬撑。缩容操作费时费力,且有风险。
5. 容易翻车的UNDO和TEMP:容器级特殊表空间的坑
5.1 本地UNDO模式下每个PDB管理各自的UNDO表空间
12c多租户架构默认采用本地UNDO模式,每个PDB都有自己的UNDO表空间,而不是像Non-CDB那样实例级共享一个UNDO。也就是说,在PDB里查V$UNDO_TABLESPACE看到的内容,只属于当前PDB。
这个设计本身合理,但带来一个管理上的坑:很多习惯老经验的人,都喜欢在一个会话里查看“全局”的UNDO空间。查询结果里出现多个同名的UNDOTBS1,因为每个容器都各自维护一个。如果搞混了容器,会看到一个明明没怎么使用的UNDO表空间,另一个PDB里的UNDO却快满了。
查看当前容器UNDO的配置,最直接的方式是:
ALTER SESSION SET CONTAINER = pdb1; SHOW PARAMETER undo_tablespace;输出会显示当前PDB的UNDO表空间名。如果想看所有容器的情况,可以直接查:
SELECT con_id, tablespace_name, contents FROM cdb_tablespaces WHERE contents = 'UNDO';在本地UNDO模式下,每个PDB的UNDO表空间可以由PDB内的DBA自行管理,比如扩容:
ALTER TABLESPACE undotbs1 ADD DATAFILE '/u01/app/oracle/oradata/ORCL/PDB1/undo_pdb1_02.dbf' SIZE 8G AUTOEXTEND ON NEXT 512M MAXSIZE 16G;还要注意一点:UNDO表空间创建时的标准写法是:
CREATE UNDO TABLESPACE undo_pdb1 DATAFILE '/u01/app/oracle/oradata/ORCL/PDB1/undo_pdb1_01.dbf' SIZE 8G;如果切换PDB的默认UNDO表空间,流程是:先创建新的UNDO表空间,然后执行:
ALTER SYSTEM SET undo_tablespace = undo_pdb1 SCOPE = BOTH;切换后旧的UNDO表空间不立即删除,而是要等症状中的事务提交完成后,再考虑使用DROP TABLESPACE,否则容易因为存在活动回滚段导致删除失败。这是很常见的翻车点。
5.2 临时表空间在CDB/PDB里的真实面貌
临时表空间向来容易出问题,CDB架构下更容易混淆。先说视图层面:CDB_TABLESPACES里CONTENTS字段等于TEMPORARY的就是临时表空间。一个PDB可以有自己的默认临时表空间,而根容器也有自己的。
临时表空间的典型报错场景是排序或临时LOB操作过多,比如大量并行查询、大数据量GROUP BY。这时候临时表空间会飞速增长。检查当前PDB默认临时表空间:
ALTER SESSION SET CONTAINER = pdb1; SELECT * FROM database_properties WHERE property_name = 'DEFAULT_TEMP_TABLESPACE';创建新临时表空间:
CREATE TEMPORARY TABLESPACE temp_pdb1 TEMPFILE '/u01/app/oracle/oradata/ORCL/PDB1/temp_pdb1_01.dbf' SIZE 8G AUTOEXTEND ON NEXT 1G MAXSIZE 32G;然后切换默认临时表空间:
ALTER DATABASE DEFAULT TEMPORARY TABLESPACE temp_pdb1;临时表空间使用率报警时,很多人容易误判为业务数据表空间紧张。我建议告警处理时一定要先区分表空间类型,看到CONTENTS='TEMPORARY'就走另一套思路:查V$TEMPSEG_USAGE定位占用临时空间的SQL,再判断是否需要调整SQL或者扩容临时文件,而不是盲目往数据表空间里加文件。
5.3 切换UNDO表空间的正确方法
刚才简单提了切换UNDO表空间,这里展开一下流程,因为很多人会忽略“旧UNDO表空间不能马上删”这一点:
- 确认当前UNDO表空间:
ALTER SESSION SET CONTAINER = pdb1; SHOW PARAMETER undo_tablespace; - 创建新UNDO表空间,大小参考当前峰值使用量。
- 执行
ALTER SYSTEM SET undo_tablespace = undo_pdb1 SCOPE = BOTH; - 等待一段时间,观察新UNDO表空间是否正常工作。
- 旧UNDO表空间暂时保留,确认没有报错和回滚段冲突后,再执行
DROP TABLESPACE undotbs1 INCLUDING CONTENTS AND DATAFILES;
我的实际经验是:切换后至少要观察一个业务高峰期再删旧文件,稳妥起见保留两三天也不为过。UNDO这种东西一旦出问题,轻则ORA-01555,重则影响整个实例的一致性读,最好不要贪图省事。
6. 表空间迁移与PDB整体把玩:从管理一个库变成管理整个容器
6.1 为什么迁移单位从表空间变成了PDB
老版本数据库迁移,通常在实例层面做表空间传输、数据泵导入导出,需要仔细处理表空间、用户、权限、序列等一堆对象。12c下这个问题反而被简化了:PDB整体就相当于一个完整的小数据库,里面包含了它的所有表空间、用户、数据、元数据。迁移的粒度从“表空间”提升到了“PDB”,操作单位完全不同。
比如从一套环境把pdb1整体搬走,主流方法是unplug/plug:
-- 源端:关闭PDB并分离 ALTER PLUGGABLE DATABASE pdb1 CLOSE; ALTER PLUGGABLE DATABASE pdb1 UNPLUG INTO '/backup/pdb1.xml'; -- 目标端:基于XML加入CDB CREATE PLUGGABLE DATABASE pdb1 USING '/backup/pdb1.xml' COPY; ALTER PLUGGABLE DATABASE pdb1 OPEN;这个过程中,整个PDB的所有表空间会随之迁移,包括数据文件、临时表空间、UNDO表空间(在本地UNDO模式下)。相比以前逐个表空间做TRANSPORTABLE TABLESPACE,可操作性提升了一大截。
当然,unplug/plug有版本和平台限制,跨平台一般还是走数据泵。但即便走数据泵,在目标端导入时,通常也是先把PDB建好,再在PDB里创建目标表空间。这样一来,表空间的管理逻辑始终与某个PDB绑定在一起,而不是与全局实例绑定。
6.2 unplug / plug / impdp中表空间的处理
用数据泵(expdp/impdp)迁移PDB里的用户数据时,表空间处理要注意两点:
- 导出时:
SCHEMAS方式导出会带上对象的表空间属性,即使目标库表空间名不同,导入时也会尽量映射。如果目标PDB里还没有同名表空间,impdp会尝试自动创建,但路径、大小取决于目标环境的OMF配置,不一定完全符合预期。 - 导入时:建议先用
impdp的SQLFILE参数生成导入脚本,检查里面CREATE TABLESPACE语句是否符合目标环境的路径规划,再决定是修改脚本还是让impdp自动处理。
我遇到过一个比较典型的坑:原来表空间数据文件路径只存在于源端某套存储,目标端挂载点结构完全不同。如果直接impdp,默认会产生一个与源端路径一致的数据文件,导致目标PDB使用错误路径或失败。解决办法是先手动在目标PDB里创建好表空间,然后impdp时通过REMAP_TABLESPACE映射:
impdp directory=DATA_PUMP_DIR dumpfile=pdb1.dmp \ schemas=app_user \ remap_tablespace=ts_app:ts_app_new \ transform=segment_attributes:none这里remap_tablespace=ts_app:ts_app_new把源端的表空间名映射到目标端新表空间名。transform=segment_attributes:none则是让段对象重新适用目标表空间的存储属性,避免把源端的存储参数带过来产生冲突。
6.3 团队协作的分工建议
CDB环境一旦有多个PDB,管理职责也需要随之调整。过去一个DBA负责整个实例的所有表空间;12c里更合理的方式是按PDB划分责任。每个PDB有自己的管理员,可以管理自己容器内的表空间,但不能干涉根容器和其他PDB。根容器的表空间空间管理仍然由超级DBA统一负责。
实际协作中有个容易踩的权限坑:某人拿着DBA角色在PDB里执行CREATE TABLESPACE可能提示权限不足,因为很多对象权限在12c里被区分为公共权限(Common Privilege)和本地权限(Local Privilege)。如果一个权限是以公共权限方式授予的,在特定PDB中可能被撤销或限制。遇到这种情况,不要直接用GRANT DBA TO ...这种老办法去“暴力解决”,而是在根容器或PDB里检查DBA_SYS_PRIVS、ROLE_TAB_PRIVS,确认具体缺的是哪个权限,再最小化补齐。这样既能解决问题,也不至于把容器间的权限边界搞得一团乱。
7. 给接手CDB表空间管理的几点个人经验
7.1 永远先看CON_NAME,再执行任何DDL
无论你是老手还是刚接触12c,每次登录数据库后,请养成一条肌肉记忆:
SELECT SYS_CONTEXT('USERENV','CON_NAME') FROM dual;然后把这条命令的输出作为所有后续操作的前提。我在多个项目里都见过类似的教训:不是技术不行,而是习惯性地用老经验直接执行脚本,结果在根容器里建了业务表空间,或者在PDB里尝试修改根容器的表空间。一条CON_NAME就能避免。
7.2 给表空间命名和数据文件路径建立统一规范
CDB里多个PDB允许同名表空间,但不能依赖这一点。万一交接、迁移、恢复时搞混了PDB,同名表空间会造成很大的排查成本。我建议在表空间命名上直接带PDB标识,比如:
- 表空间:
ts_app_pdb1 - 数据文件:
ts_app_pdb1_01.dbf - 临时表空间:
temp_pdb1 - UNDO表空间:
undo_pdb1
这样从名字上就能知道是哪个PDB的,也能在CDB_DATA_FILES里快速过滤。
7.3 留一套“表空间速查”脚本,关键时刻不慌
维护CDB表空间,我一般会在每台数据库服务器上留一组常用SQL脚本:
- 表空间整体视图:
cdb_tablespaces+cdb_data_files汇总; - 容器信息:
v$containers; - 当前会话容器:
SYS_CONTEXT('USERENV','CON_NAME'); - 临时段占用:
v$tempseg_usage。
脚本本身不长,但遇到事故时能省下大量手敲命令的时间。尤其是多个PDB一起告警的时候,一套脚本把所有容器的状态打出来,比逐个人肉切换容器高效太多。
7.4 从个人体会谈12c表空间管理的整体感受
带了一些项目之后,我越来越觉得CDB表空间管理的核心,其实不在于CREATE TABLESPACE的那条SQL怎么写,而在于是否建立了“容器边界”意识。表空间是容器内部的资源,不是实例全局的资源。理解了这一点,后面所有操作都是顺理成章的事。
另外要提醒的是,Oracle版本迭代很快,12c之后又有19c、21c等更新版本,但CDB的基础逻辑是一脉相承的。把12c这套表空间管理机制理清楚,后续版本里的“容器—表空间—数据文件”三层关系就不会再绕晕。
如果不确定某个PDB里表空间的实际归属,最简单的方法还是回到最基础的那条SQL:
SELECT con_id, tablespace_name, file_name, bytes/1024/1024/1024 AS size_gb FROM cdb_data_files ORDER BY con_id, tablespace_name;把结果拉出来,对着v$containers里的容器名一比对,很多疑惑瞬间就散了。这也算是我接手CDB表空间管理以来,最想分享给大家的一条实战心得。