【问题标题】:Select people with latest balance for one credit card being greater than for another选择一张信用卡的最新余额大于另一张信用卡的人
【发布时间】:2016-07-26 19:14:36
【问题描述】:

在 PostgreSQL 9.5.3 数据库中,我有一个 credit_card_balances 表,它引用了一个 persons 表,该表跟踪与特定人关联的各种信用卡的余额:

CREATE TABLE persons (
  id serial PRIMARY KEY,
  name text
);

CREATE credit_card_balances (
  id serial PRIMARY KEY,
  card_provider text, 
  person int REFERENCES persons,
  balance decimal, 
  timestamp timestamp
);

credit_card_balances 的示例行:

id  |  card_provider | person  | balance | timestamp
123 |  visa          | 1234    | 1.00    | 16-07-26 17:00

我需要检索同时拥有“visa”和“amex”卡的人的集合,以便“visa”卡上的最近余额大于最近余额在“美国运通”卡上。

对于每个(person, card_provider),表中最多可能有大约 100 行。理想情况下,输出列是:

person, provider1_balance, provider2_balance, provider1_timestamp, provider2_timestamp

我知道我可以做类似的事情

SELECT DISTINCT ON (card_provider) *
FROM credit_card_balances 
WHERE person=1234
ORDER BY card_provider, timestamp DESC;

获取特定人员每张卡的最新余额。但我不确定如何对所有人执行此操作并验证上述条件,或者这是否是正确的方法。

编辑:作为答案中的部分建议,我也可以做类似的事情

SELECT * from credit_card_balances b1, credit_card_balances b2
WHERE b1.person = b2.person
AND (b1.card_provider = 'amex' 
     AND b1.timestamp in
        (SELECT MAX(time_stamp) 
         FROM credit_card_balances 
         WHERE card_provider = 'amex'))

AND (b2.card_provider = 'visa'
     AND <... same as above>)
AND b1.balance > b2.balance;

但我注意到这会导致糟糕的表现。所以我认为这不是一个好的选择。

【问题讨论】:

  • 缺少通常的嫌疑人:表定义(显示数据类型和约束)和 Postgres 版本。最佳答案取决于这些,以及基数和值频率。平均每个(person, card_provider) 大约有多少行?结果中所需的列也会膨胀。最后的第一件事:在同一个数据库中是否有一个包含不同人员的表?通常,你有一个类似person 的表。
  • @ErwinBrandstetter 感谢 cmets 的帮助 - 我现在已将所有这些信息添加到帖子中!
  • 好多了! timestamp 未定义 NOT NULL?

标签: sql postgresql greatest-n-per-group relational-division


【解决方案1】:

这个问题是两个经典的结合:

鉴于您更新的规范,并且每个 (person, card_provider) 最多 100 行,我预计此查询将比我们目前的查询快得多:

SELECT a.person
     , a.balance   AS amex_balance
     , v.balance   AS visa_balance
     , a.timestamp AS amex_timestamp
     , v.timestamp AS visa_timestamp
FROM   persons p
CROSS  JOIN LATERAL (
   SELECT balance, timestamp
   FROM   credit_card_balances 
   WHERE  person = p.id
   AND    card_provider = 'amex'  -- more selective credit card first to optimize
   ORDER  BY timestamp DESC
   LIMIT  1
   ) a
JOIN   LATERAL (
   SELECT balance, timestamp
   FROM   credit_card_balances 
   WHERE  person = p.id
   AND    card_provider = 'visa'  -- 2nd cc
   ORDER  BY timestamp DESC
   LIMIT  1
   ) v ON v.balance > a.balance;

索引支持至关重要。这将是该案例的理想选择:

CREATE INDEX ON credit_card_balances (person, card_provider, timestamp DESC, balance);

balance 添加为最后一个索引列只有在您从中获得仅索引扫描时才有意义。

这是假设timestamp被定义为NOT NULL,否则你可能需要添加需要NULLS LAST来查询索引。

相关:


对于每个(person, card_provider)

只有 行,使用DISTINCT ON 的方法可能更快。单独的persons 表无济于事。最佳位置取决于许多因素。

假设至少有几张张不同的信用卡。

DISTINCT ON 用于一张信用卡,LATERAL 用于另一张信用卡:

SELECT a.person
     , a.balance   AS amex_balance
     , v.balance   AS visa_balance
     , a.timestamp AS amex_timestamp
     , v.timestamp AS visa_timestamp
FROM  (
   SELECT DISTINCT ON (person)
          person, balance, timestamp
   FROM   credit_card_balances 
   WHERE  card_provider = 'amex'  -- the more selective credit card first
   ORDER  BY person, timestamp DESC
   ) a
JOIN  LATERAL (
   SELECT balance, timestamp
   FROM   credit_card_balances 
   WHERE  card_provider = 'visa'
   AND    person = a.person
   ORDER  BY timestamp DESC
   LIMIT  1
   ) v ON v.balance > a.balance

DISTINCT ON为每张信用卡,然后加入:

SELECT a.person
     , a.balance   AS amex_balance
     , v.balance   AS visa_balance
     , a.timestamp AS amex_timestamp
     , v.timestamp AS visa_timestamp
FROM  (
   SELECT DISTINCT ON (person)
          person, balance, timestamp
   FROM   credit_card_balances 
   WHERE  card_provider = 'amex'
   ORDER  BY person, timestamp DESC
   ) a
JOIN  (
   SELECT DISTINCT ON (person)
          person, balance, timestamp
   FROM   credit_card_balances 
   WHERE  card_provider = 'visa'
   ORDER  BY person, timestamp DESC
   ) v USING (person)
WHERE  v.balance > a.balance;

或者,我的最爱:一张DISTINCT ON 用于两张信用卡,然后使用HAVING 条件过滤聚合:

SELECT person
     , max(balance)   FILTER (WHERE card_provider = 'amex') AS amex_balance
     , max(balance)   FILTER (WHERE card_provider = 'visa') AS visa_balance
     , max(timestamp) FILTER (WHERE card_provider = 'amex') AS amex_timestamp
     , max(timestamp) FILTER (WHERE card_provider = 'visa') AS visa_timestamp
FROM  (
   SELECT DISTINCT ON (person, card_provider)
          person, card_provider, balance, timestamp
   FROM   credit_card_balances 
   WHERE  card_provider IN ('amex', 'visa')
   ORDER  BY person, card_provider, timestamp DESC
   ) c
GROUP  BY person
HAVING max(balance) FILTER (WHERE card_provider = 'visa')
     > max(balance) FILTER (WHERE card_provider = 'amex');

聚合 FILTER 子句需要 Postgres 9.4+:

【讨论】:

  • 感谢您提供的精彩而翔实的答案,以及您对使我的问题变得更好的建议!您的第一个解决方案非常适合我的需求。对于未来阅读本文的人来说,当他说索引支持至关重要时,他并没有胡闹
【解决方案2】:

使用自联接。比如:

SELECT * from credit_card_balances b1, credit_card_balances b2
WHERE b1.person = b2.person
  AND b1.card_provider = 'amex'
  AND b2.card_provider = 'visa'
  AND b1.balance > b2.balance;

将此与您已经提出的或多或少的结果相结合,使用视图使查询更易于理解。

CREATE VIEW most_recent_balance AS
  SELECT DISTINCT ON (person, card_provider) *
    FROM credit_card_balances 
   GROUP BY id, person
   ORDER BY person, card_provider, timestamp DESC;

用这个 most_recent_balance 视图代替自联接查询中的表。

【讨论】:

  • 感谢您的回复-我想说我坚持的方面是将这部分与另一部分结合起来。我已经想出了这个部分和另一个部分,但是将它们放在一起却很麻烦。
  • 您可以使用视图执行此操作,但我不确定性能是否会比更新后的查询更好。稍后我会用视图更新我的答案。
  • 您的 DISTINCT ONGROUP BY 子句相互矛盾。删除 GROUP BY 会使其浮动。
【解决方案3】:

你可以用嵌套的选择和窗口函数来做到这一点

select * from (
     select *, 
       rank() over(partition by card_provider order by balance desc) as rank 
     from credit_card_balances
) credit_card_balances_ranked
where rank = 1

【讨论】:

    猜你喜欢
    • 2020-07-22
    • 1970-01-01
    • 2014-10-26
    • 2020-07-30
    • 1970-01-01
    • 1970-01-01
    • 2015-06-04
    • 1970-01-01
    • 2011-05-05
    相关资源
    最近更新 更多