使用 Postgres 在 varchar 列上使用 distinct/group 进行慢速查询

Slow query with distinct/group by on varchar column with Postgres

我有一个 company table 和一个 industry table,具有多对多关系 table 链接两者,名为 company_industrycompany table 目前大约有 750.000 行。

现在我需要一个查询来查找给定行业的所有唯一城市名称,其中至少有一家公司。所以基本上我必须找到与给定行业相关的所有公司和 select 这些公司的唯一城市名称。

我可以编写很好地执行此操作的查询,但达不到我正在寻找的性能。事先我对性能有点怀疑,因为 city_name 列的类型是 VARCHAR。不幸的是,我目前 没有 能够将数据库架构更改为更规范化的内容的自由。

我做的第一件事是在 city_name 列上添加索引,然后我尝试了以下查询。

SELECT c.city_name AS city
FROM industry AS i 
INNER JOIN company_industry AS ci ON (ci.industry_id = i.id)
INNER JOIN company AS c ON (c.id = ci.company_id)
WHERE i.id = 288
GROUP BY city;

上面的查询平均需要大约两秒钟的时间来执行。将GROUP BY替换为DISTINCT时也是如此。下面是上述查询的执行计划。

HashAggregate  (cost=56934.21..56961.61 rows=2740 width=9) (actual time=2421.364..2421.921 rows=1962 loops=1)
  ->  Hash Join  (cost=38972.69..56902.50 rows=12687 width=9) (actual time=954.377..2411.194 rows=12401 loops=1)
        Hash Cond: (ci.company_id = c.id)
        ->  Nested Loop  (cost=0.28..13989.91 rows=12687 width=4) (actual time=0.041..203.442 rows=12401 loops=1)
              ->  Index Only Scan using industry_pkey on industry i  (cost=0.28..8.29 rows=1 width=4) (actual time=0.015..0.018 rows=1 loops=1)
                    Index Cond: (id = 288)
                    Heap Fetches: 0
              ->  Seq Scan on company_industry ci  (cost=0.00..13854.75 rows=12687 width=8) (actual time=0.020..199.087 rows=12401 loops=1)
                    Filter: (industry_id = 288)
                    Rows Removed by Filter: 806309
        ->  Hash  (cost=26036.52..26036.52 rows=744152 width=13) (actual time=954.113..954.113 rows=744152 loops=1)
              Buckets: 4096  Batches: 64  Memory Usage: 551kB
              ->  Seq Scan on company c  (cost=0.00..26036.52 rows=744152 width=13) (actual time=0.008..554.662 rows=744152 loops=1)
Total runtime: 2422.185 ms

我尝试将查询更改为使用如下子查询,这使查询速度大约提高了一倍。

SELECT c.city_name
FROM company AS c
WHERE EXISTS(
  SELECT 1
  FROM company_industry
  WHERE industry_id = 288 AND company_id = c.id
)
GROUP BY c.city_name;

以及此查询的执行计划:

HashAggregate  (cost=47108.71..47136.11 rows=2740 width=9) (actual time=1270.171..1270.798 rows=1962 loops=1)
  ->  Hash Semi Join  (cost=14015.50..47076.98 rows=12690 width=9) (actual time=194.548..1251.785 rows=12401 loops=1)
        Hash Cond: (c.id = company_industry.company_id)
        ->  Seq Scan on company c  (cost=0.00..26036.52 rows=744152 width=13) (actual time=0.008..537.856 rows=744152 loops=1)
        ->  Hash  (cost=13856.88..13856.88 rows=12690 width=4) (actual time=194.399..194.399 rows=12401 loops=1)
              Buckets: 2048  Batches: 1  Memory Usage: 436kB
              ->  Seq Scan on company_industry  (cost=0.00..13856.88 rows=12690 width=4) (actual time=0.012..187.449 rows=12401 loops=1)
                    Filter: (industry_id = 288)
                    Rows Removed by Filter: 806309
Total runtime: 1271.030 ms

更好,但希望你们能帮助我做得更好。

基本上,查询的昂贵部分似乎是查找唯一的城市名称(正如预期的那样),即使在列上有索引,性能也不够好。我在分析执行计划方面很生疏,但我把它们包括在内,这样你们就可以确切地看到发生了什么。

如何更快地检索此数据?

我正在使用 Postgres 9.3.5,下面的 DDL:

CREATE TABLE company (
  id SERIAL PRIMARY KEY NOT NULL,
  name VARCHAR(150) NOT NULL,
  city_name VARCHAR(50),
);

CREATE TABLE company_industry (
  company_id INT NOT NULL REFERENCES company (id) ON UPDATE CASCADE,
  industry_id INT NOT NULL REFERENCES industry (id) ON UPDATE CASCADE,
  PRIMARY KEY (company_id, industry_id)
);

CREATE TABLE industry (
  id SERIAL PRIMARY KEY NOT NULL,
  name VARCHAR(100) NOT NULL
);

CREATE INDEX company_city_name_index ON company (city_name);

如果您想要在毫秒范围内进行此查询,那么您应该 de-normalize 您的数据,方法是将另一列 city_name 添加到连接点 table company_industry 并为其编制索引.

这样你只会查询(未测试) SELECT DISTINCT(c.city_name) FROM company_industry ci GROUP BY ci.industry_id HAVING COUNT(ci.company_id) >= 1

两个查询计划中都有一个 Seq Scan on company_industry 实际上应该是(位图)索引扫描。 Seq Scan on company.

也是如此

似乎是 缺少索引 的问题 - 或者您的数据库中有问题。如果出现问题,请在继续之前进行备份。检查成本设置和统计信息是否有效:

  • Keep PostgreSQL from sometimes choosing a bad query plan

如果设置好,我会查看相关指标(详见下文)。也许 REINDEX 可以修复它:

REINDEX TABLE company;
REINDEX TABLE company_industry;

也许你需要做更多:

  • Optimize Postgres query on timestamp range

此外,您可以简化查询:

SELECT c.city_name AS city
FROM   company_industry ci
JOIN   company          c ON c.id = ci.company_id
WHERE  ci.industry_id = 288
GROUP  BY 1;

备注

如果您的 PK 约束在 (company_id, industry_id) 上,请在 (industry_id, company_id) 上添加另一个(唯一的)index逆序!).为什么?

Seq Scan on company 同样麻烦。 company(id)好像没有索引,但是你的ER图是PK的,不会吧?
最快的选择是在 (id, city_name) - if 上设置多列索引(并且仅当)您从中获得仅索引扫描。

因为您已经有了给定行业的 ID,所以根本不需要包含 table industry table。

ON 子句中的表达式不需要括号。

这很不幸:

Unfortunately I do currently not have the liberty of being able to change the database schema to something more normalized.

您的简单架构对于小型 table 来说很有意义,几乎没有冗余,而且对可用缓存内存几乎没有任何压力。但是城市名称在大 table 中可能是高度冗余的。 规范化 会显着缩小 table 和索引大小,这是影响性能的最重要因素。
具有冗余存储的非规范化形式有时可以加速目标查询,有时不能,这取决于。但它总是 对其他一切产生不利影响。冗余存储会占用更多可用缓存,因此必须尽快清除其他数据。即使如果你在局部获得一些东西,你可能会失去整体。
在这种特殊情况下,为 city_id int 列获取不同的值也会便宜得多,因为 integer 值比(可能很长的)字符串更小且比较速度更快。 company(id, city_id) 的多列索引将小于 (id, city_name) 的多列索引,并且处理速度更快。在 折叠许多重复项之后再加入一个 相对便宜。

如果您需要最佳性能,您可以随时添加一个 MATERIALIZED VIEW 用于特殊目的,具有预先计算的结果(易于聚合并在 industry_id 上有索引),但要避免大量冗余数据在你的鼎盛时期 tables.