mlog$_表中“重复”记录源于多个物化视图共享日志且消费进度不一致,导致snaptime$$水位线未推进、旧条目反复读取;本质是日志清理边界被滞后的mv卡死,而非物理写入重复。

物化视图日志(MLOG$_ 表)重复记录不是“写入了两遍”,而是同一行变更被多个物化视图消费时,因日志未及时标记或清理,导致后续刷新反复读取旧条目——本质是 SNAPTIME$$ 水位线未推进、或多个 MV 共享日志但消费进度不一致。
为什么 MLOG$_ 里会出现“重复”条目
Oracle 不删除已消费的日志行,只更新 SNAPTIME$$ 字段标记“该 MV 已读到此处”。当多个物化视图共享一张日志表,而其中某个 MV 长期未刷新(比如作业 BROKEN 或 LAST_REFRESH_DATE 滞后),整张日志的清理边界就被它卡死。其他正常刷新的 MV 每次仍要扫描全量日志,再靠 SNAPTIME$$ 过滤——这会造成逻辑上“重复处理”,也拖慢性能。
-
DBA_MVIEW_LOGS.LAST_REFRESH_DATE显示的是“所有引用该日志的 MV 中最晚的一次刷新时间”,不是单个 MV 的水位 - 执行
DBMS_MVIEW.PURGE_LOG只清SNAPTIME$$ 的行,但不会物理删掉;若某 MV 停摆三个月,这三个月的日志就一直堆着 - 如果建日志时用了
INCLUDING NEW VALUES,一次UPDATE会生成两条:一条OLD_NEW$$ = 'O',一条= 'N'——这不是 bug,是设计行为;但若 MV 查询没用到旧值,这两条都算冗余
建日志时就控制字段粒度,从源头减少“无效重复”
重复记录体积大,根源常在日志本身太宽。不是所有列都需要进日志,尤其当物化视图 SQL 只引用其中几列时。
- 只声明真正被 MV 查询引用的列:
WITH ROWID (id, status, updated_at),而不是WITH ROWID全字段 - 避免无谓的
INCLUDING NEW VALUES:仅当 MV 定义中显式用了OLD_NEW$或需要判断UPDATING(column)才加;否则删掉它,UPDATE只记一条 - 不用
SEQUENCE$$就别加SEQUENCE:它增加维护开销,且对纯INSERT/DELETE场景无意义;改用ROWID + PRIMARY KEY更轻量
刷新后立刻推进水位,防止日志滞留
手动刷新或定时作业执行完 DBMS_MVIEW.REFRESH 后,必须确认 SNAPTIME$$ 真的前进了。否则下次刷新仍从老位置开始,等于重放。
- 查当前 MV 水位:
SELECT LAST_REFRESH_DATE FROM DBA_MVIEWS WHERE MVIEW_NAME = 'MY_MV' - 查日志中对应时间戳是否匹配:
SELECT MIN(SNAPTIME$$), MAX(SNAPTIME$$) FROM MLOG$_BIG_TABLE;若最大值远大于LAST_REFRESH_DATE,说明有新变更未被消费 - 若发现
LAST_REFRESH_DATE滞后,先查DBA_SCHEDULER_JOBS确认刷新作业是否DISABLED或状态异常;再手工跑一次DBMS_MVIEW.REFRESH('MY_MV', 'F')强制推进
多 MV 共享日志时,定期校准消费边界
一个 MLOG$_ 被三个 MV 共用,只要其中一个卡住,其他两个就无法清理日志。不能等它自然恢复,得主动干预。
- 列出所有依赖该日志的 MV:
SELECT MVIEW_NAME, LAST_REFRESH_DATE FROM DBA_MVIEWS WHERE MASTER = 'BIG_TABLE' - 识别滞后者:按
LAST_REFRESH_DATE排序,找出明显落后的那个 MV 名称 - 若该 MV 已弃用,直接
DROP MATERIALIZED VIEW LOG ON big_table重建日志(前提是确认无其他业务依赖) - 若需保留,但又不想让它拖累全局,可用
DBMS_MVIEW.PURGE_MVIEW_FROM_LOG(mvid => 123)清理它专属的旧记录——mvid必须从DBA_BASE_TABLE_MVIEWS查,不是DBA_MVIEWS
最易被忽略的点:日志表空间增长不是因为“写得多”,而是因为“没人收尾”。SNAPTIME$$ 是软水位,不推进就不会触发清理,哪怕所有 MV 都配置了自动刷新,只要有一个失败,整个链就僵住。盯着 LAST_REFRESH_DATE 比盯着日志大小更重要。











