企业微信 PC 端本地数据库结构中的巧妙设计

本文仅对部分文件做一个简单分析,不对正确性负责, 分析用到的文件来源于网络, 可能为企微sqlite数据库,也可能不是,大家看个乐就好,如有雷同,纯属巧合。
重点是学习其中的巧妙设计。

目录

  • 一、总览

  • 二、跨库关联的核心标识符

  • 三、各库表字段定义

    • 3.1 user.db --- 通讯录 / 联系人

    • 3.2 company.db --- 企业 / 应用

    • 3.3 crm.db --- 客户 / 教育 / 侧边栏

    • 3.4 govern.db --- 政务网格

    • 3.5 session.db --- 会话

    • 3.6 message.db --- 消息正文

    • 3.7 message_lookup.db --- 消息索引

    • 3.8 file.db --- 文件

    • 3.9 forever_store.db --- 收藏 / 待办 / 公告 / 表情

    • 3.10 tencent_doc_list.db --- 腾讯/微信文档

    • 3.11 kv.db --- 通用 KV / 探针

  • 四、跨库 ER 图(按类别)

    • 4.1 组织与人员域

    • 4.2 会话与消息域

    • 4.3 客户与 CRM 域

    • 4.4 文件 / 收藏 / 待办域

    • 4.5 文档域

  • 五、巧妙的设计


一、总览

这 11 个库是企业微信 PC 客户端的本地缓存/落盘数据,按业务域拆分到不同的 SQLite 文件中。整体可分为五大业务域:

数据库 业务域 职责(一句话) 表数
user.db 组织/人员 通讯录:内部成员、部门、外部联系人、微信好友、标签 39
company.db 组织/人员 企业信息(自有/外部企业)、应用、关键词拦截规则 7
crm.db 客户/CRM 客户管理、快捷回复、教育行业(家长/学生/班级)、会话侧边栏 59
govern.db 组织/人员 政务版「网格」组织节点 3
session.db 会话/消息 会话列表、会话成员、草稿、未读、置顶、@提醒 41
message.db 会话/消息 消息正文(主表 + 客服 + 小表 + 待删 + 撤回) 27
message_lookup.db 会话/消息 消息轻量索引(按会话/发送者快速定位) 2
file.db 文件 消息中的文件元数据、下载断点、本地加密缓存 5
forever_store.db 文件/协同 收藏、待办、公告、自定义表情(不随漫游清理) 18
tencent_doc_list.db 文档 腾讯/微信文档「我创建的/最近浏览」列表 5
kv.db 基础设施 通用键值、性能探针上报缓存 2

设计共性:

  • 大量「主键 + protobuf blob」表(字段名常为 *_pb / pb_content / raw_pb / content / data),结构化字段只抽出用于检索/排序的少数几列,完整对象序列化进 blob。
  • 多个库各自带一张 general_kv(key,value) 表,存本库范围内的杂项配置/游标。
  • 表名常带版本后缀(_v2/_v4/_v9/_v11/V50),是客户端迭代时的 schema 版本演进痕迹。

二、跨库关联的核心标识符

这些 ID 在多个库间充当「外键」(SQLite 未声明物理外键,靠应用层 JOIN):

标识符 类型 含义 出现位置(库.表.列)
vid / user_id int64(如 1688850851994195 用户唯一号(企微内部 ID) user.user_table.id;session.conversation_user_table.user_id;message..sender_id;crm. 各 *_id
conversation_id text,前缀区分类型 会话文本 ID。S: 单聊、R: 群聊、G:/C: 其他 session.conversation_table.id;message.message_table.conversation_id;file.file_table4.conversation_id
con_numeric_id int 会话数字 ID(自增),与 conversation_id 一一对应 session.conversation_table.con_numeric_id;message_lookup.*.con_numeric_id
server_id int64 消息服务端 ID(跨设备稳定) message..server_id;message_lookup..server_id;file.file_table4.server_id
message_id int 消息本地自增 ID message.message_table.message_id;message_lookup.*.message_id;file.file_table4.message_id
corp_id / corpany_id int 企业 ID user.user_table.corp_id;company.external_company_table_v2.corpany_id
department_id / party_id int64 部门 ID(party 为教育版「班级/组织节点」) user.department_tableV2.id;user.user_dept_tableV2.department_id;crm.party_table_v9.party_id
customer_id int 客户 ID(对应某个 external user 的 vid) crm.customer_info_table_v4.customer_id
collection_id int 收藏条目 ID forever_store.collectionV50.id;file.file_table4.collection_id
group_id / label_id int 外部联系人分组 / 标签 ID user.external_usergroup*;crm.customer_labelV2.id

关键映射链:

复制代码
复制代码隐藏代码`用户:   user_table.id (vid)
          ├── user_dept_tableV2.user_id ── department_id ─→ department_tableV2.id
          ├── external_user_relation_v3.user_id          (我加的外部联系人)
          └── corp_id ─→ company.external_company_table_v2.corpany_id

会话:   conversation_table.id (text `"S:.."`/`"R:.."`)  ⇄  con_numeric_id (int)
          │                                              │
          │ message_table.conversation_id(text)          │ message_lookup_table.con_numeric_id(int)
          └── conversation_user_table.conversation_id ── user_id (vid)

消息:   message_table.message_id ⇄ message_lookup_table.message_id ⇄ file_table4.message_id
        message_table.server_id  ⇄ message_lookup_table.server_id`

三、各库表字段定义

说明:blob/二进制列统一标注为「pb/blob」;general_kv 等纯键值表只列一次说明。

3.1 user.db --- 通讯录 / 联系人

核心表是 user_table(人)、department_tableV2(部门)、user_dept_tableV2(人-部门关系),以及外部联系人/微信好友相关表。

user_table --- 所有已知用户(内部成员 + 外部联系人)的档案

类型 说明
id int PK 用户 vid
name / english_name / real_name / verifying_name text 名称、英文名、实名、审核中名称
email / create_mail / has_bind_email text/int 邮箱、企业邮箱、是否已绑定
mobile / phone / office_phone / internation_code text 各类电话、国际区号
number / account text 工号、账号
birthday / gender int 生日、性别
level / authority / info_level / display_order int 等级、权限、信息可见级别、展示顺序
avator_url / workcardimage_url / has_updated_work_card_image text/int 头像、工牌图
corp_id int 所属企业(→ company.external_company_table_v2.corpany_id)
position / address text 职务、地址
external_corp_name / external_job text 外部联系人显示的企业名/职务
unionid / user_vcode / circle_name / name_status text/int 微信 unionid、校验码等
pb_content blob 完整用户对象 protobuf

department_tableV2 --- 部门

类型 说明
id int PK 部门 ID
name / englishname text 部门名
parent_id int 上级部门(树形)
pre_id / display_order / sequence int 同级前驱、展示序、排序值
partner_count / user_count int 合作伙伴数、人数
dual_id / level / circleid / corpany_id / virtual_party int 层级、圈子、企业、是否虚拟部门

dept_tree_table --- 部门树的「孩子-兄弟」链(id / first_child_id / next_sibling_id)。

user_dept_tableV2 --- 用户与部门多对多关系

类型 说明
department_id, user_id int PK(复合) 部门、用户
job text 该部门下的职务
is_main_job / is_border_man / is_virtual int 是否主部门/边界人/虚拟
sort / staff_type / circleid / attr2 int 排序、成员类型、圈子
user_department_hash int 关系哈希
pb_extra blob 扩展 pb

外部联系人 / 微信好友相关:

主键 用途
external_user_relation_v3 user_id 我添加的外部联系人关系:备注 remarks/real_remarks、5 个备注电话、来源类型、企业备注、添加时间等
external_apply_v7 user_id 外部联系人申请记录:status、reason、推荐类型、昵称、申请时间
external_user_ids / blacklist_external_userids / delete_external_userV1 value/user_id 外部用户 ID 集合 / 黑名单 / 已删除
external_user_group_info_table group_id 外部联系人分组(name/create_time)
external_user_group_relationship_table_v2 group_id,user_id 分组↔用户
externaluser_to_labelV1 userid,labelid 外部用户↔标签(labelid → crm.customer_labelV2.id)
wx_friend user_id 微信好友(nick_name/wx_id/flag)
wechat_contactV1 wxid 微信联系人(name/avator/is_recommend)
wechatuser_to_labelV1 openid,labelid 微信用户↔标签
wx_open_user_info wx_open_id 微信用户备注信息(备注名 + 5 个备注电话)
out_contacts2 / out_contacts_kv id / key 外部联系人原始数据缓存(data blob)
mail_independent_contacts / _kv id / key 邮件独立联系人

其他:

主键 用途
colleague user_id 同事关系(create_seq/flag)
company_chain / company_group4 id 关联企业/企业互联链信息
corp_remark_member_new_v2 corp_id 企业维度的成员备注
virtual_corp_remark_name_v2 corp_name 虚拟企业备注名
user_dept_tableV2 见上
room_robot robotid 群机器人(roomid/creator/name/head_url/webhook 等)
inner_fwV2 / my_inner_fwV1 userid 内部客服(kf)账号信息
user_source userid 用户来源
user_work_time userid 工作时间(content blob)
user_info_token key 用户信息 token
star_user_table / important_user_table / vip_contact id/userid 星标 / 重要 / VIP 联系人
can_build_index_user_ids / requirement_plaza_contact_userids value 可建索引用户 / 需求广场联系人
half_self_attr_table_v1 id,fieldname 自定义字段过滤配置
wechat_workmate / general_kv key KV 杂项
3.2 company.db --- 企业 / 应用
主键 用途
external_company_table_v2 corpany_id 外部企业详细信息:name/short_name/type/staff_count/logo/认证类型 auth_type/认证执照 auth_licence/认证时间/域名/是否互联网行业/是否海外企业 等约 30 列
external_company_info_table corpany_id 外部企业补充信息(corpany_pb blob)
self_corp_list_table corpany_id 我所属的企业列表(self_corp_info blob)
application_info app_id,business_id,circle_id 工作台应用信息(pb_content blob,app_open_id 索引)
application_kv key 应用相关 KV
hidden_corp_app_info_table key 隐藏的企业应用信息
keyword_intercept_rule_table rule_id 敏感词/关键词拦截规则(rule_ver + rule_data blob)
3.3 crm.db --- 客户 / 教育 / 侧边栏

该库混合了三块功能:客户管理(CRM)教育行业组织(家长/学生/班级)会话右侧栏 & 快捷回复

客户管理:

主键 用途
customer_info_table_v4 customer_id 客户主表:follow_id(跟进人)、create/update/shift_time、predecessor_id(前任跟进人)、relatioin_flag、data_version
customer_tag_info_table_v1 (customer_id,tag_type 索引) 客户标记:备注 remark/desc/mobiles/remark_url/company_remark/follow_id
customer_status id 客户状态
customer_labelV2 id 客户标签定义:name/data_type/label_groupid/label_type/business/service_groupid/sort_order
customer_used_label label_id 已用标签
shared_customer3 id 共享客户(operator_staff_id/sequence)
staff_table staff_id,staff_type 员工(含状态 staff_state)
transfer_tabel transfer_union_id 在职/离职继承转移
crm_fw_info fw_id 客服/服务窗信息(name/url)
crm_service_group_v2 + idx_crm_service_group_index_table* 服务组(含 FTS5 全文索引表组)
frq_crm_service_group_index_table data_id 服务组使用频率
outer_payment_merch_info merch_id 外部收款商户
product_albums / sended_product_albums / product_album_sync_keys pic_id,type 产品图册

快捷回复 (个人版 personal / 企业版 corp,及客户专用 crmcustomer*):

表族 主键 用途
rapid_reply_groups_(corp|personal)_v2 group_id 快捷回复分组
rapid_reply_items_(corp|personal)_v2 reply_id 快捷回复条目(group_id 归属)
rapid_reply_meta_info_v2 bussiness_group_id 元信息
rapid_reply_(corp_)?table rapid_reply_id 旧版快捷回复(content 文本直存)
rapid_reply_sync_keys key 同步游标
crm_customer_rapid_reply_* 同上 客户场景专用的整套快捷回复(结构同上)

教育行业 (家长/学生/班级,多用 _v9 后缀):

主键 用途
party_table_v9 party_id 组织节点(班级/年级,树形 parent_id,pb_content blob)
student_table_v9 student_id 学生(party_id 班级、student_number、staff_id 关联老师)
guardian_table_v9 guardian_id 家长(name/mobile/openid/attribute/xid)
guardian_relation_table_v9 student_id,guardian_id 家长↔学生关系(guardian_type=父/母...)
teacher_pos_table_v9 --- 教师任职(teacher_id/party_id/subject/real_name)
organization_node_table_v9 staff_id,party_id 员工↔组织节点
subject_table --- 学科
homework_draft_table_v2 homework_draft_id 作业草稿

会话右侧栏 & banner:

主键 用途
conversation_right_sidebar_table / _status_table conversation_id 会话右侧栏选中项 / 状态(→ session.conversation_table.id)
*_banner_table*(b2b/personal/common_crop/open_kf/student/right_banner_v5 等十余张) right_banner_id 各场景的运营 banner(item_order + item_pb)
room_notify_draft / room_welcome_list_first_page_cache_table conv_id/key 群通知草稿 / 群欢迎语缓存
labels_kv_table / spm_single_storage key/value KV / 单值存储
3.4 govern.db --- 政务网格
主键 用途
grid_node_table_v4 grid_id 政务版「网格」节点:parent_id(树)、grid_name、grid_type、member_contains_me / admin_contains_me(我是否为成员/管理员)、raw_pb
kv_table / sync_key_table key KV / 同步游标
3.5 session.db --- 会话

核心是 conversation_table(会话)与 conversation_user_table(会话成员),其余为会话维度的各种状态表。

conversation_table --- 会话主表(单聊/群聊统一)

类型 说明
con_numeric_id int PK 自增 会话数字 ID(被 message_lookup 引用)
id text UNIQUE 会话文本 ID(S: 单聊 / R: 群聊)
name text 会话名
create_time / modify_time / create_user_id int 创建/修改时间、创建者 vid
last_message_time / last_message_id int 最后一条消息时间/ID
is_sticked / is_marked / is_blocked / is_collect int 置顶/标记/免打扰/收藏
status / flag / fold_status int 状态、标志位、折叠状态
notice_content / notice_user_id / notice_time blob/int 群公告内容/发布者/时间
customer_room_type / personal_room_type int 客户群 / 个人群类型
from_xid / to_vid / ex_owner int 单聊双方、群主
session_id / roomname_remark / personal_room_avatar_url text 会话 session、群名备注、群头像
room_info_pb / room_welcome_message / school_room_info / type_content / conv_extra_info_pb / local_member_info blob 各类 protobuf 扩展

conversation_user_table / increment_conversation_user_table --- 会话成员

类型 说明
conversation_id, user_id PK(复合) 会话(text)、成员 vid
join_time / join_scene / invite_user_id int 加入时间/场景/邀请人
gag_type / is_admin int 禁言类型 / 是否管理员
as_vid / nick_name / is_classroom_nickname int/text 群昵称等

其他会话状态表(多为 KV/标记型):

主键 用途
conversation_extra_table id 我在该会话的最后发送时间
conversation_member_nickname_table id 自增 群成员昵称(room_id/userid/nickname)
draft_table_1 / conversation_long_article_draft_table conversation_id/key 草稿 / 长文草稿
unread_conversation_table conversation_id 未读游标(begin/current_cursor、unread_count)
need_report_unread_count_conversation_table / unread_confirm_message_table 未读上报 / 确认
at_user_message_table (conversation_id,message_id 索引) @我的消息
top_message / conversation_*notice* key 置顶消息 / 公告尺寸
important_contacts_message_table / new_important_message_table 重要联系人消息
conversation_illegal_table id 违规消息标记
conversation_receipt_mode_* / receipt_mode_count key/conv_id 回执(已读)模式
conversation_mini_program_* / _recently_* key 小程序列表
chain_room_*(4 张) key 互联群(邀请/升级/时间戳/缓存)
crm_customer_conversation_relation2 / inner_customer_conversation_relation / outer_customer_conversation_relationV2 / vip_conversation_relation conversation_numid/key 会话↔客户/客服关系(连接 crm 域)
room_version_2 / need_refresh_room_table / upload_conversation_avatar key/room_id 群版本/刷新/头像上传
conversation_tag_*conversation_confirm_tablemeeting_conv_unread_cntfinancial_chat_infoshield_conversation_unread_flag_tablepull_message_finish_conversationincomplete_conversation_tabledelete_announce_tableconversation_new_notice_tableconversation_undone_payment_message_tablecontrol_message 各异 标签/确认/会议未读/理财会话/屏蔽/拉取状态/草稿等状态位
3.6 message.db --- 消息正文

多张结构几乎相同的消息表,区别在用途(主表 / 客服 / 小表 / 待删 / 撤回)。

message_table (及 message_small_tableneed_deleted_message 结构相同)--- 消息主表

类型 说明
message_id int PK 自增 本地消息 ID
server_id int64 服务端消息 ID
sequence int 会话内序号
sender_id int64 发送者 vid
conversation_id text 所属会话(→ session.conversation_table.id)
content_type int 消息类型(文本/图片/文件/语音/系统消息...)
send_time int 发送时间
flag int 标志位(已读/失败等)
content blob 消息正文 protobuf
extra_content / local_extra_content blob 服务端/本地扩展
devinfo / msg_from_devinfo / from_app_id / client_id int/text 设备来源、应用来源、客户端去重 ID
local_extra_content_translate_info / _time_nlp / _approval_nlp blob 本地 NLP:翻译 / 时间识别 / 审批识别

kf_message_tableV1 --- 客服(KF)消息,比主表多 receive_idkf_id(客服账号)。

消息辅助表:

主键 用途
message_read_state_table / message_small_table_read_state_table / need_deleted_message_read_state_table message_id 已读状态(read_state_pb)
message_client_id message_id 客户端 ID 去重 + 发送失败未通知标志
message_revoke_record_table_v2 con_nid,appinfo 撤回记录
msg_voice2text message_id 语音转文字结果(voice_id/text/collapse)
message_appinfo / original_appinfo / message_hash_appinfo_table / message_appinfo_able msgid 消息 appinfo(用于排序/分段定位)及其哈希
local_need_delete_messages_conversations_2 / need_deleted_messages_conversation / cancle_upload_message_file_table conversation_id/key 待删除/取消上传
conv_read_msg_last_svrid_table key 各会话最后已读 server_id
history_message_section_table / need_pre_download_message / retry_send_item_kv_table / appinfo_updated_messageid_table key/value 历史分段/预下载/重发/更新标记
c2b_payment_status_table / c2b_payment_transferid_table key 支付状态/转账 ID
e2e_pending_svrid_table / e2ee_contact_creds_info_table value/key 端到端加密待处理/凭据
general_kv key KV
3.7 message_lookup.db --- 消息索引

轻量「索引库」,不存正文,用于按会话/发送者快速定位消息(配合 message.db 使用)。

说明
message_lookup_table message_id PK, server_id, con_numeric_id, send_time, sequence 消息 → 会话(数字 ID) 的索引。索引:(con_numeric_id,send_time)、(con_numeric_id,server_id)、server_id
message_sender_lookup_table message_id PK, server_id, con_numeric_id, sender_id, flag 消息 → 发送者索引。索引:(con_numeric_id,sender_id)、server_id

注意:这里用的是 con_numeric_id(int),需经 session.conversation_table 与 message.db 的 conversation_id(text)互转。

3.8 file.db --- 文件
主键 用途
file_table4 origin,message_id,file_index 消息附件元数据:message_type/extension_type、server_id/server_type、name/size/md5、sender_id、conversation_id、collection_id(→ 收藏)、url、receive_time、info_extension(blob)
download_file_point file_id 下载断点(check_point/file_path/last_time)
local_encrypt_file cache_type,cache_key 本地加密文件缓存(status/journal_data)
general_kv key KV

file_table4 索引覆盖 conversation_id / sender_id / server_id / md5 / name / size / receive_time / message_type / extension_type,便于「文件管理」页多维筛选。

3.9 forever_store.db --- 收藏 / 待办 / 公告 / 表情

「永久存储」库------不随消息漫游清理的数据。

收藏:

主键 用途
collectionV50 id 自增 收藏条目:time/server_id/from_type/content_type/app_info/进度/是否本地删除
collection_messageV50 id 自增 收藏的「合并转发/聊天记录」里的子消息(collection_id 归属、消息内容、发送者中英文名、会话名)
local_file_connection id 自增 收藏↔本地文件(collection_id/fileid/md5)
p2p_collection_info_v1 (msg_id,collection_id) 点对点收藏信息

待办(todo,有 4 套:当前/旧/new_todo_id/follower):

主键 用途
todo_v4 / msg_todo_v4 / old_todo_v4 id 待办:content/status/creator_id/message_server_id/remind_timestamp/create_time/todo_rawpb 等
todo_follower_v4 / msg_todo_follower_v4 / old_todo_follower_v4 todo_id,follower_id 待办关注人
new_todo_id value 待办 ID 游标

公告:

主键 用途
announce_table id 群公告:time/subject/summary/sender/is_read/attachment_count/image_url/url/status
announce_to_msgsvrid announceid 自增 公告↔消息 server_id

表情 & 其他:

主键 用途
emotion_group_info local_group_id 自增 自定义表情分组(group_id/icon)
emotion_info --- 表情明细(file_id/aes_key/url/md5/尺寸/封面)
text_emotion_background_group groupid 文字表情背景
conversation_tab_pb_table id 会话标签页定义(name/order/tag_pb)
conversation_tag_relationship_table_v9 conversation_id,id 会话↔标签页关系
3.10 tencent_doc_list.db --- 腾讯/微信文档
主键 用途
tencent_doc_my_create_list_v11 id,doc_platform_type 我创建的文档列表:title、type(0文档/1表格/2收集表)、creator_id、modify/open_time、share_code、doc_mail_url、desc、doc_item_pb
tencent_doc_recent_view_list_v11 id,doc_platform_type 最近浏览文档列表(结构同上)
tencent_doc_prepare_fast_jump_info_v4 pre_fast_id 快速跳转预备信息(doc_url/cookie/expire_time/is_sharecode)
tencent_doc_js_write_data_cache_v4 js_data_key,doc_platform_type JS 写入数据缓存
wedoc_open_id_v1 wwid,openid 文档场景 wwid↔openid 映射
3.11 kv.db --- 通用 KV / 探针
主键 用途
general_kv key 全局 KV(如各种升级标记 HasUpgraded*
wcprobe_report_table_1 buffer_id 性能探针上报缓存:scene/begin_time/end_time/cost/error_type/launch_time/buffer 等

四、跨库 ER 图(按类别)

SQLite 中未声明物理外键,下列关系为应用层 JOIN 语义。

4.1 组织与人员域

涉及 user.dbcompany.dbcrm.db(教育)、govern.db。以「用户 vid」「部门/组织节点」「企业」为中心。

4.2 会话与消息域

涉及 session.dbmessage.dbmessage_lookup.dbfile.dbforever_store.db。核心枢纽是 conversation_table(同时持有 text id 与 int con_numeric_id)。

4.3 客户与 CRM 域

涉及 crm.db 与跨库的 user.db(外部用户)、session.db(会话关系)。

4.4 文件 / 收藏 / 待办域

涉及 file.dbforever_store.db,并回连 message.dbsession.db

4.5 文档域

涉及 tencent_doc_list.db,以「文档 id」为中心,creator_id 回连用户。

五、巧妙的设计

1. 按业务域物理分库

做法 :不是一个大 .db 装下所有表,而是拆成 11 个库:消息、会话、通讯录、企业、客户、文件、收藏、文档......

为什么巧妙 :SQLite 的并发粒度是「整个数据库文件 」级别的写锁。把高频写的消息库(message.db)和低频写的通讯录库(user.db)放在一起,收一条消息就会和「刷新通讯录」抢同一把锁。物理分库后:

  • 各库有独立的写锁------收消息不阻塞通讯录同步,互不干扰;
  • 各库有独立的 page cache------扫消息不会把通讯录的热页挤出缓存;
  • 损坏隔离------某个库文件损坏不会带走全部数据;
  • 可独立备份/迁移/清理 ------见 forever_store.db(收藏/待办/公告/表情)单独成库,正是因为它们「不随消息清理而删除」,物理隔离后清理消息时根本不需要碰这个库。

证据:11 个 .db 文件按域划分;forever_store(永久存储)这个命名本身就点明了「与可清理数据隔离」的意图。


2. 胖表瘦索引:消息正文与索引分库

做法message.db 存消息正文(含 contentextra_content 等 blob),另起一个 message_lookup.db,只放两张纯标量的窄表:

复制代码
复制代码隐藏代码`message_lookup_table(message_id, server_id, con_numeric_id, send_time, sequence)
message_sender_lookup_table(message_id, server_id, con_numeric_id, sender_id, flag)`

为什么巧妙 :IM 最高频的操作是「按会话翻页 」和「按发送者筛选 」。如果直接在胖消息表上建 (conversation_id, send_time) 索引,每次翻页虽然走索引,但回表读到的行带着巨大的 blob,page cache 被正文撑爆。

拆出的 lookup 表每行只有几十字节,索引为 (con_numeric_id, send_time) / (con_numeric_id, server_id) / (con_numeric_id, sender_id)

  • 翻页/定位先在小表 里二分定位,拿到 message_id 再去正文表精确点查;
  • 小表整张都能常驻内存,正文按需加载;
  • 物理分库进一步保证「翻页扫索引」与「写入新消息正文」不互相锁。

这是经典的covering index / 冷热分离思路做到了物理分库级别。

证据:message_lookup.db 仅 2 表 5 列且全标量;索引全部以 con_numeric_id 打头。


3. 双 ID:文本主键 + 数字主键

做法:每个会话同时拥有两个 ID------

  • conversation_table.id:文本,如 S:1688850851994195_1688852792312821(业务语义、跨设备稳定);
  • conversation_table.con_numeric_id:本地自增整数。

正文库用文本 conversation_id,索引库用整数 con_numeric_id

为什么巧妙

  • 整数索引又小又快message_lookup 里每条消息都要存一份会话引用,用 int 比存几十字节的文本主键,索引体积和比较成本都大幅下降------而消息表动辄百万行,这个差距被放大百万倍。
  • 文本 ID 承载语义且跨端一致S:/R: 前缀、双方 vid 直接编码在串里,换设备登录也不变;
  • 两者在 conversation_table 里一一映射,需要时一次 JOIN 互转。

用「对内整数、对外文本」的双 ID,兼得了索引效率语义稳定

证据:conversation_table 同时声明 con_numeric_id INTEGER PRIMARY KEY AUTOINCREMENTid TEXT UNIQUE


4. 标量列 + protobuf blob 混合存储

做法 :几乎每张业务主表都是「几个抽出来的标量列 + 一个 protobuf blob 」。例如 user_table 抽出 name/mobile/corp_id... 但保留 pb_contentparty_table_v9 抽出 party_name/parent_id 但保留 pb_content;快捷回复表只留 group_id 加 raw_pb

为什么巧妙 :这是「关系型的查询能力 」与「文档型的演进灵活性」的折中:

  • 抽出的标量列正是用于索引、排序、JOIN 的那几个字段,保证查询性能;
  • 其余字段全塞进 blob------加字段不需要 ALTER TABLE ,改一下 .proto 定义即可,老客户端读到不认识的字段直接忽略。对一个要持续迭代、还要兼容多版本客户端的产品,这避免了无穷无尽的库迁移;
  • 协议复用:同一份 protobuf 既走网络传输又落本地盘,序列化/反序列化代码一套。

代价是 blob 内容对 SQL 不透明(不能 WHERE blob 里的字段),但凡是需要查询的字段都已经被抽成列了,所以这个代价被刻意规避掉了。

证据:列名模式 pb_content / *_pb / raw_pb / content / item_pb / doc_item_pb 遍布所有库。


5. 用「表」做生命周期分区与软删除

做法message.db 里有多张结构完全相同 的消息表:message_table(主)、message_small_tableneed_deleted_message(待删除);待办也有 todo_v4 / old_todo_v4 / msg_todo_v4 三套同构表。

为什么巧妙 :朴素做法是在主表加一个 status/is_deleted 列然后 WHERE status=... 过滤。但在百万行大表上:

  • 软删除标记位会让主索引被已删除行污染,每次查询都要过滤掉它们;
  • 删除/恢复要更新大表的行(连带 blob 重写)。

把不同生命周期的数据搬到不同的表

  • 主表 message_table 永远只装"活跃"消息,索引干净、查询不带过滤条件;
  • need_deleted_message 就是回收站,删除 = 把行移动过去,主表立刻变小;
  • old_todo_v4 保留迁移前的旧待办,新逻辑只读 todo_v4,需要时再回看旧表。

用「表」而非「列」做分区,让最热的主表始终保持精简。

证据:need_deleted_messagemessage_table 列定义逐字相同;todo_v4/old_todo_v4 同构并存。


6. 已读状态单独成表:对抗写放大

做法 :消息的已读回执不放在消息行里,而是单独一张 message_read_state_table(message_id, read_state_pb),三类消息表各配一张(主表 / small / 待删)。

为什么巧妙 :群里一条消息,每多一个人读,已读状态就要更新一次。若已读状态是消息行的一个字段,更新它就要重写整行 ------而消息行带着 content 大 blob,重写一次几 KB 起步,几百人的群一条消息就是几百次大 blob 重写,写放大惊人。

拆成独立小表后,更新已读只重写 (message_id, read_state_pb) 这么一条窄记录,正文 blob 一个字节都不动。这是针对「高频更新的小字段 vs 低频更新的大字段」的标准拆分。

证据:message_read_state_table / message_small_table_read_state_table / need_deleted_message_read_state_table 三张专表,列只有 message_id + read_state_pb。


7. 主键里编码类型:会话 ID 前缀

做法 :会话文本 ID 用前缀编码类型------S: 单聊、R: 群聊,单聊串里还直接拼了双方 vid(S:vidA_vidB)。单聊和群聊统一存在同一张 conversation_table

为什么巧妙

  • 省一个类型列、省一次判断:从 ID 本身就能 O(1) 看出会话类型,日志/调试时一眼可读;
  • 单聊 ID 可由双方推导S:vidA_vidB 让客户端无需查库就能算出与某人单聊的会话 ID,秒开会话;
  • 多态统一表:单聊/群聊共用一张表和一套消息链路,业务代码不必分叉。

把"少量枚举型元信息"编码进主键字符串,是一种轻量的多态设计。

证据:样本 S:1688850851994195_1688852792312821R:10707630871388967;message_table 中 R: 占多数、S: 少量。


8. 无处不在的 general_kv:schema 的逃生舱

做法user.dbfile.dbmessage.dbkv.db 等几乎每个库都自带一张 general_kv(key TEXT PK, value)

为什么巧妙 :一个成熟客户端有成百上千个零散状态:「是否已执行过某次升级」(HasUpgradedCustomerConversationUnreadTuples)、「AI 机器人 ID 列表」(kAIBotUserIdListKey)、各种功能开关和一次性标记。

为每一个都建表/加列既臃肿又频繁触发迁移。general_kv 提供了一个schema-less 的扩展点

  • 新增一个标记 = 写一个新 key,零 DDL、零迁移
  • 每个库各自带一张,让 KV 落在与其语义相关的库里(升级标记跟着对应业务库走),而不是全塞进一个全局 KV 造成锁竞争。

这是在强 schema 的关系库里,给"不值得建表的小数据"留的一条逃生通道。

证据:多库均有 general_kv;样本 key HasUpgradedCustomerConversationUnreadTupleskAIBotUserIdListKey


9. 增量同步:sync_key 游标与增量表

做法 :多处出现 sync_key_table / *_sync_keys(如 rapid_reply_sync_keyscrm_customer_rapid_reply_sync_keysproduct_album_sync_keys),以及成对的全量表 + 增量表(conversation_user_table vs increment_conversation_user_table)。

为什么巧妙 :客户端不可能每次启动都全量拉取通讯录/会话/快捷回复。增量同步协议需要在本地持久化一个「我同步到哪了」的游标:

  • sync_key 存服务端给的游标,下次只请求「比这个游标更新的变更」,省流量、省时间;
  • 增量表先承接本次拉到的变更,再合并进全量表------合并过程出错也不会污染全量数据,具备一定的事务/回滚友好性。

这是把「服务端增量推送」落地到本地的标准骨架。

证据:govern.dbsync_key_table;crm 的多张 *_sync_keys;session 的 increment_conversation_user_table


10. 版本后缀 + IF NOT EXISTS:可灰度的 schema 演进

做法 :表名普遍带版本号------customer_info_table_v4party_table_v9tencent_doc_my_create_list_v11collectionV50;建表语句一律 CREATE TABLE IF NOT EXISTS

为什么巧妙 :当一张表的结构需要重大改动(不是简单加列),与其原地 ALTER 冒险迁移,不如:

  • 建一张新版本表_v11),新代码写新表,必要时从旧表迁移数据;
  • 旧表可保留一段时间用于回滚懒迁移(用户用到时才搬);
  • IF NOT EXISTS 让初始化幂等------无论全新安装还是升级覆盖,跑同一段建表脚本都安全,不会因表已存在而报错。

版本号 + 幂等建表,让 schema 演进可灰度、可回滚,而不是一次性的"惊险大迁移"。

证据:_v2/_v4/_v9/_v11/V50 后缀广泛存在;所有 CREATE TABLE IF NOT EXISTS


11. 左孩子右兄弟:部门树的双表示

做法:部门同时有两种表示------

  • department_tableV2.parent_id:每个部门指向父节点(自底向上);
  • dept_tree_table(id, first_child_id, next_sibling_id)左孩子右兄弟链表(自顶向下)。

为什么巧妙:两种遍历方向各有最优结构:

  • 要「展开某部门下的子部门 」(组织架构树 UI 最常见操作):用 parent_idWHERE parent_id=? 再排序;而 first_child_id + next_sibling_id顺着链表一路拿到全部直接子节点,天然有序、无需排序、无需扫描,O(子节点数);
  • 要「往上找祖先 」:用 parent_id 逐级上跳。

用一份冗余的「孩子-兄弟」链表换取展开操作的高效,是 N 叉树存储的经典技巧。

证据:dept_tree_table(first_child_id, next_sibling_id)department_tableV2.parent_id 并存。


12. client_id 去重:弱网下的幂等

做法 :消息表带 client_id,并专设 message_client_id(message_id, client_id, send_failed_unnotified) 表,client_id 建索引。

为什么巧妙:弱网下「发消息」极易出现:客户端发出 → 网络超时未收到 ACK → 重试 → 实际上服务端两条都收了。若无去重,用户会看到重复消息。

client_id客户端在发送前生成的唯一标识

  • 服务端/客户端以 client_id幂等键,重试不会产生重复;
  • send_failed_unnotified 标记「发送失败但还没提示用户」,支撑失败重发与 UI 提示的解耦。

这是分布式系统里「生产者侧幂等」在 IM 客户端的落地。

证据:message_client_id 表 + client_id_index_ 索引;多张消息表均含 client_id 列。


13. 本地 vs 服务端扩展字段分离 + NLP 结果缓存

做法 :消息表把扩展内容分成 extra_content(服务端下发)与 local_extra_content(本地生成),并进一步细分出三列本地 NLP 结果:

复制代码
复制代码隐藏代码`local_extra_content_translate_info   `-- 翻译`
local_extra_content_time_nlp         `-- 时间识别("明天下午3点"→日程)`
local_extra_content_approval_nlp     `-- 审批识别

为什么巧妙

  • 来源分离:服务端扩展与本地计算结果各占一列,同步时服务端数据不会覆盖本地算出来的东西,反之亦然;
  • 缓存昂贵计算 :翻译、时间抽取、审批意图识别都不便宜(可能要调模型/服务)。把结果落库缓存,同一条消息只算一次,重开会话直接读缓存,避免每次渲染都重算。

把「计算结果」也当作一等数据持久化,是用存储换计算的典型权衡。

证据:message_table 末尾三列 local_extra_content_translate_info / _time_nlp / _approval_nlp;另有 msg_voice2text 缓存语音转文字。


14. 复用 SQLite 原生 FTS5 做全文搜索

做法crm.db 里给「服务组」建了全文索引,出现一组影子表:

复制代码
复制代码隐藏代码idx_crm_service_group_index_table`        (fts5 虚拟表, tokenize=fts5word)
idx_crm_service_group_index_table_config
idx_crm_service_group_index_table_content
idx_crm_service_group_index_table_data
idx_crm_service_group_index_table_docsize
idx_crm_service_group_index_table_idx`

为什么巧妙 :搜索需求(按关键词找客户/服务组)不必引入额外的搜索引擎或自己写分词倒排------直接用 SQLite 内置的 FTS5 扩展。那一组 _config/_content/_data/_docsize/_idx 是 FTS5 自动维护的内部表,tokenize=fts5word 还指定了适配中文场景的分词器。

零额外依赖地拿到全文检索能力,是"把数据库已有能力吃干榨净"的体现。

证据:... using fts5(content, ..., tokenize=fts5word) 及其 5 张配套影子表。


15. 收藏的二级结构与去重复用

做法 :收藏在 forever_store.db 里是两级结构------

  • collectionV50:一条收藏(可以是单条消息,也可以是一整段「合并转发的聊天记录」);
  • collection_messageV50:该段聊天记录里的每条子消息collection_id 归属,含发送者中英文名、会话名快照);
  • local_file_connection(collection_id, fileid, md5):把收藏与本地已下载文件关联。

为什么巧妙

  • 支持复合收藏:一条收藏天然可以是「一串聊天记录」,靠主从两表表达"一对多",而不是把整段记录塞进一个字段;
  • 快照冗余:子消息存了当时的发送者名、会话名------即便原会话/成员后来变了,收藏里看到的仍是收藏那一刻的样子(收藏本就该是"凝固的历史");
  • 文件去重复用local_file_connectionmd5 + fileid 把收藏指向已下载的本地文件,避免「收藏一个文件就重新下一份」。

证据:collectionV50 + collection_messageV50(带 chiness_name/english_name/conversation_name 快照列)+ local_file_connection(md5)


总结:贯穿全局的几条原则

把上面 15 点抽象一下,能看到几条反复出现的设计哲学:

原则 体现
冷热/读写分离 分库(§1)、正文与索引分库(§2)、已读状态拆表(§6)、热小字段独立(§13)
对内高效、对外稳定 双 ID(§3)、整数索引 + 文本语义(§3)、主键编码类型(§7)
关系型 × 文档型折中 标量列 + protobuf blob(§4)------可查的抽成列,可变的塞进 blob
用结构换性能 表分区做软删除(§5)、左孩子右兄弟树(§11)、计算结果落库(§13)
为演进而设计 版本后缀 + 幂等建表(§10)、KV 逃生舱(§8)、增量同步游标(§9)
分布式正确性 client_id 幂等去重(§12)、增量表合并(§9)
吃干榨净已有能力 复用 SQLite FTS5(§14)、一套 protobuf 走网络又落盘(§4)

核心 takeaway:不要让一张大表既要查得快、又要改得勤、还要长得稳。这套 schema 反复在做的,就是把互相冲突的诉求拆到不同的表、不同的库、不同的字段里,让每一部分都能针对自己的访问模式做到最优。

相关推荐
tianxiaxue115 小时前
企微客服与客户对话分析进行回复时效统计?
企业微信
运维行者_21 小时前
Applications Manager中的Redis监控
大数据·服务器·数据库·人工智能·网络协议
悦数图数据库1 天前
图数据库选型指南 2026:从架构、性能、AI 适配三个维度看 悦数科技
数据库·人工智能·架构
handler011 天前
【MySQL】常用命令总结(库与表增删查改)
运维·数据库·mysql·命令·总结
week@eight1 天前
Linux - Doris
linux·运维·数据库·mysql
cdbqss11 天前
VB2026 菜单生成基类 BqGetMenuStrip
数据库·经验分享·学习·oracle·vb
洛水水1 天前
Redis 分布式锁详解:实现与缺陷
数据库·redis·分布式
韶博雅1 天前
oracle中表和列转大写
数据库·oracle