Typecho 数据库表结构解析与直接 SQL 维护实战:批量改标题、清理垃圾评论与修订版本

为什么要懂 Typecho 的数据库表结构

后台点点鼠标就能发文章、改标题、删评论,为什么还要去碰数据库?答案是:后台能做的是「一条一条改」,数据库能做的是「一万条一起改」。当你从别的博客系统搬家过来、需要批量替换正文里的旧域名;当你中了垃圾评论轰炸,后台翻页删到手抽筋;当你想统计自己到底写了多少字——这些后台要么做不到,要么做不到高效做的事,直接写 SQL 反而最省事。

当然,直接操作数据库意味着没有任何「撤销」按钮。所以在动手之前,必须先搞明白表之间的关系,以及哪些字段能动、哪些绝对不能动。这篇文章就把 Typecho 的数据表结构拆开讲清楚。

Typecho 的四张核心表

一个标准的 Typecho 安装(表前缀默认 typecho_)里,真正重要的表有四张:

  • typecho_contents —— 核心中的核心。文章、独立页面、草稿、附件、修订版本,全部存在这张表里,靠 type 字段区分。
  • typecho_relationships —— 内容与分类、标签的关联关系表。
  • typecho_metas —— 分类和标签本身的信息(名称、别名、描述、父级)。
  • typecho_comments —— 评论表。

此外还有 typecho_users(用户)、typecho_options(站点配置,序列化存储)、typecho_fields(自定义字段,Handsome 主题的缩略图等就存这里)。

typecho_contents 表字段详解

先看这张最重要的表的完整结构:

DESCRIBE typecho_contents;

关键字段逐个说明:

cid:自增主键,就是文章 ID,也是前台 URL /{cid}.html 里的那个数字。这个值绝对不要手动改——一旦改动,所有指向旧 URL 的链接和搜索引擎索引全部失效。

title:文章标题,普通文本。

slug:URL 别名。留空时 Typecho 用 cid 生成 URL;填了就变成 /slug.html。批量改 slug 是 SEO 迁移时的常见操作。

created / modified:创建时间和修改时间,都是 Unix 时间戳整数,不是日期字符串。这是新手最容易踩的坑——直接用 UPDATE ... SET created = '2026-01-01' 会把字段写坏。正确写法见后文。

text:正文内容,longtext 类型。注意:这里存的是原始内容,如果发布时勾选了「Markdown」,存的就是 Markdown 源码;zz1984 这类发布纯 HTML 的站点,存的就是 HTML 原文。

type:内容类型。post = 文章,page = 独立页面,attachment = 附件,post_draft = 草稿,revision = 修订版本。做任何批量操作前,第一步永远是 WHERE type='post',否则你会把页面、附件、草稿一起改掉。

status:发布状态。publish = 已发布,hidden = 隐藏,private = 私密,waiting = 待审核。

authorId:作者 ID,关联 typecho_users.uid

commentsNum:评论数缓存字段。删评论后这个值不会自动更新,需要手动重算,否则页面显示的评论数和实际不符。

views:浏览量。Handsome 等主题会往这里写。

动手前的第一件事:备份

说再多安全规范,不如养成一个习惯。任何批量 UPDATE/DELETE 之前,先跑这条命令:

mysqldump -uroot -p zz1984 typecho_contents > /root/backup_contents_$(date +%F_%H%M).sql

导出的 SQL 文件可以直接 mysql -uroot -p zz1984 < 备份文件.sql 恢复。几十 MB 的库导出只要几秒,换来的是「改错了也能回滚」的底气。这一条比本文其他所有内容都重要。

实战一:批量替换正文里的旧域名

网站换了域名,几年前的正文里还写着一堆旧域名的绝对链接。手动改不现实,用 REPLACE() 函数批量替换:

-- 先查有多少条会被影响,心里有数
SELECT COUNT(*) FROM typecho_contents
WHERE type='post' AND text LIKE '%old-domain.com%';

-- 确认数量合理后再执行替换
UPDATE typecho_contents
SET text = REPLACE(text, 'https://old-domain.com', 'https://new-domain.com')
WHERE type='post' AND text LIKE '%https://old-domain.com%';

注意 WHERE 条件里的 LIKE 一定要和 REPLACE 的目标字符串一致,否则可能出现「替换了但条件没命中」或者「条件命中但没替换内容」的困惑结果。改完记得清空站点缓存,并 modified 时间不用动(Typecho 不会因为你改了库就更新它,这是好事,避免所有文章时间戳一起变化影响 SEO)。

务必加 WHERE type='post'。忘了加的话,附件记录、独立页面的内容也会被一起替换。

实战二:清理垃圾评论并重算评论数

评论轰炸之后,后台一条条删效率极低。用 SQL 按特征批量清理:

-- 第一步:先看垃圾评论长什么样,找出特征
SELECT coid, author, mail, text, created FROM typecho_comments
WHERE text LIKE '%http%' ORDER BY coid DESC LIMIT 20;

-- 第二步:确认特征后删除(举例:正文包含推广短链且无中文)
DELETE FROM typecho_comments
WHERE text LIKE '%bit.ly%' OR text LIKE '%t.cn%';

-- 第三步:删除待审核状态的垃圾
DELETE FROM typecho_comments WHERE status='waiting' AND author='';

删完评论后,typecho_contents.commentsNum 这个缓存字段已经不准了,需要用一条 UPDATE 重算:

UPDATE typecho_contents c
SET commentsNum = (
    SELECT COUNT(*) FROM typecho_comments m
    WHERE m.cid = c.cid AND m.status = 'approved'
)
WHERE c.type = 'post';

这条语句把每篇文章的评论数重新按「已通过审核的评论数」计算一遍,页面上显示的评论数立刻恢复正确。

实战三:检查并清理修订版本

Typecho 保存草稿和编辑时会生成修订版本(type='revision'),长期累积会让 contents 表膨胀。看看你积了多少:

SELECT type, COUNT(*) AS cnt, ROUND(SUM(LENGTH(text))/1024/1024, 2) AS mb
FROM typecho_contents GROUP BY type ORDER BY cnt DESC;

典型输出会显示 post 有几十条、revision 却有几百条。清理修订版本(只删 revision,不碰 post):

DELETE FROM typecho_contents WHERE type='revision';

删除后建议执行一次 OPTIMIZE TABLE typecho_contents; 回收磁盘空间。对 InnoDB 来说,OPTIMIZE TABLE 会重建表,几万行的表执行几秒到几十秒,建议在低峰期做。

实战四:按时间统计自己的写作量

想看看自己这些年到底写了多少字、多少篇?一条 SQL 就能给你答案:

SELECT
    DATE_FORMAT(FROM_UNIXTIME(created), '%Y') AS yr,
    COUNT(*) AS posts,
    ROUND(SUM(CHAR_LENGTH(text))/2) AS approx_words
FROM typecho_contents
WHERE type='post' AND status='publish'
GROUP BY yr ORDER BY yr;

这里用了 FROM_UNIXTIME() 把时间戳转成可读日期——这正好呼应前面说的「created 是时间戳」的坑。CHAR_LENGTH 按字符数统计(不是 LENGTH 按字节),对中文更准确;除以 2 是对内容里混着 HTML 标签的一个粗略折算。

实战五:按时间戳修改文章发布时间

如果你从别的系统导入文章,时间戳可能全是导入时间。想改成真实发布时间:

-- 把 cid=1091 的文章发布时间改为 2026-08-15 10:30:00
UPDATE typecho_contents
SET created = UNIX_TIMESTAMP('2026-08-15 10:30:00'),
    modified = UNIX_TIMESTAMP('2026-08-15 10:30:00')
WHERE cid = 1091 AND type='post';

核心是 UNIX_TIMESTAMP() 这个函数——它把日期字符串转成时间戳。千万不要直接写日期字符串,那会让字段变成 0 或者报错。

踩坑清单与安全习惯

把上面这些操作的风险点总结成一张清单:

  • 永远先 SELECT 再 UPDATE/DELETE,用同样的 WHERE 条件先看会命中哪些行、命中多少行。
  • 永远带着 WHERE type='post',除非你明确知道要操作别的类型。
  • 时间字段只认时间戳,写入用 UNIX_TIMESTAMP(),读取用 FROM_UNIXTIME()
  • 别动 cid,它是 URL 和所有外链的锚点。
  • 改完清缓存,Typecho 和主题都有缓存,不改缓存你会以为改失败了。
  • MySQL 命令行加 --safe-updates,可以防止忘记 WHERE 的全表更新事故:mysql --safe-updates -uroot -p
  • 操作前 mysqldump,这一条重复三遍也不为过。

常见问题答疑

问:直接改数据库会不会导致 Typecho 后台显示不同步? 不会。Typecho 后台的文章列表、编辑页都是实时从 typecho_contents 里读的,你改了库,刷新后台立刻就能看到变化。唯一需要注意缓存的是主题层面——Handsome 等主题会把热门文章、相关推荐、文章字数等做成静态缓存或 Redis 缓存,改库之后要清一下主题缓存才能看到最新效果。后台自带的「清理缓存」入口或删除 usr/cache/ 目录都可以。

问:为什么我执行 UPDATE 之后提示 0 rows affected? 三种常见原因。一是 WHERE 条件写错了,实际没有匹配到任何行——用同样的条件跑一次 SELECT COUNT(*) 验证;二是 MySQL 客户端默认开启了「只在数据真正变化时才算影响行数」,如果新旧值一模一样,也会报 0;三是修改了但没提交事务(如果你手动开了 BEGIN),记得 COMMIT。第三种最容易被忽略,尤其是习惯了图形化工具自动提交的人。

问:怎么安全地做「先备份再改」这件事?有没有更省心的办法? 最省心的做法是把两件事合并成一条命令,用 && 串起来:mysqldump -uroot -p zz1984 typecho_contents > /root/bak_$(date +%s).sql && mysql -uroot -p zz1984 -e "你的 UPDATE 语句"。这样只有备份成功才会执行修改,避免「忘了备份就动手」。更进一步,把这条命令写进一个 shell 脚本,每次替换 SQL 语句即可,形成肌肉记忆。

问:relationships 和 metas 表要不要一起动?改分类名怎么办? 改分类名比改文章标题麻烦一点,因为它涉及两张表。typecho_metas 存的是分类本身的 name(显示名)和 slug(URL 别名),typecho_relationships 只存 cidmid 的对应关系,不存名称。所以只改 metas 表的 name 就够了,relationships 不需要动——它是通过 mid 关联的,分类名变了关联关系依然成立。改 slug 要谨慎,因为它会改变分类归档页的 URL,旧链接需要 301 跳转。

问:操作出错、数据被改坏了,怎么恢复? 这就是为什么第一步必须是 mysqldump。恢复分两种情况:如果只是 contents 表被改坏,用 mysql -uroot -p zz1984 < 备份文件.sql 直接导入覆盖(备份文件里是 CREATE TABLE + INSERT,会重建整张表);如果误删了数据想精确恢复,可以在导入前先 DROP TABLE typecho_contents; 再导入。恢复完再执行一次 OPTIMIZE TABLE 重建索引。记住:能恢复的前提永远是「改之前导出了备份」,没有备份就只能靠 binlog 做时间点恢复,那个流程复杂得多。

小结

后台是给日常操作用的,数据库是给批量维护用的。理解了 contents 表的 type/status 字段、时间戳的存储方式、以及 relationshipsmetas 的关联关系,你就能用一条 SQL 完成后台要点几百次鼠标的工作。但这份能力伴随责任——SELECT 先行、备份在手、WHERE 带 type,这三板斧记住了,你就能安全地在自己的数据库里做批量维护。

Last modification:September 21st, 2026 at 12:25 pm

Leave a Comment