我在Postgres数据库中有一个表users
,它与另一个表users_attributes
有一对多关系,这是一个简单的键值类型表,带有users
表的外键列。
create table users(
id: uuid primary key,
name: varchar
);
create table users_attributes(
attribute_id: integer primary key,
user_id: uuid references users(id),
attribute_name: varchar,
attribute_value: varchar
);
字符串
我需要根据users_attributes
表中的attribute_name和attribute_value过滤users
。我已经尝试过这个查询,它可以工作,但执行时间要长得多:
select * from users u
left join users_attributes ua1 on u.id = ua1.user_id and ua1.attribute_name = 'dog_name'
left join users_attributes ua2 on u.id = ua2.user_id and ua2.attribute_name = 'cat_name'
where ua1.attribute_value = 'Spot' and ua2.attribute_value = 'Mittens';
型
对于我需要过滤用户的每个属性,连接的数量都会增加。这导致查询变慢(根据连接的数量在4-10秒之间),因为大约有10万个用户。查询上的解释计划支持这一点。
如何以更快的方式查询用户?
2条答案
按热度按时间t5zmwmid1#
这种类型的查询是典型的“craigslist查询”,基于属性(制造商,型号等)进行搜索......它也可以应用于约会网站,例如。
让我们建立一些测试数据。
字符串
我没有把属性名放在users_attrs中,因为这应该放在一个单独的表中。
为了简单起见,我没有使用属性值。无论我们在(attribute_id,user_id)还是(attribute_id,attribute_value,user_id)上使用索引,结果都是相同的,以便于性能测量。在搜索时,重要的是概率分布,换句话说,搜索条件的选择性。
例如,假设你在一个约会网站上寻找“你附近25-30岁的女性”。首先根据“性别”进行搜索是一个糟糕的策略,因为它的选择性为50%,所以数据库将不得不读取一半的表,然后由于其他标准而拒绝大部分。首先使用最具选择性的标准会带来更好的性能。因此,我模拟了概率分布。
所以我们有一百万用户,有20个属性;有些是非常频繁的,比如属性1,它在50%的用户中设置,而其他的是罕见的,比如20,它只在2.4%的用户中设置。
型
让我们对非常常见的属性1和2进行简单的搜索:
型
观察结果:
现在让我们搜索一个具有多个值的属性,我将通过搜索id(1或2)和3来模拟。
型
计划变更:在前面的例子中,索引会按顺序产生user_id,允许有效的合并连接。在这种情况下,它不会,所以postgres使用哈希。上面的注解同样适用。
现在让我们搜索两个稀有属性。
型
这非常好,它速度很快,行计数估计很好,最终结果是可用的:通过一点排序,用户应该可以在其中找到一些东西。
请注意,那些破坏你的服务器的搜索查询总是无用的,它们试图返回一个巨大的行,即使它运行得很快,也没有人会读到结果。
现在让我们搜索一个常见属性和两个罕见属性。
型
这也很好。我故意把连接的顺序放错了:PG注意到了这两个罕见的属性,并对连接进行了重新排序,以便首先搜索它们(中间的合并连接返回659行)。然后它检查结果行是否具有公共属性,保留335行。因此它避免了扫描具有公共属性#1的500 k行,这正是我们想要的
在你的例子中,对于属性值,它有点复杂,因为postgres积累的和查询规划器使用的默认统计数据只是每列的。所以你可能想在(attribute_id,attribute_value)上启用multivariate statistics以获得更好的估计。
但最重要的是如上所述的正确指数。
如果每个属性的属性值是固定的(即多项选择题),那么你可以为所有的属性值对分配一个id号,我的例子直接适用。
你的问题也完全Map到.全文搜索。你可以使用全文搜索引擎,它们是专门为此优化的。比如,如果属性是dog_name ='rex',你可以将用户的所有属性放在一个文本字段中,比如“dog_name_rex”.
Postgres确实有一个fulltext模块,但它不是那么快。但是,如果你可以将你的问题Map到“为所有属性-值对分配一个id号”,那么你可以使用它的后端,即模块intarray:
型
它更快一点,扩展性更好。在你的table大小上,不值得麻烦。
x3naxklr2#
LEFT JOIN
和WHERE
条件的混合在逻辑上是没有意义的。参见:基本重写:
字符串
基本上,这是一个relational-division的情况。
有很多方法可以做到这一点。最好的查询风格取决于你的基数,你的典型过滤器,以及你正在优化的内容。这里有一个完整的武器库:
我给出的查询是最快的选项之一。当然,你需要匹配索引。通过适当的规范化,一切都会更有效,其中属性名称移动到单独的表
attribute
中,attribute_id
是指向那里的整数FK。user_attribute(attribute_id, user_id)
(两个integer
列)上的索引是理想的。请参阅:查询将把属性名解析为整数ID(在查询中显式或隐式),并继续使用这些ID。