事情要从一次发布窗口的报错说起。应用侧有个自动化部署流程,其中一步是在一个 19c 数据库里通过存储过程动态建表,结果报出了:
ORA-01950: no privileges on tablespace 'TBS_APP_DATA'第一反应通常是“用户没配额”,但查下来却让人懵:登录用户app_exec的默认表空间是TBS_APP_DATA,数据库里也建过角色,角色又带着UNLIMITED TABLESPACE,手动在 SQL*Plus 里执行同样的建表语句也能成功。也就是说,表面上权限文件没有任何问题,报错却很诚实:在这个 tablespace 上,当前执行上下文就是没有权限。
这种“权限明明有,却报 no privileges on tablespace”的现场,我后面又遇到过几次,问题都藏在执行环境和权限传递方式上。这篇就把这次排查的完整过程、背后原理和另外几种高频诱因一次说清楚。
1. 症状:权限表看着“井井有条”,偏偏报 ORA-01950
先说这次现场的具体配置,方便对号入座。
数据库版本是 Oracle 19c,用户情况简化之后大概是这样:
- 用户:
app_exec - 默认表空间:
TBS_APP_DATA - 临时表空间:
TEMP - 直接授予的系统权限:
CREATE SESSION、CREATE TABLE - 角色:
APP_TS_ROLE - 角色内包含:
UNLIMITED TABLESPACE
从 DBA 视角看,app_exec既建了表,又有无限表空间权限,还套了一层角色,怎么看都不该报“no privileges on tablespace”。
但报错就是在自动化流程里出现了,而且是一闪而过,日志里只有一句:
ORA-01950: no privileges on tablespace 'TBS_APP_DATA'自动化部署的代码大致是这样:
-- 伪代码:部署平台调用 PL/SQL 包,包内部执行 DDL BEGIN PKG_DEPLOY.RUN_CREATE_TABLE; END;包PKG_DEPLOY内部再对CREATE TABLE做动态执行,表名和表结构都是拼接出来的。建表语句本身没问题,权限预期也没问题,但执行到EXECUTE IMMEDIATE 'CREATE TABLE ...'那一步,数据库直接拿配额问题打回。
更蹊跷的是,同一个人用同样的app_exec登录 SQL*Plus,手工执行同一条CREATE TABLE语句,能建出来。
-- 手工执行,成功 CREATE TABLE T_TEST (ID NUMBER);这就形成了一个很典型的“矛盾现场”:手工可以,脚本不行;权限齐全,报错却是权限类错误。
2. 先还原理:ORA-01950 里的 privileges 指的是什么
很多人一看到ORA-01950: no privileges on tablespace就本能地跑去看USER_TS_QUOTAS,这没错,但不全面。这个错误里的 “privileges”,指的是“在这个表空间上建立段的授权”,也就是常说的tablespace quota(表空间配额)。
2.1 建一张表,其实需要两类授权
在 Oracle 里,建表不是一个“有 CREATE TABLE 就能随便建”的动作。严格来说,至少要同时满足两个条件:
- 拥有对象 DDL 权限,比如
CREATE TABLE,保证你有权在自身 schema 或指定 schema 里建表。 - 拥有目标表空间的存储授权,也就是表空间配额,保证你有权在该表空间里分配存储段。
第二类授权有两种存在形式:
- 用户级显式配额:
ALTER USER app_exec QUOTA 100M ON TBS_APP_DATA - 系统权限
UNLIMITED TABLESPACE,等价于所有表空间都不受限
所以,如果用户只有CREATE TABLE,而在某个表空间上既没有显式配额,也没有UNLIMITED TABLESPACE权限,在这个表空间建表就会触发ORA-01950。
2.2 配额的“显式配额”和“系统权限”是两回事
操作习惯上,很多人把UNLIMITED TABLESPACE当成一种“通用配额”,但它本质上是一个系统权限,不是表空间对象上的一个属性。
这两者有明显区别:
- 显式配额是用户与表空间之间的绑定关系,比如
QUOTA UNLIMITED ON TBS_APP_DATA。 UNLIMITED TABLESPACE是给用户或角色授予的一条系统权限,像一把万能钥匙,对所有表空间生效。- 显式配额无法授予给角色,语法上就不存在
GRANT QUOTA ... TO role这种写法;但UNLIMITED TABLESPACE可以被授予给角色。
熟悉权限体系的人看到这里应该已经察觉到关键点了:既然UNLIMITED TABLESPACE是通过角色授予的,那问题就可能出在“角色有没有被当前执行环境认可”。
2.3 顺手把容易混淆的两个报错分开
排查时最怕把ORA-01950和ORA-01536混在一起。它们经常成对出现,但含义完全不同。
| 报错 | 含义 | 典型提示 |
|---|---|---|
ORA-01950: no privileges on tablespace 'xxx' | 用户在该表空间上没有任何可用配额 | 没授权、授权丢失、授权在执行环境里不生效 |
ORA-01536: space quota exceeded for tablespace 'xxx' | 用户有配额,但配额已经被用完 | 配额耗尽,需要扩容或提升配额上限 |
ORA-01950更像“敲门权限都没有”,ORA-01536是“门能进但里面空间不够”。这次报的ORA-01950,说明问题不在空间容量,而在于权限识别链路。
2.4 排查入口:别只盯着一张视图看
排查这类问题,数据字典视图的分工要先理清楚:
| 视图 | 作用 | 注意点 |
|---|---|---|
DBA_USERS | 默认表空间、临时表空间、账户状态 | 决定“没写表空间时建到哪” |
USER_TS_QUOTAS/DBA_TS_QUOTAS | 显式表空间配额 | 无法完全体现UNLIMITED TABLESPACE权限带来的效果 |
DBA_SYS_PRIVS/USER_SYS_PRIVS | 直接授予用户的系统权限 | 不含通过角色继承的权限 |
DBA_ROLE_PRIVS/USER_ROLE_PRIVS | 用户被授予的角色 | 只说明“有角色”,不说明“角色已启用” |
ROLE_SYS_PRIVS | 角色拥有的系统权限 | 用来反查角色内容 |
SESSION_ROLES/SESSION_PRIVS | 当前会话实际生效的角色和权限 | 真正的运行时授权快照 |
SESSION_ROLES和SESSION_PRIVS是这次排查中最关键的视图,因为它们回答的是一个和“用户被授予了什么”完全不同的问题:“当前这个 session 在这个时刻到底认哪些权限”。
3. 模拟复现:同一个 DDL,直接 SQL 能建,包内执行就报错
光有理论不够,我后来把现场缩小成了一个最小复现环境。下面这段在任何 19c 测试库上都可以重现,不建议直接在生产环境折腾。
3.1 先搭一个最小环境
-- 建两个测试表空间 CREATE TABLESPACE TBS_APP_DATA DATAFILE '/u01/app/oracle/oradata/ORCL/tbs_app_data01.dbf' SIZE 100M AUTOEXTEND ON NEXT 10M; CREATE TABLESPACE TBS_OTHER DATAFILE '/u01/app/oracle/oradata/ORCL/tbs_other01.dbf' SIZE 100M; -- 建测试用户 CREATE USER app_exec IDENTIFIED BY "Oracle123" DEFAULT TABLESPACE TBS_APP_DATA TEMPORARY TABLESPACE TEMP; -- 只给最基础的建表权限 GRANT CREATE SESSION TO app_exec; GRANT CREATE TABLE TO app_exec; -- 建一个角色,把 UNLIMITED TABLESPACE 放到角色里 CREATE ROLE APP_TS_ROLE; GRANT UNLIMITED TABLESPACE TO APP_TS_ROLE; -- 把角色授给用户 GRANT APP_TS_ROLE TO app_exec;注意,这里故意没有给app_exec任何显式配额,也没有直接授予UNLIMITED TABLESPACE。唯一的“无限表空间能力”在角色APP_TS_ROLE里。
3.2 直接 SQL 会话:成功
用app_exec登录 SQL*Plus:
CONNECT app_exec/"Oracle123"@ORCL CREATE TABLE T_TEST (ID NUMBER);结果是成功的。因为在普通 SQL 会话里,默认所有角色都会启用,APP_TS_ROLE带着的UNLIMITED TABLESPACE在会话内有效,所以表空间配额检查可以直接通过。
3.3 匿名块:也能成功
在 SQL*Plus 里直接用匿名块,同样成功:
BEGIN EXECUTE IMMEDIATE 'CREATE TABLE T_TEST_ANON (ID NUMBER)'; END;涉及一个很多人会忽略的细节:匿名块属于调用者权限上下文,全局会话里的角色依然处于启用状态,因此角色里的UNLIMITED TABLESPACE依然有效。
3.4 定义者权限包:失败
接着建一个存储过程或者包:
CREATE OR REPLACE PACKAGE PKG_DEPLOY AS PROCEDURE RUN_CREATE_TABLE; END PKG_DEPLOY; / CREATE OR REPLACE PACKAGE BODY PKG_DEPLOY AS PROCEDURE RUN_CREATE_TABLE IS BEGIN EXECUTE IMMEDIATE 'CREATE TABLE T_TEST_PKG (ID NUMBER)'; END RUN_CREATE_TABLE; END PKG_DEPLOY; /然后执行:
EXEC PKG_DEPLOY.RUN_CREATE_TABLE;结果立刻复现:
ORA-01950: no privileges on tablespace 'TBS_APP_DATA' ORA-06512: at "APP_EXEC.PKG_DEPLOY", line 4 ORA-06512: at line 1同一个用户,同一个CREATE TABLE,匿名块能过,命名包不能过。这就是典型到不能再典型的“奇怪 ORA-01950”。
3.5 再补一个对照组:直接授权后,包内立刻成功
为了解决疑问,把权限改成直接授予方式:
ALTER USER app_exec QUOTA UNLIMITED ON TBS_APP_DATA;再次执行:
EXEC PKG_DEPLOY.RUN_CREATE_TABLE;成功。关掉显式配额直接给系统权限也可以:
ALTER USER app_exec QUOTA 0 ON TBS_APP_DATA; GRANT UNLIMITED TABLESPACE TO app_exec;包内同样能成功。
所以症结已经很清晰:角色在非匿名 PL/SQL 存储程序里“隐身”了。
4. 根因:definer-rights 存储程序默认“不认角色”
这条规则是 Oracle 安全模型的默认设计,不是 bug。
4.1 什么是定义者权限
默认情况下,Oracle 存储过程、函数、包、触发器都是以定义者权限(Definer Rights)运行的。也就是说,无论调用者是谁,过程体执行时都以创建者的身份和权限去解析 SQL、执行 DDL、访问对象。
在这个机制里,为了保证安全边界,数据库会做一个关键动作:暂时禁用当前会话中的角色,只保留那些直接授予给调用者的权限。
这个设计的理由很直白:角色通常是一批权限的集合,如果定义者权限的代码也能看见角色,那么一个普通用户只要拥有调用某个过程的权限,就可能“顺走”过程里用角色继承来的额外能力。比如过程里有一段动态 SQL 使用了CREATE ANY TABLE,而这个权限来自某个角色,那么任何能调用这个过程的人都可能间接获得这个能力,权限边界就失控了。
所以 Oracle 在存储程序的调用栈里会比较“强硬”地关闭角色识别。要注意的是,关闭的不是整个会话的角色,而是当前 PL/SQL 执行环境对“角色权限”的可见性。
4.2 一个通俗的类比
你可以把普通 SQL 会话想象成员工带着门禁卡进机房,门禁卡上绑定了多个权限组,所以整个楼层都能进。但进入某个重要设备间后,内部机械只认柜台上登记的“直接授权”,你身上那层“权限组”在系统眼里是不存在的。手动操作时你是“带卡的全能状态”,进入包体后你只剩“直接挂牌”的状态。
所以:
- 手工执行
CREATE TABLE,走的是会话级权限判断,角色生效,建表成功。 - 进入
PKG_DEPLOY这个定义者权限包后,角色被临时隐藏,UNLIMITED TABLESPACE等同于不存在,报ORA-01950。
4.3 不只是表空间配额会这样
这个坑并不只影响ORA-01950。凡是依赖角色继承来的系统权限,在定义者权限 PL/SQL 中做动态 SQL 时都可能失效:
CREATE ANY TABLEDROP ANY TABLEALTER ANY INDEXSELECT ANY TABLE- 等等
很多自动化运维工具为了图快,把角色当成权限发放的默认方式,最后在“工具调用存储过程执行 DDL”这种场景下就会连环踩雷。
4.4 为什么“权限已给”这么具有迷惑性
DBA 最初看到ORA-01950时,往往会去查DBA_TS_QUOTAS,发现确实没有显式配额,再一查用户又有UNLIMITED TABLESPACE权限,于是疑惑更大。关键在于,权限是否生效,和权限记录是否“存在”是两回事。
DBA_SYS_PRIVS里看不到通过角色继承的权限,这是正常的;DBA_ROLE_PRIVS里有角色记录,也是正常的;但如果在执行上下文里角色被禁用,那UNLIMITED TABLESPACE在运行时就是“不存在”的。
这次案例里,CREATE TABLE是直接授权的,所以建表 DDL 能过权限检查;但表空间配额检查发生在建立段的时候,它看到的不是“职责文件里写没写”,而是“当前执行上下文认不认”。权限记录看得见,运行时状态看不见,就成了“奇怪的 ORA-01950”。
5. 修复:三种改法,按架构精度取舍
针对这次的问题,有三条整改路径。没有绝对最优,取决于你的权限管控粒度。
5.1 方案一:把表空间配额变成用户级直接授权(最推荐)
直接给用户app_exec设置表空间配额,不再依赖角色传递。
如果这个用户自身 schema 的表就应该建在这个表空间上,最稳妥的做法是:
ALTER USER app_exec QUOTA UNLIMITED ON TBS_APP_DATA;如果希望限制容量,可以精确到一个数值:
ALTER USER app_exec QUOTA 500M ON TBS_APP_DATA; ALTER USER app_exec QUOTA 500M ON TBS_OTHER;这样权限从“角色+系统权限”变成了“用户+表空间绑定”,清清爽爽,也不会被存储过程的角色隐藏机制影响。
提示:显式配额是表空间级、用户级的对象关系,这种授权在定义者权限 PL/SQL 里依然有效,因为它不依赖角色。
这个方案对自动化部署最友好。业务方哪怕完全不懂 Oracle 安全模型,只要知道“这个用户在哪些表空间能建表”,就不会再出这种诡异问题。
5.2 方案二:直接授予 UNLIMITED TABLESPACE 给用户
如果建表目标表空间比较多,而且权限要求没那么严格,可以简单粗暴:
GRANT UNLIMITED TABLESPACE TO app_exec;这个和显式配额一样属于直接授权,在定义者权限 PL/SQL 中依然有效。
但我不建议一上来就用它。UNLIMITED TABLESPACE意味着这个用户在所有表空间都不受限,一旦用户习惯不好,可能会把表建到SYSTEM、SYSAUX等地方,安全边界直接失去意义。能用显式配额解决,就用显式配额。
5.3 方案三:将包改为调用者权限 AUTHID CURRENT_USER
把包改成调用者权限,让过程执行时沿用调用者会话里的当前角色:
CREATE OR REPLACE PACKAGE PKG_DEPLOY AUTHID CURRENT_USER AS PROCEDURE RUN_CREATE_TABLE; END PKG_DEPLOY; / CREATE OR REPLACE PACKAGE BODY PKG_DEPLOY AUTHID CURRENT_USER AS PROCEDURE RUN_CREATE_TABLE IS BEGIN EXECUTE IMMEDIATE 'CREATE TABLE T_TEST_PKG (ID NUMBER)'; END RUN_CREATE_TABLE; END PKG_DEPLOY; /注意,AUTHID CURRENT_USER不仅会影响角色可见性,还会影响未限定对象名的解析方式。包内部所有 SQL 引用都开始以“调用者 schema”为基准去解析。如果你的包原来依赖定义者拥有的表或视图,schema 一变,很可能会报ORA-00942: table or view does not exist。
所以这个方案严格来说不是“改一个关键字就完事”,而是动摇了整个代码执行上下文,需要重新做回归测试。一般只在无法变更用户权限、且包内部引用对象比较简单的场景下考虑。
5.4 方案四:把 DDL 从包里拆出来
如果这套自动化流程根本不需要通过 PL/SQL 执行 DDL,那更合理的设计是让部署平台直接以app_exec用户连接数据库,用 SQL*Plus 或 JDBC 执行脚本。普通会话默认启用角色,UNLIMITED TABLESPACE自然生效,问题直接消失。
这种调整涉及部署架构,但能从根本上避免“定义者权限包 + 动态 DDL”这种高危组合。
5.5 验证手段
改完之后,建议跑一遍完整的验证路径:
-- 1. 查看显式配额 SELECT * FROM DBA_TS_QUOTAS WHERE USERNAME = 'APP_EXEC'; -- 2. 查看直接系统权限 SELECT * FROM DBA_SYS_PRIVS WHERE GRANTEE = 'APP_EXEC'; -- 3. 查看角色继承 SELECT * FROM DBA_ROLE_PRIVS WHERE GRANTEE = 'APP_EXEC'; -- 4. 重新执行原包 EXEC PKG_DEPLOY.RUN_CREATE_TABLE; -- 5. 确认表对象已经建立 SELECT TABLE_NAME, TABLESPACE_NAME FROM DBA_TABLES WHERE OWNER = 'APP_EXEC' AND TABLE_NAME = 'T_TEST_PKG';如果第四条能成功,第五条能查到表落在TBS_APP_DATA,问题就闭环了。
6. 同错不同因:另外几种“看起来有权限却失败”的常见现场
ORA-01950不是只有“存储过程里角色被禁用”这一种奇怪玩法。下面这几种场景我也在不同项目里见过,表现都是“权限检查没问题,实际建表失败”,但根因各不相同。
6.1 默认表空间和配额对不上
用户有好几个表空间的配额,但创建表时没有显式写TABLESPACE子句。按照规则,Oracle 会把段建到用户的默认表空间里,如果默认表空间恰好没有任何配额,即便其他表空间配额充足,一样会报ORA-01950。
排查顺序很简单:
-- 看默认表空间 SELECT USERNAME, DEFAULT_TABLESPACE FROM DBA_USERS WHERE USERNAME = 'APP_EXEC'; -- 看所有表空间配额 SELECT * FROM DBA_TS_QUOTAS WHERE USERNAME = 'APP_EXEC';如果DEFAULT_TABLESPACE = TBS_OTHER,而配额全在TBS_APP_DATA,那就需要明确建表目标:
CREATE TABLE T_TEST_TMP (ID NUMBER) TABLESPACE TBS_APP_DATA;或者直接把默认表空间和配额对齐。
6.2 大小写/引号表空间名不一致
这个坑主要出在手工创建的“大小写敏感表空间”上。Oracle 默认把未加引号的标识符转为大写,如果建表空间时用了带引号的混合大小写名称,比如:
CREATE TABLESPACE "Apps_Data" DATAFILE '/u01/app/oracle/oradata/ORCL/apps_data01.dbf' SIZE 100M;后续写配额时写成大写APPS_DATA,就可能出现“权限名对不上”的深层问题。虽然通常直接报ORA-00959: tablespace 'APPS_DATA' does not exist,但插件式架构下偶尔也会以权限异常方式呈现。
处理方式是统一命名规范:表空间名一律不带引号,让数据库统一转为大写,后续所有授权和 DDL 都按大写写法执行。
6.3 12c 以后 CDB/PDB 权限隔离
12c 以后,表空间配额是容器级别的,不能在 CDB$ROOT 和 PDB 之间混用。普通本地用户的管理通常发生在 PDB 内部,如果你习惯在 CDB$ROOT 里做ALTER USER ... QUOTA,授权很可能没有落到实际使用的 PDB 里。
排查时先确认当前容器:
SHOW CON_NAME;然后在正确的 PDB 容器下执行授权,例如:
ALTER SESSION SET CONTAINER = ORCLPDB1; ALTER USER app_exec QUOTA UNLIMITED ON TBS_APP_DATA;很多人从 11g 升到 19c 后容易踩到这一条,因为所见“用户”存在,查权限也在,但就是不在同一个容器内。
6.4 默认角色被关闭
另一种隐藏情况是,角色确实授给了用户,但通过ALTER USER ... DEFAULT ROLE NONE或者DEFAULT ROLE ALL EXCEPT ...把角色从“默认启用列表”里剔除了。这种情况下,用户一登录,角色就不会自动启用,UNLIMITED TABLESPACE在普通会话里都不生效,更不用说存储过程。
检查命令:
-- 查用户默认角色情况 SELECT * FROM DBA_ROLE_PRIVS WHERE GRANTEE = 'APP_EXEC'; -- 查当前会话里实际的启用角色 SELECT * FROM SESSION_ROLES;如果确认角色被默认禁用,直接启用:
ALTER USER app_exec DEFAULT ROLE ALL;这类问题通常伴随着“之前还能建表,现在突然不行,而且没有做任何权限变更”的现象,因为角色默认状态的调整往往在系统维护文档里被一笔带过。
6.5 常见场景汇总
| 场景 | 核心疑惑点 | 快速诊断 | 推荐处理 |
|---|---|---|---|
| 定义者权限包内 DDL | 手工成功、包内失败 | 关注SESSION_ROLES与存储程序上下文 | 显式配额或直接授权 |
| 默认表空间无配额 | 其他表空间有配额 | 对比DBA_USERS与DBA_TS_QUOTAS | 修正默认表空间或显式指定目标表空间 |
| 表空间名大小写混乱 | 授权语句看不出问题 | 检查DBA_TABLESPACES原始名称 | 统一命名规范 |
| CDB/PDB 容器错位 | 用户存在但授权不带入 SQL 会话 | 先SHOW CON_NAME再排查DBA_TS_QUOTAS | 在正确容器内授权 |
| 角色默认不启用 | 角色记录存在但会话里失效 | 查SESSION_ROLES/DBA_ROLE_PRIVS | 调整DEFAULT ROLE设置 |
7. 这次排查给我留下的几个习惯
这件事之后,我在每次处理类似权限问题时都会多问自己一句:现在看到的是“数据字典里的权限”,还是“当前执行上下文真正生效的权限”?
后来我也在团队里定了一套排查ORA-01950的固定动作,按顺序执行,基本不会漏:
- 先看
DBA_USERS里的默认表空间和账户状态。 - 再看
DBA_TS_QUOTAS里有没有显式配额。 - 然后查
DBA_SYS_PRIVS和DBA_ROLE_PRIVS,确认UNLIMITED TABLESPACE是直接授予还是角色继承。 - 再查
ROLE_SYS_PRIVS,确认角色内部到底装了什么。 - 最后查
SESSION_ROLES,只有当前会话里实际启用的角色才对运行时判断有影响。
如果用户是通过 PL/SQL 包执行 DDL,还要额外确认包到底是不是定义者权限,以及包内 SQL 是否有未限定的对象引用。
还有一个更实际的心得:权限治理上,尽量把“建表能力”做成用户和表空间的直接绑定,而不是把UNLIMITED TABLESPACE这种万能权限塞进角色里到处挂载。角色适合批量分发“通用功能权限”,但表空间配额这种高度依赖执行环境的能力,越直白越好排错。
这次故障最后只改了一行授权语句就解决了,但排查过程带出来的认知,比那一行语句值钱得多。以后再看到ORA-01950: no privileges on tablespace,你至少能多想一层:权限记录有,和权限在运行时被认,是两码事。