如何编写此查询以避免使用笛卡尔积?

时间:2016-09-30 19:00:17

标签: sql join left-join cartesian-product

我想为订单显示CSV导出,显示每个order_item发货的仓库_id(如果有的话)。

为简洁起见,这是相关的架构:

create table o (id integer);

订单有很多order_items:

create table oi (id integer, o_id integer, sku text, quantity integer);

对于CSV中的每个order_item,我们希望显示出货地点的仓库数据。但是这不存储在order_items中。它存储在货件中。

订单可以分为多个来自不同仓库的货件。

create table s (id integer, o_id integer, warehouse_id integer);

货件也有许多装运项目:

create table si (id integer, s_id integer, oi_id integer, quantity_shipped integer);

我如何为每个order_item提取warehouse_id,因为Warehouse_id在货件上,而不是每个订单都已发货(可能没有货件记录或shipment_items)。

我们正在做这样的事情(简化):

select oi.sku, s.warehouse_id from oi 
left join s on s.o_id = oi.o_id;

但是,如果订单有2个订单商品,请让他们称之为sku A和B.然后该订单分为两个货件,其中A从仓库发货,50' 50'然后第二批货从'200'发货。

我们想要的是CSV输出,如:

 sku | warehouse_id
-----|--------------
  A  |           50
  B  |          200

但我们得到的是某种笛卡尔积:

=================================

Here is the sample data:

select * from o;
 id
----
  1
(1 row)

select * from oi;
 id | o_id | sku | quantity
----+------+-----+----------
  1 |    1 | A   |        1
  2 |    1 | B   |        1
(2 rows)

select * from s;
 id | o_id | warehouse_id
----+------+--------------
  1 |    1 |           50
  2 |    1 |          200
(2 rows)

select * from si;
 id | s_id | oi_id
----+------+------
  1 |    1 |    1
  2 |    2 |    2
(2 rows)

select oi.sku, s.warehouse_id from oi left join s on s.o_id = oi.o_id;
 sku | warehouse_id
-----+--------------
 A   |           50
 A   |          200
 B   |           50
 B   |          200
(4 rows)

更新========

每个斯宾塞,为了更清晰,我添加了一个具有不同pk ID的不同示例。以下是2个示例订单。订单2包含项目A,B,C。 A,B从装运200发货,C从装运201装运。订单3有2件E和A. E尚未发货,A从同一仓库装运两次' 700'(就像它在后面订购)。

# select * from o;
 id
----
  2
  3
(2 rows)

# select * from oi;
 id  | o_id | sku | quantity
-----+------+-----+----------
 100 |    2 | A   |        1
 101 |    2 | B   |        1
 102 |    2 | C   |        1
 103 |    3 | E   |        1
 104 |    3 | A   |        2
(5 rows)

# select * from s;
 id  | o_id | warehouse_id
-----+------+--------------
 200 |    2 |          700
 201 |    2 |          800
 202 |    3 |          700
 203 |    3 |          700
(4 rows)

# select * from si;
 id  | s_id | oi_id
-----+------+-------
 300 |  200 |   100
 301 |  200 |   101
 302 |  201 |   102
 303 |  202 |   104
 304 |  203 |   104
(5 rows)

我认为这样可行,我使用左连接来保存报表中的order_items,无论订单是否已发货,我使用group by来压缩来自同一仓库的多个货件。我相信这就是我的需要。

# select oi.o_id, oi.id, oi.sku, s.warehouse_id from oi left join si on si.oi_id = oi.id left join s on s.id = si.s_id group by oi.o_id, oi.id, oi.sku, s.warehouse_id order by oi.o_id;
 o_id | id  | sku | warehouse_id
------+-----+-----+--------------
    2 | 102 | C   |          800
    2 | 101 | B   |          700
    2 | 100 | A   |          700
    3 | 104 | A   |          700
    3 | 103 | E   |
(5 rows)

2 个答案:

答案 0 :(得分:1)

订购已发货的商品......

SELECT oi.id
     , oi.sku
     , s.warehouse_id
  FROM oi
  JOIN si ON si.oi_id = oi.id
  JOIN s  ON s.id     = si.s_id

订购尚未发货的商品,使用反联接排除si中匹配行的行

SELECT oi.id
     , oi.sku
     , s.warehouse_id
  FROM oi
  JOIN s ON s.o_id = oi.o_id      -- fk to fk shortcut join
    -- anti-join
  LEFT
  JOIN si ON si.oi_id = oi.id
 WHERE si.oi_id IS NULL

但这仍将产生(部分)笛卡尔积。我们可以添加GROUP BY子句来折叠行......

 GROUP BY si.oi_id

这不会避免产生中间笛卡儿产品;添加GROUP BY子句会折叠该集合。但是,从s列值中匹配的哪些行将从中返回,这是不确定的。

这两个查询可以与UNION ALL操作结合使用。如果我这样做,我可能会添加一个鉴别器列(每个查询中的一个附加列具有不同的值,这将告诉哪个查询返回了一行。)

此套装可能符合OP问题中列出的规范。但我不认为这真的是需要返回的集合。确定一个物品应该从哪个仓库发货可能涉及多个因素......订购的总数量,每个仓库中可用的数量,可以从一个仓库订购,哪个仓库更靠近交货目的地等等。

我不想让任何人认为此查询确实是一个"修复"对于笛卡尔积问题...这个查询只是隐藏了一个更大的问题。

答案 1 :(得分:0)

我认为你需要si表:

select oi.sku, s.warehouse_id
from si join
     oi
     on si.o_id = oi.o_id join
     s
     on s.s_id = si.s_id;

si似乎是表之间的正确联结表。我不确定为什么还有另一个不使用它的连接键。