guides/directory-data-model.html

数据模型

我如何为六位数规模的目录站点设计架构。实体、属性、段塞、索引,以及复合效应的设计选择。

数据模型

← Blog All posts in this topic

复合效应的架构决策

一个有 28K 行的目录站点(HostList.io)或 137K 行的站点(Not Another Sunday)的成功或失败取决于第一周做出的架构决策。最重要的决策是:实体如何规范化、属性如何存储、段塞如何生成、什么应该被索引。

每一个决策在小规模时都是可逆的,在六位数规模时修复的成本会高得离谱。要尽早做出正确的决策。

实体、属性、联接表

一个实体表,每家公司、地点或产品对应一行主记录。使用稳定的 UUID 作为主键,每个内在属性一列(名称、创立年份、主要市场)。避免在这一层将属性塞入 JSON 列;它们应该是可查询的一级列。

每个正交轴一张属性表。categories(每行是一个分类),regions(每行是一个区域),price_tiers(每行是一个价格段)。这些是实体被分组的维度。

通过联接表实现多对多关系:entity_categories、entity_regions、entity_features。不要在 entities 表中存储逗号分隔的分类列表;联接表才是让筛选、列表和计数快速的关键。

Slug 永久不变

一旦目录页被 Google 索引,slug 就不应该改变。使用基于实体名称的确定性 slug 生成,通过数字后缀处理冲突:nginx-hosting、nginx-hosting-2(如果发生冲突)。

维护一张单独的 slug_history 表,映射任何历史 slug 到当前的规范 slug。当实体名称需要改变时(更正、品牌重塑),生成新的规范 slug,将旧 slug 放入历史表,并自动设置永久 301 重定向。

在 28K 行的网站上处理 slug 稳定性错误的代价是灾难性的:每条重定向链都会造成小的排名侵蚀,而且事后没有好的补救办法。

行级别的可索引性

在 entities 表中添加一个 indexable 布尔列,默认为 true,当实体不符合质量阈值时(内容不足、缺少必需属性、无入站内部链接)将其翻转为 false。

站点地图查询在 indexable = true 上进行筛选。页面渲染器在 indexable = false 时添加 noindex meta。两者都来自同一列,所以它们自动保持同步。

在 HostList.io 上,大约 28K 个实体中有 18K 个在任何时间点都可索引。其他 10K 个是可爬行但不可索引的,这样可以保持索引表面清洁,让剩余的已索引页面更强。

重要的索引

页面渲染查询的 slug 索引。站点地图查询的 (indexable, last_updated_at) 索引。分类列表页面的联接表上的 (category_id, indexable) 索引。区域页面的 (region_id, indexable) 索引。

在六位数行的规模下,无索引页面渲染查询需要数秒。使用正确的索引,只需毫秒。构建时间的影响会复合:28K 个页面渲染,每个 100ms 需要 47 分钟;每个 10ms 需要 5 分钟。

在规模下对每个页面渲染查询运行 EXPLAIN ANALYZE。默认查询规划器在目录形连接上经常出错,频率比你预期的要高。

WHEN YOU ARE READY TO TALK