资讯动态

Oracle EBS R12表结构解析:多组织隔离、VPD与主数据设计

发布时间:2026/10/27 17:04:18 来源:尧图企业网站定制
简介Oracle EBS R12 表结构资料包面向从事EBS维护、二次开发与数据迁移的技术人员聚焦R12数据模型与数据库架构的快速查阅需求。包内共114个文件由58个PDF和56个HTML文档组成压缩包约6.01MBPDF便于离线阅读、打印标注HTML适合在浏览器中按模块检索与跳转。目前已有1605人学习下载具有一定参考热度。内容围绕财务、供应链、人力资源、项目管理、销售服务等模块展开涉及数据字典、核心交易表及其关联方式、职责与角色权限控制以及升级迁移中的数据转换和验证思路可帮助读者定位业务字段、梳理表间关系并为自定义报表、触发器、存储过程和索引优化提供依据适合需要系统查表与理解EBS数据模型的中初级技术人员使用。1. 表结构的真相为什么 Oracle EBS R12 里一个业务对象总是散在五张表第一次翻 Oracle EBS R12 表结构的人多半带着“一张表对应一个业务对象”的预期进来然后被现实打脸。查一张应付发票AP_INVOICES_ALL 只给抬头行在 AP_INVOICE_LINES_ALL会计分配在 AP_INVOICE_DISTRIBUTIONS_ALL付款计划在 AP_PAYMENT_SCHEDULES_ALL供应商干脆不在 AP 的命名体系里得绕到 AP_SUPPLIERS 甚至 HZ_PARTIES。这不是建模没做完而是一套在上世纪九十年代定型的结构把“锁粒度可控、并发不打架、加字段不动 DDL”排在范式之前。搞懂它为什么这么拆你才知道查数时该在哪一层停手也才不会把接口和报表写成玄学。2. 多组织与 _ALLORG_ID 到底是怎么把一张表切成 N 个租户EBS 的多组织不是“一个公司一个库”也不是“一个公司一套表”而是所有业务单位的数据挤在同一张物理表里靠一列 ORG_ID 区分。这一列是整个表结构里最容易被忽略、也最容易让人翻车的地方——你少写一个条件报表数字就翻几倍你多写一个条件用户又说查不到单。理解这一层等于拿到了读 EBS 表的钥匙。2.1 _ALL 不是视图的标记是「带 ORG_ID 的物理表」的命名约定在 EBS 的命名体系里后缀_ALL是一条硬约定这张对象是物理表里面存了所有业务实体Operating Unit的数据每一行都带 ORG_ID。不带_ALL的同名对象绝大多数是视图视图的定义里藏着一个针对 ORG_ID 的过滤谓词。所以 PO_HEADERS_ALL 是表PO_HEADERS 是视图AP_INVOICES_ALL 是表AP_INVOICES 是视图。视图不是简单的列裁剪它在编译期就把“只看当前 OU”这件事写死进了 SQL 文本。你用普通账号查 PO_HEADERS 觉得一切正常换成直连数据库的工具行为就开始飘原因就在这里。先确认你手里的名字到底是表还是视图这一步值得养成习惯-- 同一个业务名背后可能是表也可能是视图 SELECT owner, object_name, object_type FROM all_objects WHERE object_name IN (PO_HEADERS, PO_HEADERS_ALL) AND object_type IN (TABLE, VIEW); -- 视图定义里通常能看到针对 ORG_ID 的过滤谓词 SELECT text FROM all_views WHERE view_name PO_HEADERS;all_objects.object_type告诉你对象种类这是判断“我查的是不是真表”的最快方式all_views.text里能直接读到谓词长什么样参数通常是MO_GLOBAL.GET_CURRENT_ORG_ID()这类会话函数而不是常量。第二件事是索引。_ALL 表上的主键和唯一索引前缀几乎都带 ORG_ID比如订单行、发票行这类表唯一性约束是“在某个 OU 内唯一”不是全局唯一。这意味着两件事跨 OU 查询如果不带 ORG_ID索引前缀用不上很容易退化成全表扫表之间的 join 如果只按业务单据号而忘了 ORG_ID理论上还可能撞上别的 OU 的同号数据。我一般会这么理解ORG_ID 在 EBS 里不是“一个过滤条件”而是所有业务主键的隐含组成部分。2.2 VPD 把过滤藏进了优化器DBMS_RLS 与策略函数_ALL表和它的视图之间那层过滤不是靠应用代码自觉加 WHERE 实现的而是数据库层面的行级安全VPD也叫 RLS。机制很直接给表挂一个策略policy策略绑定一个 PL/SQL 函数这个函数在语句解析时被调用返回一段字符串数据库把这段字符串当成额外的 WHERE 谓词拼进去。这套东西在 EBS 里的落地方式是策略名通常形如 ORG_SEC处理函数是 MO_GLOBAL.ORG_SECURITY。想验证某一刻到底有哪些表被挂了策略直接查数据字典-- 看某几张核心表上挂了哪些行级安全策略 SELECT object_name, policy_name, policy_function, statement_types, enable FROM dba_policies WHERE object_name IN (AP_INVOICES_ALL, PO_HEADERS_ALL, MTL_SYSTEM_ITEMS_B);statement_types说明策略对 SELECT、INSERT、UPDATE、DELETE 中的哪些生效policy_function就是那个返回谓词的函数。字段enable为 YES 时任何走这张表的语句都会被改写包括你从 SQL*Plus 手敲的那条。这里有个代价必须说清楚谓词是运行时动态拼的所以同一条 SQL 在不同会话里可能生成不同的执行计划。你昨天调好的索引今天换个人跑就慢了不一定是统计信息的问题可能是策略函数返回的谓词形状变了。更麻烦的是策略函数的性能——它是逐语句调用的一旦函数内部去查大表或者走了递归 SQL整个查询都会被拖慢。所以看 EBS 的执行计划永远要在“已经初始化过上下文”的会话里看否则你看到的是一份没有谓词的假计划。2.3 CLIENT_INFO 与 MO_GLOBAL.INIT一条 SQL 背后的会话状态策略函数不直接去查组织表那样太慢。它读的是会话级的上下文变量——也就是DBMS_APPLICATION_INFO.SET_CLIENT_INFO写进去的那串字符或者SYS_CONTEXT里的自定义命名空间。这串字符里装的是当前会话能看到哪些 OU。当用户从 Forms 界面登录时EBS 会根据职责Responsibility算出这个职责对应的 OU 集合然后调用 MO_GLOBAL.INIT 把它们写进上下文。整个过程对最终用户完全透明他点开一个表单会话里已经带好了“我能看哪几个 OU”。问题出在绕过 Forms 的时候。第三方工具、ETL 抽取程序、直连的 SQL 客户端默认会话里没有这串上下文策略函数读不到值返回的谓词就可能变成“什么都看不到”或者“什么都看得到”两种都很难排查。手工补初始化是这样的-- 在直连会话里模拟 Forms 的上下文初始化 BEGIN mo_global.init(PO); -- 按应用初始化参数是应用短名 mo_global.set_policy_context(S, 204); -- S单 OU204 是目标 org_id END; /mo_global.init的入参是应用短名PO、AP、AR 等它决定按哪个应用的配置去算 OU 集合set_policy_context的第一个参数是访问模式S表示单组织、M表示多组织第二个参数才是具体的 ORG_ID。多组织模式下上下文里装的是一组 ID谓词会变成ORG_ID IN (...)的形式这也是 R12 的 MOAC多组织访问控制能在一个界面上跨 OU 操作的基础。注意在直连会话里调过set_policy_context之后别指望它能跨会话保留。会话一断上下文就没了下次连上来还得重来一遍。做批量抽取时把这段初始化写进连接池的初始化脚本里比每次手工敲可靠得多。3. 主数据的三根支柱PARTY、ITEM、CCID 如何压扁实体关系业务单据表看着多但真正决定查询复杂度的是三类主数据往来单位、物料、会计科目。它们在 R12 里各有一套压缩策略——统一实体、组织级复制、整数化引用。理解这三套策略剩下的表基本是排列组合。3.1 HZ_PARTIES 的 TCA 模型客户和供应商为什么共用一套表在更早的版本里客户在 AR 模块有一套表供应商在 PO 模块有一套表同一个人既买又卖就得在两个地方各录一遍地址也对不上。R12 做了一次大迁移把往来单位统一到 TCATrading Community Architecture模型下核心是 HZ_PARTIES一个人、一个组织、一个团体在库里就是一行。围绕这张表挂了一圈卫星表HZ_PARTY_SITES 记录这个实体在某个地址上有过一个“站点”HZ_LOCATIONS 存地址本身HZ_CUST_ACCOUNTS 是客户账户HZ_CUST_ACCT_SITES_ALL 把账户和站点连起来。供应商这边AP_SUPPLIERS 通过 PARTY_ID 指回 HZ_PARTIES供应商地点在 AP_SUPPLIER_SITES_ALL。这套设计的收益很实在一个实体只有一份档案地址复用重名可去重代价是查询要多跳几层。而且 R12 里 PO_VENDORS 这个名字还在但它已经是对 AP_SUPPLIERS 的兼容层新供应商在某些情况下从这里查不全这是踩坑高发区后面单独说。从供应商回溯到实体和地址大概长这样-- 从供应商一路回溯到 party 与地址信息 SELECT s.vendor_id, s.vendor_name, p.party_id, p.party_type, loc.address1, loc.city, loc.country FROM ap_suppliers s JOIN hz_parties p ON p.party_id s.party_id JOIN hz_party_sites ps ON ps.party_id p.party_id JOIN hz_locations loc ON loc.location_id ps.location_id WHERE s.vendor_name LIKE 某%;ap_suppliers.party_id是通往统一模型的外键hz_party_sites一个 party 可能有多行所以这条 SQL 的结果行数会大于供应商数做报表时要么加站点类型过滤要么先聚合。party_type区分 PERSON、ORGANIZATION、GROUP决定了下游哪些属性字段有意义。3.2 MTL_SYSTEM_ITEMS_B 与 ORGANIZATION_ID物料是全局的还是组织级的这个问题我被问过很多次答案有点别扭INVENTORY_ITEM_ID 全局唯一但 MTL_SYSTEM_ITEMS_B 里同一个物料有 N 行每个分配到的组织一行。机制是这样的EBS 先指定一个主组织master orgMTL_PARAMETERS 里ORGANIZATION_ID MASTER_ORGANIZATION_ID的那一行就是它。物料在主组织里定义主组织那一行相当于模板分配到其他组织时系统从模板复制出一行允许在组织级属性上覆盖比如提前期、安全库存、成本方法。-- 找到主组织再去主组织那一行取物料定义 SELECT organization_id, master_organization_id, organization_code FROM mtl_parameters WHERE organization_id master_organization_id;master_organization_id这个字段在非主组织的行上指向主组织在主组织自己那行上等于自己的 ORGANIZATION_ID所以用等值条件就能精准捞出主组织。这解释了一个常见困惑只按 INVENTORY_ITEM_ID 查 MTL_SYSTEM_ITEMS_B会拿到一堆行字段值还不完全一样。不是数据脏是你没带 ORGANIZATION_ID。查询物料主数据时要么显式指定组织要么先定位主组织再查别让它自己飘。3.3 GL_CODE_COMBINATIONS 与 CCID会计科目被压成一个整数会计科目在 EBS 里不是字符串是一个整数——CODE_COMBINATION_ID社区里简称 CCID。每一个合法的科目组合在 GL_CODE_COMBINATIONS 里占一行SEGMENT1 到 SEGMENT30 分别对应科目弹性域的各个段CHART_OF_ACCOUNTS_ID 标识它属于哪套科目表。子账模块的分布表全都只存这个整数AP_INVOICE_DISTRIBUTIONS_ALL、RA_CUST_TRX_LINE_GL_DIST_ALL、MTL_TRANSACTION_ACCOUNTS连总账的 GL_JE_LINES 也是。要看科目长什么样必须 join 回组合表。-- 从总账凭证明细回连科目组合翻译成人能读的段值 SELECT jel.je_header_id, jel.code_combination_id, gcc.segment1 AS 公司段, gcc.segment2 AS 成本中心, gcc.segment3 AS 科目段, jel.accounted_dr, jel.accounted_cr FROM gl_je_lines jel JOIN gl_code_combinations gcc ON gcc.code_combination_id jel.code_combination_id WHERE jel.je_header_id :header_id;accounted_dr/accounted_cr是已折算的本位币金额和entered_dr/entered_cr原币要区分开做报表时选错列是很典型的“数对不上”。这个设计的性能收益极大GL_JE_LINES 动辄上亿行如果每行都存三段字符串存储和索引都会爆压成一个 NUMBER 之后行变窄join 的又是相对小的组合表。代价是可读性归零任何取数都必须回连。还有一个副作用——组合表会膨胀因为它是各段取值的笛卡尔积中被“实际用到”的稀疏子集一旦有人在科目上乱建组合这张表就会长出一堆垃圾行进而拖慢所有 join。4. 通用列与弹性域ATTRIBUTE1-15 和 SEGMENT1-30 的工程学EBS 要卖给不同行业、不同国家的客户每个客户都想加几个自己的字段但没人愿意为了加字段停机打补丁。这套需求催生了弹性域机制也留下了表结构里最“丑”的一批列。4.1 描述性弹性域15 个通用列的存取机制随便打开一张 EBS 业务表你大概率能看到ATTRIBUTE_CATEGORY、ATTRIBUTE1到ATTRIBUTE15这一组列类型统一是 VARCHAR2(150)。这就是描述性弹性域DFF的物理落地客户的“自定义字段”全部塞进这 15 个通用列具体每个位置代表什么记在元数据表里。元数据在 FND_DESCRIPTIVE_FLEXS 和 FND_DESCR_FLEX_COL_USAGE 这一组表里记录了哪个应用的哪张表、哪个上下文category、第几个 attribute 对应什么含义、什么校验。应用层读元数据决定界面上显示什么标签数据库层对这 15 列一无所知。这种做法在商业上极其成功——客户加字段不需要 DDL升级不会冲突DBA 不用给业务账号开 ALTER 权限。但代价全压在查询端所有值都是字符串。-- 按上下文取某个自定义字段注意值本身是字符型 SELECT invoice_id, attribute_category, attribute1 AS 合同号, attribute3 AS 项目编号, TO_DATE(attribute5, YYYY/MM/DD) AS 立项日期 FROM ap_invoices_all WHERE attribute_category 某行业扩展;attribute_category必须作为条件之一因为同一个 attribute 位置在不同 category 下含义完全不同不加这个条件等于把几种语义混在一起。attribute5存的是日期但列类型是 VARCHAR2比较大小必须显式 TO_DATE 并给出格式直接attribute5 2024-01-01在数据格式不统一时结果不可控。4.2 关键性弹性域组合表与账户生成器的校验链路关键性弹性域KFF走的是另一条路。它不塞进通用列而是给每个 KFF 配一张组合表会计科目用 GL_CODE_COMBINATIONS资产用 FA_ASSET_KEY_COMBINATIONS物料类别用 MTL_CATEGORY_SETS 相关的一组表。组合表的每一行就是一个“段值的合法组合”主键是组合 ID。业务表存的都是这个 ID。校验则走 FND_FLEX_KEYVAL 这类 API把各段值传进去它去查值集value set验证每个段值是否合法再查组合表验证这个组合是否已存在、是否启用不存在就按规则创建一行。社区里把这段逻辑叫“账户生成器”因为它真的会在你录入时往组合表插数据。想看某个 KFF 的段结构查这两张表就够了-- 查会计科目 KFFGL#的段定义 SELECT ifs.id_flex_name, seg.segment_num, seg.segment_name, seg.flex_value_set_id FROM fnd_id_flex_structures ifs JOIN fnd_id_flex_segments seg ON seg.id_flex_num ifs.id_flex_num AND seg.application_id ifs.application_id AND seg.id_flex_code ifs.id_flex_code WHERE ifs.id_flex_code GL# AND ifs.application_id 101 ORDER BY seg.segment_num;id_flex_code是弹性域代码会计科目的固定是GL#application_id 101对应总账应用。segment_num决定段的顺序flex_value_set_id指向这个段允许取哪些值。组合表里的 SEGMENT1、SEGMENT2 顺序就是由这里的 segment_num 决定的不是随便排的。组合表的稀疏特性值得留意理论上三段各 100 个值就是百万级组合实际业务只会用到其中几千个。所以组合表行数增长是业务行为驱动的不是配置驱动的——这也解释了为什么有些环境里 GL_CODE_COMBINATIONS 只有几万行有些却有上百万行。4.3 WHO 列谁在什么时候改了这一行EBS 每张业务表都有一组标准审计列社区里叫 WHO 字段CREATED_BY、CREATION_DATE、LAST_UPDATED_BY、LAST_UPDATE_DATE、LAST_UPDATE_LOGIN再外加并发相关的REQUEST_ID、PROGRAM_APPLICATION_ID、PROGRAM_ID、PROGRAM_UPDATE_DATE。填充这组列的不是数据库触发器而是 Forms 层和并发程序统一调用 FND_STANDARD 这组 API在 INSERT/UPDATE 时把当前用户 ID、当前时间、当前登录 ID 写进去。所以如果有人在数据库里直接 UPDATE这组列不会自动更新数据看起来就像“从没被改过”给事后追溯留下一个黑匣子。LAST_UPDATE_LOGIN是这组列里最有价值也最少人用的一个它存的是 FND_LOGINS.LOGIN_ID能反查到那次登录的会话信息。-- 追溯某张发票最后一次是谁、在哪个会话里改的 SELECT h.invoice_id, h.last_updated_by, u.user_name, h.last_update_date, h.last_update_login, l.start_time AS 登录时间 FROM ap_invoices_all h LEFT JOIN fnd_user u ON u.user_id h.last_updated_by LEFT JOIN fnd_logins l ON l.login_id h.last_update_login WHERE h.invoice_id :invoice_id;fnd_user.user_id和last_updated_by对应fnd_logins.login_id和last_update_login对应两个 join 都是 LEFT因为历史数据里可能残留已删除的用户或者空值。l.start_time是那次会话的开始时间配合last_update_date基本能还原操作时序。注意这组列记录的是“最后一次”不是变更历史。要看完整的修改轨迹得去各模块自己的历史表或者审计表别指望 WHO 列给你后悔药。5. 避坑排查五条从「数对不上」倒推的经验表结构读得再熟落到真实取数还是会翻车。下面这几条是我在实际排查里反复遇到的按现象、原因、解决三段写遇到类似症状可以拿来对号。5.1 组织隔离类漏掉 ORG_ID 的两种表现第一条数字翻倍。现象是报表金额比单据界面大好几倍而且倍数不固定。原因是 join 了两张 _ALL 表却只按业务单据号关联没带 ORG_ID同一个单据号在多个 OU 里都存在笛卡尔式地放大了结果。解决办法是把 ORG_ID 补进 join 条件或者在进入查询前用MO_GLOBAL.SET_POLICY_CONTEXT把会话限定到单个 OU让 VPD 帮你挡掉。我一般倾向后者因为人总会忘。第二条什么都查不到。现象是同一条 SQL 在 A 同事那里正常在 ETL 服务器上返回零行。原因是 ETL 会话没有初始化上下文策略函数读不到 OU 列表谓词变成一个永假条件。解决办法是在连接初始化时调MO_GLOBAL.INIT或者对明确只跑单 OU 的抽取任务显式设置上下文再执行。5.2 对象指向类视图、同义词与模型迁移第三条新供应商查不到。现象是业务说供应商明明建好了用 PO_VENDORS 查就是没有。原因是 R12 把供应商主体迁到了 AP_SUPPLIERS 加 HZ_PARTIESPO_VENDORS 只是兼容层某些新建路径下数据不一定同步反映。解决办法是把查询改到 AP_SUPPLIERS需要地址就继续往下 join AP_SUPPLIER_SITES_ALL。老代码里所有FROM po_vendors都值得过一遍。第四条同一条 SQL 两个工具结果不一样。现象是 Forms 里查到的行数比 SQLPlus 里少。原因是 Forms 走的是不带_ALL的视图视图里带着 ORG_ID 过滤SQLPlus 里你直接查了_ALL表看到了所有 OU 的数据。解决办法是先确认自己查的是表还是视图需要看全量就明确查_ALL需要模拟用户视角就查视图并初始化上下文。这条听着简单但排查时最容易绕进去。5.3 数据类型与精度类字符型与时间戳的暗坑第五条日期区间总少一天。现象是按日期过滤的单据总是漏掉当天的。原因是 EBS 的日期列是 DATE 类型Oracle 的 DATE 带时分秒BETWEEN 月初 AND 月末里的月末被解释成月末零点当天白天的数据全被排除。解决办法是改成左闭右开区间 月初 AND 次日零点或者对列做 TRUNC 后比较代价是索引失效大表上慎用。这个坑在 DFF 的字符型日期字段上更严重因为还要额外处理格式不统一的问题。6. 这套结构的取舍为什么它丑却没被替换掉把前面几章串起来看EBS R12 的表结构其实是一组非常一致的取舍而不是一堆历史遗留的随机产物。核心约束只有三条业务账号不能有 DDL 权限所以扩展字段只能塞进通用列多个业务单位要共享一套表所以隔离只能靠行级策略而不是分库分表单据量会到亿级所以引用必须压成整数。这三条约束在九十年代是硬约束在今天已经不是了但结构改不动——因为改一张表意味着改几千个表单、几千个并发程序、无数客户的自定义逻辑。设计选择当年解决的问题今天付出的代价通用列 ATTRIBUTE1-15客户加字段不动 DDL升级不冲突类型信息丢失查询必须靠元数据翻译组合表 CCID科目引用压成一个整数行变窄可读性归零任何取数都要回连_ALL 表 VPD多 OU 共享物理表隔离在数据库层执行计划随会话变化直连工具行为不可预期主数据统一到 TCA客户与供应商不再重复建档查询链路变长兼容层语义模糊WHO 列而非全量审计每行只多几个字段写入开销可控只能看到最后一次修改追溯要另找历史表如果要在现有环境里验证自己对这套结构的理解我建议用一条具体的业务流做端到端反推从一张发票的界面号码出发依次定位抬头、行、分配、科目组合、供应商 party、WHO 列每跳一次都问自己“这个 join 靠的是业务主键还是代理主键ORG_ID 在哪一层被过滤掉了”。能一路走通且行数始终对得上说明你对这一层的机制是真的理解了而不是背下了表名。再往深一层可以对比子账和总账的存储差异子账的分布表按单据存明细总账的 GL_JE_LINES 按期间存汇总后的凭证行两者的 CCID 引用方式一样但粒度完全不同。搞清楚这个差异多币种、跨期调整这类看着很玄的问题大多能自己推出来。最后说个教训。早年我图快直接对着 _ALL 表写报表 SQL在测试环境一路顺畅上线当天数字全错——测试库只有一个 OU生产库有七个。从那以后我每次写跨模块取数都会强制走一遍“确认对象是表还是视图、确认 ORG_ID 在哪一层生效、确认日期列的时分秒怎么处理”这三步哪怕多花十分钟。这份按模块整理的表结构资料我建议就当字典放在手边遇到不确定的表先查关联关系和主键构成比在线上试错划算得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑