nee*_*zer 6 postgresql performance timestamp greatest-n-per-group postgresql-performance
我正在进入我的神秘查询的下一个级别。看起来在一个存在的内部有一个子选择,但在同一个表上。我认为这可能可以通过INNER JOIN
更高的方式简化。
使用 PostgreSQL 9.4.2。
表定义 ( /d+
):https : //gist.github.com/neezer/879f5d3649ca1903c6f3
基数:
billing_pricequote
: 1,462,625 行
billing_pricequotestatus
: 3,331,657 行
billing_lineitem
: 43,687,855 行
这是原始查询,不建议对里面的子EXISTS
查询进行修改:
SELECT i.quote_id, i.acct_id AS account_id, SUM(i.delta_amount) AS amt
FROM billing_lineitem i
INNER JOIN billing_pricequote pq ON i.quote_id = pq.id
WHERE pq.date_applied AT TIME ZONE 'PST' BETWEEN '2016-02-02T00:00:00'::timestamp
AND '2016-03-03T22:27:41.734102-08:00'::timestamptz
AND EXISTS(
SELECT s1.quote_id
FROM billing_pricequotestatus s1
INNER JOIN (
SELECT DISTINCT ON (quote_id) quote_id, MAX(created_at) AS max_created_at
FROM billing_pricequotestatus
WHERE quote_id=i.quote_id
GROUP BY quote_id, created_at
ORDER BY quote_id, created_at DESC
) AS s2
ON s1.quote_id = s2.quote_id
AND s1.created_at = s2.max_created_at
WHERE s1.name IN ('adjustment','payment','billable')
)
GROUP BY i.quote_id, i.acct_id
;
Run Code Online (Sandbox Code Playgroud)
我注意到看着怪异的部分是SELECT
上billing_pricequotestatus
,然后在里面的同桌另一子查询INNER JOIN
。
我尝试通过我的其他 SO 帖子中的修改来改变它:
SELECT i.quote_id, i.acct_id AS account_id, SUM(i.delta_amount) AS amt
FROM billing_lineitem i
INNER JOIN billing_pricequote pq ON i.quote_id = pq.id
WHERE pq.date_applied AT TIME ZONE 'PST' BETWEEN '2016-02-02T00:00:00'::timestamp
AND '2016-03-03T22:27:41.734102-08:00'::timestamptz
AND EXISTS(
SELECT quote_id, MAX(created_at) AS max_created_at
FROM billing_pricequotestatus
WHERE quote_id=i.quote_id
AND name IN ('adjustment','payment','billable')
GROUP BY quote_id
)
GROUP BY i.quote_id, i.acct_id
;
Run Code Online (Sandbox Code Playgroud)
这将我的执行时间减少了一半(~40 秒到~20 秒),但产生的结果略有不同(原始查询返回 28,895 行,但我的新查询返回 28,917 行)。我不清楚为什么我的修改没有产生等效的输出(它需要)。
EXPLAIN ANALYZE
对于两个查询
解释原始查询 depesz.com 的分析。
非常感谢您对此的任何帮助/指导!
我尝试用 a 更新@ypercube?? 的答案LATERAL JOIN
,并且性能似乎大致相同(每个人的获胜次数都相同,不到一秒):
SELECT i.quote_id, i.acct_id AS account_id, SUM(i.delta_amount) AS amt
FROM billing_lineitem i
INNER JOIN billing_pricequote pq ON i.quote_id = pq.id
LEFT JOIN LATERAL
( SELECT name
FROM billing_pricequotestatus
WHERE quote_id = i.quote_id
ORDER BY created_at DESC
LIMIT 1
) pqs ON true
WHERE pq.date_applied AT TIME ZONE 'PST' BETWEEN '2016-02-02T00:00:00'::timestamp
AND '2016-03-03T22:27:41.734102-08:00'::timestamptz
AND pqs.name IN ('adjustment', 'payment', 'billable')
GROUP BY i.quote_id, i.acct_id
;
Run Code Online (Sandbox Code Playgroud)
有没有其他建议可以让这个低于 10 秒?
据我了解,您的子查询的目的是:
选择最新相关条目billing_pricequotestatus
具有合格name
.
我不清楚为什么我的修改没有产生等效的输出
第一个查询从中选择最新的行billing_pricequotestatus
并检查是否name
符合条件 ( name IN ('adjustment','payment','billable')
)。
第二个查询是倒退的:它检查任何符合条件的行name
(不仅仅是最后一个)。此外,在EXISTS
半连接中计算聚合也没有意义。你不想那样。它不是等价的。
因此,您会从第二个查询中获得更多行。
这个谓词一团糟。低效且可能不正确 - 或者至少是一个滴答作响的炸弹:
WHERE pq.date_applied AT TIME ZONE 'PST'
BETWEEN '2016-02-02T00:00:00'::timestamp
AND '2016-03-03T22:27:41.734102-08:00'::timestamptz
Run Code Online (Sandbox Code Playgroud)
该列date_applied
的类型为timestamptz
。该构造AT TIME ZONE 'PST'
将其转换为类型,timestamp
并按硬编码为时区缩写“PST”的时间偏移进行移位 - 这是一个糟糕的举动。它使表达式不可sargable。这更昂贵,更重要的是,排除了在date_applied
.
更糟糕的是,时区缩写'PST'
不知道 DST 或任何历史性的时间变化。如果您的时区有(或过去有)夏令时,并且您的时区跨越不同的 DST 时段,则您当前的表达很可能不正确:
您需要使用适用的时区名称而不是缩写来获得一致的本地时间 - 这甚至更昂贵。
还有另一个问题:虽然列值被硬编码的时间偏移量 ('PST') 移动,你的上限'2016-03-03T22:27:41.734102-08:00'::timestamptz
是作为timestamptz
和静默强制匹配数据类型提供的timestamp
。由于未提供明确的时间偏移,因此强制转换默认为当前 session的时区。因此,您可以根据会话的当前时区设置获得不同的结果。我想不出一个有意义的用例。
不要做任何这些。不要翻译timestamptz
列date_applied
于当地时间可言,像你这样不混合数据类型和不不同的方式来铸造混合。相反,按原样使用该列并提供timestamptz
参数。
SELECT i.quote_id, i.acct_id AS account_id, sum(i.delta_amount) AS amt
FROM billing_pricequote pq
JOIN LATERAL (
SELECT name
FROM billing_pricequotestatus
WHERE quote_id = pq.id
ORDER BY created_at DESC
LIMIT 1
) pqs ON pqs.name IN ('adjustment', 'payment', 'billable')
JOIN billing_lineitem i ON i.quote_id = pq.id
WHERE pq.date_applied BETWEEN (timestamp '2016-02-02T00:00:00' AT TIME ZONE 'PST') -- !
AND timestamptz '2016-03-03T22:27:41.734102-08:00'
GROUP BY 1,2;
Run Code Online (Sandbox Code Playgroud)
注意LATERAL
连接,而不是,让它LEFT JOIN
INNER JOIN
立即实现你的谓词。
或者使用@ypercube 概述的等效相关子查询。不确定哪个更快。
另外请注意,我的基础LATERAL JOIN
上billing_pricequote
-前加入到大表billing_lineitem
。这样我们可以尽早消除行,这应该更便宜。
目前,您将获得:
对 billing_pricequote pq 进行 Seq 扫描
仅选择了 150 万行中的 7 万行,大约为 5%。索引date_applied
可能会有所帮助,但作用不大。然而,这种多列索引应该有助于大幅如果你能得到仅索引扫描的吧:
CREATE INDEX foo ON billing_pricequotestatus (quote_id, created_at DESC, name);
Run Code Online (Sandbox Code Playgroud)
使用name_id
代替比name
下面建议的更有效。
Postgres 高估了您的时间范围的选择性:
(cost=0.00..88,546.50 rows=7,313 width=4) (实际时间=2.353..767.408 rows=70,623 loops=1)
增加列的统计目标可能会有所帮助date_applied
。详情在这里:
示例billing_pricequotestatus
:
name
似乎是几种可能的类型之一。将更多规范化并仅使用 4 字节integer
引用查找表而不是varchar(20)
在 3.3M 行中重复,这将有助于提高性能。此外,像我演示的那样重新排序列(如果可能)会有所帮助:
Column | Type | Modifiers
------------+--------------------------+------------------------------------------
id | integer | not null default nextval('...
quote_id | integer | not null
created_at | timestamp with time zone | not null
updated_at | timestamp with time zone | not null
name_id | integer | not null REFERENCES name_table(name_id)
notes | text | not null
Run Code Online (Sandbox Code Playgroud)
请参阅上面关于对齐和填充的链接。要测量行大小:
而“名称”不是一个好的标识符。我会用一些描述性的东西来代替。
我认为EXISTS
子查询:
AND EXISTS(
SELECT s1.quote_id
FROM billing_pricequotestatus s1
INNER JOIN
( SELECT DISTINCT ON (quote_id) quote_id, MAX(created_at) AS max_created_at
FROM billing_pricequotestatus
WHERE quote_id=i.quote_id
GROUP BY quote_id, created_at
ORDER BY quote_id, created_at DESC
) AS s2
ON s1.quote_id = s2.quote_id
AND s1.created_at = s2.max_created_at
WHERE s1.name IN ('adjustment','payment','billable')
)
Run Code Online (Sandbox Code Playgroud)
可以简化为相关子查询:
AND ( SELECT name
FROM billing_pricequotestatus
WHERE quote_id = i.quote_id
ORDER BY created_at DESC
LIMIT 1
) IN ('adjustment', 'payment', 'billable')
Run Code Online (Sandbox Code Playgroud)
索引(quote_id, created_at DESC, name)
将有很大帮助。
如果你的Postgres版本是9.3以上的,也可以用LATERAL
join来写,可能会提高效率。
您在 SO 上发布的第一个问题不包括 ,WHERE quote_id = i.quote_id
因此其他人不可能知道子查询是相关的。您在那里得到的答案对于这种情况是正确的。
归档时间: |
|
查看次数: |
1351 次 |
最近记录: |