如何有效地获得“最近的对应行”?

Tom*_*lis 68 postgresql performance greatest-n-per-group query-performance

我有一个一定很常见的查询模式,但我不知道如何为它编写有效的查询。我想查找与“最近日期不晚于”另一个表的行相对应的表的行。

inventory比如说,我有一张表格,它代表了我在某一天持有的库存。

date       | good | quantity
------------------------------
2013-08-09 | egg  | 5
2013-08-09 | pear | 7
2013-08-02 | egg  | 1
2013-08-02 | pear | 2
Run Code Online (Sandbox Code Playgroud)

和一张表,“价格”说,它保存了某一天的商品价格

date       | good | price
--------------------------
2013-08-07 | egg  | 120
2013-08-06 | pear | 200
2013-08-01 | egg  | 110
2013-07-30 | pear | 220
Run Code Online (Sandbox Code Playgroud)

如何有效地获得库存表每一行的“最新”价格,即

date       | pricing date | good | quantity | price
----------------------------------------------------
2013-08-09 | 2013-08-07   | egg  | 5        | 120
2013-08-09 | 2013-08-06   | pear | 7        | 200
2013-08-02 | 2013-08-01   | egg  | 1        | 110
2013-08-02 | 2013-07-30   | pear | 2        | 220
Run Code Online (Sandbox Code Playgroud)

我知道这样做的一种方法:

select inventory.date, max(price.date) as pricing_date, good
from inventory, price
where inventory.date >= price.date
and inventory.good = price.good
group by inventory.date, good
Run Code Online (Sandbox Code Playgroud)

然后再次加入这个查询到库存。对于大表,即使执行第一次查询(不再次加入库存)也非常慢。但是,如果我只是使用我的编程语言从库存表中max(price.date) ... where price.date <= date_of_interest ... order by price.date desc limit 1为每个查询发出一个查询,同样的问题很快就会解决date_of_interest,所以我知道没有计算障碍。但是,我更愿意使用单个 SQL 查询来解决整个问题,因为它允许我对查询结果进行进一步的 SQL 处理。

有没有标准的方法可以有效地做到这一点?感觉它必须经常出现,并且应该有一种方法可以为它编写快速查询。

我正在使用 Postgres,但将不胜感激 SQL 通用答案。

Erw*_*ter 57

这在很大程度上取决于环境和确切的要求。考虑一下我的评论

简单的解决方案

随着DISTINCT ON在Postgres的:

SELECT DISTINCT ON (i.good, i.the_date)
       i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM   inventory  i
LEFT   JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER  BY i.good, i.the_date, p.the_date DESC;
Run Code Online (Sandbox Code Playgroud)

返回的行是有序的。看:

或者使用NOT EXISTS标准 SQL(适用于我知道的每个 RDBMS):

SELECT i.the_date, p.the_date AS pricing_date, i.good, i.quantity, p.price
FROM   inventory  i
LEFT   JOIN price p ON p.good = i.good AND p.the_date <= i.the_date
WHERE  NOT EXISTS (
   SELECT FROM price p1
   WHERE  p1.good = p.good
   AND    p1.the_date <= i.the_date
   AND    p1.the_date >  p.the_date
   );
Run Code Online (Sandbox Code Playgroud)

相同的结果,但具有任意排序顺序 - 除非您添加ORDER BY.
根据数据分布、确切要求和索引,其中任何一个都可能更快。看:

每个商品只有几行,DISTINCT ON通常更快,并且您会在其之上获得排序结果。但是在某些情况下,其他查询技术要(快得多)。见下文。

使用子查询计算最大值/最小值的解决方案通常较慢。然而,具有 CTE 的变体通常更慢。(CTE 使用 Postgres 12 改进。)

简单的视图(就像另一个答案提出的那样)在 Postgres 中根本没有帮助性能。

db<>fiddle here
旧的sqlfiddle

正确的解决方案

字符串和排序规则

首先,您的表格布局是次优的。这可能看起来微不足道,但规范化您的架构可以大有帮助。

字符类型 ( text, varchar, ...)排序是根据 current 完成的COLLATION。通常,您的数据库会使用一些本地规则集,例如在我的情况下:de_AT.UTF-8. 通过以下方式了解:

SHOW lc_collate;
Run Code Online (Sandbox Code Playgroud)

这使得排序和索引查找变慢。您的字符串(商品名称)越长越好。如果您实际上并不关心输出(或排序顺序)中的整理规则,则使用以下命令可以更快COLLATE "C"

SELECT DISTINCT ON (i.good COLLATE "C", i.the_date)
       i.the_date, p.the_date AS pricing_date, i.good, p.price
FROM   inventory  i
LEFT   JOIN price p ON i.good = p.good AND i.the_date >= p.the_date
ORDER  BY i.good COLLATE "C", i.the_date, p.the_date DESC;
Run Code Online (Sandbox Code Playgroud)

请注意在两个地方添加的排序规则。
在我的测试中速度是我的两倍,每行有 20k 行和非常基本的名称('good123')。

指数

如果您的查询应该使用索引,则包含字符数据的列必须使用匹配的排序规则(good在示例中):

CREATE INDEX inventory_good_date_desc_collate_c_idx
ON price(good COLLATE "C", the_date DESC);
Run Code Online (Sandbox Code Playgroud)

阅读我上面链接相关答案的最后两章。

您甚至可以在同一列上拥有多个具有不同排序规则的索引 - 如果您还需要根据其他查询中的另一个(或默认)排序规则对商品进行排序。

归一化

冗余字符串(良好的名称)使表和索引膨胀,这使一切变得更慢。适当的表格布局可以避免大部分问题。看起来像这样:

CREATE TABLE good (
  good_id serial PRIMARY KEY
, good    text   NOT NULL
);

CREATE TABLE inventory (
  good_id  int  REFERENCES good (good_id)
, the_date date NOT NULL
, quantity int  NOT NULL
, PRIMARY KEY(good_id, the_date)
);

CREATE TABLE price (
  good_id  int     REFERENCES good (good_id)
, the_date date    NOT NULL
, price    numeric NOT NULL
, PRIMARY KEY(good_id, the_date));
Run Code Online (Sandbox Code Playgroud)

主键自动提供(几乎)我们需要的所有索引。
根据缺失的详细信息,在第二列上以降序排列的多列索引price可能会提高性能:

CREATE INDEX price_good_date_desc_idx ON price(good, the_date DESC);
Run Code Online (Sandbox Code Playgroud)

同样,排序规则必须与您的查询匹配(见上文)。

由于 Postgres 9.2仅索引扫描的“覆盖索引”可以提供更多帮助 - 特别是如果表包含额外的列,使表远大于索引。

这些结果查询要快得多:

DISTINCT ON

SELECT DISTINCT ON (i.the_date)
       i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM   inventory  i
JOIN   good       g USING (good_id)
LEFT   JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
ORDER  BY i.the_date, p.the_date DESC;
Run Code Online (Sandbox Code Playgroud)

NOT EXISTS

SELECT i.the_date, p.the_date AS pricing_date, g.good, i.quantity, p.price
FROM   inventory  i
JOIN   good       g USING (good_id)
LEFT   JOIN price p ON p.good_id = i.good_id AND p.the_date <= i.the_date
AND    NOT EXISTS (
   SELECT 1 FROM price p1
   WHERE  p1.good_id = p.good_id
   AND    p1.the_date <= i.the_date
   AND    p1.the_date >  p.the_date
   );
Run Code Online (Sandbox Code Playgroud)

db<>在这里小提琴
sqliddle


更快的解决方案

如果这仍然不够快,可能会有更快的解决方案。

递归CTE JOIN LATERAL//相关子查询

特别是对于每个商品具有多个价格的数据分布:

物化视图

如果您需要经常快速地运行它,我建议您创建一个物化视图。我认为可以安全地假设,过去日期的价格和库存很少变化。计算一次结果并将快照存储为物化视图。

Postgres 9.3+ 自动支持物化视图。您可以轻松地在旧版本中实现基本版本。

  • 您推荐的“price_good_date_desc_idx”索引显着提高了我的类似查询的性能。我的查询计划的成本从 `42374.01..42374.86` 降到了 `0.00..37.12`! (3认同)

coc*_*lla 6

仅供参考,我使用了 mssql 2008,所以 Postgres 不会有“包含”索引。但是,使用下面显示的基本索引将在 Postgres 中从散列连接更改为合并连接:http : //explain.depesz.com/s/eF6(无索引) http://explain.depesz.com/s/j9x(在连接标准上有索引)

我建议将您的查询分为两部分。首先,一个视图(不旨在提高性能)可用于各种其他上下文,表示库存日期和定价日期的关系。

create view mostrecent_pricing_dates_per_good as
select i.good,i.date i_date,max(p.date)p_date
  from inventory i
  join price p on i.good = p.good and i.date >= p.date
 group by i.good,i.date;
Run Code Online (Sandbox Code Playgroud)

然后,如果查询(例如使用左连接查找没有最近定价日期的库存),您的查询可以变得更简单,更容易操作:

select i.good
       ,i.date inventory_date
       ,i.quantity
       ,p.date pricing_date
       ,p.price       
  from inventory i
  join price p on i.good = p.good
  join mostrecent_pricing_dates_per_good x 
    on i.good = x.good 
   and p.date = x.p_date
   and i.date = x.i_date
Run Code Online (Sandbox Code Playgroud)

这产生以下执行计划:http : //sqlfiddle.com/#!3/24f23/1 没有索引

...所有扫描都进行了完整排序。请注意哈希匹配的性能成本占总成本的大部分......而且我们知道表扫描和排序很慢(与目标相比:索引搜索)。

现在,添加基本索引以帮助您的连接中使用的标准(我不声称这些是最佳索引,但它们说明了这一点):http : //sqlfiddle.com/#!3/5ec75/1 带基本索引

这表明有所改善。嵌套循环(内连接)操作不再占用查询的任何相关总成本。其余的成本现在分散在索引查找中(扫描库存,因为我们正在拉动每个库存行)。但是我们仍然可以做得更好,因为查询会提取数量和价格。要获得该数据,在评估连接标准后,必须执行查找。

最后一次迭代在索引上使用“包含”,使计划更容易滑过并从索引本身中获取额外请求的数据。所以查找消失了:http : //sqlfiddle.com/#!3/5f143/1 在此处输入图片说明

现在我们有一个查询计划,其中查询的总成本在非常快速的索引查找操作中平均分配。这将接近于尽善尽美。当然其他专家可以进一步改进这一点,但解决方案清除了几个主要问题:

  1. 它在您的数据库中创建了易于理解的数据结构,更容易在应用程序的其他区域组合和重用。
  2. 所有成本最高的查询运算符都已使用一些基本索引从查询计划中剔除。

  • 这很好(对于 SQL-Server)但是针对不同的 DBMS 进行优化,虽然它有相似之处,但也有严重的差异。 (3认同)

Chr*_*ers 6

正如 Erwin 和其他人所指出的,一个高效的查询依赖于很多变量,而 PostgreSQL 非常努力地基于这些变量优化查询执行。一般来说,你想要写的清晰度第一,然后你找出瓶颈后修改的性能。

此外,PostgreSQL 有很多技巧,您可以使用它们来提高效率(一个是部分索引),因此根据您的读/写负载,您可以通过仔细研究索引来优化这一点。

首先要尝试的是做一个视图并加入它:

CREATE VIEW most_recent_rows AS
SELECT good, max(date) as max_date
FROM inventory
GROUP BY good;
Run Code Online (Sandbox Code Playgroud)

在执行以下操作时,这应该表现良好:

SELECT price 
  FROM inventory i
  JOIN goods g ON i.goods = g.description
  JOIN most_recent_rows r ON i.goods = r.goods
 WHERE g.id = 123;
Run Code Online (Sandbox Code Playgroud)

然后你就可以加入了。查询最终将加入针对基础表的视图,但假设您在(日期,按该顺序好)上有一个唯一索引,您应该很高兴(因为这将是一个简单的缓存查找)。这将在查找几行时非常有效,但如果您试图消化数百万价格的商品,则效率会非常低。

您可以做的第二件事是向库存表添加一个 most_recent bool 列和

create unique index on inventory (good) where most_recent;
Run Code Online (Sandbox Code Playgroud)

然后,当插入商品的新行时,您可能希望使用触发器将 most_recent 设置为 false。这增加了更多的复杂性和更多的错误机会,但它是有帮助的。

同样,这很大程度上取决于适当的索引。对于最近的日期查询,您可能应该有一个日期索引,可能还有一个以日期开头并包括您的连接条件的多列索引。

在下面更新Per Erwin 的评论,看来我误解了这一点。重新阅读这个问题,我完全不确定要问什么。我想在更新中提到我看到的潜在问题是什么,以及为什么这让这个问题不清楚。

所提供的数据库设计在 ERP 和会计系统中没有真正使用 IME。它适用于假设的完美定价模型,在该模型中,给定产品的给定日期出售的所有商品都具有相同的价格。然而,这并非总是如此。甚至货币兑换之类的事情也不是这样(尽管有些模型假装确实如此)。如果这是一个人为的例子,那就不清楚了。如果它是一个真实的例子,那么在数据层面上的设计存在更大的问题。我将在这里假设这是一个真实的例子。

不能假设日期单独指定给定商品的价格。任何业务的价格都可以按交易对手协商,有时甚至可以按交易协商。出于这个原因,您确实应该将价格存储在实际处理库存进出的表(库存表)中。在这种情况下,您的日期/货物/价格表仅指定了一个可能会根据协商进行更改的基本价格。在这种情况下,这个问题从一个报告问题变成了一个事务性的问题,一次对每个表的一行进行操作。例如,您可以在给定日期查找给定产品的默认价格,如下所示:

 SELECT price 
   FROM prices p
   JOIN goods g ON p.good = g.good
  WHERE g.id = 123 AND p."date" >= '2013-03-01'
  ORDER BY p."date" ASC LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

使用价格指数(好,日期),这将表现良好。

我这是一个人为的例子,也许更接近你正在做的事情会有所帮助。


Gar*_*thD 5

如果您碰巧拥有 PostgreSQL 9.3(今天发布),那么您可以使用 LATERAL JOIN。

我无法对此进行测试,并且以前从未使用过它,但是从文档中我可以看出,语法类似于:

SELECT  Inventory.Date,
        Inventory.Good,
        Inventory.Quantity,
        Price.Date,
        Price.Price
FROM    Inventory
        LATERAL
        (   SELECT  Date, Price
            FROM    Price
            WHERE   Price.Good = Inventory.Good
            AND     Price.Date <= Inventory.Date
            ORDER BY Price.Date DESC
            LIMIT 1
        ) p;
Run Code Online (Sandbox Code Playgroud)

这基本上相当于SQL-Server 的 APPLY,并且在 SQL-Fiddle 上有一个用于演示目的的工作示例


小智 1

使用从库存到价格的连接,连接条件将价格表中的记录限制为库存日期或之前的记录,然后提取最大日期,其中该日期是该子集中的最高日期

因此,对于您的库存价格:

 Select i.date, p.Date pricingDate,
    i.good, quantity, price        
 from inventory I join price p 
    on p.good = i.good
        And p.Date = 
           (Select Max(Date from price
            where good = i.good
               and date <= i.Date)
Run Code Online (Sandbox Code Playgroud)

如果任何指定商品的价格在同一天更改多次,并且这些列中实际上只有日期而没有时间,则可能需要对联接应用更多限制以仅选择一个价格更改记录。