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


53

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

例如,我有一张桌子,inventory它代表我当天持有的库存。

date       | good | quantity
------------------------------
2013-08-09 | egg  | 5
2013-08-09 | pear | 7
2013-08-02 | egg  | 1
2013-08-02 | pear | 2

还有一张表,上面写着“价格”,该表保存了某天的商品价格

date       | good | price
--------------------------
2013-08-07 | egg  | 120
2013-08-06 | pear | 200
2013-08-01 | egg  | 110
2013-07-30 | pear | 220

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

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

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

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

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

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

我正在使用Postgres,但是希望使用SQL通用答案。


3
由于效率问题,因此选择迁移到DBA.SE。我们可以用几种不同的方式来编写查询,但这并不能使其更快。
ypercubeᵀᴹ

5
您实际上一次查询一次就需要所有商品吗?似乎不太可能的要求?更常见的是,将检索特定日期的价格或特定商品(特定日期)的价格。这些替代查询可以更轻松地受益于(适当的)索引。我们还需要知道:基数(每个表中有几行?),包括完整的表定义。数据类型,约束,索引等(\d tbl在psql中使用),您的Postgres版本和min。/最大 每件商品的价格数量。
Erwin Brandstetter 2013年

@ErwinBrandstetter您要我接受答案吗?我真的没有资格知道哪个是最好的,尽管您的投票最多,我很乐意接受。
Tom Ellis 2015年

仅在回答您的问题或为您工作时接受。如果这可以帮助相关案例,您甚至可以发表评论,说明如何进行。如果您认为自己的问题仍未解决,请告诉我们。
Erwin Brandstetter

1
那我不得不道歉,因为尽管我收到了看似出色的答案,但我不再致力于解决引发该问题的问题,因此我无处判断哪个是最佳答案,或者如果确实有任何答案真的很适合我的用例。如果有DBA.Stackexchange ettiquette,在这种情况下我应该遵循,请告诉我。
汤姆·埃利斯

Answers:


42

这在很大程度上取决于情况和确切的要求。考虑我对这个问题的评论

简单的解决方案

随着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;

有序的结果。

或使用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 1 FROM price p1
   WHERE  p1.good = p.good
   AND p1.the_date <= i.the_date
   AND p1.the_date >  p.the_date
   );

结果相同,但排序顺序任意-除非添加ORDER BY
根据数据分布,确切的要求和索引,这两者之一可能会更快。
通常,DISTINCT ON胜利者是您,您将获得排序结果。但是对于某些情况,其他查询技术却要快得多。见下文。

带有子查询以计算最大值/最小值的解决方案通常较慢。但是,带有CTE的变体通常较慢。

单纯的观点(如另一个答案所建议的那样)完全没有帮助Postgres表现。

SQL提琴。


正确的解决方案

字符串和排序规则

首先,您的表布局欠佳。这看似微不足道,但规范化架构可能会走很长一段路。

必须根据语言环境(尤其是COLLATION字符类型(text,,varchar...)进行排序。您的数据库很可能使用一些本地规则集(例如,在我的情况下:)。找出与:de_AT.UTF-8

SHOW lc_collate;

这会使排序和索引查找变慢。字符串(商品名称)越长,效果越差。如果您实际上并不关心输出中的排序规则(或根本不考虑排序顺序),那么添加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;

请注意我如何在两个地方添加排序规则。
在我的测试中,速度是以前的两倍,每行有2万行,并且具有非常基本的名称('good123')。

指数

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

CREATE INDEX inventory_good_date_desc_collate_c_idx
ON price(good COLLATE "C", the_date DESC);

确保阅读有关SO的相关答案的最后两章:

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

归一化

多余的字符串(好名字)也会使您的表和索引膨胀,从而使一切变慢。使用适当的表布局,您可以避免大多数问题。可能看起来像这样:

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));

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

CREATE INDEX price_good_date_desc_idx ON price(good, the_date DESC);

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

在Postgres 9.2或更高版本中,仅索引扫描的“覆盖索引”可能会有所帮助-尤其是如果您的表包含更多列,使该表大大大于覆盖索引。

这些结果查询要快得多:

不存在

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
   );

区别开

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;

SQL提琴。


更快的解决方案

如果那还不够快,那么可能会有更快的解决方案。

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

特别是对于每件商品很多价格的数据分发:

物化视图

如果您需要经常快速运行它,建议您创建一个实例化视图。我认为可以肯定地说,过去日期的价格和库存很少变化。计算一次结果然后将快照存储为实例化视图。

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


3
price_good_date_desc_idx您推荐的索引大大改善了我的类似查询的性能。我的查询计划从成本42374.01..42374.86降低到了0.00..37.12
cimmanon

@cimmanon:太好了!您的核心查询功能是什么?不存在?继续吗?通过...分组?
Erwin Brandstetter

使用DISTINCT ON
cimmanon

6

仅供参考,我使用的是mssql 2008,因此Postgres将没有“ include”索引。但是,使用下面显示的基本索引将在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;

然后,如果进行查询(例如使用左联接来查找没有最近定价日期的库存),您的查询就可以变得更简单,更易于处理其他类型的查询:

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

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

...所有扫描都是完整的。注意,哈希匹配的性能成本占据了总成本的大部分……并且我们知道表扫描和排序的速度很慢(与目标:索引查找相比)。

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

这显示出改善。嵌套循环(内部联接)操作不再占用查询的任何相关总成本。其余成本现在分散在索引查找中(扫描库存,因为我们提取了每个库存行)。但是我们仍然可以做得更好,因为查询可以提取数量和价格。为了获得该数据,在评估联接条件之后,必须执行查找。

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

现在,我们有了一个查询计划,其中查询的总成本在非常快的索引查找操作之间平均分配。这将接近所获得的一切。当然其他专家可以进一步改善这一点,但是该解决方案消除了两个主要问题:

  1. 它会在数据库中创建可理解的数据结构,以便于在应用程序的其他区域中进行组合和重用。
  2. 使用一些基本索引已将所有最昂贵的查询运算符从查询计划中排除。

3
这很好(对于SQL Server),但是针对不同的DBMS进行优化,尽管它具有相似之处,但也存在严重差异。
ypercubeᵀᴹ

@ypercube是真的。我添加了一些有关Postgres的条件。我的意图是,无论DBMS的特定功能如何,此处说明的大多数思想过程都将适用。
cocogorilla

答案非常深入,因此我将需要一些时间来尝试一下。我会让你知道我的生活。
汤姆·埃利斯

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;

这基本上等效于SQL-Server的APPLY,并且出于演示目的,在SQL-Fiddle上有一个有效的示例


5

正如Erwin和其他人所指出的那样,高效的查询取决于很多变量,而PostgreSQL非常努力地基于这些变量来优化查询执行。通常,您要先为清楚起见编写代码,然后在确定瓶颈后再进行性能修改。

此外,PostgreSQL还有很多技巧可以使事情变得更加高效(部分索引为一个),因此根据您的读/写负载,您可能可以通过仔细研究索引来对此进行优化。

尝试做的第一件事就是创建一个视图并将其加入:

CREATE VIEW most_recent_rows AS
SELECT good, max(date) as max_date
FROM inventory
GROUP BY good;

在执行以下操作时,此方法应表现良好:

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;

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

您可以做的第二件事是将一个most_recent bool列添加到库存表中,然后

create unique index on inventory (good) where most_recent;

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

同样,很多情况取决于适当的索引。对于最近的日期查询,您可能应该有一个日期索引,并且可能有一个多列的索引,该索引以日期开头,并包括您的加入条件。

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

提供的数据库设计没有将IME与ERP和会计系统一起实际使用。它可以在假设的完美定价模型中工作,在该模型中,给定产品在给定日期销售的所有商品都具有相同的价格。然而,这并非总是如此。诸如货币兑换之类的东西甚至都不是这种情况(尽管某些模型假装确实如此)。如果这是人为的例子,目前尚不清楚。如果这是一个真实的例子,则在数据级别的设计存在更大的问题。我在这里假设这是一个真实的例子。

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

 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;

使用价格指数(有效期,日期),效果会很好。

我这是一个人为的示例,也许更接近您正在研究的内容会有所帮助。


most_recent方法绝对适用于最新价格。不过,OP似乎需要对于每个库存日期的最新价格。
Erwin Brandstetter

好点子。重新阅读虽然我发现了所建议的数据存在一些实际的实际缺陷,但是我无法确定这是否只是一个人为的示例。作为一个人为的例子,我无法告诉我们缺少了什么。也许也有更新指出这一点。
克里斯·特拉弗斯

@ChrisTravers:这是一个人为的示例,但是我不能随意发布正在使用的实际模式。也许您可以说说您发现的实际缺陷。
汤姆·埃利斯

我不认为这是准确的,但是担心这个寓言中丢失的问题。稍微靠近一点会有所帮助。问题在于定价时,某天的价格很可能是默认价格,因此,您不会将其仅用作交易条目的默认报告,因此您感兴趣的查询通常只在几行之内。时间。
克里斯·特拉弗斯

3

另一种方法是使用窗口函数lead()获取价格中每一行的日期范围,然后between在加入库存时使用。我实际上已经在现实生活中使用了它,但是主要是因为这是我解决该问题的第一个想法。

with cte as (
  select
    good,
    price,
    date,
    coalesce(lead(date) over(partition by good order by date) - 1
            ,Now()::date) as ndate
  from
    price
)

select * from inventory i join cte on
  (i.good = cte.good and i.date between cte.date and cte.ndate)

SqlFiddle


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)

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


不幸的是,似乎并没有加快速度。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.