使用 Postgres 在 varchar 列上使用 distinct/group 进行慢速查询
Slow query with distinct/group by on varchar column with Postgres
我有一个 company
table 和一个 industry
table,具有多对多关系 table 链接两者,名为 company_industry
。 company
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.
我有一个 company
table 和一个 industry
table,具有多对多关系 table 链接两者,名为 company_industry
。 company
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.