暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

在 PostgreSQL 中处理大型结果集

虽然层面搜索(faceting)这个词对您来说可能听起来很陌生,但几乎可以肯定您在网购中曾遇到过它。正是这些有用的小框在浏览网上商店或在目录中搜索时出现,告诉您如何进一步缩小搜索结果范围(例如,按颜色)以及将剩下多少项目。它们对用户非常有帮助——因此您自然会希望在您的应用程序中实现它们。本文将研究如何做到这一点。

从概念上讲,层面搜索(faceting)有两个步骤:首先,获取大量事物并将其缩小到当前搜索结果。让我们称其为基本查询。其次,从匹配的项集中提取可用于缩小范围的属性和值(也称为层面facet)。

 让我们从文档数据库的模式开始。

    CREATE TABLE documents (

    id int4 primary key,

    created timestamptz not null,

    finished timestamptz,

    category_id int4 not null,

    tags text[],

    type mimetype,

    size int8,

    title text

);

复制

假设您想查看以下分布:

  • 类型(Type)

  • 类别(Category)

  • 开始和结束时间戳(Start and end timestamps)

  • 尺寸(Size)

  • 标签(Tags)

在这里,有几种不同的层面。最简单的例子是type,您可以只使用每个不同的类型作为一个层面,并计算结果集中的出现次数,例如:type=application/pdf (1234)。另一个更简单的是 category_id,你也可以直接计算列可以取的每个值的匹配行数。与 type 的区别在于您要查找类别的名称并用于显示。类别也可以是分层的,但现在让我们将其作为“可视化问题”抛在一边。我们稍后会讨论您可以做的其他事情。

什么算作一个层面?分类变量和连续变量

type和category_id层面的共同点是它们是所谓的分类变量——它们可以采用有限且相对较小的一组值。您无需对它们进行任何操作即可将它们计为层面。您所考虑的其他属性称为连续变量(continuous variables),几乎每一行都有一个唯一的值,但有些值彼此接近,有些则相距甚远。为每一行创建一个独特的层面对用户来说并不是特别有用。

连续变量的分桶

处理连续变量的典型方法是使用桶。将整个值空间划分为更大的块,并将这些块视为分类变量。例如,对于时间戳,您可以认为来自单个月或年的所有条目都是相同的,或者对于大小,您可以任意为小、中、大文档选择一些范围。

复合变量

最后,标签(tags )可以称为复合变量,因为您可以将每个结果拆分为它具有的多个不同层面的值。从概念上讲,它们与分类变量没有太大区别,主要区别在于不同类别的计数加起来不等于结果行数。另外一个重要的考虑因素是相反的——选择一个标签并不排除选择其他标签。

计算层面

计算层面的一种简单方法是运行搜索查询,然后按层面值分组并计算结果:

SELECT type, COUNT(*) FROM documents WHERE ... GROUP BY 1;

SELECT category_id, COUNT(*) FROM documents WHERE ... GROUP BY 1;

SELECT date_trunc('month', started), COUNT(*) FROM documents WHERE ... GROUP BY 1;

SELECT date_trunc('month', finished), COUNT(*) FROM documents WHERE ... GROUP BY 1;

SELECT width_bucket(size, array[0,1000,5000,10000,50000,100000,500000]), COUNT(*) FROM documents WHERE ... GROUP BY 1;

复制

对于标签(tags),需要稍微复杂一点的查询来展开值,但一般形状是相似的。标签还可以存储在更传统的数据模型的关联表中,但这不会对您在这里所做的事情产生太大影响。

SELECT tag, COUNT(*) FROM documents, LATERAL unnest(tags) tag WHERE ... GROUP BY 1;
复制

缺点

这种方法有一些缺点。首先是您必须为每个要层面搜索的属性重新执行查询。还有很多重复在进行,很难对结果让一个通用的用户端处理。不幸的是,在 PostgreSQL 中无法从单个查询中获取多个结果集。

另一种方法

如果要在单个结果集中获得所有层面,则必须做出一些让步。SQL 类型系统不支持可变数据类型,因此您必须将所有内容转换为文本。但是如果您接受这一点,您可以将查询转换为一种形式,其中横向子查询返回每个匹配行中存在的所有层面:

SELECT facet_name, facet_value, COUNT(*)

FROM documents, LATERAL (VALUES

        ('type'type::text),

        ('category_id', category_id::text),

        ('created', date_trunc('month', created)::text),

        ('finished', date_trunc('month', finished)::text),

        ('size', width_bucket(size, array[0,1000,5000,10000,50000,100000,500000])::text)

            UNION ALL

        SELECT

    ) facets(facet_name, facet_value)

WHERE ...

GROUP BY 1, 2;



复制

在这里,您将像往常一样运行基本查询,但随后为每一行创建多个facet_name、facet_value列表的中间行。然后按这对进行分组,计算每对找到了多少。(此处担心翻译不准,附原文:Here you are running your base query the same as always, but then for each row you create multiple intermediary rows of the form facet_name, facet_value. Then you just group by that pair and count up how many of each pair you found.)

小层面搜索与大结果集

这种方法在较小的结果集下工作得相当好。如果您愿意动态生成查询,则可以通过不浪费时间计算已经过滤到一个值的方面来进行一些优化。但是,当您开始处理数万到数十万行时,响应时间开始变得明显。在大约 50 万个匹配结果中,执行时间超过 1 秒,即使允许大量并行工作也是如此。如果您有数亿个文档并且要匹配其中的很大一部分,那么执行时间很容易就会达到几分钟。

魔术表演?

那么如果你有更多的东西要统计会怎样呢?那些使用黑魔法的大公司难道普通人不可以使用吗?当然不是。

快速计算层面搜索计数的技巧是以一种能够真正快速地与匹配某些条件的行列表相交并计算结果集大小的方式排列数据。这是使用倒排索引完成的,倒排索引为每个层面值存储匹配文档列表。

我们在名为 pgfaceting 的 PostgreSQL 扩展中实现了相同的方法,该扩展通过PostgreSQL 包装器扩展使用 roaring 位图。使用此扩展,您需要预先定义哪些方面需要预先计算索引:

CREATE EXTENSION pgfaceting;

SELECT faceting.add_faceting_to_table(

    'documents',

    key => 'id',

    facets => array[

        faceting.datetrunc_facet('created''month'),

        faceting.datetrunc_facet('finished''month'),

        faceting.plain_facet('category_id'),

        faceting.plain_facet('type'),

        faceting.bucket_facet('size', buckets => array[0,1000,5000,10000,50000,100000,500000])

    ]

);

复制

幕后——

…您创建一个“用户空间”倒排索引表,其中包含每个层面搜索值的匹配文档列表。(目前还不支持复合面,但计划很快添加它们)索引大小可以相当合理。您使用的演示数据集有 100M 文档,倒排索引最终占表大小的 1%。

查询性能如何?

因为这本身就是一个索引,您实际上可以使用它来快速确定基本查询的结果集。因此,即使您选择 60M 行,您也可以在 155 毫秒内计算出结果。这还没有使用任何并行性,只是一个核心(CPU的单个核)。

SELECT facet_name, count(distinct facet_value), sum(cardinality)

FROM faceting.count_results('documents'::regclass,

    filters => array[row('category_id', 24)]::faceting.facet_filter[])

GROUP BY 1;

 facet_name | count |   sum   

------------+-------+----------

 created    |   154 | 60812252

 finished   |   154 | 60812252

 size       |     7 | 60812252

 type       |     8 | 60812252

(4 rows)

 

 Time: 155.228 ms

复制

下一步计划!

在以后的博客文章中,您将看到如何为自己实现这种出色的性能、如何处理分层数据、如何完成并行索引构建、如何启用增量维护等等。

由于 pgfaceting扩展 刚刚发布,查询 API 是经过深思熟虑实现的替代品;对于前几个版本,您可以期待一些 API 更改,需要在升级和其他粗糙边缘之间重建。请查看并留下反馈,说明您希望如何使用它来帮助我们塑造 API 。与开源一样,动手帮助也很受欢迎。

点击此处阅读原文

↓↓↓

文章转载自开源软件联盟PostgreSQL分会,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论