简介:企业工商信息并非静态字典:名称、法定代表人、注册资本、经营范围、注册地址都可能多次变更。本文以一家二十余年间发生七次以上登记变更的企业为例,讲解如何以稳定主键、缓慢变化维度(SCD Type 2)与变更事件表构建追踪模型,实现任意时点快照查询、完整变更链路还原与曾用名反查,并给出入仓流程与工程实践建议。
业务系统接入企业工商信息时,常见做法是把它当成一张静态字典表:一企业一行,只存“当前状态”。但真实世界的经营主体是持续演化的。以一家成立于 1999 年 3 月的办公设备购销企业为例(下文称“示例企业”,名称做匿名化处理),其在二十余年间至少发生过七次可登记的工商变更:
这份“变更履历”对数据系统提出了三个必须回答的问题:
这三个问题分别对应主数据管理中的实体归一、时序版本化与变更溯源能力。下文给出一套可直接落地的建模方案与查询范式。
痛点一:历史丢失。 只保留最新值的表,无法回答任何“过去某时点”的问题。风控回溯、历史合同比对、审计取证都依赖时点快照,静态表在这些场景下直接失效。
痛点二:关联断裂。 外部数据往往以企业名称做关联。示例企业三次更名意味着:以旧名称登记的合同、诉讼、舆情记录,若不做曾用名映射,就会变成无法归属的“孤儿数据”。
痛点三:变更黑盒。 直接 UPDATE 覆盖旧行,“何时变的、为什么变、谁改的”全部丢失。上游数据出现争议时,系统既无法自证清白,也无法重现任意历史版本。
痛点四:复合变更无法结构化。 示例企业 2004 年(注册资本+经营范围)、2019 年(法定代表人+企业类型)、2021 年(名称+经营范围)都是“一次登记、多字段联动”的复合变更。若只按行整体覆盖,既标注不出变更类型,也无法支撑字段级统计,例如“本年度发生经营范围变更的主体数量”。
企业名称是属性,不是身份。建模第一原则是选定跨版本不变的稳定标识,业界通行做法是以统一社会信用代码作为业务主键;对早年“15 位注册号”与“18 位信用代码”并存的历史数据,再补一张标识映射表。维度表内部另设代理键 company_key,供事实表外键关联,隔离业务编码调整的影响。
维度表按变更日切分版本,示例数据如下(日期为日粒度,2021 与 2025 年的变更日以月初示意):
| company_key | credit_code | company_name | legal_rep | capital(万元) | business_scope(摘要) | company_type | valid_from | valid_to | is_current |
|---|---|---|---|---|---|---|---|---|---|
| 1001 | 9144XXXXXXXXXXXX | 名称A | 张某 | 50 | 复印机及办公设备购销 | 自然人投资或控股 | 1999-03-22 | 2002-07-23 | 0 |
| 1002 | 同上 | 名称B | 张某 | 50 | 办公设备购销 | 自然人投资或控股 | 2002-07-24 | 2004-03-24 | 0 |
| 1003 | 同上 | 名称B | 张某 | 200 | 办公设备购销(调整) | 自然人投资或控股 | 2004-03-25 | 2006-05-17 | 0 |
| 1004 | 同上 | 名称B | 张某 | 200 | 经营范围(二次调整) | 自然人投资或控股 | 2006-05-18 | 2015-04-20 | 0 |
| 1005 | 同上 | 名称B | 张某 | 200 | 同上(地址变更) | 自然人投资或控股 | 2015-04-21 | 2019-12-30 | 0 |
| 1006 | 同上 | 名称B | 李某 | 200 | 同上 | 自然人独资 | 2019-12-31 | 2021-06-30 | 0 |
| 1007 | 同上 | 名称C | 李某 | 200 | 非居住房地产租赁、清洁、物业、建筑智能化工程 | 自然人独资 | 2021-07-01 | 2025-08-31 | 0 |
| 1008 | 同上 | 名称D | 李某 | 200 | 同上 | 自然人独资 | 2025-09-01 | 9999-12-31 | 1 |
关键约定:
valid_from 为该版本生效首日(含),valid_to 为失效日(含),当前版本填 9999-12-31 并置 is_current = 1;valid_to 取变更日前一天;维度表回答“某时点是什么状态”,事件表回答“状态如何一步步演变”。设计上引入“变更批次”:一次登记变更对应一个批次(batch_id + event_date + change_type),批次下挂若干字段级明细:
| batch_id | event_date | change_type | field_name | old_value | new_value |
|---|---|---|---|---|---|
| E001 | 2002-07-24 | 名称变更 | company_name | 名称A | 名称B |
| E002 | 2004-03-25 | 注册资本及经营范围变更 | registered_capital | 50 | 200 |
| E002 | 2004-03-25 | (同批次) | business_scope | 办公设备购销 | 办公设备购销(调整) |
| E003 | 2019-12-31 | 法定代表人及企业类型变更 | legal_representative | 张某 | 李某 |
| E003 | 2019-12-31 | (同批次) | company_type | 自然人投资或控股 | 自然人独资 |
| E004 | 2021-07-01 | 名称及经营范围变更 | company_name | 名称B | 名称C |
| E005 | 2025-09-01 | 名称变更 | company_name | 名称C | 名称D |
这一层带来三个直接收益:变更类型可统计(按 change_type 聚合产出年度变更分布);可回放(维度表本质是事件表的物化结果,必要时可从事件流重建全部版本);幂等(对 (credit_code, event_date, field_name) 建唯一约束,重复投递不产生脏数据)。
2019 年 12 月 31 日的变更,数据团队可能在数周后才从数据源感知。更严谨的双时态设计同时保留两套时间:event_date(事实层面的生效/登记时间)用于业务查询,recorded_at(系统层面的入仓时间)用于审计与迟到、更正数据的重放。多数场景保留 recorded_at 字段即可,不必过度设计。
查询一:当前生效信息
SELECT company_name, legal_representative, registered_capital,business_scope, company_typeFROM dim_companyWHERE credit_code = :credit_code AND is_current = 1;
查询二:任意时点快照(静态表无法回答的问题)
SELECT company_name, legal_representative, company_type, business_scopeFROM dim_companyWHERE credit_code = :credit_codeAND valid_from <= DATE '2020-06-01'AND valid_to >= DATE '2020-06-01';
2020-06-01 命中第 6 个版本区间,返回“李某 / 自然人独资”——这正是追溯类业务最典型的诉求。
查询三:完整变更链路还原
SELECT event_date, change_type, field_name, old_value, new_valueFROM company_change_eventWHERE credit_code = :credit_codeORDER BY event_date, field_name;
查询四:按曾用名反查当前主体
SELECT d.credit_code, d.company_name AS current_nameFROM company_name_alias aJOIN dim_company dON d.credit_code = a.credit_code AND d.is_current = 1WHERE a.alias_name = :input_name;
别名表可直接由事件表中 field_name = 'company_name' 的记录派生维护,输入“名称A”同样能命中当前主体“名称D”。
推荐流水线共六步:源数据定时接入 → 按信用代码将新快照与仓内当前版本对齐 → 逐字段差异检测 → 同日差异归并为一个批次写入事件表 → 关闭旧版本并插入新版本 → 变更事件分发给风控、图谱、检索等下游消费。
差异检测的核心是 NULL 安全比较,避免 NULL 参与等值比较时误判:
-- 字段级差异检测(示意,逐字段展开或由元数据驱动统一处理)SELECT o.credit_code, 'legal_representative' AS field_name,o.legal_representative AS old_value,n.legal_representative AS new_valueFROM snapshot_prev oJOIN snapshot_curr n USING (credit_code)WHERE n.legal_representative IS DISTINCT FROM o.legal_representative;
版本切换示意:
-- 关闭当前版本(日粒度:变更日前一天为旧版本终点)UPDATE dim_companySET valid_to = :event_date - 1, is_current = 0WHERE credit_code = :credit_code AND is_current = 1;-- 写入新版本INSERT INTO dim_company (credit_code, company_name, valid_from, valid_to,is_current, version_no /*, ...其余字段*/)VALUES (:credit_code, :new_name, :event_date, DATE '9999-12-31', 1, :next_version);
事件表唯一约束保证了整条流水线可安全重跑:重放旧批次只会被约束拦截,不会污染维度历史。
(credit_code, is_current) 支撑当前查询;(credit_code, valid_from, valid_to) 支撑时点快照;事件表按 event_date、change_type 建辅助索引支撑统计。企业主体信息的价值,一半在“当前状态”,另一半在“变更轨迹”。以稳定主键锚定身份、以 SCD Type 2 维度表保存时点版本、以变更事件表沉淀变更明细,三者配合“当前查询、时点快照、变更链路、曾用名反查”四类查询范式,就能把示例企业那样一段跨越二十余年、七次变更、三次更名的历史,完整且可查询地装进数据模型。对任何需要接入企业主数据的团队而言,这套基础能力值得优先建设。