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 中根本没有帮助性能。
首先,您的表格布局是次优的。这可能看起来微不足道,但规范化您的架构可以大有帮助。
按字符类型 ( 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)
如果这仍然不够快,可能会有更快的解决方案。
JOIN LATERAL//相关子查询特别是对于每个商品具有多个价格的数据分布:
如果您需要经常快速地运行它,我建议您创建一个物化视图。我认为可以安全地假设,过去日期的价格和库存很少变化。计算一次结果并将快照存储为物化视图。
Postgres 9.3+ 自动支持物化视图。您可以轻松地在旧版本中实现基本版本。
仅供参考,我使用了 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

现在我们有一个查询计划,其中查询的总成本在非常快速的索引查找操作中平均分配。这将接近于尽善尽美。当然其他专家可以进一步改进这一点,但解决方案清除了几个主要问题:
正如 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)
使用价格指数(好,日期),这将表现良好。
我这是一个人为的例子,也许更接近你正在做的事情会有所帮助。
如果您碰巧拥有 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)
如果任何指定商品的价格在同一天更改多次,并且这些列中实际上只有日期而没有时间,则可能需要对联接应用更多限制以仅选择一个价格更改记录。
| 归档时间: |
|
| 查看次数: |
131118 次 |
| 最近记录: |