【问题标题】:Using same column multiple times in WHERE clause在 WHERE 子句中多次使用同一列
【发布时间】:2018-05-01 07:06:54
【问题描述】:

我有一个下表结构。

USERS

PROPERTY_VALUE

PROPERTY_NAME

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'

我明白,因为查询有 pn.id = 1 AND pn.id = 2 它总是会失败,因为 pn.id 可以是 1 或 2,但不能同时是两者。那么如何重写它以使其适用于 n 个属性?

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

澄清我的要求

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

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

拥有 4 个属性包括“property1”和“property2”的用户符合条件。但是,由于缺少“property2”,只有一个属性“property1”的用户将被排除在外。

【问题讨论】:

  • OR 而不是 AND?
  • OR 为同一用户返回 2 个结果,分别针对 'property1' 和 'property2'。由于这两个属性都属于同一个用户,我想获得一条记录作为回报
  • 需要ORgroup byhaving count(*)=2
  • @Mike 尝试使用 'OR, group by 并且 count(*)=2' 但没有帮助。在我的情况下,我必须在 group by 中添加 6 列,但它总是返回空结果。
  • 请将数据发布为文本,切勿以图片形式发布。并且您应该提供表定义(CREATE TABLE 语句)。

标签: sql postgresql relational-division


【解决方案1】:

这是 的情况。我添加了标签。

索引

假设USER_PROPERTY_MAP(property_value_id, user_id) 上的 PK 或 UNIQUE 约束 - 按此顺序排列以使我的查询更快。相关:

您还应该在PROPERTY_VALUE(value, property_name_id, id) 上有一个索引。同样,按此顺序列。仅当您从中得到index-only scans 时才添加最后一列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'

没有访问表 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);

仅在 VALUES 表达式中添加/删除行。或者删除WITH 子句和JOIN,因为没有属性过滤器

这类查询(计算所有部分匹配)的问题性能。我的第一个查询不太动态,但通常要快得多。 (只需使用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

dbfiddle here

The manual about recursive CTEs.

增加的复杂性不适用于额外开销超过任何好处或差异可以忽略不计的小型表。但它的扩展性要好得多,并且越来越优于具有不断增长的表和越来越多的属性过滤器的“计数”技术。

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

优化性能

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

第三次查询(递归 CTE)无法实现这种“deus-ex-machina”干预。为了提高性能(可能很多),您必须自己先放置更多选择性过滤器。但即使是最坏情况下的排序,它仍然会胜过计数查询。

相关:

更多血腥细节:

手册中有更多解释:

【讨论】:

  • 但是用户可以拥有任意数量的属性,而不仅仅是 2。根据您的解决方案,对于 n 个属性,我必须加入 user_property_map n 次,对吗?我正在浏览你分享的链接。看起来真的很有用。谢谢
  • @ivish:我为底层性能机制添加了更多解释。
  • 对于任何尝试在 SQLite 中使用第三个查询的人,请将“(property_name_id, value)”移动到“input”之后,因为 SQLite 不接受列定义。 :) 这些天,SQLite 的强大程度一定会让您感到惊讶。
【解决方案2】:
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
      )

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

如果propery_name_id 已知,则查询中不需要property_name 表。

【讨论】:

  • 谢谢。我会尝试并返回。
【解决方案3】:

如果你只想过滤:

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

或者,如果您需要用户按匹配数降序排列,您可以这样做:

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

【讨论】:

  • 谢谢。我将验证您的解决方案并更新您。
【解决方案4】:
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') )
OR ( pn.id = 2 AND pv.id IN (SELECT id FROM property_value WHERE value like 
'102'))

OR (...)
OR (...)

你不能做 AND 因为没有这样的情况 id 是 1 和 2 对于 SAME ROW,你为每一行指定 where 条件!

如果你运行一个简单的测试,比如

SELECT * FROM users where id=1 and id=2 

您将获得 0 个结果。为了实现这一点,使用

 id in (1,2) 

 id=1 or id=2

该查询可以进一步优化,但我希望这是一个好的开始。

【讨论】:

  • 同意,id=1 和 id=2 将不起作用。但是使用 OR 会为同一个用户返回多条记录,这不是我想要的
  • 你能显示你试图得到的输出是什么吗?因为当然您也可以按用户分组,但不确定您希望如何查看结果
  • 编辑了帖子以解释我在查询输出中到底想要什么
  • 谢谢,你能真正输入结果吗?至于我还不清楚,你想要每个用户一行吗?或者只是来自表用户的列?抱歉,有时很难理解确切的需求:)
  • 抱歉不能更好地解释它。我想从用户表中获取与 WHERE 子句中使用的属性匹配的所有记录。在上面的示例数据中,只有一个用户具有 WHERE 子句中使用的 2 个属性。所以我想查询从用户表中返回单个用户记录。
【解决方案5】:

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

(SELECT id FROM property_value WHERE value like '101') and
(SELECT id FROM property_value WHERE value like '102') 

所以和上面的 cmets 一样,使用 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');

【讨论】:

  • 使用 OR 不会产生我想要的结果
  • @ivish 试试我更新的查询。如果不满意,请在您的问题中添加您想要的输出:)
  • 试过了。与 OR 一样,它也返回多 (2) 条记录。我已经编辑了帖子以包含所需的输出
  • @ivish 如果您添加所需的输出表,它会更有帮助
【解决方案6】:

如果你只想要 U 中的不同列,它是:

SELECT DISTINCT u.* 
  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.[value] = '101')
     OR (pn.id = 2 AND pv.[value] = '102')

请注意,我使用pv.[value] = 而不是子查询来重新获取 id...这是简化。

【讨论】:

  • 谢谢。我会尝试并返回。
【解决方案7】:

如果我正确理解你的问题,我会这样做。

SELECT u.id, u.user_name, u.city FROM users u 
WHERE (SELECT count(*) FROM property_value v, user_property_map m 
WHERE m.user_id = u.id AND m.property_value_id = v.id AND v.value IN ('101', '102')) = 2

这应该返回具有 IN 子句中列出的所有属性的用户列表。 2 代表搜索的属性数。

【讨论】:

    【解决方案8】:

    假设您要选择 USERS 表中的所有字段

    SELECT u.* 
    FROM USERS u
    INNER JOIN 
    (
        SELECT USERS.id as user_id, COUNT(*) as matching_property_count
        FROM USERS
        INNER JOIN (
            SELECT m.user_id, n.name as property_name, v.value
            FROM PROPERTY_NAME n
            INNER JOIN PROPERTY_VALUE v ON n.id = v.property_name_id
            INNER JOIN USER_PROPERTY_MAP m ON m.property_value_id = v.property_value_id
            WHERE  (n.id = @property_id_1 AND v.value = @property_value_1) -- Property Condition 1
                OR (n.id = @property_id_2 AND v.value = @property_value_2) -- Property Condition 2
                OR (n.id = @property_id_3 AND v.value = @property_value_3) -- Property Condition 3
                OR (n.id = @property_id_N AND v.value = @property_value_N) -- Property Condition N
        ) USER_PROPERTIES ON USER_PROPERTIES.user_id = USERS.id
        GROUP BY USERS.id
        HAVING COUNT(*) = N     --N = the number of Property Condition in the WHERE clause
        -- Note : 
        -- Use HAVING COUNT(*) = N if property matches will be "MUST MATCH ALL"
        -- Use HAVING COUNT(*) > 0 if property matches will be "MUST MATCH AT LEAST ONE"
    ) USER_MATCHING_PROPERTY_COUNT ON u.id = USER_MATCHING_PROPERTY_COUNT.user_id
    

    【讨论】:

      猜你喜欢
      • 2016-09-07
      • 1970-01-01
      • 2014-02-16
      • 2021-11-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多