想象你要在一片空地上规划并建起一座城市。你不能随心所欲地乱建:每栋楼都要有全城唯一的门牌号(主键);登记簿上的每个地址必须真实指向某栋楼(外键);写字楼密集的 CBD 旁边,自然会冒出配套餐厅(联合分布);楼与楼之间还要按“先有住宅、再有商户”的顺序施工。单表合成数据像是在盖一栋孤零零的楼——只要楼本身结构正常就行;而多表关系合成,是在建一整座城市,难的不是某一栋楼,而是楼与楼之间严丝合缝的咬合关系。 这篇文章要讲的,就是“如何合成多张互相关联的表”。你会看到:如何给表间关系建模、如何守住主外键约束、如何按正确顺序生成、如何保持表间的联合分布、如何验证参照完整性,以及在大表场景下怎么提速。全程用“城市规划”这座城来打比方——听完这堂规划课,你就能亲手建造自己的合成数据之城。

岗位关联

掌握多表关系合成,是数据库测试数据生成与关系型数据隐私保护的必备技能。它让你合成的数据既有真实关联、又不泄露隐私,直接决定 AI 企业能否安全地用合成数据替代敏感真实数据。

目录
  1. 可视化演示
  2. 表间关联建模
  3. 主外键约束
  4. 条件生成策略
  5. 联合分布保持
  6. 参照完整性验证
  7. 大表性能优化
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

通过可视化最近最少使用(LRU)缓存的插入、查询和淘汰过程,直观理解缓存命中、未命中与淘汰机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

LRU缓存初始化 缓存容量:'+S.capacity+',待执行操作序列 队首为最久未使用,队尾为最近使用 访问时移至队尾,满时淘汰队首 点击自动播放或手动步进开始演示

表间关联建模

想象你被任命为一座新城的规划师。开工前,你不会急着浇混凝土,而是先摊开图纸,标出哪里建住宅区、哪里建商业街、哪里留学校——还要在图纸上画清楚“谁挨着谁、谁的服务对象是谁”。没有这张蓝图,楼盖得再漂亮,城市也是散的。

多表关系合成也一样,起手动作是“关联建模”。数据库里没有一张孤立的表:用户表与订单表靠 user_id 相连,订单表与商品表靠 product_id 相连。你要先识别出谁是被引用的“父表”(用户、商品),谁是引用别人的“子表”(订单)。最常见的是一对多关系:一个用户拥有多个订单。此外还有一对一(用户与用户详情)与多对多(订单与商品,通常借助中间表)——关系类型不同,建模方式也不同。

建模的产出物是“外键映射”,即明确子表的哪个字段指向父表的哪个主键字段。对它最严格的要求,用集合论表达是:子表外键的全体取值,必须被父表主键的全体取值完全包住。

这张蓝图是后面所有环节的宪法。如果图纸上把商场画在了一片没有住宅的荒地,施工队再怎么努力,商场也会因为“没有登记地址”而无法开业——蓝图上的每一种偏差,都会在后续步骤里放大成连锁问题。

πFK(Rchild)⊆πPK(Rparent)\displaystyle \pi_{FK}(R_{child}) \subseteq \pi_{PK}(R_{parent})
这个公式的意思是:子表里所有外键值组成的集合,必须完全落在父表主键值组成的集合里。换句话说——每一位住户登记的地址,都必须在这座城市真实存在的门牌号表上找得到。
  1. \(R_{parent}\)(父表)被引用的数据表,比如用户表,是“城市里的住宅楼”。
    RparentR_{parent}
  2. \(\pi_{PK}(R_{parent})\)(投影主键)取出父表主键列的全部取值,得到“全城真实门牌号集合”。
    πPK(Rparent)={1,2,…,n}\pi_{PK}(R_{parent}) = \{1, 2, \dots, n\}
  3. \(R_{child}\)(子表)包含外键字段的表,比如订单表,是“登记了住户地址的商场”。
    RchildR_{child}
  4. \(\pi_{FK}(R_{child})\)(投影外键)取出子表外键列的全部取值,得到“商户登记的所有地址”。
    πFK(Rchild)\pi_{FK}(R_{child})
  5. \(\subseteq\)(子集)要求“登记地址集合”完全落在“真实门牌号集合”内,一条都不能悬空。
    πFK(Rchild)⊆πPK(Rparent)\pi_{FK}(R_{child}) \subseteq \pi_{PK}(R_{parent})

主外键约束

城市必须有一套铁律:全城门牌号唯一,且所有登记地址必须真实存在。这两个规矩,对应关系数据库里的主键唯一性和外键引用规则——它们是规划条例,谁都不能违反。

合成数据时,主键往往用“自增发号器”:每生成一行,发号器加一。只要发号器从 1 开始、从不重置,唯一性就天然成立。外键则从“已存在的门牌号列表”里挑一个填进去,而不是拍脑袋随机编。随机编一个数字,很可能编出一扇不存在的门。

还有一个容易忽略的决策是级联行为。真实数据库里,删除父表记录时,子表关联记录按外键约束有三种选择:CASCADE(连带删除)、SET NULL(外键置空)、RESTRICT(拒绝删除)。合成数据场景通常不删除,但如果你要模拟“用户注销后其订单被清除”的业务逻辑,就必须在设计阶段定好并写入合成规则。

这些条例看似琐碎,却决定数据“导不导得进去”。违反主键唯一性,数据库直接报错;违反外键引用,导入成功但查询时全是悬空的死路。规划条例先行,后面施工才不返工。

∀r1,r2∈R: r1.PK=r2.PK⇒r1=r2\displaystyle \forall r_1, r_2 \in R: \ r_1.PK = r_2.PK \Rightarrow r_1 = r_2
这段符号表达的是主键的铁律:任意两行记录,如果主键值相同,那它们只能是同一行。门牌号一旦撞号,整座城市的管理系统就崩了。
  1. \(r_1, r_2 \in R\)任取表中的两行记录,可以相同也可以不同。
    r1,r2∈Rr_1, r_2 \in R
  2. \(\forall\)(全称量词)对表中所有可能的行组合都成立,没有例外。
    ∀\forall
  3. \(r_1.PK = r_2.PK\)这两行记录的主键值相等,如同两栋楼挂上了同一块门牌号。
    r1.PK=r2.PKr_1.PK = r_2.PK
  4. \(\Rightarrow r_1 = r_2\)因为主键唯一,相等只能推出“它们是同一行”,绝不可能是两行不同的记录。
    ⇒r1=r2\Rightarrow r_1 = r_2

条件生成策略

建城有严格的施工顺序:先建住宅楼,住户入住,然后商场、学校才能开门——它们的登记地址才有人可指。反过来,商场先开张,住户还没影,登记簿上全是空头地址。多表合成的“施工队”也要排先后。

排顺序的算法叫拓扑排序。把每张表看作一个节点,外键看作一条有向边(子表指向父表),凡是“没有依赖”的表排在最前,依赖前置节点完成后才轮到它。这就是“父表驱动”:先完整生成父表全部记录,把主键集合收集成一张“已批准门牌号清单”,再生成子表。

子表的每条记录,外键从清单中等概率随机抽一个。数学期望上,每个父表记录被引用的次数大致均匀——如果业务上需要“热门用户订单更多”,就把均匀分布改成加权分布。关键原则是:外键的候选池永远来自已生成的真实主键,而不是脱离城市凭空造出的号码。

这个“先父后子”的顺序看起来简单,却最容易在工程里出错。有人图省事同时生成两张表,结果外键指向了尚不存在的主键——就像商场开张时住户还没入住一样,最后只能返工清洗。按拓扑次序施工,才是最省事的省事。

P(FK=k)=1∣PKparent∣,k∈PKparent\displaystyle P(FK = k) = \frac{1}{|PK_{parent}|}, \quad k \in PK_{parent}
这是子表外键的采样规则:每个候选主键被选中的概率相等,且候选集合只包含父表里真实存在的主键。就像商场登记住户地址时,只能从已经交付的住宅楼门牌号里选。
  1. \(PK_{parent}\)(父表主键集合)已建好楼的门牌号列表,合成父表后收集得到。
    PKparentPK_{parent}
  2. \(k \in PK_{parent}\)外键只能从这些已存在的主键里挑选。
    k∈PKparentk \in PK_{parent}
  3. \(|PK_{parent}|\)(集合规模)目前共有多少栋楼、多少可用主键。
    ∣PKparent∣|PK_{parent}|
  4. \(\frac{1}{|PK_{parent}|}\)(均匀概率)每一栋楼被指中的概率相等,谁也不偏心。
    1∣PKparent∣\frac{1}{|PK_{parent}|}
  5. \(P(FK = k)\)合成子表记录时,外键指向第 \(k\) 个主键的概率。
    P(FK=k)P(FK = k)

联合分布保持

一座城市的魅力,在于它的“格局”:市中心办公楼成群,周边自然围出一圈快餐店;高档住宅区附近,才有奢侈品商场。如果无视这种规律,把商场盖进城中村,城市看起来每栋楼都没问题,但住起来全不对味。

对应到数据上,这就是“联合分布”。父表的属性会显著影响子表的属性:高收入用户大概率有大额订单,低收入用户即使偶然大额,占比也低。单表合成看不到这层关联;多表合成如果忽略它,生成的合成数据表面合法(外键都在),但做数据分析或模型训练时,结论会与现实严重偏离。

保持联合分布的通用做法是“条件生成”:先从父表取到这一行的属性值,再按条件概率分布采样子表属性。例如先统计“收入等级=高”的用户其订单金额的均值与方差,合成时以该均值为中心生成金额。这个条件概率,本质上是联合分布与边际分布之比。

有了这层“格局感”,合成数据才真正可用。它不再是两堆碰巧能关联的记录,而是一座有生活气息的城市:每个街区的业态分布、每个收入群体的消费习惯,都跟真实世界对得上。

P(C∣P)=P(C∩P)P(P)\displaystyle P(C \mid P) = \frac{P(C \cap P)}{P(P)}
翻译成大白话:在“用户是高收入”这个前提下,生成“大额订单”的概率,等于“高收入且大额订单”同时出现的次数,除以“高收入”出现的总次数。这就是城市格局的数学表达——知道街区住什么人,就知道该开什么店。
  1. \(P\)(父表事件)比如“用户收入等级=高”,是城市里的一个街区特征。
    PP
  2. \(C\)(子表事件)比如“订单金额≥800元”,是街区里某类商店的业态。
    CC
  3. \(P(P)\)(父表边际概率)整个城市中高收入用户所占的比例。
    P(P)P(P)
  4. \(P(C \cap P)\)(联合概率)高收入用户群体里,大额订单出现的比例。
    P(C∩P)P(C \cap P)
  5. \(P(C \mid P)\)(条件概率)已知用户是高收入的条件下,订单大额的概率——这就是从父表到子表的“格局传导”。
    P(C∣P)P(C \mid P)

参照完整性验证

楼盖完了、路铺好了,城市还不能交付——质检员要逐户核对登记册:每一个登记的地址,是不是都对应一栋真实存在的楼?这一关,叫参照完整性验证。

合成流程里,这个质检员是一段自动检查程序:遍历子表每一行,查它的外键值是否在父表主键集合里。不在集合里的,就是“孤儿记录”——登记册上的地址指向了不存在的楼。此外还要排查循环引用:A 表引用 B 表、B 表又引用 A 表,谁先生成都不对,必须在建模阶段拆解(加中间表或允许其中一列暂空)。

别小看这道工序。很多失败的合成数据项目,不是生成阶段出问题,而是少了验收这一步:数据导入测试库的那一瞬间,参照完整性约束把整批数据拦在门外。质检员的存在,让“理论上成立”变成“实际上成立”。

Violation(Rc,Rp)=∣{rc∈Rc∣rc.FK∉πPK(Rp)}∣\displaystyle \text{Violation}(R_c, R_p) = \left|\{ r_c \in R_c \mid r_c.FK \notin \pi_{PK}(R_p) \}\right|
这个公式数的是“悬空外键”的数量:统计所有不落在父表主键集合里的子表外键。合格标准是这个数等于 0——一条死信都不能有。
  1. \(\{ r_c \in R_c \mid \dots \}\)(集合构造)从子表里挑出满足条件的行,组成一个待查集合。
    {rc∈Rc∣… }\{ r_c \in R_c \mid \dots \}
  2. \(r_c.FK \notin \pi_{PK}(R_p)\)(悬空条件)该行外键不在父表主键集合里,即“登记的地址不存在”。
    rc.FK∉πPK(Rp)r_c.FK \notin \pi_{PK}(R_p)
  3. \(\left|\dots\right|\)(集合大小)数一数有多少条悬空引用,相当于统计死信总数。
    ∣⋯ ∣\left|\cdots\right|
  4. \(\text{Violation} = 0\)(验收标准)违规数为零,参照完整性合格,城市可以交付。
    Violation(Rc,Rp)=0\text{Violation}(R_c, R_p) = 0

大表性能优化

城市人口过千万时,施工队面临新的敌人——时间。一栋楼一栋楼地盖,十年都交付不了。现代施工的答案是预制构件、流水线和并行作业。大表合成遇见的正是同样的困境。

朴素的多表生成是两层循环:生成每一行子表记录时,都去父表里“挨家挨户找门牌”。父表一百行、子表一万行,就是一百万次查找;父表一千万行、子表十亿行,计算量就是天文数字。三个优化手段立刻上场。

第一,批量预生成:父表一次性生成 N 行并收集主键集合,而不是生成一行子表就去查一次;第二,映射缓存:把“主键→父表记录”的映射存进哈希表,查找从线性扫描降到常数时间;第三,并行分片:按主键范围把数据切成互不重叠的片段,多进程并行生成,最后统一做完整性检查。三个手段叠加,性能差距可以达到几个数量级。

优化之后,合成一座千万级人口的城市不再需要十年——从“每个门牌号都要扫一遍全城”变成“门牌号一查即得”。性能与正确性兼得,这才是工程级的合成能力。

T朴素(n,m)=O(n×m)→哈希缓存O(n+m)\displaystyle T_{\text{朴素}}(n, m) = O(n \times m) \xrightarrow{\text{哈希缓存}} O(n + m)
这是“门牌号查找”的效率跃迁:朴素方法每查一次门牌要逛遍全城,哈希缓存则是一本按门牌号编号的字典,翻到对应页就能查到——城市再大,查一栋楼也只需要一瞬间。
  1. \(n\)(父表行数)住宅楼的总数。
    nn
  2. \(m\)(子表行数)需要登记地址的记录总数。
    mm
  3. \(O(n \times m)\)(朴素复杂度)每条子表记录都全表扫描一次父表,时间随两表规模乘积爆炸。
    O(n×m)O(n \times m)
  4. 哈希缓存(城市门牌号字典)预先建立“主键→记录”的映射,查找从 \(O(n)\) 降到 \(O(1)\)。
    →哈希缓存\xrightarrow{\text{哈希缓存}}
  5. \(O(n + m)\)(优化后复杂度)建表一次、查找一次,总耗时与两表规模之和成正比。
    O(n+m)O(n + m)

AI 应用场景

数据库测试数据生成

阶段:测试数据合成(数据准备阶段)

解决的问题:开发新功能需要一个包含用户表、订单表、商品表三张关联表的 MySQL 测试库。生产库不能碰,手工编造又很难保证外键关系不断——订单的 user_id 经常指向不存在的用户,接口一查就 500。

具体案例:某电商团队的 QA 同学用多表合成器生成 5 万用户 + 20 万订单 + 8 万商品的测试库。合成器先建用户表收集主键,再生成订单时从主键集合采样,订单的 user_id 全部指向真实存在的用户。接口回归测试再也没出现“外键悬空”的诡异报错。

隐私保护的数据共享

阶段:数据脱敏与发布

解决的问题:医院想向研究机构分享“患者-诊断-处方”三表数据,但直接做字段脱敏会破坏表间关联——诊断记录指向的患者 ID 经过脱敏后,在患者表里对不上号,研究根本无法开展。

具体案例:研究团队用多表合成器生成 100 万条患者记录及关联的诊断、处方数据。保持“老年患者多慢病、慢病患者药方更长”的联合分布,同时患者 ID 全部替换为合成主键。研究机构拿到的数据可以正常做关联分析,且无法反推出真实患者身份。

金融风控模型训练

阶段:模型训练数据准备

解决的问题:银行风控模型需要“客户-账户-交易流水”三表联合特征,但真实交易数据涉及个人隐私无法直接用于训练。单表生成又丢失了客户与交易之间的关联——高净值客户的交易模式与普通客户完全不同。

具体案例:算法团队用条件生成策略,先合成客户表(含收入等级、职业、年龄),再按“收入等级决定交易金额均值、职业决定交易时段分布”的条件规则生成交易流水。最终风控模型在合成数据上的 AUC 与真实数据训练结果仅差 3%,顺利通过业务验收。

Python 演示

python63 行
# -*- coding: utf-8 -*-"""多表关系数据合成器:保持主外键约束与联合分布"""import randomimport pandas as pd  class MultiTableSynthesizer:     def __init__(self, seed=42):        random.seed(seed)        self.parent_pks = []        self.parent_map = {}        self.child_rows = []     def generate_parent(self, n=100):        for i in range(1, n + 1):            rec = {                "user_id": i,                "age": random.randint(18, 70),                "income_level": random.choice(["低", "中", "高"]),            }            self.parent_pks.append(i)            self.parent_map[i] = rec     def generate_child(self, n=300):        if not self.parent_pks:            raise ValueError("请先调用 generate_parent 生成父表")        for i in range(1, n + 1):            fk = random.choice(self.parent_pks)            parent = self.parent_map[fk]            amount = self._sample_amount(parent["income_level"])            self.child_rows.append({                "order_id": i,                "user_id": fk,                "amount": amount,            })     def _sample_amount(self, level):        base = {"低": 100, "中": 500, "高": 1000}[level]        return max(10, int(random.gauss(base, base * 0.3)))     def verify_integrity(self):        pk_set = set(self.parent_pks)        orphans = [r for r in self.child_rows                   if r["user_id"] not in pk_set]        return len(orphans) == 0, len(orphans)     def to_dataframes(self):        parent = pd.DataFrame(list(self.parent_map.values()))        child = pd.DataFrame(self.child_rows)        return parent, child  if __name__ == "__main__":    syn = MultiTableSynthesizer(seed=42)    syn.generate_parent(100)    syn.generate_child(300)    ok, orphans = syn.verify_integrity()    print(f"参照完整性通过: {ok}, 孤儿记录数: {orphans}")    parent_df, child_df = syn.to_dataframes()    print("父表样例:\n", parent_df.head(3))    print("子表样例:\n", child_df.head(3))    print("订单金额分布:\n", child_df["amount"].describe().round(1))
目的:这是一个完整的多表关系合成器,演示“先父后子”的生成顺序、外键从主键集合采样、联合分布条件生成,以及最后的参照完整性验证。
  1. random.seed(seed)固定随机种子,保证每次生成的合成数据可复现,测试时不会“今次对、下次错”。
  2. self.parent_pks.append(i)每生成一个父表记录就登记主键,凑齐“有效门牌号”集合。
  3. self.parent_map[i] = rec建立主键到记录的哈希映射,为后续 O(1) 查找铺路。
  4. random.choice(self.parent_pks)从已存在的主键中随机选一个作为外键,天然满足参照完整性。
  5. parent = self.parent_map[fk]通过哈希表拿父表记录,避免全表扫描,这就是“映射缓存加速”。
  6. base = {"低": 100, "中": 500, "高": 1000}[level]联合分布核心:父表收入等级直接决定子表订单金额的均值。
  7. random.gauss(base, base * 0.3)围绕均值加高斯噪声,让金额分布贴近真实数据而非一成不变。
  8. r["user_id"] not in pk_set完整性检查:外键不在主键集合里就是孤儿记录,必须揪出来。

常见误区

误区“我先把每张表单独合成好,反正字段分布都对,拼在一起不就完事了?”

正解直接拼接会出现外键悬空——子表外键指向父表中不存在的主键,就像把两栋楼的图纸分开画好再拼在一起,结果是窗户对着没有房间的墙。正确做法是先生成父表并收集主键集合,再生成子表时从集合中采样外键,让每条引用都有真实落点。

为何常见:市面上单表合成工具普及,很容易让人误以为多表只是“多跑几次”,忽略了表间约束的重建。

误区“外键不就是一个整数吗?我 `random.randint(1, 1000)` 生成不就行了?”

正解随机整数可能落在父表主键范围之外,或破坏联合分布。比如父表只有 2000 个用户,FK=5000 就是孤儿。正确做法是从父表已生成的主键集合中等概率采样,让每个 FK 都指向一扇真实存在的门。

为何常见:把“外键”理解成普通数字字段,忘了它的语义是“引用”,数字只是一个地址的写法。

误区“我可以先生成子表,最后把缺失的父表补上,反正完整性检查能兜底。”

正解这是本末倒置。子表的外键分布依赖父表主键集合;先定子表再补父表,父表就变成了“将就”子表的附属品,联合分布严重变形。就像先建写字楼再平地插住宅楼,城市布局反而被楼决定。应用父表驱动、拓扑排序生成。

为何常见:将验证环节视作事后补救,而非前置设计约束,忽略了生成顺序本身就是正确性的一部分。

误区“只要外键没有空值,参照完整性就 OK 了吧?”

正解NULL 只是非空约束,参照完整性还要检查外键值是否存在于父表主键集合。用 LEFT JOIN 后父表主键为空的记录数来查,才能发现悬空外键。就像地址写了不等于地址存在——门牌号填得再工整,楼不存在照样是死信。

为何常见:数据库最直观的约束是 NOT NULL,参照完整性更隐蔽,初学者很容易跳过。

面试题

Q1. 多表关系合成相比单表合成,最核心的区别是什么?

单选题 · easy

  • A.表里记录数量更多了
  • B.需要保持表间外键关联← 正确答案
  • C.生成速度要快得多
  • D.不需要真实数据参考

答案:B。多表与单表合成的本质差异在于“关联”:子表外键必须指向父表已存在的主键,否则数据无法导入也无法关联查询。记录数量、速度都不是核心,真实数据参考与否也不取决于单表或多表。就像建城市和盖大楼的区别不在楼多楼少,而在楼与楼之间的咬合关系。

Q2. 生成子表记录时,外键值应当从哪里选取?

单选题 · easy

  • A.父表已生成的主键集合← 正确答案
  • B.任意随机生成的小整数
  • C.子表已有记录的自增编号
  • D.数据库配置文件的默认值

答案:A。外键的语义是“引用”,必须指向真实存在的主键,所以只能从父表已生成的主键集合中采样。随机整数可能越界,子表自增编号与父表无关,配置文件默认值更是无源之水。这就像商场登记住户地址,必须从已交付楼栋的门牌号列表里选。

Q3. 合成多表数据最典型的应用场景是什么?

单选题 · easy

  • A.生成关联测试数据库← 正确答案
  • B.替代数据库双主键
  • C.修改真实数据分布
  • D.删除冗余外键字段

答案:A。多表合成最核心的应用是为开发测试生成带真实关联关系的数据库,让接口联调和回归测试可以安全进行。它不修改真实数据分布、不替代主键设计、更不是用来删外键的,而是“复制一座城的结构,再换一批居民”。

Q4. 以下哪种生成顺序最适合多表关系数据合成?

单选题 · mid

  • A.子表先于父表生成
  • B.父表先于子表生成← 正确答案
  • C.两张表同时生成
  • D.顺序不影响结果

答案:B。父表驱动是黄金法则:先完整生成父表并收集主键集合,子表生成时才能从集合中采样外键。先子后父会导致外键无目标,同时生成会引入竞态和悬空引用。就像必须先有住宅楼,商场才能登记住户地址。

Q5. 多表合成中“联合分布保持”的含义是什么?

单选题 · mid

  • A.各表独立生成互不影响
  • B.父表特征影响子表特征← 正确答案
  • C.子表特征反向决定父表
  • D.所有表分布完全一致

答案:B。联合分布是指父表属性与子表属性之间的关联,比如高收入用户的订单金额普遍更高。多表合成要用条件概率建模这种“父表→子表”的影响方向。子表反向决定父表、各表独立、分布一致,都会破坏真实世界的因果结构。

Q6. 主键唯一性约束最严格的表达是什么?

单选题 · mid

  • A.主键允许出现重复值
  • B.任意两行主键互不相同← 正确答案
  • C.主键可以根据外键变化
  • D.主键存储空间尽量最小

答案:B。主键的唯一性要求表中任意两行记录的主键值都不相同,这是定位一条记录的基石。允许重复、随外键变化都等于放弃了“唯一门牌号”原则。存储空间优化与唯一性是两回事,空间再小也不能重复。

Q7. 保持参照完整性需要做哪些验证?

多选题 · mid

  • A.外键值指向存在的主键← 正确答案
  • B.主键数值保持唯一← 正确答案
  • C.外键字段不允许为空← 正确答案
  • D.外键值必须小于一千

答案:ABC。参照完整性包含三件事:外键必须指向真实存在的主键、主键本身必须唯一、外键通常不允许为空(否则无法引用)。外键值大小与具体业务有关,没有“必须小于一千”这种约束。这三项缺一不可,就像地址、门牌号和大门缺一不可。

Q8. 关于多表生成的拓扑排序,正确的说法有哪些?

多选题 · mid

  • A.无依赖的表先生成← 正确答案
  • B.父表必须先于子表← 正确答案
  • C.循环引用需要先打破← 正确答案
  • D.顺序不影响完整性

答案:ABC。拓扑排序的原则是“先无依赖、后有依赖”:没有外键依赖的表最先生成,父表必须先于子表,循环引用(A 引用 B、B 引用 A)必须通过中间表或允许空值来打破。顺序恰恰是保证完整性的关键,认为顺序无关是错误认知。

Q9. 大表合成时“映射缓存”的加速原理是什么?

单选题 · hard

  • A.把父表主键存进哈希表← 正确答案
  • B.减少需要生成的行数
  • C.跳过子表生成步骤
  • D.用随机数模拟外键值

答案:A。映射缓存把“主键→父表记录”存进哈希表,查找从 O(n) 降到 O(1)。它不减少行数、不跳过子表、更不偷懒用随机数模拟外键——而是给每栋楼装了一本门牌号字典,查地址从“满城找”变成“翻一页就到”。

Q10. 下面对“孤儿记录”的判断,哪些是正确的?

多选题 · hard

  • A.外键悬空未指向任何主键← 正确答案
  • B.被多个外键引用的主键
  • C.左连接后父表为空的记录← 正确答案
  • D.外键数值较大的子表记录

答案:AC。孤儿记录指外键没有指向任何有效主键的子表记录,在 SQL 中表现为对父表做 LEFT JOIN 后父表字段为 NULL。被多个外键引用是正常的一对多关系,外键数值大小与是否孤儿无关——只有“地址不存在”才是孤儿。