在WHERE子句中多次使用相同的列

ivi*_*ish 11 sql postgresql relational-division

我有一个下表结构.

USERS

USERS数据

PROPERTY_VALUE

PROPERTY_VALUE数据

PROPERTY_NAME

PROPERTY_NAME数据

USER_PROPERTY_MAP

USER_PROPERTY_MAP数据

我正在尝试从users表中检索具有匹配属性的用户property_value.

单个用户可以拥有多个属性.这里的示例数据有2个用户'1'的属性,但是可以有2个以上.我想在WHERE子句中使用所有这些用户属性.

如果用户具有单个属性但是对于多于1个属性失败,则此查询有效:

SELECT * FROM users u
INNER JOIN user_property_map upm ON u.id = upm.user_id
INNER JOIN property_value pv ON upm.property_value_id = pv.id
INNER JOIN property_name pn ON pv.property_name_id = pn.id
WHERE (pn.id = 1 AND pv.id IN (SELECT id FROM property_value WHERE value like '101')
AND pn.id = 2 AND pv.id IN (SELECT id FROM property_value WHERE value like '102')) and u.user_name = 'user1' and u.city = 'city1'
Run Code Online (Sandbox Code Playgroud)

我理解,因为查询已经pn.id = 1 AND pn.id = 2失败,因为pn.id它可以是1或2但不能同时进行.那么如何重新编写它以使其适用于n个属性?

在上面的示例数据中,只有一个用户具有id = 1WHERE子句中使用的匹配属性.查询应返回包含USERS表的所有列的单个记录.

澄清我的要求

我正在开发一个应用程序,在UI上有一个用户列表页面,列出了系统中的所有用户.此列表包含用户ID,用户名,城市等信息 - 表中的所有列USERS.用户可以拥有上面数据库模型中详述的属性.

用户列表页面还提供基于这些属性搜索用户的功能.搜索具有2个属性'property1'和'property2'的用户时,该页面应该只获取并显示匹配的行.根据上面的测试数据,只有用户'1'符合账单.

具有4个属性(包括 'property1'和'property2')的用户符合条件.但是由于缺少"property2",将仅排除只有一个属性"property1"的用户.

Erw*_*ter 7

这是一个例子.我在标题中添加了标签.

索引

假设对USER_PROPERTY_MAP(property_value_id, user_id)- 列的PK或UNIQUE约束按此顺序使我的查询快速.有关:

你还应该有一个索引PROPERTY_VALUE(value, property_name_id, id).再次,按此顺序排列.id仅当您从中获取仅索引扫描时才添加最后一列.

对于给定数量的属性

有很多方法可以解决它.对于两个属性,这应该是最简单和最快的之一:

SELECT u.*
FROM   users             u
JOIN   user_property_map up1 ON up1.user_id = u.id
JOIN   user_property_map up2 USING (user_id)
WHERE  up1.property_value_id =
      (SELECT id FROM property_value WHERE property_name_id = 1 AND value = '101')
AND    up2.property_value_id =
      (SELECT id FROM property_value WHERE property_name_id = 2 AND value = '102')
-- AND    u.user_name = 'user1'  -- more filters?
-- AND    u.city = 'city1'
Run Code Online (Sandbox Code Playgroud)

不访问表PROPERTY_NAME,因为您似乎已根据示例查询将属性名称解析为ID.否则,您可以PROPERTY_NAME在每个子查询中添加连接.

我们在这个相关问题下汇集了一系列技术:

对于未知数量的属性

@Mike@Valera在各自的答案中都有非常有用的查询.为了使这更加动态:

WITH input(property_name_id, value) AS (
      VALUES  -- provide n rows with input parameters here
        (1, '101')
      , (2, '102')
      -- more?
      ) 
SELECT *
FROM   users u
JOIN  (
   SELECT up.user_id AS id
   FROM   input
   JOIN   property_value    pv USING (property_name_id, value)
   JOIN   user_property_map up ON up.property_value_id = pv.id
   GROUP  BY 1
   HAVING count(*) = (SELECT count(*) FROM input)
   ) sub USING (id);
Run Code Online (Sandbox Code Playgroud)

仅在VALUES表达式中添加/删除行.或者删除WITH子句和JOINfor no属性过滤器.

问题与此类查询(计算所有部分匹配)的性能.我的第一个查询动态性较差,但通常要快得多.(只需测试EXPLAIN ANALYZE.)特别是对于更大的表和越来越多的属性.

两全其美?

这种具有递归CTE的解决方案应该是一个很好的折衷方案:快速动态:

WITH RECURSIVE input AS (
   SELECT count(*)     OVER () AS ct
        , row_number() OVER () AS rn
        , *
   FROM  (
      VALUES  -- provide n rows with input parameters here
        (1, '101')
      , (2, '102')
      -- more?
      ) i (property_name_id, value)
   )
 , rcte AS (
   SELECT i.ct, i.rn, up.user_id AS id
   FROM   input             i
   JOIN   property_value    pv USING (property_name_id, value)
   JOIN   user_property_map up ON up.property_value_id = pv.id
   WHERE  i.rn = 1

   UNION ALL
   SELECT i.ct, i.rn, up.user_id
   FROM   rcte              r
   JOIN   input             i ON i.rn = r.rn + 1
   JOIN   property_value    pv USING (property_name_id, value)
   JOIN   user_property_map up ON up.property_value_id = pv.id
                              AND up.user_id = r.id
   )
SELECT u.*
FROM   rcte  r
JOIN   users u USING (id)
WHERE  r.ct = r.rn;          -- has all matches
Run Code Online (Sandbox Code Playgroud)

dbfiddle 在这里

关于递归CTE的手册.

增加的复杂性并不代表小额表,其中额外的开销超过任何利益,或者差异可以忽略不计.但它的扩展性能要好得多,而且越来越优于"计数"技术,增长表和越来越多的属性过滤器.

计数技术必须访问所有user_property_map给定属性过滤器的所有行,而此查询(以及第一个查询)可以尽早消除不相关的用户.

优化性能

使用当前表统计信息(合理设置,autovacuum运行),Postgres了解每列中的"最常见值",并将在第一个查询中重新排序连接,以首先评估最具选择性的属性过滤器(或至少不是最不具选择性的属性过滤器).达到一定限度:join_collapse_limit.有关:

对于第三个查询(递归CTE),这种"deus-ex-machina"干预是不可能的.为了帮助提高性能(可能很多),您必须先自己安排更多选择性过滤器.但即使是最坏情况的排序,它仍然会胜过计数查询.

有关:

更多血腥细节:

手册中有更多解释:


Mik*_*ike 6

SELECT *
  FROM users u
 WHERE u.id IN(
         select m.user_id
           from property_value v
           join USER_PROPERTY_MAP m
             on v.id=m.property_value_id 
          where (v.property_name_id, v.value) in( (1, '101'), (2, '102') )
          group by m.user_id
         having count(*)=2
      )
Run Code Online (Sandbox Code Playgroud)

要么

SELECT u.id
  FROM users u
 INNER JOIN user_property_map upm ON u.id = upm.user_id
 INNER JOIN property_value pv ON upm.property_value_id = pv.id
 WHERE (pv.property_name_id=1 and pv.value='101')
    OR (pv.property_name_id=2 and pv.value='102')
 GROUP BY u.id
HAVING count(*)=2
Run Code Online (Sandbox Code Playgroud)

property_name如果propery_name_id是kown,则查询中不需要表.


Val*_*era 5

如果您只想过滤:

SELECT users.*
FROM users
where (
    select count(*)
    from user_property_map
    left join property_value on user_property_map.property_value_id = property_value.id
    left join property_name on property_value.property_name_id = property_name.id
    where user_property_map.user_id = users.id -- join with users table
    and (property_name.name, property_value.value) in (
        values ('property1', '101'), ('property2', '102') -- filter properties by name and value
    )
) = 2 -- number of properties you filter by
Run Code Online (Sandbox Code Playgroud)

或者,如果您需要根据匹配数量下降的用户,您可以:

select * from (
    SELECT users.*, (
        select count(*) as property_matches
        from user_property_map
        left join property_value on user_property_map.property_value_id = property_value.id
        left join property_name on property_value.property_name_id = property_name.id
        where user_property_map.user_id = users.id -- join with users table
        and (property_name.name, property_value.value) in (
            values ('property1', '101'), ('property2', '102') -- filter properties by name and value
        )
    )
    FROM users
) t
order by property_matches desc
Run Code Online (Sandbox Code Playgroud)


Bha*_*mar 2

您正在AND两个pn.id=1和之间使用运算符pn.id=2。那么你如何得到答案就在这之间:

(SELECT id FROM property_value WHERE value like '101') and
(SELECT id FROM property_value WHERE value like '102') 
Run Code Online (Sandbox Code Playgroud)

所以就像上面的评论一样,使用or运算符。

更新1:

SELECT * FROM users u
INNER JOIN user_property_map upm ON u.id = upm.user_id
INNER JOIN property_value pv ON upm.property_value_id = pv.id
INNER JOIN property_name pn ON pv.property_name_id = pn.id
WHERE pn.id in (1,2) AND pv.id IN (SELECT id FROM property_value WHERE value like '101' or value like '102');
Run Code Online (Sandbox Code Playgroud)