依赖条件的大表的连接很慢

时间:2016-07-06 22:58:13

标签: sql postgresql indexing count postgresql-performance

当表很小时,此查询的合理时间。我试图确定瓶颈是什么,但我不确定如何分析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&customersproduct_categories表的方法并进行INNER SELECT。两者都有相同的时间。

这是指向EXPLAIN的链接:https://explain.depesz.com/s/9lhr

Postgres版本:

  

在x86_64-unknown-linux-gnu上的PostgreSQL 9.4.5,由gcc(GCC)4.8.2 20140120(Red Hat 4.8.2-16)编译,64位

表和索引:

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行

3 个答案:

答案 0 :(得分:5)

您的查询,重写和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列表中的连接子句和订单表中。这可能适用于具有相对原始的查询规划器的某个其他RDBMS。虽然它对Postgres也没有伤害,但它对查询的性能没有影响 - 假设默认服务器配置。 The manual:

  

明确的内部联接语法(INNER JOINCROSS JOIN或简单的JOIN)   在语义上与列出FROM中的输入关系相同,所以它   不会限制加入顺序

大胆强调我的。还有更多,请阅读手册。

密钥设置为join_collapse_limit(默认 8 )。无论您如何安排表格以及是否将条件写为WHEREJOIN条款,Postgres查询计划程序都会以任何预期最快的方式重新排列您的4个表格。没有任何区别。 (对于无法自由重新排列的其他类型的连接,情况也是如此。)

  

重要的是,这些不同的连接可能性给出了   语义上等效的结果,但可能有很大的不同   执行成本。因此,规划师将探索所有这些   尝试找到最有效的查询计划。

相关:

最后,WHERE id IN (<subquery>) 通常等同于加入。对于右侧的重复匹配值,它不会在左侧乘以行。对于查询的其余部分,子查询的列不可见。连接可以将行与重复值相乘,列可见 在这两种情况下,您的简单子查询都会挖出一个唯一的列,因此在这种情况下没有任何有效差异 - 除了IN (<subquery>)通常(至少有点)更慢且更冗长。使用连接。

您的查询

索引

product_categories 有34行。除非您计划添加更多内容,否则索引对此表没有帮助。顺序扫描总是更快。删除 index_product_categories_on_organization_id

customers 有6,970行。索引开始有意义。但是根据EXPLAIN输出,您的查询使用了4,988个查询。索引上只有index-only scan的宽度远小于表格的宽度可能会有所帮助。假设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;

然而:

  

过滤器删除的行:0&#34;

好像这个条件没有用处?有size > 0的行是不是真的?

创建这些索引后,需要ANALYZE表。

表统计

一般来说,我看到许多糟糕的估计。 Postgres 低估了几乎每一步都返回的行数。我们看到的嵌套循环对于更少的行会更好。除非这不太可能巧合,否则您的表统计数据已经过时了。您需要访问autovacuum的设置,可能还需要访问两个大表的每个表设置 performance_analysestotal_sales

您已经运行了VACUUMANALYZE,这使得查询速度变慢,according to your comment。这并没有多大意义。我会在这两个表上运行VACUUM FULL一次(如果你能负担得起专属锁)。否则试试pg_repack 有了所有可疑的统计数据和错误的计划,我会考虑在您的数据库上运行完整的vacuumdb -fz yourdb。这会在原始条件下重写所有表格和索引,但定期使用并不好。它也很昂贵,会长时间锁定你的数据库!

在此期间,请查看数据库的费用设置。 相关:

答案 1 :(得分:1)

虽然理论上 优化器应该能够做到这一点,但我经常发现这些变化可以大大提高性能:

  • 使用正确的连接(而不是where id in (select ...)
  • 命令对from子句中的表的引用,以便在每次连接时返回最少的行,尤其是第一个表的条件(在where子句中)应该是最严格的(并且应该使用索引)
  • 将联接表上的所有条件移动到联接的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) 

这种方法首先将数据缩小到一个时期,因此它将在未来扩展(保持大致不变),因为每个时期的销售数量将大致保持不变。

答案 2 :(得分:0)

估计不准确。 Postgres的计划程序使用错误的嵌套循环 - 尝试通过语句set enable_nestloop to off惩罚nest_loop。