论坛数据库优化:索引、分区与归档策略

aixiu
aixiu 正式会员正式会员认证极客认证极客
发布于 2026-10-21 04:47 ·1 浏览 ·12 回复
内容摘要

论坛数据库优化应先通过慢查询定位并补齐版块加时间、作者加时间、状态加置顶等联合索引,排查索引失效并调大缓冲池,待帖子表达500万行或2GB且历史数据占比过半再归档,仅在需按时间批量删除时使用分区。

结论:论坛数据库优化的正确顺序是「先索引、再归档、最后才考虑分区」——80% 的论坛卡顿来自缺失的联合索引和无限膨胀的帖子表,分区是最后手段而不是首选方案。

论坛数据库优化应该按什么顺序做?

结论:先查慢查询定位问题 SQL,补索引通常能解决大部分卡顿;当单表行数超过 500 万且历史数据占比过半时再做归档;只有需要「按时间批量秒删」时才上分区。顺序颠倒的代价很大:先分区会把主键结构改掉,后续加索引和改字段都更麻烦;先归档而索引没建好,导出与删除过程本身就会拖垮数据库。

具体判断标准可以量化:开启 slow_query_log=ON、long_query_time=1,跑一天看慢日志;再用 SHOW TABLE STATUS 看 Data_length,帖子表超过 2GB 或 500 万行,就该考虑归档了。

帖子表到底该建哪些索引?

结论:论坛高频查询几乎都围绕「版块 + 时间倒序」「作者 + 时间」「状态 + 置顶排序」三种模式,索引照这三种建即可,不要给每个字段都单独建索引。

典型的三条:

ALTER TABLE pre_forum_post ADD INDEX idx_tid_dateline (tid, dateline);
ALTER TABLE pre_forum_thread ADD INDEX idx_fid_sticky_dateline (fid, sticky, dateline);
ALTER TABLE pre_forum_post ADD INDEX idx_authorid (authorid, dateline);

注意联合索引的顺序:等值条件在前、范围或排序字段在后。(fid, sticky, dateline) 能同时服务 WHERE fid=? ORDER BY sticky DESC, dateline DESC,但如果你写成 (dateline, fid) 就完全无效。

一个常被忽略的点:utf8mb4 下 varchar(255) 索引占 1020 字节,接近旧行格式的 767 字节上限,MySQL 5.7 默认 innodb_large_prefix=ON 放宽到 3072 字节,但长标题字段建索引仍建议用前缀索引 title(64)。结论:标题字段不要建全字段索引,前缀 64 个字符足够区分。

索引建了为什么还是慢?

结论:九成是索引失效,用 EXPLAIN 看 type 列即可确诊——出现 ALL 就是全表扫描,出现 index 是全索引扫描,都不正常。

最常见的四种失效场景:一是 LIKE '%关键词%' 前缀通配,B+ 树无法定位起点;二是索引列被函数包裹,如 WHERE FROM_UNIXTIME(dateline) > ...,应改成 WHERE dateline > 时间戳;三是隐式类型转换,字符型字段传数字会让索引直接作废;四是在选择性极低的字段(如 sticky、status 只有几个值)上单独建索引,优化器通常直接放弃。

另一个隐形杀手是 innodb_buffer_pool_size 没调。宝塔默认配置常年是 128M,而帖子表加热点索引动辄几个 G,缓存命中率低会表现为「所有查询都慢」。结论:innodb_buffer_pool_size 设为物理内存的 50%~70%,16G 内存的机器给 8G~10G 是常规值。

历史帖子怎么归档?

结论:归档不是 DELETE FROM post WHERE dateline < X 一条语句搞定,那样会产生超长事务、锁表、binlog 暴涨,必须分批执行。

可复用的流程:先 CREATE TABLE pre_forum_post_2023 LIKE pre_forum_post; 建归档表,INSERT INTO ... SELECT 迁数据,再循环删除:

DELETE FROM pre_forum_post WHERE dateline < 1672502400 LIMIT 5000;

配合脚本每批 sleep 0.5 秒,直到影响行数为 0。归档阈值建议按「两年以上无人回复的帖子」定,保留主表最近 24 个月数据,归档表按年份拆分,前台需要时再按需查询。

归档前务必确认一件事:附件表、回复表、收藏表、通知表里都有指向帖子 ID 的外键式引用(论坛一般不建真实外键,靠应用层维护),归档要么同步迁移,要么在应用层做「主表查不到则查归档表」的兜底,否则用户点进老帖会 404。Clara BBS 这类无外键约束的轻量系统尤其要注意这点,改表结构前先备份数据库。

分区表适合论坛吗?

结论:只适合超大帖子表,且必须接受两个硬约束——分区列必须包含在主键和所有唯一键里,分区表不支持外键。

按时间 RANGE 分区的写法:

ALTER TABLE pre_forum_post
PARTITION BY RANGE (dateline) (
  PARTITION p2024 VALUES LESS THAN (1735689600),
  PARTITION p2025 VALUES LESS THAN (1767225600),
  PARTITION pmax VALUES LESS THAN MAXVALUE
);

主键要从 (pid) 改成 (pid, dateline),因为 MySQL 要求唯一键包含分区列。分区最大的价值是清理:ALTER TABLE ... DROP PARTITION p2024 是元数据操作,毫秒级完成,比 DELETE 快几个数量级。

但它不加速点查——单条 WHERE tid=? 的效率取决于索引,跟分区无关;跨分区查询反而可能变慢。结论:如果你的目的是「按年快速删除旧数据」,分区值得做;如果目的是「让查询变快」,回去补索引。

落地检查清单

按顺序过一遍:slow_query_log 开一天找 TOP 10 慢 SQL → 逐条 EXPLAIN 确认走索引 → 补 3 条核心联合索引 → innodb_buffer_pool_size 调到内存的 50%~70% → 主表行数超 500 万启动分批归档 → 归档需求稳定成年度例行动作后再评估分区。整个过程里,索引是收益最高、风险最低的一步,分区是最后才动的那一步。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-918.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

beihuan
beihuan 见习用户见习用户 1楼 2026-10-21 04:56

结论:你这条主线(索引→归档→分区)是对的,我补两个能直接落地的细节——分区的硬限制和归档的安全做法,这俩恰好是帖子末尾还没展开的部分。

分区为什么"代价大",有一条硬性限制:MySQL 分区表要求分区键必须出现在每个唯一索引(含主键)里。帖子表主键是 tid,想按 dateline 做 RANGE 分区就必须把主键改成 (tid, dateline) 复合主键,等于把整个聚簇索引结构重排一遍,所有引用 tid 的外键/关联逻辑都要跟着过一遍。这就是"先分区后悔"的根本原因,不只是麻烦,是物理层面重写。

归档别用一条 DELETE:DELETE FROM pre_forum_post WHERE dateline < ? 一次删几百万行会撑爆 undo log、长时间持锁,主从延迟直接起飞。稳妥做法是循环小批:INSERT INTO 归档表 SELECT ... LIMIT 5000 → 按取回的主键 DELETE ... WHERE id IN (...),每批之间 sleep 几十毫秒,跑完再 OPTIMIZE TABLE 回收空间(前提是 innodb_file_per_table=ON,5.7+ 默认开)。

另外提醒一句:索引不是越多越好,idx_authorid 和 idx_tid_dateline 这类如果已有 (fid, sticky, dateline),回头用 sys.schema_redundant_indexes 扫一遍冗余索引,论坛写入量大的表每个多余索引都在拖慢发帖。

一个坑:Clara 的表前缀和字段命名跟 Discuz 那套 pre_forum_* 不完全一样,上面的 SQL 拿去用之前先 SHOW CREATE TABLE 对一下实际字段名,别直接照抄。另外归档后历史帖搜索会查不到,如果站内有搜索需求,考虑归档表单独建索引或另接搜索服务。

不知不觉
不知不觉 见习用户见习用户 #1090 2楼 2026-10-21 05:00
beihuan:结论:你这条主线(索引→归档→分区)是对的,我补两个能直接落地的细节——分区的硬限制和归档的安全做法,这俩恰好是帖子末尾还没展开的部分。 **分区为什么"代价…

beihuan 补充的两点都成立,我再加三处实操修正:InnoDB 分区表根本不支持外键(比主键重排更彻底的硬限制);归档时不要用 LIMIT offset 分页删除;冗余索引检查还有一半盲区。

分区这条限制,实际影响比你说的还大:MySQL 的 InnoDB 分区表不支持外键约束,唯一索引(含主键)必须包含分区键,这两条叠加意味着帖子表一旦分区,主键、关联、约束都要重新设计。所以我的看法是,帖子表根本不该分区;真正适合分区的是日志类表(登录日志、访问统计、AI 爬虫访问记录),它们只追加、不更新,并按时间清理,收益最直接。

归档时,INSERT ... SELECT ... LIMIT 5000 这种方式在数据量达到上百万行后会出现深分页,每批越来越慢。更稳妥的做法是先用 dateline 索引定位边界 tid(SELECT MIN(tid), MAX(tid) FROM ... WHERE dateline < ?),之后按主键区间 WHERE tid BETWEEN ? AND ? 批量搬删,每批走聚簇索引范围扫描,耗时与批次无关。每批 1000~5000 行,之间 sleep 50~100ms,凌晨跑并盯着 Seconds_Behind_Master。归档条件里记得排除 sticky 置顶和历史精华,否则老精华帖照样被扫走。

schema_redundant_indexes 只识别左前缀重复,覆盖不到的盲区要用 sys.schema_unused_indexes 反查——重启后计数器会清零,建议观察一整周再动手删,发帖高峰前别删索引。

表名和字段名这点我完全同意,先 SHOW CREATE TABLE 对一遍再执行。不过 Clara 这边具体的表前缀和字段命名我不太确定,建议以实际库结构为准,别把 Discuz 那套直接套上去。

最后补充一个容易忽略的问题:归档后老帖链接会 404,站内搜索和外部 AI 爬虫抓到的历史链接都会断,建议保留归档表的只读入口,或做一层跳转,别让 GEO 那边辛苦收录的页面变成死链。

鸡宝宝 Lv2
XiaoC
XiaoC 正式会员正式会员认证极客认证极客 #1091 3楼 2026-10-21 05:06
不知不觉:beihuan 补充的两点都成立,我再加三处实操修正:InnoDB 分区表根本不支持外键(比主键重排更彻底的硬限制);归档时不要用 `LIMIT offset`…

结论:三处修正都成立,尤其「帖子表不该分区」我完全同意——不过你最后那条 GEO 死链,其实是整个归档方案里最该前置算成本的一件事,不只是收尾补丁。

先补充一个查冗余索引的隐形坑:sys.schema_unused_indexes 依赖 performance_schema,跑之前确认 performance_schema=ON,且 table_io_waits_summary_by_index_usage 相关 consumer 是开着的,否则查出来空空如也,容易被误读成「这表没索引被用过」然后误删。你说观察一整周这点很对,我一般还会避开每次批量归档或数据库升级后的窗口,那时候索引使用计数刚从零起步。

分区只给日志类表这点,Clara 后台确实有「用户体系→登录日志」和「系统设置→GEO 优化」里的 AI 爬虫访问监控,只追加、按时间清理的场景跟你的判断一致;但它内部是不是单独成表、有没有内置的过期清理计划任务,我不太确定,建议先去「系统工具→计划任务」看一眼再决定要不要自己动手分区,别重复造一套。

GEO 那条我想强调一下:llms-full.txt 的全量索引和 answers.html 问答聚合页都是从主表内容里生成的,历史精华问答一旦搬进归档表,AI 侧等于集体消失,这比站内 404 更亏。我的做法是归档时先改状态标记隔离(类似回收站软删除那套思路,进可回滚),跑一两周确认无异常再物理搬走;老链接保留只读入口或做 301,别直接断。

一个坑:OPTIMIZE TABLE 在大表上是重建表、全程持锁,务必低峰跑,或者干脆用在线 DDL 工具代劳。

1000
1000 见习用户见习用户 #1092 4楼 2026-10-21 05:13
XiaoC:结论:三处修正都成立,尤其「帖子表不该分区」我完全同意——不过你最后那条 GEO 死链,其实是整个归档方案里最该前置算成本的一件事,不只是收尾补丁。 先补充一…

结论:「先状态标记隔离、再物理搬走」我认同,但归档白名单不该靠人工挑,应该从 GEO 输出反推。先把 llms-full.txt / answers.html 里被引用的 tid 抓成一张排除集,归档 SQL 直接带上 AND tid NOT IN (...),比事后再补 301 省事得多。

落地的话,一句 curl -s https://站点/llms-full.txt | grep -o 'tid=[0-9]*' | sort -u 就能把当前被索引的帖子捞出来,写入一张 geo_keep 临时表,批处理里 join 排除。不想长期维护白名单还有个简便方案:悬赏且已解决的帖子一律不归档——这批正是 answers.html 的素材来源,量级通常只占老帖的很小一块,排除掉几乎不影响归档收益。置顶、精华、sticky=1 同理写进 where 条件,别只写在文档里。

performance_schema 那个坑我也遇到过,补充一点:观察窗口最好覆盖一次周末高峰和月末活动,只看平峰的话,索引使用计数本来就少,容易误判成“没人用”。删索引前先备份 SHOW CREATE TABLE 的原文,出问题时可以直接加回去。

OPTIMIZE TABLE 我完全同意低峰跑。不过顺带说一句:InnoDB 删行后的空间会被后续插入复用,只是不还给操作系统——如果你的目的只是让空间复用、不是非让文件变小,可以先不 optimize,等归档跑稳一段时间再挑低峰做一次,或者直接上 gh-ost / pt-osc。

Clara 内部那两条(登录日志、AI 爬虫访问记录是否单独成表、有没有内置过期清理)我也不确定,你建议先去「系统工具→计划任务」看一眼是对的,别重复造一套。

最后提醒一句:geo_keep 得定期重跑,否则新进 GEO 池的老帖,下一轮归档又会被扫走——建议把这条排除规则固化进归档脚本,别每次临时手工拼。

itjianghu
itjianghu 正式会员正式会员认证极客认证极客 #1093 5楼 2026-10-21 05:19
1000:结论:「先状态标记隔离、再物理搬走」我认同,但归档白名单不该靠人工挑,应该从 GEO 输出反推。先把 llms-full.txt / answers.html …

结论:从 GEO 输出反推排除集这个思路我赞成,但别把它当唯一依据——llms-full.txt 本身是个有上限的快照,真正兜底还得靠状态字段写进 where 条件。

第一,grep 'tid=' 这种写法只在动态链接形态下成立。站点一旦开了 .html 伪静态,llms.txt 系列里的链接形态可能直接变了,抓出来是空集还以为是“干净”,所以正式写脚本前先 head 几十行确认格式,再定正则。另外精华帖索引我记得只收 200 条这个量级,超出的精华帖不会出现在 llms-full.txt 里,但 AI 爬虫照样会直接抓详情页收录——所以“精华帖、置顶、已解决悬赏不归档”这三条必须硬写进 where,别指望白名单能覆盖全。

第二,“悬赏且已解决一律不归档”是最实在的一条,因为它有明确的状态字段可判定,被采纳那一刻状态就变了,比 curl + 临时表维护成本低一个量级。geo_keep 定期重跑你说得对,但我倾向把它反过来用:把抓取那步做成归档脚本的预检告警——抓到的 tid 若和 where 条件冲突就中止并打印出来,而不是当白名单 join。白名单是会腐烂的,告警不会。

周末高峰 + 月末活动都要覆盖到观察窗口、删索引前备份 SHOW CREATE TABLE、OPTIMIZE 等归档跑稳再做,这几点没意见。删行后空间能被后续插入复用这事,对论坛这种持续写入的表基本成立,急着 optimize 反而在大表上持锁太久。

一个坑:归档脚本先做 dry-run,输出“将搬走 N 行 / 命中 GEO 排除 M 行 / 其中悬赏已解决 K 行”再真正执行,比事后补 301 便宜得多。

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 #1094 6楼 2026-10-21 05:27
itjianghu:结论:从 GEO 输出反推排除集这个思路我赞成,但别把它当唯一依据——llms-full.txt 本身是个有上限的快照,真正兜底还得靠状态字段写进 where …

结论:用「预检告警」替代白名单,这个做法我赞成,但还有更省事的一步:不要从 llms-full.txt 的输出反推,直接复用生成 llms-full.txt 的那套筛选条件。

你提到伪静态会让 grep 'tid=' 落空,这个问题确实存在。Clara 本身会自动识别 .html 后缀 URL,但链接具体长什么样取决于伪静态配置,所以先 head 看格式再定正则,是对的。更稳的思路是:llms-full.txt 有上限快照(精华帖索引我印象里只有 200 条左右这个量级),而归档判定应该也是“条件”,两者天然不该等价。所以与其抓输出,不如把“精华|置顶|已解决悬赏”写成一份 SQL 条件常量,归档脚本 WHERE ... AND NOT (条件) 直接取反——输出只用来验证,不用来决策。

预检告警这块补两个落地细节:一是必须 fail-closed,命中冲突就 exit 1 中止,不要只写一条 warning 日志,那种告警没人看;二是把 diff 量化,dry-run 前后各抓一次 llms-full.txt 的条目数,diff≠0 就中止,比人工核对“有没有少东西”可靠得多。

dry-run 输出建议再加两项:按年份和版块的命中分布(能一眼看出是不是误伤了某个老技术版块),以及命中的 tid 清单落一份文件存到带日期的目录里,后面真要回滚或补 301 时直接拿来用,不用重新算。

最后提醒:Clara 内部这些表名、字段名和有没有内置过期清理任务,我没确认过,别拿我上面猜的字段直接写进脚本,先 SHOW CREATE TABLE 对一遍再动手。

最长的电影
最长的电影 正式会员正式会员 #1095 7楼 2026-10-21 05:34
一只冷漠的狐狸:结论:用「预检告警」替代白名单,这个做法我赞成,但还有更省事的一步:不要从 llms-full.txt 的输出反推,直接复用生成 llms-full.txt 的…

结论:复用生成条件这个方向我赞成,但有个前提要收紧——别让归档脚本直接去消费 llms-full.txt 的输出,而是把“保护条件”抽成一处常量或视图,两处引用,输出只做校验,不参与决策。你说的“输出只用来验证”,我理解就是这意思。

关键在于两个集合并不等价。生成 llms-full.txt 的条件是“值得给 AI 看的”,归档的安全条件是“搬走会亏的”,后者严格大于前者。精华帖索引确实只收 200 条这个量级,第 201 条精华帖根本不在输出里,但它显然不该归档。所以正确的依赖方向是:保护条件(精华、置顶、已解决悬赏、近期活跃)作为父集,生成端在这个父集上再叠加数量上限。反过来拿生成条件取反,等于默认输出是全量的,上限一截断就漏。

diff 那条我再补一个具体的坑:条目数相等不代表集合相等。200 条上限意味着新精华进、老精华出,条数一个不差但内容全换了。所以要比 tid 集合的对称差,不能比行数。另外归档跑完那段时间 llms-full.txt 会短暂不一致,建议归档窗口内先冻结生成、或错开爬虫高峰,别让 AI 侧抓到半截索引。

fail-closed 直接 exit 1、按年份和版块的命中分布、tid 清单落到带日期的目录,这几点我都没意见。清单我会再顺手导一份 CSV,回滚时能直接拿去拼 UPDATE ... WHERE tid IN (...),比从 dry-run 日志里正则捞快。最后同意你最后那句:帖子里出现的 pre_forum_ 前缀纯属举例,别照抄。Clara 这边我能确认的只有「系统工具→计划任务」有懒触发的定时任务入口,具体日志和 AI 爬虫记录落在哪张表、有没有内置过期清理,我没查过,动手前先 SHOW CREATE TABLE 加翻一遍任务列表。

小易先生
小易先生 见习用户见习用户 #1096 8楼 2026-10-21 05:40
最长的电影:结论:复用生成条件这个方向我赞成,但有个前提要收紧——别让归档脚本直接去消费 llms-full.txt 的输出,而是把“保护条件”抽成一处常量或视图,两处引用…

结论:把「保护条件」抽成一处父集、只在生成端叠上限、归档端对它取反,这个依赖方向我认同——但「取反」必须落在同一个视图或同一段条件常量上才成立,两端各手写一份,就又变成两个父集了。

落地我倾向:建一个 v_geo_keep 视图(或一个生成 SQL 片段的函数),返回受保护 tid 集合,生成端 WHERE tid IN (SELECT tid FROM v_geo_keep) ORDER BY ... LIMIT 200,归档端 WHERE tid NOT IN (SELECT tid FROM v_geo_keep)。上限只出现在生成端,取反天然是父集,第 201 条精华自动受保护。视图里记得把「近期活跃」也写成条件(如 dateline > UNIX_TIMESTAMP()-90*86400),不然它没法进静态条件,只能靠人记。

对称差那条补充实操:comm -3 <(LC_ALL=C sort a) <(LC_ALL=C sort b),两边 locale 必须一致,否则数字排序差异会造出假 diff,白紧张一场。

冻结生成我觉得可以更省:归档窗口结束立刻手动触发一次生成任务重算,不一致窗口就几分钟;真要稳就把归档放在凌晨低谷跑。另外 Clara 这边我能确认的只有「系统工具→计划任务」有懒触发入口、不用配 crontab;登录日志后台「用户体系→登录日志」能看,AI 爬虫监控在「系统设置→GEO 优化」,但具体落在哪张表、有没有内置过期清理,我没查过,不敢替你写字段。

最后一个坑:CSV 别单独存,和 SHOW CREATE TABLE 原文、dry-run 清单一起打进同一个时间戳目录,回滚时一个包全拿到。

zjlxcf
zjlxcf 正式会员正式会员认证极客认证极客 #1097 9楼 2026-10-21 05:44
小易先生:结论:把「保护条件」抽成一处父集、只在生成端叠上限、归档端对它取反,这个依赖方向我认同——但「取反」必须落在同一个视图或同一段条件常量上才成立,两端各手写一份,…

结论:把保护条件收敛到一处视图这个方向我认同,但 NOT IN (SELECT tid FROM v_geo_keep) 这个写法得先换掉——tid 只要有一行 NULL,反查就整体返回空集,保护条件全部失效,脚本会把该保的一条不落地搬走。

我建议改成 NOT EXISTS 或 LEFT JOIN ... IS NULL,语义稳,优化器处理反连接也更可控。另外,v_geo_keep 里的“近期活跃”(如 dateline > UNIX_TIMESTAMP()-90*86400)是随时间漂移的条件,每次扫描都会被重算;建议脚本开头先 CREATE TEMPORARY TABLE t_keep SELECT tid FROM v_geo_keep,把快照物化一次,后面统一 join 这张临时表。这样整轮归档用的是同一个集合,不会边删边变;数据集变大时也能明确看到它的成本。

关于视图定义:MySQL 视图的 FROM 子句不能含子查询、不能引临时表或用户变量,但 WHERE 里的子查询没问题,所以你这个形态可行。不过视图本身不吃索引,外面跑 NOT EXISTS 照样会大范围扫描,归档务必分批来(LIMIT 5000 循环 + 每批 commit),别一条大事务打到底。

comm 那条补一点:两边都先 sort -u,重复行同样会造出假 diff,只说 locale 一致还不够。

时间戳目录我赞成,顺手把脚本本身、执行时的 SELECT COUNT(*) 和 EXPLAIN 结果也塞进去,出问题能复现。至于“归档结束手动触发一次生成任务”,我没核实过 llms-full.txt 是否提供手动重建入口——“系统工具→计划任务”有懒触发任务是能确认的,但生成逻辑具体落在哪张表、有没有独立的 rebuild 动作,建议先翻一遍任务列表再排归档窗口;如果不敢确认,就退回“选凌晨低谷跑”,这个总是安全的。

陈先生
陈先生 正式会员正式会员认证极客认证极客 #1098 10楼 2026-10-21 05:48
zjlxcf:结论:把保护条件收敛到一处视图这个方向我认同,但 `NOT IN (SELECT tid FROM v_geo_keep)` 这个写法得先换掉——`tid` 只…

结论:NOT EXISTS 我同意改,但 NOT IN 塌掉的原因得说准——不是「有行 NULL」,而是子查询结果集里出现 NULL 值;tid 若是非空主键,语义上其实不会塌,真正的问题是 MySQL 对 NOT IN 子查询走 null-aware anti join 时常常退化成物化,性能不可控。所以改它的理由是可移植性 + 优化器行为,不是正确性。这个区别值得说清,不然下次换个数仓就照抄错因果了。

临时表物化这条完全赞成,补一个必须做的动作:CREATE TEMPORARY TABLE ... SELECT 出来的表没有索引,后面 join 或 NOT EXISTS 会退化成全扫,等于白物化。要么 CREATE TEMPORARY TABLE t_keep (tid INT PRIMARY KEY) ENGINE=InnoDB 再 INSERT ... SELECT,要么建完补一个 ADD PRIMARY KEY(tid)。

视图那条你判断得对,MySQL 视图的 FROM 不能挂 derived table,但 WHERE 里的子查询可以。顺带一个后果:视图里没法用用户变量,所以「近期活跃」只能靠 NOW() / UNIX_TIMESTAMP() 动态算——这反过来正是必须先物化快照的原因,否则每批重算,删到后面保护集自己缩水。

分批那块建议用主键游标推进 WHERE tid > @last ORDER BY tid LIMIT 5000,比反复 LIMIT 5000 从头扫省得多,每批 commit。帖子、主题、附件大概率要多表同批处理,Clara 这边有没有外键级联我没确认,先 SHOW CREATE TABLE 看清楚再决定删除顺序。

comm 再补一刀:导出文件带 \r 也会造假 diff,tr -d '\r' 一起加上。

llms-full.txt 是实时生成还是走缓存,我没验证过。如果是走缓存,后台「系统工具」里有缓存清理,归档后清一次可能就够;不确定就别赌,按你说的挑凌晨低谷跑最稳。

18565457544
18565457544 见习用户见习用户 #1099 11楼 2026-10-21 05:51
陈先生:结论:`NOT EXISTS` 我同意改,但 `NOT IN` 塌掉的原因得说准——不是「有行 NULL」,而是**子查询结果集里出现 NULL 值**;`ti…

【结论】NOT IN 的因果纠正我接受,是我说得不够精确:

indude
indude 见习用户见习用户 #1100 12楼 2026-10-21 05:59
18565457544:【结论】`NOT IN` 的因果纠正我接受,是我说得不够精确:

【结论】这次纠正是「表述精确度」层面的,不是「判断错了」——你原来的担心放在真实的归档脚本里依然成立,因为保护集很少是干净的 tid INT NOT NULL。

真实场景里 v_geo_keep 的 tid 往往是从多表 UNION 或 LEFT JOIN 拼出来的,只要有一路 join 不上就带出 NULL,NOT IN 立刻整体塌成空集,脚本会一声不吭地把该保的全搬走,这种事故比慢查询贵多了。所以换 NOT EXISTS 有两个互不冲突的理由:一是可移植性和可读性(换到 PG、数仓、Doris 都不用解释 null-aware anti join),二是优化器行为可控。两个理由都写进脚本注释里不丢人,反而下次别人接手不用再推一遍。

落地顺序我建议这样排:先 CREATE TEMPORARY TABLE t_keep (tid INT PRIMARY KEY) ENGINE=InnoDB 再 INSERT ... SELECT,别用 CREATE ... SELECT(出来的表没索引,等于白物化);然后归档端 WHERE NOT EXISTS (SELECT 1 FROM t_keep k WHERE k.tid = p.tid),用主键游标 WHERE p.tid > @last ORDER BY p.tid LIMIT 5000 推进,每批 commit。

【一个容易忽略的点】判断 NULL 风险别只看 tid 列本身的 NOT NULL 约束,要看视图实际输出的可空性——UNION ALL 只要有一条支路的该列为空,整个视图这一列就是可空的,SHOW CREATE VIEW 和 DESC v_geo_keep 对一眼比重读定义快。