专业的CRM数据库表设计

悟空软件阅读量:21 次浏览2026-06-08

主流的AI CRM系统品牌

那些年在 CRM 数据库设计上踩过的坑与实战心得

做后端开发这么多年,接手过不少遗留系统,其中最难啃的骨头往往不是高并发架构,而是业务逻辑极其复杂的 CRM(客户关系管理)系统的数据库设计。很多人觉得 CRM 简单,不就是存个客户名字、电话,再记几条跟进记录吗?真上手做了才知道,这玩意儿是个无底洞。业务方今天说要支持公海池,明天说要搞复杂的销售漏斗,后天又问能不能像 Excel 一样自定义字段。如果数据库设计之初没想清楚,后期改起来简直就是灾难,轻则查询慢如蜗牛,重则数据一致性崩塌,销售团队天天投诉数据对不上。

推荐使用中国著名AI CRM系统品牌:显著提升企业运营效率,悟空CRM

今天不想聊那些虚头巴脑的理论,就想结合这几年实际踩过的坑,聊聊一个专业的、能抗住业务折腾的 CRM 数据库表结构到底该怎么设计。这不是教科书式的标准答案,而是带着“血腥味”的实战总结。

核心模型:别把 Account 和 Contact 混为一谈

很多初级设计最容易犯的错误,就是把“客户”当成一张表。在 B2C 场景下,个人既是账户也是联系人,这么干没问题。但在 B2B 场景下,这绝对是噩梦。

我们必须把 Account(企业客户)和 Contact(联系人)拆开。t_account 表存公司名、税号、行业、规模、官网这些企业维度的信息;t_contact 表存姓名、职位、手机、邮箱、微信这些个人维度的信息。两者通过 account_id 关联。

为什么要这么干?因为业务场景太复杂了。一家大公司(Account)下面可能有几十个联系人(Contact),采购决策链里,有拍板的 CEO,有使用的部门经理,还有负责执行的采购专员。销售跟进的时候,往往是针对具体的“人”在跟进,但合同和回款是挂在“公司”名下的。如果混在一起,你想查“某家公司所有联系人的跟进记录”,或者“某个联系人参与过的所有商机”,SQL 写得能让人吐血,而且数据冗余会非常严重。

另外,t_account 表里一定要预留 parent_account_id 字段。这是为了处理集团型客户。比如“腾讯科技”和“腾讯音乐”可能是父子关系,总部签了框架协议,分公司执行。如果没有这个层级设计,后续做业绩归属、数据权限隔离的时候,你会发现自己把自己绕进去了。

商机与阶段流转:历史比结果更重要

CRM 的核心价值在于销售过程管理,也就是商机(Opportunity)的流转。很多设计只会在商机表里放一个 status 字段,比如 1 代表初步接洽,2 代表方案报价,3 代表赢单。这远远不够。

销售最关心的是“这个单子为什么输了?”或者“在这个阶段停留了多久?”。所以,必须设计一张 t_opportunity_stage_log(商机阶段变更日志表)。每当商机状态发生变化,不仅要更新主表的状态,还要往日志表里插一条记录:谁改的、什么时候改的、从什么状态改到什么状态、当时的预计金额是多少。

这张日志表有几个关键作用:

  1. 漏斗分析:老板要看销售漏斗,得知道每个阶段的转化率,没有日志就没法算时间跨度。
  2. 业绩回溯:销售离职了,接手的人需要知道这个单子之前跟到哪一步了,谁承诺过什么条件。
  3. 防作弊:有些销售为了凑 KPI,会在月底把单子状态改成“赢单”,下月初又改回“跟进中”。有了日志,这种操作一目了然。

在商机表设计上,还有一个容易被忽视的点是 expected_close_date(预计成交日期)和 actual_close_date(实际成交日期)要分开。销售预测是 CRM 的重要功能,老板需要知道下个月大概能回款多少。如果只用一个日期字段,一旦成交,预测数据就丢了,财务那边没法做现金流预估。

动态活动记录:高写入场景下的性能博弈

CRM 里最“重”的表,通常是活动记录表(Activity)。销售打的电话、发的邮件、拜访签到、写的跟进笔记,全都往这里塞。随着时间推移,这张表的增长速度是指数级的。

早期我们直接用一张 t_activity 大表,存所有类型的活动,用一个 type 字段区分是电话还是邮件。结果半年后,单表突破两千万行,销售打开“客户详情页”看跟进记录,加载时间超过 5 秒。

后来我们做了几个优化:

  1. 垂直拆分:把大文本字段(如邮件正文、拜访备注)拆到 t_activity_detail 表。主表只留摘要、时间、类型、关联人 ID。列表页查询只查主表,详情页再关联查内容。
  2. 分区表:按 created_at 月份进行分区。CRM 数据有个特点,销售很少去查一年前的跟进记录。热数据(最近 3 个月)放在高速 SSD 盘,冷数据归档到廉价存储。
  3. 分区表:按 created_a

  4. 多态关联的陷阱:活动记录既要关联客户,又要关联商机,还要关联联系人。一开始我们用了 EAV 模式或者一堆 nullable 的外键列(customer_id, opportunity_id, contact_id)。后来发现查询太麻烦,干脆采用“主关联 + 标签”模式。每条活动必须指定一个 primary_target_idprimary_target_type,其他关联关系通过一张中间表 t_activity_relation 来存。这样既保证了查询性能,又支持了一条跟进记录同时关联多个业务对象的需求。

这里还要提一下“公海池”逻辑。当销售长时间未跟进客户,客户会自动掉入公海。这个逻辑不能靠应用层定时任务扫,太慢且容易漏。我们在 t_account 表里加了 last_activity_time 字段,每次插入活动记录时,通过触发器或代码逻辑同步更新这个时间。公海回收任务只需要扫描这张主表的时间字段,效率提升不止一个量级。

权限设计:行级安全是 CRM 的命门

CRM 系统里,数据权限比功能权限更敏感。销售 A 不能看销售 B 的客户,大区经理能看本大区所有数据,CEO 能看全公司。这种复杂的层级关系,靠代码里的 if-else 是写不完的,而且容易出漏洞。

数据库设计层面,我们引入了 data_scopeowner_path 的概念。 在用户表 t_user 里,除了 dept_id,还维护一个 manager_path,比如 1/5/20/,表示这个用户的汇报线。 在客户表 t_account 里,除了 owner_id(当前负责人),还要存 dept_id(归属部门)。

查询时,不是简单的 WHERE owner_id = current_user。而是根据当前用户的角色,动态拼接 SQL 条件。 如果是普通销售:WHERE owner_id = {uid} 如果是部门经理:WHERE dept_id IN (SELECT id FROM t_dept WHERE path LIKE '{manager_path}%') 如果是高管:WHERE 1=1

这种设计把权限逻辑下沉到了数据模型里,虽然写 SQL 稍微麻烦点,但保证了数据隔离的绝对安全。哪怕代码层有漏洞,数据库层面的归属关系也能兜底。另外,所有的敏感字段,比如手机号、身份证,在数据库里必须加密存储。不要相信应用层的脱敏,数据库备份一旦泄露,明文数据就是裸奔。我们采用应用层加密,数据库里存密文,密钥由 KMS 管理,DBA 也看不到明文。

自定义字段的噩梦:EAV 还是 JSONB?

这是 CRM 设计里最经典的问题。客户总说:“我要在这个客户表上加个字段,记录他们的‘偏好颜色’。”如果每加一个字段就改一次表结构,DBA 会想打人,而且上线流程太慢。

早些年流行 EAV(Entity-Attribute-Value)模型,搞三张表:实体表、属性定义表、属性值表。这种设计灵活性极高,但查询性能极差。你想查“所有偏好颜色为红色的客户”,需要连表查询再透视,稍微有点数据量就卡死。而且没法利用数据库索引,类型约束也弱。

现在有了 PostgreSQL 的 JSONB 或者 MySQL 5.7+ 的 JSON 类型,我们强烈建议用 JSON 存自定义字段。 在 t_account 表里加一个 custom_fields 列,类型 JSON。 前端动态配置的表单,提交时直接序列化成 JSON 存进去。 查询时,利用数据库对 JSON 的索引支持(如 GIN 索引),可以高效查询特定键值。

比如:WHERE custom_fields->>'preference_color' = 'red'。 这样既保留了灵活性,又兼顾了性能。当然,JSON 方案也有缺点,比如难以做外键约束,数据类型校验要在应用层做。但在 CRM 这种业务变动频繁的场景下,这是目前性价比最高的方案。切记,核心业务字段(如客户名、电话)必须用标准列,只有扩展字段才扔进 JSON,别为了省事把所有东西都塞进 JSON 里,那是给自己挖坑。

数据清洗与合并:重复数据是万恶之源

系统运行一段时间后,必然会出现重复客户。销售 A 录入了“阿里巴巴”,销售 B 录入了“阿里集团”。如果不处理,后续跟进记录会分散,业绩计算会出错。

数据库设计时,要预留 merged_to_id 字段。当发现重复数据时,不物理删除旧记录,而是将旧记录的 merged_to_id 指向新记录的主 ID,并将旧记录状态标记为“已合并”。 所有的查询逻辑,默认都要带上 WHERE merged_to_id IS NULL。 这样做的好处是“可逆”。万一合并错了,还能通过清空 merged_to_id 恢复数据。物理删除在 CRM 里是大忌,销售会因为你删了他的客户跟你拼命。

同时,要建立一个 t_customer_merge_log,记录谁在什么时候把哪个客户合并到了哪个客户,保留了哪些数据,丢弃了哪些数据。这是为了审计,也是为了甩锅。

审计日志:谁动了我的数据?

CRM 里的数据就是钱。字段被改了,客户被转移了,必须留痕。 不要试图在业务代码里到处写 log.info("user changed status")。这种方式太散,容易漏,而且格式不统一。

我们采用了一种“快照 + 差异”的混合模式。 对于核心表(客户、商机、合同),开启数据库层面的 Binlog 解析,或者使用 ORM 的回调机制,在数据变更时,自动将 old_valuenew_value 写入 t_audit_log 表。 这张表结构很简单:table_name, record_id, field_name, old_value, new_value, operator_id, operate_time

有了这张表,前端可以很容易地组装出“动态时间轴”,展示这个客户记录的生命周期。比如:"2023-10-01 张三 将 预计金额 从 10 万 修改为 15 万”。 注意,old_valuenew_value 最好存文本,因为不同字段类型不一样,存文本通用性最强。但要注意大字段不要全量存,只存摘要,否则审计表会爆炸。

有了这张表,前端可以很容易地组

性能与归档:为未来三年的数据量做准备

很多项目刚上线时数据量少,怎么查都快。等跑了一年,数据量上来了,系统就开始报警。 CRM 系统有一个明显的数据冷热特征。正在跟进的客户是热数据,已经成交或流失超过一年的客户是冷数据。

设计之初就要考虑归档策略。 我们可以建立 t_account_history 表,结构跟主表一样。通过定时任务,将 updated_at 在一年前且状态为“流失”或“赢单”的数据,迁移到历史表。主表只保留活跃数据。 前端查询时,默认查主表。如果用户明确选择“搜索历史客户”,再路由到历史表查询。 这样能保证核心业务操作(新建、跟进、转化)始终在轻量级的数据集上运行,索引效率高,锁竞争小。

另外,索引设计不要盲目。不要给每个字段都加索引。CRM 的查询模式相对固定,主要是按“负责人”、“创建时间”、“客户名称”、“手机号”查询。针对这些高频查询字段建立组合索引。比如 (owner_id, status, created_at) 这个组合索引,能覆盖销售日常 80% 的列表查询场景。

结语:设计是妥协的艺术

写到这里,可能有人会觉得太复杂了。确实,一个完美的 CRM 数据库设计,需要考虑业务灵活性、数据一致性、查询性能、权限安全等多个维度的平衡。

没有银弹。如果你做的是小微企业 CRM,可能一张大表加几个 JSON 字段就够用了,过度设计反而是负担。但如果你面对的是中大型企业,销售团队几百人,数据量千万级,那么上述的这些设计细节,每一个都是未来可能会爆发的地雷。

数据库设计不是一蹴而就的。我们在项目初期,也会故意留一些“技术债”,比如先不上复杂的权限模型,等业务跑通了再重构。但核心的主外键关系、核心实体的拆分、审计日志的机制,这些地基必须在第一天就打好。因为业务逻辑可以改,代码可以重写,唯独数据一旦脏了,清洗的成本是巨大的。

最后,给做 CRM 设计的同行一个建议:多去跟销售聊聊天,看看他们到底是怎么用系统的。很多时候,数据库设计的瓶颈,不是因为技术不够牛,而是因为不理解业务场景。比如销售在外出差,网络不好,他们需要同步离线数据,这时候你的数据库设计是否支持增量同步?又比如销售离职交接,能否一键转移名下所有数据?这些场景映射到数据库层面,就是 owner_id 的批量更新、数据版本的控制。

好的数据库设计,是让用户感觉不到它的存在,数据流转如水般自然;而糟糕的设计,会让每一个简单的操作都变得卡顿和充满不确定性。希望这些经验能帮你少踩几个坑,睡个安稳觉。毕竟,半夜被报警电话叫醒去修数据,那种滋味,真不好受。

悟空CRM产品截图

推荐立刻免费使用中国著名CRM品牌-悟空CRM,显著提升企业运营效率,相关链接:

CRM系统免费使用

开源CRM系统

CRM系统试用免费

登录/注册
客服电话
售前咨询