主存储
让读始终是一次查表
“这个客户名下所有还没关闭的订单。”“这个购物车里有几件东西。”这类读,一个应用一秒钟要做上千次,而在关系型数据库里,每一次都是一条背后跟着查询计划器的查询。kevy 可以直接把答案备好。
为什么合适
键值存储被挡在应用数据之外,通常只因为一句反对:可是我需要按 key 以外的东西去查。这句反对是对的,而二级索引正是为它准备的。
索引是声明出来的,不是建出来的。你写清楚 key 的前缀和字段,写入路径会把它维持在最新。一个带过滤的列表于是重新变回一次查表——没有计划器,没有扫描,没有查询。
视图更进一步,在写入时就把聚合维持在最新,于是一个计数、一个合计是读出来的,不是算出来的。这正是大多数应用真正在向 ORM 索要的东西,也正是它们的数据库忙成那样的原因。
怎么做
# your data, written the way you would anyway
HSET order:1001 customer 881 status open total 4400
HSET order:1002 customer 881 status paid total 8400
HSET order:1003 customer 902 status open total 1200
# one index per field you want to look up by
IDX.CREATE idx:cust ON PREFIX order: FIELD customer TYPE i64 KIND range
IDX.CREATE idx:status ON PREFIX order: FIELD status TYPE str KIND range
# the read that would have been a query
IDX.QUERY idx:cust EQ 881
-> 1) "0" # cursor
2) 1) "order:1001" 2) "881"
3) "order:1002" 4) "881"
# two conditions at once
IDX.QUERY COMPOSE AND idx:cust EQ 881 idx:status EQ open
-> 1) "0"
2) 1) 1) "order:1001"
# a VIEW keeps the answer ready on the WRITE path, so the read
# never recomputes it. (the parens are separate arguments.)
VIEW.CREATE v:open881 QUERY ( AND idx:cust EQ 881 idx:status EQ open ) ORDER BY idx:cust
VIEW.QUERY v:open881
-> 1) "0"
2) 1) "order:1001" 2) "881"
代价是什么
索引和视图的账,是每次写的时候付的,不是读的时候——这就是这笔交易。对读多的服务路径,它是对的;对写多的日志,它是错的。这里没有 join,以后也不会有:索引回答的是哪些 key 匹配了这些字段,而不是把两个集合连起来。如果你的读确实需要 join,就把它留在 Postgres 里。我们把每一种关系型负载在这里的代价都写下来了,包括那些答案是别搬的。