hstore:在一格里装键值映射
当模型不断长出松散的属性时,hstore 把答案放进同一行,而不是越连越长的关联查询链。每个产品迟早都会长出当初没人预料到的配置:按租户的开关、对接第三方的令牌、展示偏好、导入时顺手带进来的残留字段。每个属性都加一列,意味着无休止的迁移;每个属性都建一张表,意味着为鸡毛蒜皮反复关联。hstore 给了中间道路——用一列装下扁平的字符串映射,专门收留那些数量多、可选、单个都不重要的属性。 这笔交易条件写得很明白:拿类型安全和约束能力,换建模的敏捷。键和值都是文本,没有嵌套结构,数据库几乎无法对内容做什么强制。真正松散的元数据配这笔交易稳赚,需要参与业务推理的数据则稳亏。每个属性落在哪一边,分清楚就是全部的手艺。 一种类型、一列搞定 启用扩展后,一个 hstore 列在一个单元格里就能存许多键值对,比额外建表去镜像结构简单得多。用扩展启用语句打开功能,再声明该列类型即可。写入的字面量很自然,更新用拼接与删除操作符只动单个键,不必手工重写整个映射。多数驱动会把这一列直接读成字典,往返不需要专门的解析层。 从迁移成本看,这是最便宜的灵活性:一个扩展、一列、零新表,存量行不需要回填——缺席的键就是不存在,而不是以空值占位。下个季度冒出新属性,连结构变更语句都不用跑,写入端直接开始带这个键。只凭这一条,hstore 就配得上那些变化快、属性集不定型的场景,否则替代方案就是每个迭代一次迁移。 用操作符查询 用 hstore 的操作符做存在性与包含判断,并用通用倒排索引托底,让这些查询在映射变大时依然快。按某个具体键去过滤,就像一次轻量查表。? 检查键是否存在,-> 取出文本值,@> 匹配一组完整的键值;它们覆盖存在性、投影和包含,不必把映射拖到客户端拆开。GIN 直接支持存在性与包含查询;如果热点查询总是取同一个键再比较值,应为那个键加表达式索引,别假定通用 GIN 能回答所有取值条件。 查询要围绕整键访问设计,别对序列化文本动模式匹配的手术。用结构操作符取值的写法尊重了映射,把整列转成文本再模糊匹配的写法则亲手毁了它:既用不上索引,又会把根本不是键的子串也命中。这条规则可以推广:操作符尊重结构,类型转换摧毁结构。评审 hstore 查询先看有没有转文本,因为文本层面的匹配又慢又错,错的那些边界数据平时还看不出来。 更富的行用 JSONB hstore 只能存字符串,当键需要真实类型或更严格的引用时,JSONB 往往更适合更富的行。纯键值映射用 hstore,值需要结构化时选 JSONB。分界线穿过三个问题:值需不需要数字、布尔或嵌套(需要就选 JSONB);值需不需要约束或外键引用(两个都不合适,该规范化建表);是不是每个值都确实是自由文本且访问模式一致(hstore 以简单和更小的存储胜出)。 两者互相迁移都只是一次类型转换,所以选错可以反悔,不必纠结。形状扁平且不明朗时从 hstore 起步,第一个嵌套值出现的那季度升到 JSONB,某个键变成业务命脉的那季度升到正式列。阶梯是扁平映射、结构化文档、规范化表——确定性涨一分再往上爬一级,永远别提前。 坏掉的样子 经典故障是变成顶梁柱的字符串:当初当装饰存的键成了关键——价格档位、访问开关、路由规则——却还住在无类型无约束的文本里。拼写稍有出入就分裂出平行现实,排序按字典序而不是数值来,没有任何约束能拦住这两样。业务逻辑开始分支依赖它的那一刻,就该把它升成带检查的正式列;迁移很便宜,它消除的歧义一点也不便宜。 第二种故障是没建索引的膨胀:映射在各行累积出成千上万个互不相同的键,查询按键过滤却没有倒排索引,读取退化成全表扫描加逐行拆包。趁键空间还没爆炸先把索引建上,并定期审计查询真正碰过哪些键——从没被过滤过的键,赶回日志或对象存储,别占着索引列的位置。 快速验证 验证操作符与索引行为: SELECT 'a=>1, b=>2'::hstore -> 'a', 'a=>1, b=>2'::hstore ? 'c'; CREATE INDEX ON prefs USING GIN (attrs); EXPLAIN SELECT * FROM prefs WHERE attrs @> 'theme=>dark'; 第一条练习取值与存在性。包含查询在数据量和选择性足够时应使用 GIN;小表选择顺序扫描可能完全正确,所以要用接近生产的数据量比较计划。 ...