Postgres数组查询

时间:2015-05-15 08:16:59

标签: sql postgresql

(以下是对我的问题的高度简化描述。公司政策不允许我详细描述实际情况。)

涉及的数据库表是:

PRODUCTS:
ID   Name
---------
1    Ferrari
2    Lamborghini
3    Volvo


CATEGORIES:
ID    Name
----------
10    Sports cars
20    Safe cars
30    Red cars

PRODUCTS_CATEGORIES
ProductID    CategoryID
-----------------------
1            10
1            30
2            10
3            20

LOCATIONS:
ID      Name
------------
100     Sports car store
200     Safe car store
300     Red car store
400     All cars r us


LOCATIONS_CATEGORIES:
LocationID    CategoryID
------------------------
100           10
200           20
300           30
400           10
400           20
400           30

请注意,这些位置并非直接连接到产品,只是类别。客户应该能够看到可以提供他们想要购买的产品所属的所有产品类别的位置列表。所以,例如:

客户想购买法拉利。这可以从类别10 30的商店获得。这给我们的商店100,300和400而不是200.

但是,如果客户想要购买沃尔沃和兰博基尼,则可以从10 20类别的商店购买。这只能为我们提供400商店。

另一位顾客想购买法拉利和沃尔沃。他们可以从10 + 20(运动和安全)或30 + 20(红色和安全)类别的商店获得。

我需要的是一个postgres查询,它接受大量产品并返回可以找到所有产品的位置。我开始使用数组和< @运算符但很快就迷路了。下面是一些示例SQL,它试图获得可以购买法拉利和兰博基尼的商店。它无法正常工作,因为它要求位置满足所有 所有所选车辆所属的类别。它仅返回位置400,但应返回位置400和100.

SELECT l.* FROM locations l
WHERE 
(SELECT array_agg(DISTINCT(categoryid)) FROM products_categories WHERE productid IN (1,2))
<@
(SELECT array_agg(categoryid) FROM locations_categories WHERE locationid = l.id);

我希望我的描述有意义。

4 个答案:

答案 0 :(得分:3)

这是查询。您应该插入所选汽车ID pc.ProductId in (1,3)的列表,最后您应该将条件更正为选定的汽车数量,所以如果您选择1和3,您应该写HAVING COUNT(DISTINCT pc.ProductId) = 2如果您选择3辆汽车那么必须是HAVING中的条件为您提供所有车辆都在这些位置的条件:

SELECT Id FROM Locations l
JOIN Locations_Categories lc on l.Id=lc.LocationId
JOIN Products_Categories pc on lc.CategoryId=pc.CategoryID
where pc.ProductId in (1,3)
GROUP BY l.id
HAVING COUNT(DISTINCT pc.ProductId) = 2

Sqlfiddle demo

例如,对于一辆车,它将是:

SELECT Id FROM Locations l
JOIN Locations_Categories lc on l.Id=lc.LocationId
JOIN Products_Categories pc on lc.CategoryId=pc.CategoryID
where pc.ProductId in (1)
GROUP BY l.id
HAVING COUNT(DISTINCT pc.ProductId) = 1

Only Ferrary demo Volvo and a Lamborghini demo

答案 1 :(得分:3)

(这基本上详细阐述了@ valex的答案,虽然我没有意识到,直到我发布;请接受@ valex不是这个。)

这可以仅使用连接和聚合来完成。

正常构建连接树,将位置映射到产品。然后将其与所需产品列表(单列值行)连接,并将连接过滤为仅匹配的产品名称。现在,无论在何处找到该产品,您都可以在一行中找到产品的位置。

现在按位置和返回位置进行分组,其中存在的产品数量等于我们要查找的数量(对于ALL)。对于任何我们省略HAVING过滤器,因为连接返回的任何位置行都是我们想要的。

所以:

WITH wantedproducts(productname) AS (VALUES('Volvo'), ('Lamborghini'))
SELECT l."ID"
FROM locations l
INNER JOIN locations_categories lc ON (l."ID" = lc."LocationID")
INNER JOIN categories c ON (c."ID" = lc."CategoryID")
INNER JOIN products_categories pc ON (pc."CategoryID" = c."ID")
INNER JOIN products p ON (p."ID" = pc."ProductID")
INNER JOIN wantedproducts wp ON (wp.productname = p."Name")
GROUP BY l."ID"
HAVING count(DISTINCT p."ID") = (SELECT count(*) FROM wantedproducts);

基本上就是你想要的。

对于“包含任何所需产品的商店”查询,请删除HAVING子句。

如果您想显示具有任何匹配项的商店,但根据匹配数进行排序,那么您也可以ORDER BY聚合。

如果您要列出可在该商店找到的商品,还可以在string_agg(p."Name")值列表中添加SELECT

如果您希望输入是数组而不是值列表,只需将VALUES (...)替换为SELECT unnest($1)并将数组作为参数$1传递,或者按字面意思写出来代替$1

答案 2 :(得分:1)

进展中的答案:(我将在获得所需结果时添加答案)

关于你的第一个问题:

  

客户想购买法拉利。这可以从商店购买   在类别10或30中。这给我们商店100,300和400但不是   200。

SELECT DISTINCT l.id, l.name
FROM Products p
LEFT JOIN Product_Categories p_c
ON p.id = p_c.ProductId
LEFT JOIN Categories c
ON p_c.CategoryId = c.id
LEFT JOIN Locations_Categories l_c
ON c.id = l_c.CategoryId
LEFT JOIN Locations l
ON l_c.LocationId = l.id
WHERE p.id = 1

第二个问题:

  

然而,如果客户想要购买沃尔沃和兰博基尼   可以从10和20类商店购买。仅限于此类商店   给我们商店400。

SELECT DISTINCT l.id, l.name
FROM Products p
LEFT JOIN Product_Categories p_c
ON p.id = p_c.ProductId
LEFT JOIN Categories c
ON p_c.CategoryId = c.id
LEFT JOIN Locations_Categories l_c
ON c.id = l_c.CategoryId
LEFT JOIN Locations l
ON l_c.LocationId = l.id
WHERE l.id in (select id
               from locations loc
               join locations_categories locat1              
               on loc.id = locat1.LocationId
               join locations_categories locat2
               on loc.id = locat2.LocationId
               where locat1.CategoryId = 10
               AND locat2.categoryId = 20)

使用INTERSECT的第二个问题的结果: intersect将交叉引用每次可以找到1个产品的所有商店:

SELECT DISTINCT l.id, l.name
FROM Products p
LEFT JOIN Product_Categories p_c
ON p.id = p_c.ProductId
LEFT JOIN Categories c
ON p_c.CategoryId = c.id
LEFT JOIN Locations_Categories l_c
ON c.id = l_c.CategoryId
LEFT JOIN Locations l
ON l_c.LocationId = l.id
WHERE p.id = 2
INTERSECT
SELECT DISTINCT l.id, l.name
FROM Products p
LEFT JOIN Product_Categories p_c
ON p.id = p_c.ProductId
LEFT JOIN Categories c
ON p_c.CategoryId = c.id
LEFT JOIN Locations_Categories l_c
ON c.id = l_c.CategoryId
LEFT JOIN Locations l
ON l_c.LocationId = l.id
WHERE p.id = 3

对于每个新产品,您都会添加一个新的INTERSECT语句,并使用所需的产品ID创建一个新的选择 SQLFIDDLE:http://sqlfiddle.com/#!15/ce97d/15

答案 3 :(得分:1)

嗯,这里很难完全避免数组,但我认为我找到了一个阵列函数较少的解决方案。

我没有选择所需的位置,而是排除了无效的位置。

WITH needed_categories AS (
  SELECT p."ID", array_agg(pc."CategoryID") AS at_least_one_should_match
  FROM Products p
  JOIN Products_Categories pc ON p."ID" = pc."ProductID"
  WHERE p."ID" IN (1, 3)
  GROUP BY p."ID"
),
not_valid_locations AS (
  SELECT DISTINCT lc."LocationID", unnest(nc.at_least_one_should_match)
  FROM Locations_Categories lc
  JOIN needed_categories nc ON NOT ARRAY[lc."CategoryID"] && nc.at_least_one_should_match 
  EXCEPT
  SELECT * FROM Locations_Categories
) 
SELECT * 
FROM Locations
WHERE "ID" NOT IN (
  SELECT "LocationID" FROM not_valid_locations
);

这是SQLFiddle:http://sqlfiddle.com/#!15/e138d/78

这样可行,但我仍然试图避免Location_Categories的双seq扫描。 汽车可以属于多个类别的事实有点棘手,我使用数组解决了这个问题,但我也试图摆脱这些。