【问题标题】:Query last N related rows per row每行查询最后 N 个相关行
【发布时间】:2014-11-15 10:39:14
【问题描述】:

我有以下查询,它为每个 station 获取最新 N observationsid

SELECT id
FROM (
  SELECT station_id, id, created_at,
         row_number() OVER(PARTITION BY station_id
                           ORDER BY created_at DESC) AS rn
  FROM (
      SELECT station_id, id, created_at
      FROM observations
  ) s
) s
WHERE rn <= #{n}
ORDER BY station_id, created_at DESC;

我在idstation_idcreated_at 上有索引。

这是我想出的唯一解决方案,可以为每个站点获取多条记录。但是它很慢(81000 条记录的表需要 154.0 毫秒)。

如何加快查询速度?

【问题讨论】:

  • 在这种情况下,分区将无济于事。您的观察表小于 8MB。它将适合服务器的内存。您的查询计划包含对观察表的 seq 扫描。问题:从数据库中查询最新的实时数据有多重要?如果您只能查询不超过 2 小时的数据,会不会有问题?你能告诉我们观察表中有多少行吗? (只是幅度)
  • 您可能希望使用 hash 在单独的列上创建索引。 CREATE INDEX name ON table USING hash (column);
  • 您有 81000 条记录。关键问题: 1.) 有多少不同的站点? 2.) 你有一张列出所有电台的表格吗?如果没有,创建和维护一个有什么问题吗? 3.) 总是:你的 Postgres 版本? 4.) observations 的表定义(CREATE 语句或 psql 中的\d observations)?一个更快的查询应该是可能的,这取决于站的数量......
  • 更多细节:这是一个开源的 Rails 应用程序,可从廉价站点收集风数据。目前只有大约 3 个站点每 5 分钟采样一次(~ 288 个观测天,当 3G 网络参差不齐时会更少。) 直播站点:blast.nugithub.com/remote-wind/remote-wind

标签: sql performance postgresql indexing query-optimization


【解决方案1】:

仅当您不需要查询最新的实时数据时,这才是一个很好的答案。

准备(需要 postgresql 9.3)

drop materialized view test;
create materialized view test as select * from (
  SELECT station_id, id, created_at,
      row_number() OVER(
          PARTITION BY station_id
          ORDER BY created_at DESC
      ) as rn
  FROM (
      SELECT
          station_id,
          id,
          created_at
      FROM observations
  ) s
 ) q WHERE q.rn <= 100 -- use a value that will be your max limit number for further queries
ORDER BY station_id, rn DESC ;


create index idx_test on test(station_id,rn,created_at);

如何查询数据:

select * from test where rn<10 order by station_id,created_at;

您的原始查询在我的机器上是 281 毫秒,而这个新查询是 15 毫秒。

如何使用新数据更新视图:

refresh materialized view test;

我有另一个解决方案,它不需要物化视图并且可以处理实时的最新数据。但鉴于您不需要最新数据,这种物化视图的效率要高得多。

【讨论】:

  • 大表的最新记录很难(几乎不可能)用物化视图覆盖,这更适合只读数据。
  • 这就是为什么我要问这样的问题,比如会有多少数据。他的观察表是 8MB。离做大还差得很远。如果他将更新/删除行,或者只是向该表添加新行,这也很有趣。我有一个轻量级的解决方案,如果只添加行,而不是更新或删除行。另一个使用索引,但会减慢新观察的插入速度。总会有一个权衡。
  • 你是对的权衡。艺术就是在这些交易中获得一笔好交易。物化视图并不是最新行的最佳工具,因为它只涵盖了过去定义的快照-除非您自动刷新每个新条目,否则要付出高昂的代价。
  • 观测数据几乎是只读的,表格仍然相对较小(80k 行),但如果我们获得一些赞助资金来建造/放置更多的气象站,可能会成倍增长。
  • 这意味着您永远不会更新或删除此表中的行。在这种情况下,最好在插入行时从触发器中更新“行号”。然后你可以在行号上创建一个索引,你的整个查询就变成了一个简单的索引扫描...
【解决方案2】:

索引

首先,多列索引会有所帮助:

CREATE INDEX observations_special_idx
ON observations(station_id, created_at DESC, id)

created_at DESC 稍微好一点,但如果没有DESC,索引仍会以几乎相同的速度向后扫描。

假设created_at 定义为NOT NULL,否则在索引查询中考虑DESC NULLS LAST

最后一列id 仅在您从中获得index-only scan 时才有用,如果您不断添加大量新行,这可能不起作用。在这种情况下,请从索引中删除 id

更简单的查询(仍然很慢)

简化您的查询,内部子选择无济于事:

SELECT id
FROM  (
  SELECT station_id, id, created_at
       , row_number() OVER (PARTITION BY station_id
                            ORDER BY created_at DESC) AS rn
  FROM   observations
  ) s
WHERE  rn <= #{n}  -- your limit here
ORDER  BY station_id, created_at DESC;

应该会快一点,但还是很慢。

快速查询

  • 假设您有相对少数个观测站和相对许多个观测站。
  • 还假设station_id id 定义为NOT NULL

真正快速,您需要松散索引扫描(尚未在 Postgres 中实现)。相关答案:

如果您有一个单独的 stations 表(这似乎很可能),您可以使用 JOIN LATERAL 模拟它(Postgres 9.3+):

SELECT o.id
FROM   stations s
CROSS  JOIN LATERAL (
   SELECT o.id
   FROM   observations o
   WHERE  o.station_id = s.station_id  -- lateral reference
   ORDER  BY o.created_at DESC
   LIMIT  #{n}  -- your limit here
   ) o
ORDER  BY s.station_id, o.created_at DESC;

如果您没有stations 的表,则最好创建并维护一个表。可能添加外键引用以强制关系完整性。

如果这不是一个选项,您可以即时提取这样的表。简单的选择是:

SELECT DISTINCT station_id FROM observations;
SELECT station_id FROM observations GROUP BY 1;

但是两者都需要顺序扫描并且速度很慢。使用 recursive CTE 使 Postgres 使用上述索引(或任何以 station_id 为前导列的 btree 索引):

WITH RECURSIVE stations AS (
   (                  -- extra pair of parentheses ...
   SELECT station_id
   FROM   observations
   ORDER  BY station_id
   LIMIT  1
   )                  -- ... is required!
   UNION ALL
   SELECT (SELECT o.station_id
           FROM   observations o
           WHERE  o.station_id > s.station_id
           ORDER  BY o.station_id
           LIMIT  1)
   FROM   stations s
   WHERE  s.station_id IS NOT NULL  -- serves as break condition
   )
SELECT station_id
FROM   stations
WHERE  station_id IS NOT NULL;      -- remove dangling row with NULL

在上述简单查询中将其用作stations 表的直接替换

WITH RECURSIVE stations AS (
   (
   SELECT station_id
   FROM   observations
   ORDER  BY station_id
   LIMIT  1
   )
   UNION ALL
   SELECT (SELECT o.station_id
           FROM   observations o
           WHERE  o.station_id > s.station_id
           ORDER  BY o.station_id
           LIMIT  1)
   FROM   stations s
   WHERE  s.station_id IS NOT NULL
   )
SELECT o.id
FROM   stations s
CROSS  JOIN LATERAL (
   SELECT o.id, o.created_at
   FROM   observations o
   WHERE  o.station_id = s.station_id
   ORDER  BY o.created_at DESC
   LIMIT  #{n}  -- your limit here
   ) o
WHERE  s.station_id IS NOT NULL
ORDER  BY s.station_id, o.created_at DESC;

这应该仍然比您的速度快 几个数量级

db小提琴here
sqlfiddle

【讨论】:

  • 感谢您提供非常详细的答案,今晚将尝试。
  • 运行时间约为 35 毫秒,这是一个巨大的改进。谢谢!
  • @papirtiger:哪一个? n = ?有或没有stations 表?您是否创建了索引(并运行ANALYZE checks)?您在EXPLAIN ANALYZE 输出中看到仅索引扫描吗?改进很好,但我看到了更好的结果。
  • 我有一个stations 表(o.stations_id 是外键)所以我尝试了第一个JOIN LATERAL 查询。添加observations_special_idx 似乎没有任何重大影响。我只在本地机器上尝试过,因为我需要等待我的合作者更新 Heroku 上的 postgres。
  • @papirtiger:除非表格不够 vacuumed 或大量写入,否则您应该看到Index Only Scan using observations_special_idx on observations 就像在小提琴中一样(检查“查看执行计划”)。您在EXPLAIN 输出中看到了吗?请注意,我使用DESC 进一步改进了索引,但这应该不会有太大的不同。索引应该在任何情况下使用,并且具有主要效果。
猜你喜欢
  • 2011-11-22
  • 2018-12-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-08
  • 1970-01-01
  • 1970-01-01
  • 2013-09-12
相关资源
最近更新 更多