假设你开了一家小面馆,账房先生阿诚同时管「流水账」和「总账」。方案 A:每天打烊后,把今天所有变动重新誊写进一本干净的总账,旧账本当场作废;方案 B:白天先在便签上快速记一笔,正本账本不动,月底再把几百张便签整理进总账。这个二选一,就是 Apache Hudi 里 Copy-on-Write(COW)与 Merge-on-Read(MOR)两种表类型的本质:COW 像「重抄账本」,MOR 像「便签+定期整理」。本文将用这个账本比喻,逐步拆解两种表类型在写入、读取、存储和场景选择上的权衡。

岗位关联

对 AI 从业者来说,理解 COW 与 MOR 的表类型权衡,是搭建实时数仓和数据湖的必备技能:选错类型会导致写入堵塞、查询变慢或存储成本失控,直接影响上层模型和 BI 的时效性。

目录
  1. 可视化演示
  2. 核心概念
  3. 写入路径
  4. 读取性能
  5. 写入性能
  6. 存储成本
  7. 场景权衡
  8. 工程优化
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态展示Copy-on-Write写入时重写Parquet与Merge-on-Read写入日志读取时合并的数据流路径,切换CoW/MoR/对比模式观察读写性能权衡差异。

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

核心概念

账房先生阿诚每天面对两种记账选择:一种是把旧账重新誊写一遍,另一种是往正本账上贴便签。Apache Hudi 的 Copy-on-Write(COW)就是「重抄账本」:每次数据更新,都把整个文件组重写为新版本;Merge-on-Read(MOR)则是「贴便签」:更新先追加到日志文件,读取时再合并。

Hudi 将一张表按主键哈希划分成若干「文件组」(FileGroup),每个文件组相当于账本的一个分区。COW 下,一个文件组同一时刻只有一个 base 文件(Parquet 列式存储),每次提交产生新版本并替换旧版本;MOR 下,一个文件组由一份 base 文件加多个 log 文件(Avro 格式)构成,base 负责存量数据,log 负责增量变更。

COW 最直观的代价是写放大:业务只改了 10 条记录,存储却要重写整个文件组。用写放大系数可以量化:

回到面馆账本,COW 是追求「账本永远干净」,MOR 是追求「先记下来再说」。理解这个差异,就理解了两种表类型的一切权衡。

WACOW=新版本文件组大小业务实际更新的记录大小\text{WA}_{\text{COW}} = \frac{\text{新版本文件组大小}}{\text{业务实际更新的记录大小}}
  1. 写放大系数 WA写入物理存储的数据量相对于逻辑变化数据量的倍数,它是存储系统成本的重要标尺。
    WA=写入物理存储的字节数业务真正变化的数据字节数\text{WA} = \frac{\text{写入物理存储的字节数}}{\text{业务真正变化的数据字节数}}
  2. 分子为新版本大小COW 每次更新要生成包含全部存量数据的新 base 文件,分子几乎等于整个文件组大小。
    新版本文件组大小=旧文件组大小+本次更新数据大小\text{新版本文件组大小} = \text{旧文件组大小} + \text{本次更新数据大小}
  3. 分母为逻辑更新大小业务视角下,只有被修改的那几十条记录才是真正变化的数据。
    更新数据大小=∑r∈本次修改size(r)\text{更新数据大小} = \sum_{r \in \text{本次修改}} \text{size}(r)

写入路径

阿诚在记每一笔账时,COW 会先把旧账本整个抄一遍再落笔,MOR 则直接往便签上写字。Hudi 的写入路径也遵循这个逻辑。

COW 写入时,先通过索引定位记录属于哪个文件组,然后读取旧 base 文件,把变更合并进去,最后将整个文件组写成新的 base 文件并原子提交。MOR 写入时,同样先定位文件组,但不需要读旧文件,而是直接把变化记录追加到该文件组的 log 文件末尾,提交一个轻量事务。索引定位这一步,相当于「找到该记哪本账」。

两条路径的耗时模型对比很清晰:

面馆里,COW 每次改账都像誊抄整本总账,MOR 则像「便签一贴,收工回家」。因此 MOR 在写入路径上天然更轻快,这也是它被选作实时写入首选的原因。

TCOW写入=T定位+T读旧文件+T写新文件;TMOR写入=T定位+T追加日志T_{\text{COW写入}} = T_{\text{定位}} + T_{\text{读旧文件}} + T_{\text{写新文件}};\quad T_{\text{MOR写入}} = T_{\text{定位}} + T_{\text{追加日志}}
  1. 定位时间无论 COW 还是 MOR,都要先通过索引找到目标文件组,这笔开销相同。
    T定位=索引查找文件组的耗时T_{\text{定位}} = \text{索引查找文件组的耗时}
  2. COW 的读写旧文件COW 必须把旧版本全部读出来再合并,文件组越大,这一步越重。
    T读旧文件∝旧 base 文件大小T_{\text{读旧文件}} \propto \text{旧 base 文件大小}
  3. COW 写新文件合并完成后要整体写出新文件,开销与文件组大小成正比。
    T写新文件∝新 base 文件大小T_{\text{写新文件}} \propto \text{新 base 文件大小}
  4. MOR 追加日志MOR 不碰旧文件,只把增量追加到 log,时间近似常数。
    T追加日志≈追加 N 条记录的序列化耗时T_{\text{追加日志}} \approx \text{追加 N 条记录的序列化耗时}

读取性能

到了晚上打烊,阿诚要回答「今天赚了多少」。如果用的是 COW,打开干净的总账就能直接报数;如果用的是 MOR,就得翻出一大摞便签,一张张和正本比对,才能算出最终结果。

COW 表查询只读取一个 Parquet base 文件,列式存储配合谓词下推,可以跳过无关列,扫描非常快。MOR 表查询需要先读 base,再把该文件组的所有 log 全部读出并合并,log 数量越多,读放大越明显。合并过程中还要处理同一主键的多版本更新,计算开销与 log 规模直接相关。

读路径的耗时公式:

COW 的账本是「翻开即读」,MOR 的账本是「翻便签再合并」。因此查询性能敏感的报表业务,通常会被建议选择 COW。

T读MOR=T读base+∑i=1kT合并logiT_{\text{读MOR}} = T_{\text{读base}} + \sum_{i=1}^{k} T_{\text{合并log}_i}
  1. 读取 base 文件MOR 的查询先加载基础数据,这和 COW 类似。
    T读base=列存扫描 base 文件的时间T_{\text{读base}} = \text{列存扫描 base 文件的时间}
  2. 遍历 k 个 log 文件每个 log 都需要被读入内存,log 越多累计时间越大。
    ∑i=1kT合并logi=Tlog1+Tlog2+⋯+Tlogk\sum_{i=1}^{k} T_{\text{合并log}_i} = T_{\text{log}_1} + T_{\text{log}_2} + \cdots + T_{\text{log}_k}
  3. 合并开销每读入一个 log,都要按主键与已有结果做一次 merge,可能触发内存排序或哈希。
    T合并logi∝logi 的记录数×主键比较成本T_{\text{合并log}_i} \propto \text{log}_i\ \text{的记录数} \times \text{主键比较成本}

写入性能

把一杯水倒进桶里,COW 要求先把整桶水重新倒一遍才能加新水,MOR 则只是往桶边贴一张纸条写上「加一杯水」。这正是两种表类型写入性能差异的直观体现。

COW 的写放大决定它不适合高频小更新:每次提交都重写整个文件组,写入磁盘的数据量巨大,但它产出的是整齐的大文件,顺序写吞吐并不低。MOR 则把单次写入压缩成「追加一条日志」,延迟极低,可以轻松应对每秒上千次的更新;但代价是 log 会碎片化,如果放任不管,会积累大量小文件,反而拖慢后续合并。

单次写入延迟对比可以简化为:

账房先生要是每笔客人都誊抄一遍总账,恐怕面馆早打烊了。MOR 的「先贴便签」让高频写入成为可能,但别忘了便签堆满后整理账本也是一笔成本。

TMOR写≈α,TCOW写≈β×S文件组T_{\text{MOR写}} \approx \alpha,\quad T_{\text{COW写}} \approx \beta \times S_{\text{文件组}}
  1. MOR 写入延迟只追加一条日志,开销近似常量 \(\alpha\),主要由网络与序列化决定。
    TMOR写=α=T网络RTT+T序列化+T落盘T_{\text{MOR写}} = \alpha = T_{\text{网络RTT}} + T_{\text{序列化}} + T_{\text{落盘}}
  2. COW 写入延迟与文件组大小 \(S_{\text{文件组}}\) 成正比,系数 \(\beta\) 表示每单位数据读+写的平均耗时。
    TCOW写=β×S文件组,β≈2×单位字节IO时间并行度T_{\text{COW写}} = \beta \times S_{\text{文件组}},\quad \beta \approx 2 \times \frac{\text{单位字节IO时间}}{\text{并行度}}
  3. 批量写场景COW 的批处理吞吐未必差,因为它用大顺序写换回了更高 IO 效率。
    COW 吞吐batch≈B批β×S文件组\text{COW 吞吐}_{batch} \approx \frac{B_{\text{批}}}{\beta \times S_{\text{文件组}}}

存储成本

旧账本和新账本并排放在柜子里,COW 若保留多版本,每份都是完整的账本,占满一整个柜子;MOR 只有一本正本加一摞便签,写得多时便签也会越堆越高。

COW 的每次提交都会产生新的完整 Parquet 文件,如果保留历史版本,存储成本随版本数量线性增长。MOR 的 base 文件长期不动,log 文件持续累积,成本增长相对平缓,但 log 中的重复主键会带来冗余存储;只有执行压缩(Compaction)后,log 才会被吸收进 base,数据新鲜度和存储成本才得到优化。

总存储成本可以表示为:

选择 COW,相当于接受「柜子里堆满完整账本」;选择 MOR,则要接受「便签虽小,越攒越多」。存储成本从来不是静态的,它取决于版本保留策略和 compaction 频率。

C总=∑版本∈COWbase文件大小+∑log∈MORlog文件大小C_{\text{总}} = \sum_{\text{版本} \in \text{COW}} \text{base文件大小} + \sum_{\text{log} \in \text{MOR}} \text{log文件大小}
  1. COW 多版本项每保留一个历史版本,就要为一份完整文件付费,版本越老越占空间。
    ∑版本base文件大小=version1+version2+⋯+versionn\sum_{\text{版本}} \text{base文件大小} = \text{version}_1 + \text{version}_2 + \cdots + \text{version}_n
  2. MOR 日志项未压缩的 log 逐条追加,日志越长冗余越多。
    ∑loglog文件大小=log1+log2+⋯+logk\sum_{\text{log}} \text{log文件大小} = \text{log}_1 + \text{log}_2 + \cdots + \text{log}_k
  3. 压缩后的成本Compaction 把 log 合并进 base 后,日志项归零,但 base 变大。
    C压缩后=新base文件大小≤C压缩前C_{\text{压缩后}} = \text{新base文件大小} \leq C_{\text{压缩前}}

场景权衡

阿诚最终要回答:面馆到底该用哪种记账方式?答案取决于客人的点单模式——如果客人频繁点面、改动快,那就用便签;如果晚上要快速对账,那就用重抄。

工程上同样如此。COW 适合「读多写少、更新不频繁、查询要求高」的场景,比如用户画像宽表、维表快照、广告报表;MOR 适合「写多读少、对写入延迟敏感、可接受合并开销」的场景,比如点击流日志、IoT 事件、CDC 变更流。读写比例是核心决策指标:

配合阈值决策规则:

没有绝对最好的表类型,只有最匹配业务模式的记账方式。面馆的客流动线变了,账本策略也要跟着变。

RW=每秒读请求数每秒写请求数\frac{R}{W} = \frac{\text{每秒读请求数}}{\text{每秒写请求数}}
选择={COW,RW≥θMOR,RW<θ\text{选择} =\begin{cases}\text{COW}, & \dfrac{R}{W} \geq \theta \\\text{MOR}, & \dfrac{R}{W} < \theta\end{cases}
  1. 读写比定义\(R\) 是每秒查询次数,\(W\) 是每秒更新次数,比值反映业务偏向。
    RW=读 QPS写 QPS\frac{R}{W} = \frac{\text{读 QPS}}{\text{写 QPS}}
  2. 阈值 \(\theta\)它由查询延迟预算和存储成本共同决定,通常取 2~10,需要压测调参。
    θ=经验阈值,例如 5\theta = \text{经验阈值,例如 } 5
  3. 决策规则当读远多于写时选 COW,让大多数请求走快路径;当写很多时选 MOR,避免写放大拖垮系统。
    COW 收益=R×(MOR读慢−COW读快),MOR 收益=W×(COW写慢−MOR写快)\text{COW 收益} = R \times (\text{MOR读慢} - \text{COW读快}),\quad \text{MOR 收益} = W \times (\text{COW写慢} - \text{MOR写快})

工程优化

便签攒多了,阿诚会在打烊后把便签一张张贴进正本,这就是 Merge-on-Read 的 Compaction。Hudi 还提供了其他表服务,让 COW 与 MOR 都可以在运行中持续优化。

Compaction 将 log 合并进 base 文件,减少读取时的合并开销;Clustering 把小文件合并成大文件,降低整体文件数量;索引类型决定「快速定位文件组」的效率,比如 Bloom filter、HBase index。这些工程手段共同决定了表类型最终产出的性能。Compaction 的调度成本需要被控制:

阿诚不会天天整理便签,但也不会让便签堆到天花板。Hudi 的表服务就是那位自动提醒「该理账了」的管家,让两种表类型都能在真实业务中良性运转。

Ccompaction=γ×log个数+δ×log总大小C_{\text{compaction}} = \gamma \times \text{log个数} + \delta \times \text{log总大小}
  1. 固定启动成本每次 compaction 都要扫描文件组并启动执行单元,\(\gamma\) 表示每个 log 的固定处理成本。
    γ×log个数=每个 log 的调度开销×log 数量\gamma \times \text{log个数} = \text{每个 log 的调度开销} \times \text{log 数量}
  2. 数据规模成本合并的数据量越大,IO 和计算开销越高,\(\delta\) 表示单位字节的合并成本。
    δ×log总大小=单位字节合并开销×总字节数\delta \times \text{log总大小} = \text{单位字节合并开销} \times \text{总字节数}
  3. 最优调度点当新增 log 的读放大成本超过 compaction 成本时,就应该触发合并。
    如果 ∑T合并log>Ccompaction,则执行 Compaction\text{如果 } \sum T_{\text{合并log}} > C_{\text{compaction}},\text{则执行 Compaction}

AI 应用场景

实时风控流水写入

阶段:数据接入与预处理

解决的问题:传统批处理导入延迟高达小时级,风控模型拿不到最新交易特征。Hudi MOR 用「追加便签」的低延迟写入承接每秒数万笔流水,模型训练和在线推理都能用到分钟级新数据。

具体案例:某支付平台将交易事件流写入 Hudi MOR 表,writes 直接追加 log,配合 10 分钟一次 compaction,特征平台读取时只合并少量 log,风控规则的召回率明显提升。

用户画像维表更新

阶段:特征存储与更新

解决的问题:用户画像维表每天被任务调度更新多次,但在线推理需要极低延迟的查询。选择 Hudi COW,让每次更新都生成干净的 base 文件,推理服务读取时只有一个文件,避免合并开销。

具体案例:某推荐系统把用户偏好维表建为 COW 表,每 15 分钟全量重写一次文件组,在线服务读 P99 稳定在 30ms 内,相比此前 MOR 的合并读降低了约 60% 延迟。

湖仓 CDC 同步

阶段:数据入湖与增量计算

解决的问题:数据库 binlog 产生的变更流是典型的「高频小更新」,直接写 COW 会造成巨大写放大。MOR 以日志追加承接 CDC 流,再通过周期 compaction 沉淀为列存 base 文件,兼顾写入新鲜度与后续 OLAP 查询。

具体案例:某电商数据团队将 MySQL binlog 接入 Hudi MOR 表,上游执行 5 万次 update/s,Hudi 只追加 log 不触发整文件重写,下游 Flink 读取合并后的数据做实时指标计算。

Python 演示

python55 行
# -*- coding: utf-8 -*-class TableSim:    def __init__(self, base_size=100):        self.base = base_size        self.logs = []        self.total_cow_writes = 0        self.total_mor_writes = 0        self.cow_read_cost = 0        self.mor_read_cost = 0     def cow_update(self, change):        # COW: 重写整个 base 文件        self.total_cow_writes += self.base        self.base += change     def mor_update(self, change):        # MOR: 只追加一条日志        self.logs.append(change)        self.total_mor_writes += 1     def cow_read(self):        # COW: 只读一个文件        self.cow_read_cost += self.base        return self.base     def mor_read(self):        # MOR: 读 base + 合并所有 log        cost = self.base + sum(self.logs)        self.mor_read_cost += cost        return cost     def compaction(self):        # 把 log 合并进 base        self.base += sum(self.logs)        self.logs = []  def run_simulation(rounds=20, change_per_round=3):    cow = TableSim()    mor = TableSim()    for i in range(rounds):        cow.cow_update(change_per_round)        mor.mor_update(change_per_round)        cow.cow_read()        mor.mor_read()        if i % 5 == 4:            mor.compaction()    print("COW 写字节:", cow.total_cow_writes)    print("MOR 写字节:", mor.total_mor_writes)    print("COW 读合并成本:", cow.cow_read_cost)    print("MOR 读合并成本:", mor.mor_read_cost)  if __name__ == "__main__":    run_simulation()
目的:用数值模拟展示 COW 的写放大与 MOR 的读放大,以及 compaction 对读成本的抑制作用。
  1. self.total_cow_writes += self.baseCOW 每次更新都要把整个文件组大小计入写入量,体现写放大。
  2. self.total_mor_writes += 1MOR 每次只追加一条日志,写入成本与文件组大小无关。
  3. self.cow_read_cost += self.baseCOW 读取的代价就是扫描一个 base 文件。
  4. cost = self.base + sum(self.logs)MOR 读取需要把 base 和所有 log 合并计算后才得到最新值。
  5. if i % 5 == 4: mor.compaction()定期 compaction 把 log 归并进 base,避免读成本无限增长。
  6. print("COW 写字节:", ...)输出对比结果,你能明显看到 COW 写远高于 MOR,但 MOR 读成本会在未压缩时走高。

常见误区

误区COW 每次更新都重写整个文件,所以它的写入性能肯定比 MOR 差很多,不适合任何实时场景。

正解COW 的慢是相对的,它慢在高频小更新产生的写放大;但在批量写入场景下,COW 的大顺序写效率并不低。实时场景还要看数据量级和更新频率,不是「用了 COW 就一定会堵」。

为何常见:大家很容易把「单次写放大」直接等同于「吞吐低」,忽略了批处理和大文件顺序写的补偿作用。

误区既然 MOR 在写入时把 log 追加好了,读取时应该也能直接读到全部数据,速度不会比 COW 慢。

正解MOR 读取时必须合并 base 文件和所有 log,log 越多,合并成本越高。只有执行完 compaction 后,读取性能才接近 COW。把 MOR 当成读性能好的表类型是一种危险误解。

为何常见:人们习惯用「写入很快」倒推「读取也很快」,忽略了存储系统中读写路径往往互相制约。

误区流式计算会产生大量小更新,COW 重写文件的开销太大,所以流式写入只能选 MOR。

正解如果流式数据本身是 append-only,比如日志追加不修改旧记录,COW 也可以高效工作;只有「高频更新同一批主键」的场景才让 COW 写放大失控。选择取决于更新类型,而不是是否用了流引擎。

为何常见:很多人把「流式」与「更新」画等号,但点击流日志本质是只追加,并不触发文件重写。

误区MOR 的 log 追加很快,那就一直追加,反正最后有 compaction 能收拾。

正解log 无限增长会带来三方面代价:读放大指数上升、小文件数量激增、表服务资源被长期占用。正确的做法是设置合理的 compaction 触发策略,在 log 数量和读延迟之间取平衡。

为何常见:只看写入延迟的局部最优,忽略了读取侧和存储侧的整体成本,是延迟敏感系统的常见认知陷阱。

误区只要算出读写比 R/W,就能确定用 COW 还是 MOR,一劳永逸。

正解读写比只是首要参考,还要考虑更新频率、单次更新规模、查询延迟预算、存储成本、compaction 调度能力。业务模式变化后,表类型也可能要通过重写或迁移来调整。

为何常见:人天性喜欢简单公式,但真实系统的权衡是多维度的,单一指标无法覆盖所有约束。

面试题

Q1. Hudi 的 Copy-on-Write 表类型在更新数据时通常发生什么?

单选题 · easy

  • A.直接在原文件上原地修改
  • B.重写整个文件组生成新版本← 正确答案
  • C.把更新追加到日志文件
  • D.删除旧文件并新建一个空文件

答案:B。COW 的核心是「重写」,每次更新都会读旧 base、合并变更,再写出整个新版本文件,而不是原地修改或只写日志。像账房先生誊写整本账,而不是改几个字。

Q2. Merge-on-Read 表类型执行查询时,数据读取路径是?

单选题 · mid

  • A.只读取 base 文件
  • B.只读取 log 文件
  • C.合并 base 文件与所有 log 文件← 正确答案
  • D.先删除日志再读取 base 文件

答案:C。MOR 的 base 保存存量数据,log 保存增量,查询时必须把二者合并才能看到最新视图。就像查账时必须把正本和所有便签摊开一起看。

Q3. COW 相比 MOR 的主要优势是?

单选题 · mid

  • A.写入延迟更低
  • B.读取性能更高← 正确答案
  • C.存储成本更低
  • D.不需要执行压缩

答案:B。COW 每次更新后只有一个干净的 base 文件,查询不需要合并日志,所以读取性能显著优于 MOR。代价是写入时要重写大文件,写放大更严重。

Q4. 以下哪种业务场景更适合使用 Merge-on-Read?

单选题 · easy

  • A.每天一次批量导入后频繁查询
  • B.高并发小事务持续更新← 正确答案
  • C.数据只读永不更新
  • D.需要极低读取延迟的在线报表

答案:B。MOR 用追加日志换低写入延迟,适合高并发小更新;但如果业务是读多写少或查询极敏感,COW 往往更合适。

Q5. Hudi 中「文件组」的核心作用是什么?

单选题 · mid

  • A.代表一张数据库表
  • B.按主键范围划分的逻辑数据分组← 正确答案
  • C.专门存放日志文件
  • D.表示 Parquet 中的一个列

答案:B。文件组是 Hudi 数据组织的基本单元,同一主键范围的记录会落在同一文件组。它决定更新时定位哪个文件集合,类似账本按科目分册管理。

Q6. 关于 COW 与 MOR 的写放大,下列哪种说法正确?

单选题 · hard

  • A.COW 写放大通常高于 MOR← 正确答案
  • B.MOR 写放大通常高于 COW
  • C.两者写放大完全相同
  • D.写放大与文件组大小无关

答案:A。COW 每次更新需要重写整个文件组,物理写入远大于逻辑变更量;MOR 只追加日志,写放大接近 1。因此 COW 的写放大通常明显更高。

Q7. MOR 中 compaction 的主要作用是?

单选题 · hard

  • A.将 log 合并进 base 文件以降低读放大← 正确答案
  • B.将 base 文件拆成更多小文件
  • C.删除所有历史版本以释放空间
  • D.降低数据写入频率

答案:A。compaction 把散落的 log 吸收进 base,减少查询时需合并的日志数量。它不拆文件也不删历史版本,只让读写路径更短。

Q8. 以下哪些场景更适合选择 MOR 表类型?

多选题 · mid

  • A.每秒大量小事务持续更新← 正确答案
  • B.需要毫秒级超低读取延迟
  • C.实时数据入库且更新频繁← 正确答案
  • D.以静态报表查询为主的业务

答案:AC。MOR 的强项是高频更新和低写入延迟,A 和 C 都是写入密集场景;B 和 D 对读性能要求高,更适合 COW。

Q9. COW 表类型的主要劣势包括哪些?

多选题 · hard

  • A.更新操作写放大严重← 正确答案
  • B.读取合并速度非常慢
  • C.频繁更新会产生大量新版本文件← 正确答案
  • D.完全无法支持增量查询

答案:AC。COW 每次更新重写整文件,写放大严重,频繁更新会产生大量新版本文件;但它读取快,且 Hudi 通过时间线仍支持增量查询,所以 B、D 不对。

Q10. 在选择 COW 还是 MOR 时,需要重点评估哪些因素?

多选题 · extreme

  • A.读写请求比例← 正确答案
  • B.数据更新频率← 正确答案
  • C.查询延迟限制← 正确答案
  • D.SQL 解析器类型

答案:ABC。读写比例决定基本方向,更新频率影响 COW 的写放大程度,查询延迟限制决定能否接受 MOR 的合并开销;SQL 解析器与表类型选择无关。