3

我使用 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 计数
从项目 AS i
LEFT JOIN host_item AS hi
开(i.id=hi.item)
按 i.id 分组
ORDER BY 计数 DESC
限制 10;

具有项目计数的主机:

选择 h.id, h.name, COUNT(hi.id) 作为计数
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 工作中使用这些答案。非常感谢你们!

4

4 回答 4

3

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

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

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

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

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

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

顺便说一句,这是ORDER BY在查询中删除的效果:通过离开LIMIT子句,您已经告诉 PostgreSQL 您只对前 N 行感兴趣。这意味着它可以从中选择 N 行并针对其中的 N 行中的每一行table1执行嵌套循环,它使用该 ID 上的索引查找该特定 ID。这就是让它变得更快的原因:您已经排除了大部分.table2table1count(*)table2table2

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

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

于 2012-10-16T16:52:28.780 回答
3

@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。(ALEFT JOIN可能会引入 NULL 值。)

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

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

对于您的情况,可能仍然不够好,但在我使用 Postgres 9.1 进行的测试中,这种形式的速度是两倍多。应该转换为 9.2,但EXPLAIN ANALYZE可以肯定地测试。

于 2012-10-17T00:55:50.873 回答
2

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

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

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

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

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

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

于 2012-10-16T16:15:45.570 回答
1

根据发布的计划,您的行数估计很好,并且计划看起来有点理智。您的主要问题是大问题,可能是以下原因所必需的ORDER BY

Sort Method: external merge Disk: 64288kB

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

您应该work_mem为此查询或(更少)全局增加 ,以获得更好的性能。尝试:

SET work_mem = '100MB';
SELECT your_query

看看它有什么不同。

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

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

如果您正在设置它并且您有很多活动连接,请不要设置那么高,因为它是按排序而不是按查询,因此某些查询可能会使用多次,而一次只有几个可能会使系统内存耗尽; 您需要将其设置得足够低,以使每个连接都可以使用 2x 或 3x ,而不会出现系统内存不足的情况。您可以使用 为每个事务设置它,使用 为每个用户,使用 为每个数据库或全局设置它。work_mempostgresql.confwork_memmax_connectionswork_memSET LOCALALTER USER ... SETALTER DATABASE ... SETpostgresql.conf

看:

于 2012-10-17T07:56:00.330 回答