问这个问题,特别是针对 Postgres,因为它对 R-tree/空间索引有很好的支持。
我们有下表,其中包含单词及其频率的树结构(嵌套集模型):
lexikon
-------
_id integer PRIMARY KEY
word text
frequency integer
lset integer UNIQUE KEY
rset integer UNIQUE KEY
和查询:
SELECT word
FROM lexikon
WHERE lset BETWEEN @Low AND @High
ORDER BY frequency DESC
LIMIT @N
我想覆盖索引会很有用,但我觉得如果范围内的值(lset, frequency, word)
太多,它可能表现不佳。lset
(@High, @Low)
有时,一个简单的索引(frequency DESC)
也可能就足够了,当使用该索引的搜索早期产生@N
与范围条件匹配的行时。
但似乎性能很大程度上取决于参数值。
有没有办法让它快速执行,无论范围(@Low, @High)
是宽还是窄,也不管高频词是否幸运地在(窄)选定的范围内?
R-tree/空间索引会有帮助吗?
添加索引,重写查询,重新设计表,没有限制。
您可以通过首先在频率较高的行中搜索来获得更好的性能。这可以通过“颗粒化”频率然后按程序逐步执行它们来实现,例如如下:
--testbed 和
lexikon
虚拟数据:granule
分析(主要用于信息和调整):首先扫描高频的功能:
结果(时间可能应该加一点盐,但每个查询运行两次以对抗任何缓存)
首先使用我们编写的函数:
然后进行简单的索引扫描:
根据您的实际数据,您可能希望改变颗粒的数量以及用于将行放入其中的函数。频率的实际分布在这里很关键,条款的预期值和所寻求的范围
limit
大小也是如此。lset
设置
我正在建立@Jack 的设置,以使人们更容易关注和比较。用PostgreSQL 9.1.4测试。
从这里开始,我采取了不同的路线:
辅助表
此解决方案不会向原始表添加列,它只需要一个小辅助表。我把它放在 schema
public
中,使用你选择的任何 schema。表如下所示:
由于该列
cond
将在动态 SQL 中进一步使用,因此您必须使该表安全。如果您不能确定适当的 current ,请始终对表进行模式限定,并从(和任何其他不受信任的角色)search_path
撤消写权限:public
该表
lex_freq
有三个目的:索引
此
DO
语句创建所有需要的索引:所有这些部分索引一起跨越表一次。它们的大小与整个表上的一个基本索引大致相同:
到目前为止,50 MB 表的索引只有 21 MB。
我在
(lset, frequency DESC)
. 第二列仅在特殊情况下有帮助。但是由于涉及的两个列都是 typeinteger
,由于PostgreSQL中结合 MAXALIGN 的数据对齐的特殊性,第二列不会使索引变得更大。这是一个小小的胜利,几乎没有任何成本。对于仅跨越单个频率的部分索引,这样做是没有意义的。那些只是在
(lset)
。创建的索引如下所示:功能
该函数在风格上与@Jack 的解决方案有些相似:
主要区别:
动态 SQL与
RETURN QUERY EXECUTE
.当我们遍历这些步骤时,不同的查询计划可能会受益。静态 SQL 的查询计划生成一次,然后重复使用——这样可以节省一些开销。但在这种情况下,查询很简单,并且值非常不同。动态 SQL 将是一个巨大的胜利。
LIMIT
每个查询步骤都是动态的。这有多种帮助:首先,仅根据需要获取行。结合动态 SQL,这也可能会生成不同的查询计划。第二:不需要
LIMIT
在函数调用中进行额外的修剪。基准
设置
我选择了四个示例,并对每个示例进行了三个不同的测试。我拿了五个中最好的一个来与暖缓存进行比较:
表单的原始 SQL 查询:
创建此索引后相同
需要与我的所有部分索引相同的空间:
功能
结果
1:总运行时间:315.458 ms
2:总运行时间:36.458 ms
3:总运行时间:0.330 ms
1:总运行时间:294.819 ms
2:总运行时间:18.915 ms
3:总运行时间:1.414 ms
1: Total runtime: 426.831 ms
2: Total runtime: 217.874 ms
3: Total runtime: 1.611 ms
1: Total runtime: 2458.205 ms
2: Total runtime: 2458.205 ms -- for large ranges of lset, seq scan is faster than index.
3: Total runtime: 0.266 ms
Conclusion
As expected, the benefit from the function grows with bigger ranges of
lset
and smallerLIMIT
.With very small ranges of
lset
, the raw query in combination with the index is actually faster. You'll want to test and maybe branch: raw query for small ranges oflset
, else function call. You could even just build that into the function for a "best of both worlds" - that's what I would do.Depending on your data distribution and typical queries, more steps in
lex_freq
may help performance. Test to find the sweet spot. With the tools presented here, it should be easy to test.我看不出有任何理由在索引中包含单词列。所以这个指数
将使您的查询快速执行。
UPD
目前没有办法在 PostgreSQL 中创建覆盖索引。在 PostgreSQL 邮件列表http://archives.postgresql.org/pgsql-performance/2012-06/msg00114.php中有关于此功能的讨论
Using a GIST index
It depends on what you mean when you fast: you obviously have to visit every row in the range because your query is
ORDER freq DESC
. Shy of that the query planner already covers this if I understand the question,Here we create a table with 10k rows of
(5::int,random()::double precision)
We index it,
We query it,
We get a
Seq Scan on t
. This is simply because our selectivity estimates let pg conclude heap access is faster than scanning an index and rechecking. So we make it more juicy by inserting 1,000,000 more rows of(42::int,random()::double precision)
that don't fit our "range".And then we requery,
You can see here we complete in 4.6 MS with an Index Only Scan,
Expanding the range to include the whole table, produces another seq scan -- logically, and growing it with another billion rows would produce another Index Scan.
So in summary,