按外键分组时使用 MAX(id) 获取整行的有效方法
Efficient way to get entire rows with MAX(id) when grouping by a foreign key
考虑表A、B和C。B和C通过外键与A关联,并且有许多具有相同A外键的B和C。
假设以下查询:
SELECT
A.pk AS pk_a,
MAX(B.id) AS new_b,
MAX(C.id) AS new_c
FROM A
INNER JOIN B ON B.fk_a = pk_a
INNER JOIN C ON C.fk_a = pk_a
GROUP BY pk_a
我想为每个 GROUP BY pk_a
.
从 B 和 C 检索整个 new_b 和 new_c 行
我当然可以将其包装为子选择,JOIN B ON b.id = new_b
,对于 C 也是如此,但是 B 和 C 很大,我想避免这种情况。
我也可以使用SELECT DISTINCT ON(A.pk) A.pk, B.*, C.*
和ORDER BY A.pk, B.id, C.id
,但那只能保证最新的B.,而不是最新的C..
还有什么我想念的方法吗?
这个怎么样:
SELECT DISTINCT
A.pk AS pk_a,
MAX(B.id) OVER(PARTITION BY pk_a) AS new_b,
MAX(C.id) OVER(PARTITION BY pk_a) AS new_c
FROM A
INNER JOIN B ON B.fk_a = pk_a
INNER JOIN C ON C.fk_a = pk_a
这是你要的吗?
SELECT abc.*
FROM (SELECT A.pk AS pk_a, b.*, c.*,
ROW_NUMBER() OVER (PARTITION BY a.pk ORDER BY b.id DESC) as seqnum_b,
ROW_NUMBER() OVER (PARTITION BY a.pk ORDER BY c.id DESC) as seqnum_c
FROM A INNER JOIN
B
ON B.fk_a = pk_a INNER JOIN
C
ON C.fk_a = pk_a
) abc
WHERE seqnum_b = 1 or seqnum_c = 1;
实际上,我认为上述内容是正确的,但您可能想要:
SELECT a.pk, b.*, c.*
FROM A INNER JOIN
(SELECT DISTINCT ON (b.fk_a) b.*
FROM b
ORDER BY b.fk_a, b.id DESC
) b
ON B.fk_a = pk_a JOIN
(SELECT DISTINCT ON (c.fk_a) c.*
FROM c
ORDER BY c.fk_a, c.id DESC
) c
ON c.fk_a = pk_a;
在 Postgres 9.5 中,您也可以使用横向连接来达到类似的效果。
对于很少行(例如平均2或3或5,取决于)在B
和C
中每行在A
, DISTINCT ON
通常是最快的。
对于A
中每行许多行,有(很多)更有效的解决方案。还有你的信息:"B and C are huge" 表示一样多。
我建议使用 ORDER BY
和 LIMIT 1
的 LATERAL
子查询,由匹配索引支持。
SELECT A.pk AS pk_a, B.*, C.*
FROM A
LEFT JOIN LATERAL (
SELECT *
FROM B
WHERE B.fk_a = A.pk -- lateral reference
ORDER BY B.id DESC
LIMIT 1
) B ON true
LEFT JOIN LATERAL (
SELECT *
FROM C
WHERE C.fk_a = A.pk -- lateral reference
ORDER BY C.id DESC
LIMIT 1
) C ON true;
假设 B.id
和 C.id
是 NOT NULL
。
您需要 至少在 FK 列上建立索引。理想情况下,multi-column 在 B (fk_a, id DESC)
和 C (fk_a, id DESC)
上建立索引。
使用LEFT JOIN
!不排除 A
中未在 B
或 C
中引用的行。在这里使用 [INNER] JOIN
将是一个邪恶的陷阱,因为您连接到两个不相关的表。
详细解释:
- Optimize GROUP BY query to retrieve latest record per user
相关:
- Select first row in each GROUP BY group?
更简单的语法和智能命名约定
上面查询的结果有一次pk_a
,两次fk_a
。无用的镇流器 - 两次相同的列名可能是一个实际问题,具体取决于您的客户。
您可以在外部 SELECT
(而不是语法快捷方式 A.*, B.*
)拼出一个列列表以避免冗余。如果有更多重复的名称,或者如果您不想要 all 列,您可能必须这样做。
但是通过智能命名约定,USING
子句可以为您折叠多余的 PK 和 FK 列:
SELECT *
FROM A
LEFT JOIN LATERAL (
SELECT * FROM B
WHERE B.a_id = A.a_id
ORDER BY B.id DESC
LIMIT 1
) B USING (a_id)
LEFT JOIN LATERAL (
SELECT * FROM C
WHERE C.a_id = A.a_id
ORDER BY C.id DESC
LIMIT 1
) C USING (a_id);
逻辑上,USING (a_id)
在这里是多余的,因为子查询中的WHERE B.a_id = A.a_id
已经以同样的方式过滤了。但是 USING
的附加效果是连接列被折叠到 one 实例。所以只有 one a_id
留在结果中。 The manual:
Furthermore, the output of JOIN USING
suppresses redundant columns:
there is no need to print both of the matched columns, since they must
have equal values. While JOIN ON
produces all columns from T1
followed
by all columns from T2
, JOIN USING
produces one output column for each
of the listed column pairs (in the listed order), followed by any
remaining columns from T1
, followed by any remaining columns from T2
.
对相同的数据使用相同的名称通常也很有意义。所以:a_id
用于 PK 和 FK 列。
考虑表A、B和C。B和C通过外键与A关联,并且有许多具有相同A外键的B和C。
假设以下查询:
SELECT
A.pk AS pk_a,
MAX(B.id) AS new_b,
MAX(C.id) AS new_c
FROM A
INNER JOIN B ON B.fk_a = pk_a
INNER JOIN C ON C.fk_a = pk_a
GROUP BY pk_a
我想为每个 GROUP BY pk_a
.
我当然可以将其包装为子选择,JOIN B ON b.id = new_b
,对于 C 也是如此,但是 B 和 C 很大,我想避免这种情况。
我也可以使用SELECT DISTINCT ON(A.pk) A.pk, B.*, C.*
和ORDER BY A.pk, B.id, C.id
,但那只能保证最新的B.,而不是最新的C..
还有什么我想念的方法吗?
这个怎么样:
SELECT DISTINCT
A.pk AS pk_a,
MAX(B.id) OVER(PARTITION BY pk_a) AS new_b,
MAX(C.id) OVER(PARTITION BY pk_a) AS new_c
FROM A
INNER JOIN B ON B.fk_a = pk_a
INNER JOIN C ON C.fk_a = pk_a
这是你要的吗?
SELECT abc.*
FROM (SELECT A.pk AS pk_a, b.*, c.*,
ROW_NUMBER() OVER (PARTITION BY a.pk ORDER BY b.id DESC) as seqnum_b,
ROW_NUMBER() OVER (PARTITION BY a.pk ORDER BY c.id DESC) as seqnum_c
FROM A INNER JOIN
B
ON B.fk_a = pk_a INNER JOIN
C
ON C.fk_a = pk_a
) abc
WHERE seqnum_b = 1 or seqnum_c = 1;
实际上,我认为上述内容是正确的,但您可能想要:
SELECT a.pk, b.*, c.*
FROM A INNER JOIN
(SELECT DISTINCT ON (b.fk_a) b.*
FROM b
ORDER BY b.fk_a, b.id DESC
) b
ON B.fk_a = pk_a JOIN
(SELECT DISTINCT ON (c.fk_a) c.*
FROM c
ORDER BY c.fk_a, c.id DESC
) c
ON c.fk_a = pk_a;
在 Postgres 9.5 中,您也可以使用横向连接来达到类似的效果。
对于很少行(例如平均2或3或5,取决于)在B
和C
中每行在A
, DISTINCT ON
通常是最快的。
对于A
中每行许多行,有(很多)更有效的解决方案。还有你的信息:"B and C are huge" 表示一样多。
我建议使用 ORDER BY
和 LIMIT 1
的 LATERAL
子查询,由匹配索引支持。
SELECT A.pk AS pk_a, B.*, C.*
FROM A
LEFT JOIN LATERAL (
SELECT *
FROM B
WHERE B.fk_a = A.pk -- lateral reference
ORDER BY B.id DESC
LIMIT 1
) B ON true
LEFT JOIN LATERAL (
SELECT *
FROM C
WHERE C.fk_a = A.pk -- lateral reference
ORDER BY C.id DESC
LIMIT 1
) C ON true;
假设 B.id
和 C.id
是 NOT NULL
。
您需要 至少在 FK 列上建立索引。理想情况下,multi-column 在 B (fk_a, id DESC)
和 C (fk_a, id DESC)
上建立索引。
使用LEFT JOIN
!不排除 A
中未在 B
或 C
中引用的行。在这里使用 [INNER] JOIN
将是一个邪恶的陷阱,因为您连接到两个不相关的表。
详细解释:
- Optimize GROUP BY query to retrieve latest record per user
相关:
- Select first row in each GROUP BY group?
更简单的语法和智能命名约定
上面查询的结果有一次pk_a
,两次fk_a
。无用的镇流器 - 两次相同的列名可能是一个实际问题,具体取决于您的客户。
您可以在外部 SELECT
(而不是语法快捷方式 A.*, B.*
)拼出一个列列表以避免冗余。如果有更多重复的名称,或者如果您不想要 all 列,您可能必须这样做。
但是通过智能命名约定,USING
子句可以为您折叠多余的 PK 和 FK 列:
SELECT *
FROM A
LEFT JOIN LATERAL (
SELECT * FROM B
WHERE B.a_id = A.a_id
ORDER BY B.id DESC
LIMIT 1
) B USING (a_id)
LEFT JOIN LATERAL (
SELECT * FROM C
WHERE C.a_id = A.a_id
ORDER BY C.id DESC
LIMIT 1
) C USING (a_id);
逻辑上,USING (a_id)
在这里是多余的,因为子查询中的WHERE B.a_id = A.a_id
已经以同样的方式过滤了。但是 USING
的附加效果是连接列被折叠到 one 实例。所以只有 one a_id
留在结果中。 The manual:
Furthermore, the output of
JOIN USING
suppresses redundant columns: there is no need to print both of the matched columns, since they must have equal values. WhileJOIN ON
produces all columns fromT1
followed by all columns fromT2
,JOIN USING
produces one output column for each of the listed column pairs (in the listed order), followed by any remaining columns fromT1
, followed by any remaining columns fromT2
.
对相同的数据使用相同的名称通常也很有意义。所以:a_id
用于 PK 和 FK 列。