我有一个表,例如
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通用的答案。
#1 楼
这在很大程度上取决于情况和确切的要求。考虑我的评论。简单解决方案
在Postgres中使用
DISTINCT ON
: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;
返回的行是有序的。请参阅:
在每个GROUP BY组中选择第一行?
还是在标准SQL中与
NOT EXISTS
配合使用(适用于我知道的每个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
);
相同的结果,但是具有任意的排序顺序-除非添加
ORDER BY
。根据数据分布,确切的要求和索引,这两种方法中的任何一种都可能更快。请参阅:
如何(或我可以)在多列上选择DISTINCT?
每件商品只有几行,
DISTINCT ON
通常会更快,并且得到排序结果顶部。但是对于某些情况,其他查询技术却要快得多。参见下文。带有子查询以计算最大值/最小值的解决方案通常较慢。但是,带有CTE的变体通常较慢。 (使用Postgres 12改进了CTE。)
纯视图(如另一个答案所建议的那样)根本无法提高Postgres的性能。
db <> fiddle hereOld sqlfiddle
正确的解决方案
字符串和排序规则
首先,您的表布局不太理想。这看似微不足道,但规范化架构可能会走很长一段路。
根据当前的
text
进行字符类型(varchar
,COLLATION
等)的排序。通常,您的数据库将使用一些本地规则集,例如我的情况: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);
阅读上面链接的相关答案的后两章。 br />您甚至可以在同一列上使用具有不同排序规则的多个索引-如果您还需要在其他查询中根据其他(或默认)排序规则对商品进行排序。
Normalize
冗余字符串(名称为好)膨胀的表和索引,这会使一切变慢。适当的表布局可以避免大多数问题。可能看起来像这样:
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“仅索引”扫描的“覆盖索引”可以提供更多帮助-特别是在表包含其他索引的情况下列,使表大大大于索引。
这些结果查询快得多:
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;
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
);
小提琴此处OLD sqliddle
更快的解决方案
如果仍然不够快,可能会有更快的解决方案。
递归CTE /
JOIN LATERAL
/相关子查询特别适用于每商品价格昂贵的数据分发:
优化GROUP BY查询以检索每个用户的最新记录
材料化视图
如果需要运行通常,我建议您创建一个实例化视图。我认为可以肯定地说,过去日期的价格和库存很少变化。一次计算结果并将快照存储为实例化视图。
Postgres 9.3+具有对实例化视图的自动支持。您可以轻松地在旧版本中实现基本版本。
评论
您推荐的price_good_date_desc_idx索引极大地提高了我的类似查询的性能。我的查询计划从42374.01..42374.86的费用降低到0.00..37.12!
–cimmanon
13年12月11日在16:04
@cimmanon:太好了!您的核心查询功能是什么?不存在?继续吗?通过...分组?
–欧文·布兰德斯特(Erwin Brandstetter)
2013年12月12日下午3:55
使用DISTINCT ON
–cimmanon
2013年12月12日14:18在
#2 楼
正如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;
然后您可以加入。该查询最终将针对基础表将视图加入视图,但是假设您有一个唯一的索引(日期,该顺序正确),那么您应该行得通(因为这将是简单的缓存查找)。这在查找几行后会很好用,但是如果您要消化数百万的商品价格,效率将非常低。
您可以做的第二件事是将most_recent添加到库存表中bool列和
create unique index on inventory (good) where most_recent;
然后,当插入商品的新行时,您想使用触发器将most_recent设置为false。这样会增加更多的复杂性和错误的机会,但是很有帮助。
同样,很多情况取决于适当的索引。对于最近的日期查询,您可能应该有一个日期索引,并且可能是一个以日期开头并包括您的加入条件的多列索引。
更新下面的Per Erwin评论,看来我误会了这个。重新阅读这个问题,我根本不确定所要问的是什么。我想在更新中提及我看到的潜在问题,以及为什么这让我不清楚。
提供的数据库设计没有将IME与ERP和会计系统一起实际使用。它可以在假设的完美定价模型中工作,在该模型中,给定产品在给定日期销售的所有商品都具有相同的价格。然而,这并非总是如此。诸如货币兑换之类的东西甚至都不是这种情况(尽管某些模型假装确实如此)。如果这是人为的例子,目前尚不清楚。如果这是一个真实的例子,则在数据级别的设计存在更大的问题。我要在这里假设这是一个真实的例子。
您不能假设仅日期指定了给定商品的价格。任何业务的价格都可以按交易对手甚至有时按交易进行协商。因此,您确实应该将价格存储在实际处理库存的表中(库存表)。在这种情况下,您的日期/商品/价格表仅指定了基础价格,该基础价格可能会根据协商而更改。在这种情况下,此问题已从报告问题变为事务性问题,并且一次处理每个表的一行。例如,您可以在给定的日期查找给定产品的默认价格,如下所示:
表现很好。
我这是一个人为的示例,也许更接近您正在研究的内容会有所帮助。
评论
most_recent方法绝对适用于最新价格。不过,OP似乎需要相对于每个库存日期的最新价格。
–欧文·布兰德斯特(Erwin Brandstetter)
2013年9月10日下午3:23
好点子。重新阅读虽然我发现了所建议的数据存在一些实际的实际缺陷,但是我无法确定这是否只是一个人为的示例。作为一个人为的例子,我无法告诉我们缺少了什么。也许也有更新指出这一点。
–克里斯·特拉弗斯(Chris Travers)
2013年9月10日下午5:05
@ChrisTravers:这是一个人为的示例,但是我不能随意发布正在使用的实际模式。也许您可以说说您发现的实际缺陷。
–汤姆·埃利斯(Tom Ellis)
2013年9月11日下午16:35
我不认为这是准确的,但是担心这个寓言中丢失的问题。稍微靠近一点会有所帮助。问题在于定价时,某天的价格很可能是默认价格,因此,您不会将其仅用作交易输入的默认报告,因此,您感兴趣的查询通常只在几行之内时间。
–克里斯·特拉弗斯(Chris Travers)
2013年9月12日7:56
#3 楼
仅供参考,我使用的是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;
这将产生以下结果执行计划:
http://sqlfiddle.com/#!3/24f23/1
...所有扫描均经过完整排序。请注意,哈希匹配的性能成本占据了总成本的大部分...并且我们知道表扫描和排序速度很慢(与目标:索引查找相比)。
现在,将基本索引添加到帮助您的联接中使用的标准(我不认为这些是最佳索引,但它们说明了这一点):
http://sqlfiddle.com/#!3/5ec75/1
这表明有所改善。嵌套循环(内部联接)操作不再占用查询的任何相关总成本。其余成本现在分散在索引查找中(扫描库存,因为我们提取了每个库存行)。但是我们仍然可以做得更好,因为查询可以提取数量和价格。要获取该数据,请在评估联接条件之后,必须执行查找。
最后一次迭代在索引上使用“ include”以使计划更容易滑倒并获取额外请求的数据直接脱离索引本身。因此查找消失了:
http://sqlfiddle.com/#!3/5f143/1
现在,我们有了一个查询计划,其中查询的总成本在非常快的索引查找操作之间平均分配。这将接近所获得的一切。当然其他专家可以进一步改善这一点,但是该解决方案消除了两个主要问题:
它在您的数据库中创建了可理解的数据结构,更易于在其他数据库中进行组合和重用。应用程序领域。
使用一些基本索引已将所有最昂贵的查询运算符从查询计划中排除。
评论
这很好(对于SQL Server),但是针对不同的DBMS进行优化,尽管它具有相似之处,但也存在严重差异。
–超立方体ᵀᴹ
2013年9月9日17:28
@ypercube是的。我添加了一些有关Postgres的条件。我的意图是,无论DBMS的特定功能如何,此处说明的大多数思想过程都将适用。
–cocogorilla
2013年9月9日18:13
答案非常深入,因此需要花费一些时间进行尝试。我会让你知道我的生活。
–汤姆·埃利斯(Tom Ellis)
2013年9月10日在12:46
#4 楼
如果您碰巧拥有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 楼
另一种方法是使用窗口函数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
#6 楼
使用从库存到价格的联接,其联接条件将价格表中的记录限制为仅在库存日期或之前的记录,然后提取最大日期,并且该日期是该子集中的最高日期所以对于您的库存价格:
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)
如果任何指定商品的价格在同一天多次变化,这些列中的日期和时间,您可能需要对联接施加更多限制以仅选择价格更改记录之一。
评论
不幸的是,似乎并没有加快速度。
–汤姆·埃利斯(Tom Ellis)
2013年9月9日15:50
评论
由于效率问题,因此选择迁移到DBA.SE。我们可以用几种不同的方式来编写查询,但这并不能使其更快。您实际上一次查询一次就需要所有商品吗?似乎不太可能的要求?更常见的是,将检索特定日期的价格或特定商品(特定日期)的价格。这些替代查询可以更轻松地从(适当的)索引中受益。我们还需要知道:基数(每个表中有几行?),包括完整的表定义。数据类型,约束,索引,...(在psql中使用\ d tbl),您的Postgres版本和min。 /最大每件商品的价格数量。
@ErwinBrandstetter您要我接受答案吗?我真的没有资格知道哪个是最好的,尽管您的投票最多,但我很乐意接受。
仅在回答您的问题或为您工作时接受。如果这可以帮助相关案例,您甚至可以发表评论,说明如何进行。如果您认为自己的问题仍未解决,请告诉我们。
那我不得不道歉,因为尽管我收到了看似出色的答案,但我不再致力于解决引发该问题的问题,因此我无处判断哪个是最佳答案,或者如果确实有任何答案真的很适合我的用例。如果在这种情况下我应该遵循一些DBA.Stackexchange ettiquette,请让我知道。