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

How to write this query to avoid cartesian product?

我想为订单创建一个 CSV 导出文件,显示 warehouse_id 每个 order_item 的发货地(如果有)。

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

create table o (id integer);

订单有很多order_item个:

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

对于 CSV 中的每个 order_item,我们希望显示一个 warehouse_id 从哪里发货。但这并没有存储在 order_item 中。它存储在发货中。

一个订单可以拆分成多个货件,可能来自不同的仓库。

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”发货,然后第二批货从“200”发货 B .

我们想要的是像这样的 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)

更新 ========

Per spencer,为了更清楚起见,我添加了一个具有不同 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)

我认为这行得通,我使用 left join 保留报告中的 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)

我认为你需要 si table:

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 似乎是 table 之间的正确连接点 table。我不确定为什么还有另一个不使用它的连接键。

已发货的订单商品...

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 问题中概述的规格。但我不认为这真的是需要归还的套装。确定一件物品应该从哪个仓库发货可能涉及多个因素...订购的总数量、每个仓库的可用数量、可以从一个仓库完成订单、哪个仓库离交货目的地更近等。

我不想给任何人留下这样的印象,即此查询实际上是 "fix" 笛卡尔积问题...此查询只是隐藏了一个更大的问题。