依靠有条件的大表的连接很慢
Count on join of big tables with conditions is slow
当 table 较小时,此查询有合理的时间。我正在尝试确定瓶颈是什么,但我不确定如何分析 EXPLAIN
结果。
SELECT
COUNT(*)
FROM performance_analyses
INNER JOIN total_sales ON total_sales.id = performance_analyses.total_sales_id
WHERE
(size > 0) AND
total_sales.customer_id IN (
SELECT customers.id FROM customers WHERE customers.active = 't'
AND customers.visible = 't' AND customers.organization_id = 3
) AND
total_sales.product_category_id IN (
SELECT product_categories.id FROM product_categories
WHERE product_categories.organization_id = 3
) AND
total_sales.period_id = 193;
我已经尝试了 INNER JOIN'ing customers
和 product_categories
table 的方法并进行了 INNER SELECT。两者时间相同。
这里是 link 解释:https://explain.depesz.com/s/9lhr
Postgres 版本:
PostgreSQL 9.4.5 on x86_64-unknown-linux-gnu, compiled by gcc (GCC) 4.8.2 20140120 (Red Hat 4.8.2-16), 64-bit
表和索引:
CREATE TABLE total_sales (
id serial NOT NULL,
value double precision,
start_date date,
end_date date,
product_category_customer_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
processed boolean,
customer_id integer,
product_category_id integer,
period_id integer,
CONSTRAINT total_sales_pkey PRIMARY KEY (id)
);
CREATE INDEX index_total_sales_on_customer_id ON total_sales (customer_id);
CREATE INDEX index_total_sales_on_period_id ON total_sales (period_id);
CREATE INDEX index_total_sales_on_product_category_customer_id ON total_sales (product_category_customer_id);
CREATE INDEX index_total_sales_on_product_category_id ON total_sales (product_category_id);
CREATE INDEX total_sales_product_category_period ON total_sales (product_category_id, period_id);
CREATE INDEX ts_pid_pcid_cid ON total_sales (period_id, product_category_id, customer_id);
CREATE TABLE performance_analyses (
id serial NOT NULL,
total_sales_id integer,
status_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
size double precision,
period_size integer,
nominal_variation double precision,
percentual_variation double precision,
relative_performance double precision,
time_ago_max integer,
deseasonalized_series text,
significance character varying,
relevance character varying,
original_variation double precision,
last_level double precision,
quantiles text,
range text,
analysis_method character varying,
CONSTRAINT performance_analyses_pkey PRIMARY KEY (id)
);
CREATE INDEX index_performance_analyses_on_status_id ON performance_analyses (status_id);
CREATE INDEX index_performance_analyses_on_total_sales_id ON performance_analyses (total_sales_id);
CREATE TABLE product_categories (
id serial NOT NULL,
name character varying,
organization_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
external_id character varying,
CONSTRAINT product_categories_pkey PRIMARY KEY (id)
);
CREATE INDEX index_product_categories_on_organization_id ON product_categories (organization_id);
CREATE TABLE customers (
id serial NOT NULL,
name character varying,
external_id character varying,
region_id integer,
organization_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
active boolean DEFAULT false,
visible boolean DEFAULT false,
segment_id integer,
"group" boolean,
group_id integer,
ticket_enabled boolean DEFAULT true,
CONSTRAINT customers_pkey PRIMARY KEY (id)
);
CREATE INDEX index_customers_on_organization_id ON customers (organization_id);
CREATE INDEX index_customers_on_region_id ON customers (region_id);
CREATE INDEX index_customers_on_segment_id ON customers (segment_id);
行数:
- 客户 - 6,970 行
- product_categories - 34 行
- performance_analyses - 1,012,346 行
- total_sales - 7,104,441 行
那里的估计不准确。 Postgres 规划器使用错误的嵌套循环 - 尝试通过语句 set enable_nestloop to off
.
来惩罚 nest_loop
虽然理论上优化器应该能够做到这一点,但我经常发现这些更改可以极大地提高性能:
- 使用正确的连接(而不是
where id in (select ...)
)
- 对
from
子句中对 table 的引用进行排序,以便在每次连接时返回最少的行,尤其是第一个 table 的条件(在 where 子句中) 应该是最严格的(并且应该使用索引)
- 将加入的 table 的所有条件移动到加入的
on
条件中
试试这个(为便于阅读而添加的别名):
select count(*)
from total_sales ts
join product_categories pc on ts.product_category_id = pc.id and pc.organization_id = 3
join customers c on ts.customer_id = c.id and c.organization_id = 3
join performance_analyses pa on ts.id = pa.total_sales_id and pa.size > 0
where ts.period_id = 193
您需要创建此索引以获得最佳性能(以允许在 total_sales 上进行仅索引扫描):
create index ts_pid_pcid_cid on total_sales(period_id, product_category_id, customer_id)
这种方法首先将数据缩小到一个时期,因此它会扩展(保持大致不变)到未来,因为每个时期的销售数量将大致保持不变。
您的查询,重写且 100% 等效:
SELECT count(*)
FROM product_categories pc
JOIN customers c USING (organization_id)
JOIN total_sales ts ON ts.customer_id = c.id
JOIN performance_analyses pa ON pa.total_sales_id = ts.id
WHERE pc.organization_id = 3
AND c.active -- boolean can be used directly
AND c.visible
AND ts.product_category_id = pc.id
AND ts.period_id = 193
AND pa.size > 0;
另一个答案建议将所有条件移动到连接子句中并在 FROM
列表中排序 table。这可能适用于具有相对原始的查询计划器的某些其他 RDBMS。但是,虽然它对 Postgres 也没有影响,但它也 对您的查询的性能没有影响 - 假设默认服务器配置。 The manual:
Explicit inner join syntax (INNER JOIN
, CROSS JOIN
, or unadorned JOIN
)
is semantically the same as listing the input relations in FROM
, so it
does not constrain the join order.
大胆强调我的。还有很多,看说明书。
按键设置为join_collapse_limit
(默认为8)。 Postgres 查询规划器将以最快的方式重新排列您的 4 tables,无论您如何排列 tables 以及您是否将条件写为 WHERE
或 JOIN
子句。没有任何区别。 (对于一些不能自由重新排列的其他类型的连接,情况并非如此。)
The important point is that these different join possibilities give
semantically equivalent results but might have hugely different
execution costs. Therefore, the planner will explore all of them to
try to find the most efficient query plan.
相关:
- Sample Query to show Cardinality estimation error in PostgreSQL
- A: Slow fulltext search due to wildly inaccurate row estimates
最后,WHERE id IN (<subquery>)
不 通常等同于连接。它不会将左侧的行与右侧的重复匹配值相乘。并且子查询的列对于查询的其余部分不可见。联接可以将具有重复值的行相乘,并且列是可见的。
在这两种情况下,您的简单子查询都会挖掘一个唯一的列,因此在这种情况下没有有效的区别 - 除了 IN (<subquery>)
通常(至少有一点)更慢和更冗长。使用联接。
您的查询
索引
product_categories
有 34 行。除非您计划添加更多,否则索引对此 table 没有帮助。顺序扫描总是会更快。掉落 index_product_categories_on_organization_id
.
customers
有 6,970 行。索引开始有意义了。但是根据 EXPLAIN
输出,您的查询使用了其中的 4,988 个。只有索引上的 index-only scan 比 table 宽得多才能有所帮助。假设 WHERE active AND visible
是常量谓词,我建议使用部分多列索引:
CREATE INDEX index_customers_on_organization_id ON customers (organization_id, id)
WHERE active AND visible;
我附加了 id
以允许仅索引扫描。该列在此查询的索引中无用。
total_sales
有 7,104,441 行。索引非常重要。我建议:
CREATE INDEX index_total_sales_on_product_category_customer_id
ON total_sales (period_id, product_category_id, customer_id, id)
同样,针对仅索引扫描。这是最重要的。
可以删除完全多余的索引index_total_sales_on_product_category_id
.
performance_analyses
有 1,012,346 行。索引非常重要。
我建议使用条件 size > 0
:
的另一个部分索引
CREATE INDEX index_performance_analyses_on_status_id
ON performance_analyses (total_sales_id)
WHERE pa.size > 0;
但是:
Rows Removed by Filter: 0"
好像这个条件没有用?是否有 size > 0
不正确的行?
创建这些索引后,您需要到ANALYZE
tables。
表格统计
一般来说,我看到很多不好的估计。 Postgres 低估了 几乎每一步返回的行数。我们看到的 嵌套循环 对于更少的行会更好。除非这是不太可能的巧合,否则您的 table 统计数据已经严重过时了。您需要访问 autovacuum 的设置,可能还需要访问两个大 table 的每个 table 设置
performance_analyses
和 total_sales
.
您已经执行了 运行 VACUUM
和 ANALYZE
,这使得查询变慢了,according to your comment。这没有多大意义。我会在这两个 table 上 运行 VACUUM FULL
一次(如果你能负担得起独占锁)。否则尝试 pg_repack
.
考虑到所有可疑的统计数据和糟糕的计划,我会考虑 运行 在您的数据库上创建一个完整的 vacuumdb -fz yourdb
。这会在原始条件下重写所有 table 和索引,但不利于定期使用。它也很昂贵,并且会长时间锁定您的数据库!
在查看的同时,还要查看您的数据库的成本设置。
相关:
- Keep PostgreSQL from sometimes choosing a bad query plan
- Postgres Slow Queries - Autovacuum frequency
当 table 较小时,此查询有合理的时间。我正在尝试确定瓶颈是什么,但我不确定如何分析 EXPLAIN
结果。
SELECT
COUNT(*)
FROM performance_analyses
INNER JOIN total_sales ON total_sales.id = performance_analyses.total_sales_id
WHERE
(size > 0) AND
total_sales.customer_id IN (
SELECT customers.id FROM customers WHERE customers.active = 't'
AND customers.visible = 't' AND customers.organization_id = 3
) AND
total_sales.product_category_id IN (
SELECT product_categories.id FROM product_categories
WHERE product_categories.organization_id = 3
) AND
total_sales.period_id = 193;
我已经尝试了 INNER JOIN'ing customers
和 product_categories
table 的方法并进行了 INNER SELECT。两者时间相同。
这里是 link 解释:https://explain.depesz.com/s/9lhr
Postgres 版本:
PostgreSQL 9.4.5 on x86_64-unknown-linux-gnu, compiled by gcc (GCC) 4.8.2 20140120 (Red Hat 4.8.2-16), 64-bit
表和索引:
CREATE TABLE total_sales (
id serial NOT NULL,
value double precision,
start_date date,
end_date date,
product_category_customer_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
processed boolean,
customer_id integer,
product_category_id integer,
period_id integer,
CONSTRAINT total_sales_pkey PRIMARY KEY (id)
);
CREATE INDEX index_total_sales_on_customer_id ON total_sales (customer_id);
CREATE INDEX index_total_sales_on_period_id ON total_sales (period_id);
CREATE INDEX index_total_sales_on_product_category_customer_id ON total_sales (product_category_customer_id);
CREATE INDEX index_total_sales_on_product_category_id ON total_sales (product_category_id);
CREATE INDEX total_sales_product_category_period ON total_sales (product_category_id, period_id);
CREATE INDEX ts_pid_pcid_cid ON total_sales (period_id, product_category_id, customer_id);
CREATE TABLE performance_analyses (
id serial NOT NULL,
total_sales_id integer,
status_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
size double precision,
period_size integer,
nominal_variation double precision,
percentual_variation double precision,
relative_performance double precision,
time_ago_max integer,
deseasonalized_series text,
significance character varying,
relevance character varying,
original_variation double precision,
last_level double precision,
quantiles text,
range text,
analysis_method character varying,
CONSTRAINT performance_analyses_pkey PRIMARY KEY (id)
);
CREATE INDEX index_performance_analyses_on_status_id ON performance_analyses (status_id);
CREATE INDEX index_performance_analyses_on_total_sales_id ON performance_analyses (total_sales_id);
CREATE TABLE product_categories (
id serial NOT NULL,
name character varying,
organization_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
external_id character varying,
CONSTRAINT product_categories_pkey PRIMARY KEY (id)
);
CREATE INDEX index_product_categories_on_organization_id ON product_categories (organization_id);
CREATE TABLE customers (
id serial NOT NULL,
name character varying,
external_id character varying,
region_id integer,
organization_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
active boolean DEFAULT false,
visible boolean DEFAULT false,
segment_id integer,
"group" boolean,
group_id integer,
ticket_enabled boolean DEFAULT true,
CONSTRAINT customers_pkey PRIMARY KEY (id)
);
CREATE INDEX index_customers_on_organization_id ON customers (organization_id);
CREATE INDEX index_customers_on_region_id ON customers (region_id);
CREATE INDEX index_customers_on_segment_id ON customers (segment_id);
行数:
- 客户 - 6,970 行
- product_categories - 34 行
- performance_analyses - 1,012,346 行
- total_sales - 7,104,441 行
那里的估计不准确。 Postgres 规划器使用错误的嵌套循环 - 尝试通过语句 set enable_nestloop to off
.
虽然理论上优化器应该能够做到这一点,但我经常发现这些更改可以极大地提高性能:
- 使用正确的连接(而不是
where id in (select ...)
) - 对
from
子句中对 table 的引用进行排序,以便在每次连接时返回最少的行,尤其是第一个 table 的条件(在 where 子句中) 应该是最严格的(并且应该使用索引) - 将加入的 table 的所有条件移动到加入的
on
条件中
试试这个(为便于阅读而添加的别名):
select count(*)
from total_sales ts
join product_categories pc on ts.product_category_id = pc.id and pc.organization_id = 3
join customers c on ts.customer_id = c.id and c.organization_id = 3
join performance_analyses pa on ts.id = pa.total_sales_id and pa.size > 0
where ts.period_id = 193
您需要创建此索引以获得最佳性能(以允许在 total_sales 上进行仅索引扫描):
create index ts_pid_pcid_cid on total_sales(period_id, product_category_id, customer_id)
这种方法首先将数据缩小到一个时期,因此它会扩展(保持大致不变)到未来,因为每个时期的销售数量将大致保持不变。
您的查询,重写且 100% 等效:
SELECT count(*)
FROM product_categories pc
JOIN customers c USING (organization_id)
JOIN total_sales ts ON ts.customer_id = c.id
JOIN performance_analyses pa ON pa.total_sales_id = ts.id
WHERE pc.organization_id = 3
AND c.active -- boolean can be used directly
AND c.visible
AND ts.product_category_id = pc.id
AND ts.period_id = 193
AND pa.size > 0;
另一个答案建议将所有条件移动到连接子句中并在 FROM
列表中排序 table。这可能适用于具有相对原始的查询计划器的某些其他 RDBMS。但是,虽然它对 Postgres 也没有影响,但它也 对您的查询的性能没有影响 - 假设默认服务器配置。 The manual:
Explicit inner join syntax (
INNER JOIN
,CROSS JOIN
, or unadornedJOIN
) is semantically the same as listing the input relations inFROM
, so it does not constrain the join order.
大胆强调我的。还有很多,看说明书。
按键设置为join_collapse_limit
(默认为8)。 Postgres 查询规划器将以最快的方式重新排列您的 4 tables,无论您如何排列 tables 以及您是否将条件写为 WHERE
或 JOIN
子句。没有任何区别。 (对于一些不能自由重新排列的其他类型的连接,情况并非如此。)
The important point is that these different join possibilities give semantically equivalent results but might have hugely different execution costs. Therefore, the planner will explore all of them to try to find the most efficient query plan.
相关:
- Sample Query to show Cardinality estimation error in PostgreSQL
- A: Slow fulltext search due to wildly inaccurate row estimates
最后,WHERE id IN (<subquery>)
不 通常等同于连接。它不会将左侧的行与右侧的重复匹配值相乘。并且子查询的列对于查询的其余部分不可见。联接可以将具有重复值的行相乘,并且列是可见的。
在这两种情况下,您的简单子查询都会挖掘一个唯一的列,因此在这种情况下没有有效的区别 - 除了 IN (<subquery>)
通常(至少有一点)更慢和更冗长。使用联接。
您的查询
索引
product_categories
有 34 行。除非您计划添加更多,否则索引对此 table 没有帮助。顺序扫描总是会更快。掉落 .index_product_categories_on_organization_id
customers
有 6,970 行。索引开始有意义了。但是根据 EXPLAIN
输出,您的查询使用了其中的 4,988 个。只有索引上的 index-only scan 比 table 宽得多才能有所帮助。假设 WHERE active AND visible
是常量谓词,我建议使用部分多列索引:
CREATE INDEX index_customers_on_organization_id ON customers (organization_id, id)
WHERE active AND visible;
我附加了 id
以允许仅索引扫描。该列在此查询的索引中无用。
total_sales
有 7,104,441 行。索引非常重要。我建议:
CREATE INDEX index_total_sales_on_product_category_customer_id
ON total_sales (period_id, product_category_id, customer_id, id)
同样,针对仅索引扫描。这是最重要的。
可以删除完全多余的索引.index_total_sales_on_product_category_id
performance_analyses
有 1,012,346 行。索引非常重要。
我建议使用条件 size > 0
:
CREATE INDEX index_performance_analyses_on_status_id
ON performance_analyses (total_sales_id)
WHERE pa.size > 0;
但是:
Rows Removed by Filter: 0"
好像这个条件没有用?是否有 size > 0
不正确的行?
创建这些索引后,您需要到ANALYZE
tables。
表格统计
一般来说,我看到很多不好的估计。 Postgres 低估了 几乎每一步返回的行数。我们看到的 嵌套循环 对于更少的行会更好。除非这是不太可能的巧合,否则您的 table 统计数据已经严重过时了。您需要访问 autovacuum 的设置,可能还需要访问两个大 table 的每个 table 设置
performance_analyses
和 total_sales
.
您已经执行了 运行 VACUUM
和 ANALYZE
,这使得查询变慢了,according to your comment。这没有多大意义。我会在这两个 table 上 运行 VACUUM FULL
一次(如果你能负担得起独占锁)。否则尝试 pg_repack
.
考虑到所有可疑的统计数据和糟糕的计划,我会考虑 运行 在您的数据库上创建一个完整的 vacuumdb -fz yourdb
。这会在原始条件下重写所有 table 和索引,但不利于定期使用。它也很昂贵,并且会长时间锁定您的数据库!
在查看的同时,还要查看您的数据库的成本设置。 相关:
- Keep PostgreSQL from sometimes choosing a bad query plan
- Postgres Slow Queries - Autovacuum frequency