LATERAL 连接是 PostgreSQL 和其他关系数据库(如 Oracle、DB2 和 MS SQL)鲜为人知的特性之一。然而,横向连接是一个非常有用的功能,看看你可以用它们完成什么是有意义的。
更仔细地检查 FROM
在我们深入研究 LATERAL 之前,有必要坐下来从更哲学的层面考虑 SQL 中的 SELECT 和 FROM 子句。下面是一个例子:
SELECT whatever FROM tab;
复制
基本上,我们可以将此语句视为一个循环。用伪代码编写此 SQL 语句类似于以下代码段:
for x in tab
loop
“do whatever”
end loop
复制
对于表中的每个条目,我们执行 SELECT 子句所说的操作。通常数据只是按原样返回。SELECT 语句可以看作是一个循环。但是如果我们需要一个“嵌套”循环呢?这正是 LATERAL 的优势所在。
横向连接:创建示例数据
让我们想象一个简单的例子。想象一下,我们有一系列产品,而且我们还有客户的愿望清单。现在的目标是为每个愿望清单找到最好的 3 个产品。以下 SQL 片段创建了一些示例数据:
CREATE TABLE t_product AS
SELECT id AS product_id,
id * 10 * random() AS price,
'product ' || id AS product
FROM generate_series(1, 1000) AS id;
CREATE TABLE t_wishlist
(
wishlist_id int,
username text,
desired_price numeric
);
INSERT INTO t_wishlist VALUES
(1, 'hans', '450'),
(2, 'joe', '60'),
(3, 'jane', '1500')
;
复制
产品表填充了 1000 种产品。价格是随机的,我们用了一个很有创意的名字来命名产品:
test=# SELECT * FROM t_product LIMIT 10;
product_id | price | product
------------+--------------------+------------
1 | 6.756567642432323 | product 1
2 | 5.284467408540081 | product 2
3 | 28.284196164210904 | product 3
4 | 13.543868035690423 | product 4
5 | 30.576923884383156 | product 5
6 | 26.572431211361902 | product 6
7 | 64.84599396020204 | product 7
8 | 21.550701384168747 | product 8
9 | 28.995584553969174 | product 9
10 | 17.31335004787411 | product 10
(10 rows)
复制
接下来,我们有一个愿望清单。
test=# SELECT * FROM t_wishlist;
wishlist_id | username | desired_price
-------------+----------+---------------
1 | hans | 450
2 | joe | 60
3 | jane | 1500
(3 rows)
复制
如您所见,愿望清单属于一个用户,我们想要推荐的这三种产品都有一个期望的价格。
运行横向连接
在提供一些示例数据并将其加载到我们的 PostgreSQL 数据库后,我们可以解决问题并尝试提出解决方案。
假设我们想用伪代码找到每个愿望的前三个产品:
for x in wishlist
loop
for y in products order by price desc
loop
found++
if found <= 3
then
return row
else
jump to next wish
end
end loop
end loop
复制
重要的是我们需要两个循环。首先,我们需要遍历愿望清单,然后查看已排序的产品列表,选择 3 并移至下一个愿望清单。
让我们看看如何使用 LATERAL-join 来做到这一点:
SELECT *
FROM t_wishlist AS w,
LATERAL (SELECT *
FROM t_product AS p
WHERE p.price < w.desired_price
ORDER BY p.price DESC
LIMIT 3
) AS x
ORDER BY wishlist_id, price DESC;
复制
我们将一步一步地完成它。您在 FROM 子句中看到的第一件事是 t_wishlist 表。LATERAL 现在可以做的是使用愿望清单中的条目来发挥它的魔力。因此,对于心愿单中的每个条目,我们选择三个产品。要弄清楚我们需要哪些产品,我们可以使用 w.desired_price。换句话说:它就像一个“带参数的连接”。FROM 子句是我们伪代码中的“外循环”,而 LATERAL 可以看作是“内循环”。
结果集如下所示:
wishlist_id | username | desired_price | product_id | price | product
-------------+----------+---------------+------------+--------------------+-------------
1 | hans | 450 | 708 | 447.0511375753179 | product 708
1 | hans | 450 | 126 | 443.6560873146138 | product 126
1 | hans | 450 | 655 | 438.0566432022443 | product 655
2 | joe | 60 | 40 | 59.32252841190291 | product 40
2 | joe | 60 | 19 | 59.2142714048882 | product 19
2 | joe | 60 | 87 | 58.78014573804254 | product 87
3 | jane | 1500 | 687 | 1495.8794483743645 | product 687
3 | jane | 1500 | 297 | 1494.4586352980593 | product 297
3 | jane | 1500 | 520 | 1490.7849437550085 | product 520
(9 rows)
复制
PostgreSQL 为每个愿望清单返回了三个条目,这正是我们想要的。这里的重要部分是 LIMIT 子句位于馈送到 LATERAL 的 SELECT 内部。因此,它限制了每个愿望清单的行数,而不是总行数。
PostgreSQL 在优化 LATERAL 连接方面做得非常好。在我们的例子中,执行计划看起来非常简单:
test=# explain SELECT *
FROM t_wishlist AS w,
LATERAL (SELECT *
FROM t_product AS p
WHERE p.price < w.desired_price
ORDER BY p.price DESC
LIMIT 3
) AS x
ORDER BY wishlist_id, price DESC;
QUERY PLAN
---------------------------------------------------------------------------------------
Sort (cost=23428.53..23434.90 rows=2550 width=91)
Sort Key: w.wishlist_id, p.price DESC
-> Nested Loop (cost=27.30..23284.24 rows=2550 width=91)
-> Seq Scan on t_wishlist w (cost=0.00..18.50 rows=850 width=68)
-> Limit (cost=27.30..27.31 rows=3 width=23)
-> Sort (cost=27.30..28.14 rows=333 width=23)
Sort Key: p.price DESC
-> Seq Scan on t_product p (cost=0.00..23.00 rows=333 width=23)
Filter: (price < (w.desired_price)::double precision)
(9 rows)
复制
LATERAL joins 非常有用,在很多情况下都可以用来加速操作,或者只是让代码更容易理解。