postgresql 如何将一个表与另一个具有最高id的表中的一行联接?

92dk7w1h  于 2023-08-04  发布在  PostgreSQL
关注(0)|答案(4)|浏览(135)

我有两个Postgres表:'user'具有唯一的行,'address'中单个用户可以有多个地址。我想将特定的“用户”行与“地址”中的一行连接起来,其中address.id是最高的max(id)
我写了下面的查询,它工作正常,但我怀疑当一个用户有很多地址时,这个查询将在返回最终结果之前产生一个非常大的中间聚合。有没有更好的方法来编写这个查询?

select "user".id, 
       "user".username,     
       "user".trader_id, 
       address.id,                                             
       address.street, 
       address.trader_id 
    from "user", address 
  where "user".id = 6 
      and  address.trader_id = "user".trader_id                         
 order by address.id desc
 limit 1;

字符串

czfnxgou

czfnxgou1#

您的查询对于获取单个用户来说已经很好了。绝对比在子查询中对整个address表运行窗口函数快得多。
您的查询,仅使用USING子句稍微简化:

SELECT trader_id, u.id AS user_id, u.username, a.id AS adr_id, a.street
FROM   "user"  u
JOIN   address a USING (trader_id)
WHERE  u.id = 6
ORDER  BY a.id DESC
LIMIT  1;

字符串
address(trader_id, id)上的多列索引将为您提供最佳性能。另外,很明显,还有一个"user"(id)的索引。(没有像你用LIMIT 1逮捕的“中间聚合”!)参见:

  • 优化GROUP BY查询以检索每个用户的最新行
  • 是否选择每个GROUP BY组中的第一行?

或者,在LATERAL子查询中使用相同的技术。用于检索一个或多个用户:

SELECT u.trader_id, u.id AS user_id, u.username, a.*
FROM   "user"  u
LEFT   JOIN LATERAL (
   SELECT a.id AS adr_id, a.street
   FROM   address a
   WHERE  a.trader_id = u.trader_id
   ORDER  BY a.id DESC
   LIMIT  1
   ) a ON true
WHERE  u.id = 6;  -- or for more than just the one


关于LATERAL子查询:

  • PostgreSQL中的LATERAL JOIN和subquery有什么区别?

还使用LEFT JOIN来保存没有任何地址的用户。

如果要使用窗口函数,请使用row_number()而不是rank()。在LATERAL子查询中执行此操作,同时只检索单个(或少数)用户,以便只涉及相关行。而且,除非你运行Postgres 16或更高版本,否则添加frame子句ROWS UNBOUNDED PRECEDING以提高性能:

SELECT u.trader_id, u.id AS user_id, u.username, a.*
FROM   "user"  u
LEFT   JOIN LATERAL (
   SELECT id AS adr_id, street
        , row_number() OVER (ORDER BY id DESC ROWS UNBOUNDED PRECEDING) AS rn
   FROM   address a
   WHERE  a.trader_id = u.trader_id
   ) a ON a.rn = 1
WHERE  u.id = 6;  -- or for more than one user


为什么是ROWS UNBOUNDED PRECEDING?请参阅:

  • 查找每个客户的前3个订单
  • 标量子查询中的窗口函数未按预期工作

或者使用DISTINCT ON

SELECT DISTINCT ON (traider_id)
       trader_id, u.id AS user_id, u.username, a.id AS adr_id, a.street
FROM   "user"  u
JOIN   address a USING (trader_id)  -- or LEFT JOIN?
WHERE  u.id = 6
ORDER  BY trader_id, a.id DESC;


请参阅:

  • 是否选择每个GROUP BY组中的第一行?

__
旁白:不要像“user”那样使用reserved words作为标识符。

2ekbmq32

2ekbmq322#

您的查询应该工作得很好并且性能良好,特别是如果您使用了适当的索引,例如对于两个表都使用了trader_id索引。(可以进行某些改进,例如删除第二个trader_id列,并为一个或两个id列使用别名。
但是,如果您想获取多个用户的信息,查询将失败,因为LIMIT 1子句只返回一行。那么,一个更通用的解决方案,你可以一次获得多个"user".id的所需信息,将使用窗口函数:

SELECT u.id AS user_id, 
       u.username,     
       trader_id, 
       a.id AS address_id,                                             
       a.street
FROM "user" u
JOIN (
    SELECT id, street, trader_id,
           rank() OVER (PARTITION BY trader_id ORDER BY id) AS rank
    FROM address) a USING (trader_id)
WHERE a.rank = 1
ORDER BY u.id;

字符串
在性能方面,您应该关注索引而不是查询结构-查询计划器将处理后者。

kd3sttzy

kd3sttzy3#

您可以跳过在查询中使用order by和limit,使用多列子查询从所有/任何用户的地址表中获取最大id。
步骤1:从地址表中写入所有用户获取最大地址ID。

select sa.trader_id,max(ss.id) as id from address as sa  group by 1

字符串
步骤2:将上述查询放入另一个查询中,以获取地址表中所需的列。

select 
            ss.* 
        from address as ss 
        where (ss.trader_id ,ss.id) in (select sa.trader_id,max(ss.id) as id from address as sa  group by 1)


步骤3:将上述查询与“user”表连接。你可以使用where条件从这里获取任何用户的最新地址id。

select "user".id, 
           "user".username,     
           "user".trader_id, 
           t1.id,                                             
           t1.street, 
           t1.trader_id 
        from "user" 
        join (
                select 
                    ss.* 
                from address as ss 
                where (ss.trader_id ,ss.id) in (select sa.trader_id,max(ss.id) as id from address as sa  group by 1)
            )t1 on "user".trader_id =t1.trader_id
              where "user".id = 6;

um6iljoc

um6iljoc4#

为了探索性能问题,创建一个能够暴露差异的测试床是很有用的。以下命令建立一个具有足够行数的环境,以演示不同查询的相对性能(调整p中的参数以模拟不同的数据特征):

CREATE TABLE users (
  id integer PRIMARY KEY,
  username text,
  trader_id integer);

CREATE TABLE addresses (
  id integer PRIMARY KEY,
  street text,
  trader_id integer);

WITH p(num_users, num_traders, mean_trader_addresses) AS (
  VALUES (20000, 10000, 100)
),
new_users AS (
  INSERT INTO users (id, username, trader_id)
  SELECT s.n, gen_random_uuid()::text, (random() * p.num_traders)::integer + 1
    FROM p
    CROSS JOIN LATERAL generate_series(1, p.num_users) s(n))
INSERT INTO addresses(id, street, trader_id)
SELECT s.n, gen_random_uuid()::text, (random() * p.num_traders)::integer % p.num_traders + 1
  FROM p
  CROSS JOIN LATERAL generate_series(1, p.num_traders * p.mean_trader_addresses) s(n);

CREATE INDEX users_by_trader_id ON users(trader_id);

CREATE INDEX addresses_by_trader_id ON addresses(trader_id);

字符串
下面列出了几个查询,每个查询都返回OP所需的结果,沿着在我的笔记本电脑上运行时的平均完成时间。
正如Erwin Brandstetter所指出的,当查询一个用户时,下面的子查询的性能很差,因为子查询必须处理所有的addresses,即使只有与单个交易者相关的那些是感兴趣的。在查询所有用户时,此查询的性能优于其他查询。

SELECT users.*, address.id, address.street, address.trader_id
  FROM users
  LEFT JOIN (SELECT addresses.id, addresses.street, addresses.trader_id,
               rank() OVER (PARTITION BY trader_id ORDER BY addresses.id DESC) AS rn
          FROM addresses) address
    ON users.trader_id = address.trader_id
       AND address.rn = 1
  WHERE users.id = 42;


3923.033 ms(所有用户为3654.100 ms)
当查询单个用户时,下一个版本的查询的性能明显优于前一个版本;但是,它在查询所有用户时的性能明显更差。

EXPLAIN (ANALYZE, buffers, VERBOSE)
SELECT DISTINCT ON (users.id) users.*, addresses.*
  FROM users
  LEFT JOIN addresses
    ON users.id = addresses.trader_id
  WHERE users.id = 42
  ORDER BY users.id, addresses.id DESC;


0.361 ms(所有用户为5072.365 ms)
第三个版本的性能在统计上与前一个版本的单用户查询性能相同,在查询所有用户时仅比第一个版本稍差。

SELECT users.*, address.*
  FROM users
  LEFT JOIN LATERAL (SELECT DISTINCT ON (addresses.trader_id) addresses.id, addresses.street, addresses.trader_id
                        FROM addresses
                        WHERE addresses.trader_id = users.id
                        ORDER BY addresses.trader_id, addresses.id DESC) address
    ON TRUE
  WHERE users.id = 42;


0.356 ms(所有用户为3963.354 ms)
通过对EXPLAIN (ANALYZE, BUFFERS, VERBOSE)的输出进行更深入的分析,可以获得更多的insite。

相关问题