【问题标题】:Postgresql count+sort performancePostgresql 计数+排序性能
【发布时间】:2012-10-16 15:57:27
【问题描述】:

我使用 postgresql 和 psycopg2 构建了一个小型库存系统。一切都很好,除了当我想创建内容的汇总摘要/报告时,由于 count()'ing 和排序,我得到了非常糟糕的性能。

数据库架构如下:

创建表主机
(
        id 序列主键,
        名称 VARCHAR(255)
);
创建表项
(
        id 序列主键,
        说明文字
);
创建表 host_item
(
        id 序列主键,
        host INTEGER REFERENCES hosts(id) ON DELETE CASCADE ON UPDATE CASCADE,
        item INTEGER REFERENCES items(id) ON DELETE CASCADE ON UPDATE CASCADE
);

还有一些其他字段,但这些字段不相关。

我想提取 2 个不同的报告: - 所有主机的列表,每个主机的项目数,从最高排序 到最低计数 - 包含每个主机数量的所有项目列表,从最高到最低计数排序

我为此目的使用了 2 个查询:

具有主机数量的项目:

SELECT i.id, i.description, COUNT(hi.id) AS count
从项目 AS i
LEFT JOIN host_item AS hi
开(i.id=hi.item)
按 i.id 分组
ORDER BY 计数 DESC
限制 10;

具有项目数的主机:

SELECT h.id, h.name, COUNT(hi.id) AS 计数
FROM 主机 AS h
LEFT JOIN host_item AS hi
开(h.id=hi.host)
按隐藏组分组
ORDER BY 计数 DESC
限制 10;

问题是:查询在返回任何数据之前运行 5-6 秒。因为这是一个基于 Web 的应用程序,所以 6 秒是不可接受的。该数据库包含大约 50k 主机、1000 个项目和 400 000 个主机/项目关系,并且在使用应用程序时(或者如果)可能会显着增加。

玩了之后,我发现通过删除“ORDER BY count DESC”部分,两个查询都会立即执行,没有任何延迟(完成查询不到 20 毫秒)。

有什么方法可以优化这些查询,以便我可以毫不拖延地对结果进行排序?我正在尝试不同的索引,但是看到计数被计算出来,可以为此使用索引。我读过 postgresql 中的 count()'ing 很慢,但它的排序给我带来了问题......

我目前的解决方法是将上述查询作为每小时作业运行一次,将结果放入一个新表中,该表在计数列上有一个索引以便快速查找。

我使用 Postgresql 9.2。

更新:按顺序查询计划:)

EXPLAIN ANALYZE
SELECT h.id, h.name, COUNT(hi.id) AS count
FROM hosts AS h
LEFT JOIN host_item AS hi
ON (h.id=hi.host)
GROUP BY h.id
ORDER BY count DESC
LIMIT 10;


 Limit  (cost=699028.97..699028.99 rows=10 width=21) (actual time=5427.422..5427.424 rows=10 loops=1)
   ->  Sort  (cost=699028.97..699166.44 rows=54990 width=21) (actual time=5427.415..5427.416 rows=10 loops=1)
         Sort Key: (count(hi.id))
         Sort Method: top-N heapsort  Memory: 25kB
         ->  GroupAggregate  (cost=613177.95..697840.66 rows=54990 width=21) (actual time=3317.320..5416.440 rows=54990 loops=1)
               ->  Merge Left Join  (cost=613177.95..679024.94 rows=3653163 width=21) (actual time=3317.267..5025.999 rows=3653163 loops=1)
                     Merge Cond: (h.id = hi.host)
                     ->  Index Scan using hosts_pkey on hosts h  (cost=0.00..1779.16 rows=54990 width=17) (actual time=0.012..15.693 rows=54990 loops=1)
                     ->  Materialize  (cost=613177.95..631443.77 rows=3653163 width=8) (actual time=3317.245..4370.865 rows=3653163 loops=1)
                           ->  Sort  (cost=613177.95..622310.86 rows=3653163 width=8) (actual time=3317.199..3975.417 rows=3653163 loops=1)
                                 Sort Key: hi.host
                                 Sort Method: external merge  Disk: 64288kB
                                 ->  Seq Scan on host_item hi  (cost=0.00..65124.63 rows=3653163 width=8) (actual time=0.006..643.257 rows=3653163 loops=1)
 Total runtime: 5438.248 ms





EXPLAIN ANALYZE
SELECT h.id, h.name, COUNT(hi.id) AS count
FROM hosts AS h
LEFT JOIN host_item AS hi
ON (h.id=hi.host)
GROUP BY h.id
LIMIT 10;


 Limit  (cost=0.00..417.03 rows=10 width=21) (actual time=0.136..0.849 rows=10 loops=1)
   ->  GroupAggregate  (cost=0.00..2293261.13 rows=54990 width=21) (actual time=0.134..0.845 rows=10 loops=1)
         ->  Merge Left Join  (cost=0.00..2274445.41 rows=3653163 width=21) (actual time=0.040..0.704 rows=581 loops=1)
               Merge Cond: (h.id = hi.host)
               ->  Index Scan using hosts_pkey on hosts h  (cost=0.00..1779.16 rows=54990 width=17) (actual time=0.015..0.021 rows=11 loops=1)
               ->  Index Scan Backward using idx_host_item_host on host_item hi  (cost=0.00..2226864.24 rows=3653163 width=8) (actual time=0.005..0.438 rows=581 loops=1)
 Total runtime: 1.143 ms

更新:这个问题的所有答案对于学习和理解 Postgres 的工作原理都非常有用。这个问题似乎没有任何明确的解决方案,但我非常感谢您提供的所有优秀答案,我将在以后使用 Postgresql 的工作中使用这些答案。非常感谢你们!

【问题讨论】:

  • 您能否同时发布原始查询和删除了ORDER BY 子句的查询的EXPLAIN ANALYZE 输出?
  • 根据要求更新了分析数据。一位朋友在 MS SQL Server 上运行了相同的查询,并表示由于更好地跟踪统计信息,它立即返回相同的结果。也许是个愚蠢的问题,但与 Postgresql 相比,SQL Server 是否更擅长进行这种聚合?
  • 请在查询/查询上运行EXPLAIN (ANALYZE, BUFFERS)。将每个计划粘贴到explain.depesz.com 并在此处链接到它,这样我们就可以看到实际发生的情况。要求人们在没有计划和统计估计的情况下优化查询只会让你得到更多有根据的猜测。使用 PostgreSQL 9.2 的仅索引扫描和相对较小的数据集,我看不出有任何理由让这些扫描速度如此之慢。它还有助于解释您在哪台计算机上运行查询、存储是什么(RAID10?SSD?单硬盘?)、RAM 和非默认 postgresql.conf 设置。
  • @invictus ... 还有其他的吗?您是否更改了任何默认的postgresql.conf 设置? (你应该有)等?请立即查看之前的评论。

标签: sql postgresql aggregate postgresql-performance


【解决方案1】:

@Gordon 和 @willglynn 提供了很多有用的背景资料来说明您的查询速度慢的原因。

一种解决方法是向表 items 和 hosts 添加一个计数器,并使用触发器使它们保持最新 - 写入操作的成本不低。
或者像你一样使用物化视图。我可能会选择那个。

为此,您仍然需要定期执行这些查询,并且它们可以得到改进。将你的第一个重写为:

SELECT id, i.description, hi.ct
FROM   items i
JOIN  (
    SELECT item AS id, count(*) AS ct
    FROM   host_item
    GROUP  BY item
    ORDER  BY ct DESC
    LIMIT  10
    ) hi USING (id);
  • 如果表items 中有一行对于表host_item 中的大多数行,则先聚合然后聚合JOIN 会更快。与@willglynn 推测的相反,这在 Postgres 9.1 中并未自动优化。

  • count(*) 在主体上比 count(col) 快 - 并且在 col 不能为 NULL 时等效。 (LEFT JOIN 可能会引入 NULL 值。)

  • 将LEFT JOIN 简化为JOIN。假设总是至少有十个不同的主机应该是安全的。对您的原始查询无关紧要,但这是此查询的要求。

  • host_item 表上的索引无济于事,items 上的 PK 涵盖了其余部分。

对于您的情况,可能仍然不够好,但在我使用 Postgres 9.1 进行的测试中,此表单的速度快了两倍以上。应转换为 9.2,但请使用EXPLAIN ANALYZE 进行测试以确保。

【讨论】:

  • 实际上,这次重写显着提高了性能。它现在快了 3 倍(但对于 Web 内容生成来说仍然太慢)。见Rewritten query。非常感谢,我将继续使用这个并尝试调整 Postgresql 服务器设置。
【解决方案2】:

正如@GordonLinoff 所说,无论涉及什么数据库,这些查询都会很慢,但了解为什么会很有帮助。考虑数据库如何执行此查询:

SELECT table1.*, count(*)
FROM table1
JOIN table2 ON table2.id1 = table1.id
GROUP BY table1.id

假设table2 包含table1 中大多数行的数据,并且两个表的大小都非常大,关系数据库将倾向于执行以下操作:

  • 扫描table2,计算id1 上的聚合,生成{ id1, count } 结果集。
  • 扫描table1。
  • 哈希连接。

添加或不添加ORDER BY count 并不会显着改变工作量:您仍然有两个表扫描和一个JOIN,您只是添加了一个排序步骤。您可能会尝试在table2 (id1) 上添加索引,但所有可以改进的只是聚合步骤:现在您不是读取两个完整的表,而是读取一个完整的表和一个完整的索引。喜悦。

如果您可以在一个或两个表上使用索引消除大多数行,那么一定要这样做。否则,该操作将始终归结为两次扫描,并且随着您的数据集变得越来越大,它的性能会越来越低。

顺便说一句,这是在查询中删除ORDER BY 的效果:通过留下LIMIT 子句,您已经告诉PostgreSQL 您只对前N 行感兴趣。这意味着它可以从 table1 中选择 N 行并针对 table2 执行嵌套循环——对于 table1 中的每一行 N 行,它使用那个身份证。这就是让它变得更快的原因:您已经排除了大部分 table2。

如果您的应用程序通常需要关联记录的计数,通常的解决方案是自己维护一个计数器。一种约定(Rails 和其他几个 ORM 原生支持)是在table1 中添加一个table2_count 列。如果您索引此计数器,ORDER BY ... LIMIT 查询将非常高效。

如果您的工具无法立即执行此操作,或者您正在使用多种工具来操作此数据库,那么触发器是更好的选择。您可以按照@GordonLinoff 的建议将其放在单独的汇总表中——这可能意味着基表中的争用较少,但在检索计数时会强制使用JOIN。我建议先在table1 中添加一个table2_count 列,并且只有在性能测量表明这是一个胜利时才将其拆分。

【讨论】:

    【解决方案3】:

    您编写的查询在任何数据库中都会很慢。与没有order by 的查询的比较很有趣。速度返回表明涉及索引。如果是这样,那么它可以从索引中找到计数。

    更公平的比较是没有order by 和没有limit 子句的查询。这样,将生成所有行,就像在带有order by 的版本中一样。基本上,数据库引擎必须评估所有行才能找到前 10 行。优化器决定是否需要对数据进行排序或采取其他方法。

    您有多种选择。首先是查看是否可以通过更改特定于 Postgres 的参数来加快查询的性能。例如,页面缓存可能太小并且可以扩展。或者,也许有一些排序优化参数可以提供帮助。

    其次,您可以按照您的建议创建一个汇总表,该汇总表由定期运行的作业构建。如果稍微过时的数据没有问题,那就没问题了。

    第三,您可以拥有汇总表,但使用触发器而不是作业来填充它。当数据发生变化时,更新各种计数。

    第四,您可以尝试其他方法。例如,也许 Postgres 优化窗口函数COUNT(*) over () 比聚合更好。或者,它可能会比order by 更好地优化聚合结果上的row_number()。或者,如果您只能使用一个值而不是 10,那么 MAX() 就足够了。

    【讨论】:

    • Postgres 不需要对所有行进行排序。它需要评估所有行,但只需要对前 N 个进行排序。所以排序时间是 NlogM 而不是 NlogN,其中 M 是限制,N 是集合的基数。
    • @dbenhur 。 . .谢谢你。我没有意识到 Postgres 会自动进行此优化,但您显然是正确的 (postgresql.org/docs/9.2/static/queries-limit.html)。
    • 自 8.3: "2007-05-03 21:13 tgl 教 tuplesort.c 关于“top N”排序,其中只需要返回前 N 个元组。我们保留一堆当前当我们扫描输入时,最好的 N 个元组和筛选新的元组。对于 M 个输入元组,这仅意味着大约 Mlog(N) 比较而不是 Mlog(M),更不用说当 N 很小时,工作空间会减少很多——避免大 M 溢出到磁盘实际上是它最吸引人的地方。补丁包括计划程序和执行程序支持,以在 ORDER BY ... LIMIT 查询中调用此工具。 wiki.postgresql.org/wiki/8.3_Changelog
    【解决方案4】:

    根据已发布的计划,您的行数估算值很好,而且计划看起来还算合理。您的主要问题是大类,可能是ORDER BY 所必需的:

    Sort Method: external merge Disk: 64288kB
    

    即使您拥有快速存储空间,也会受到影响。如果您在单个硬盘驱动器或(更糟糕的)RAID5 阵列上,那将会非常非常慢。随着 Erwin 更新后的查询,这种排序消失了,但增加 work_mem 仍然可能会获得一些性能。

    您应该增加work_mem,对于这个查询或(更少)全局获得更多更好的性能。试试:

    SET work_mem = '100MB';
    SELECT your_query
    

    看看它有什么不同。

    您可能还想使用random_page_cost 和seq_page_cost 参数来查看不同的余额是否会产生更适合您的环境的成本估算,从而使计划者选择更快的查询。对于像这样的相对少量的数据,其中大部分数据将缓存在 RAM 中,我会从 random_page_cost = 0.22 和 seq_page_cost = 0.2 之类的东西开始。您可以像 work_mem 一样为他们使用 SET,例如:

    SET work_mem = '100MB';
    SET random_page_cost = 0.22;
    SET seq_page_Cost = 0.2;
    SELECT your_query
    

    如果您将work_mem 设置在postgresql.conf 中并且您有很多活动连接,那么不要将work_mem 设置得那么高,因为它是按排序而不是按查询进行的,因此某些查询可能多次使用work_mem,一次使用几次可能会使系统内存耗尽;您需要将其设置得足够低,以便max_connections 中的每个连接都可以使用 2x 或 3x work_mem,而不会导致系统内存不足。您可以使用SET LOCAL 为每个事务设置它,使用ALTER USER ... SET 为每个用户设置它,使用ALTER DATABASE ... SET 为每个数据库设置它,或者在postgresql.conf 中全局设置它。

    见:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-07-14
      • 1970-01-01
      • 1970-01-01
      • 2010-11-27
      • 1970-01-01
      • 2023-02-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多