优化大型子表的日期查询:GiST 还是 GIN?

Posted

技术标签:

【中文标题】优化大型子表的日期查询:GiST 还是 GIN?【英文标题】:Optimize date query for large child tables: GiST or GIN? 【发布时间】:2010-05-20 04:43:08 【问题描述】:

问题

72 个子表,每个子表都有一个年份索引和一个站点索引,定义如下:

CREATE TABLE climate.measurement_12_013
(
-- Inherited from table climate.measurement_12_013:  id bigint NOT NULL DEFAULT nextval('climate.measurement_id_seq'::regclass),
-- Inherited from table climate.measurement_12_013:  station_id integer NOT NULL,
-- Inherited from table climate.measurement_12_013:  taken date NOT NULL,
-- Inherited from table climate.measurement_12_013:  amount numeric(8,2) NOT NULL,
-- Inherited from table climate.measurement_12_013:  category_id smallint NOT NULL,
-- Inherited from table climate.measurement_12_013:  flag character varying(1) NOT NULL DEFAULT ' '::character varying,
  CONSTRAINT measurement_12_013_category_id_check CHECK (category_id = 7),
  CONSTRAINT measurement_12_013_taken_check CHECK (date_part('month'::text, taken)::integer = 12)
)
INHERITS (climate.measurement)

CREATE INDEX measurement_12_013_s_idx
  ON climate.measurement_12_013
  USING btree
  (station_id);
CREATE INDEX measurement_12_013_y_idx
  ON climate.measurement_12_013
  USING btree
  (date_part('year'::text, taken));

(外键约束稍后添加。)

由于全表扫描,以下查询运行非常缓慢:

SELECT
  count(1) AS measurements,
  avg(m.amount) AS amount
FROM
  climate.measurement m
WHERE
  m.station_id IN (
    SELECT
      s.id
    FROM
      climate.station s,
      climate.city c
    WHERE
        /* For one city... */
        c.id = 5182 AND

        /* Where stations are within an elevation range... */
        s.elevation BETWEEN 0 AND 3000 AND

        /* and within a specific radius... */
        6371.009 * SQRT( 
          POW(RADIANS(c.latitude_decimal - s.latitude_decimal), 2) +
            (COS(RADIANS(c.latitude_decimal + s.latitude_decimal) / 2) *
              POW(RADIANS(c.longitude_decimal - s.longitude_decimal), 2))
        ) <= 50
    ) AND

  /* Data before 1900 is shaky; insufficient after 2009. */
  extract( YEAR FROM m.taken ) BETWEEN 1900 AND 2009 AND

  /* Whittled down by category... */
  m.category_id = 1 AND

  /* Between the selected days and years... */
  m.taken BETWEEN
   /* Start date. */
   (extract( YEAR FROM m.taken )||'-01-01')::date AND
    /* End date. Calculated by checking to see if the end date wraps
       into the next year. If it does, then add 1 to the current year.
    */
    (cast(extract( YEAR FROM m.taken ) + greatest( -1 *
      sign(
        (extract( YEAR FROM m.taken )||'-12-31')::date -
        (extract( YEAR FROM m.taken )||'-01-01')::date ), 0
    ) AS text)||'-12-31')::date
GROUP BY
  extract( YEAR FROM m.taken )

迟钝来自这部分查询:

  m.taken BETWEEN
    /* Start date. */
  (extract( YEAR FROM m.taken )||'-01-01')::date AND
    /* End date. Calculated by checking to see if the end date wraps
      into the next year. If it does, then add 1 to the current year.
    */
    (cast(extract( YEAR FROM m.taken ) + greatest( -1 *
      sign(
        (extract( YEAR FROM m.taken )||'-12-31')::date -
        (extract( YEAR FROM m.taken )||'-01-01')::date ), 0
    ) AS text)||'-12-31')::date

这部分查询匹配选定的日期。例如,如果用户想要查看有数据的所有年份的 6 月 1 日至 7 月 1 日之间的数据,则上述子句仅与那些日子匹配。如果用户想查看 12 月 22 日和 3 月 22 日之间的数据,同样对于所有有数据的年份,上述子句计算 3 月 22 日是在下一年的 12 月 22 日,因此相应地匹配日期:

目前日期固定为 1 月 1 日至 12 月 31 日,但将参数化,如上所示。

计划中的 HashAggregate 显示成本为 10006220141.11,我怀疑这是天文数字。

对正在执行的测量表(本身既没有数据也没有索引)执行全表扫描。该表从其子表中聚合了 2.73 亿行。

问题

索引日期以避免全表扫描的正确方法是什么?

我考虑过的选项:

杜松子酒 GiST 重写 WHERE 子句 将 year_taken、month_taken 和 day_taken 列与表格分开

你有什么想法?

谢谢!

【问题讨论】:

这个缓慢的部分有什么作用?看不懂。 @Tometzky:我已经用一张显示查询参数的图片更新了问题。 您说您的表按年份和站点分区,但您的约束不匹配 - 计划者可能没有适当地修剪。更不用说,如果您通过全开运行它来跨越所有分区,那么您的成本就会大幅上涨。减少或更改分区,可能有帮助。 @rfusca:分区(我希望)不是问题;问题是正在执行全表扫描,因为计划程序无法将计算的日期(从字符串)与实际日期进行比较。因此它调用全表扫描。我的一个想法是传递两组日期。对于Dec 22 to Mar 22,查询将查看当前年份的Dec 22 to Dec 31 和当前年份之后的年份Jan 1 to Mar 22 我的意思是,是的,这是真的,但你也可能在做 72 表扫描,不是吗? 【参考方案1】:

您的问题是您有一个取决于日期计算的 where 子句。如果数据库需要获取每一行并在知道日期是否匹配之前对其进行计算,那么数据库就无法使用索引。

除非您将其重写为数据库具有固定范围以检查哪些不依赖于要检索的数据的形式,否则您将始终必须扫描表。

【讨论】:

@Cobusive:我的印象是 GIN 和 GiST 可以避免这种情况。另外,我正在寻找最好的解决方案。全表扫描不是一种选择;有近3亿行! :-) mysql 可以在 5 秒内执行查询; PostgreSQL 尚未完成。【参考方案2】:

试试这样的:

create temporary table test (d date);

insert into test select '1970-01-01'::date+generate_series(1,50*365);

analyze test

create function month_day(d date) returns int as $$
  select extract(month from $1)::int*100+extract(day from $1)::int $$
language sql immutable strict;

create index test_d_month_day_idx on test (month_day(d));

explain analyze select * from test
  where month_day(d)>=month_day('2000-04-01')
  and month_day(d)<=month_day('2000-04-05');

【讨论】:

因为表格没有正确排序,所以速度很慢。我通过(1)按主键顺序重新插入数据来解决此问题; (2) 添加了一个集群索引。【参考方案3】:

我认为要在这些分区上高效运行,我会让您的应用在日期范围方面更加智能。让它生成一个实际的日期列表来检查每个分区,然后让它在分区之间生成一个带有 UNION 的查询。听起来您的数据集非常静态,因此日期索引上的 CLUSTER 也可以大大提高性能。

【讨论】:

我想过这个;我可以让应用程序生成 WHERE 子句的一部分,但是我会紧密耦合两个完全不同的系统:网站和报告引擎。 你可以将 where 子句的动态生成移动到 pl/pgsql 函数中,然后你可以让它构建查询,它只是一个“select * from my_proc(parameters..)” -和以前没有什么不同。 这也是一个有趣的提议。一旦我让基本查询工作(并且快速),我会更多地研究这个。谢谢!

以上是关于优化大型子表的日期查询:GiST 还是 GIN?的主要内容,如果未能解决你的问题,请参考以下文章

优化数据库中大表的查询(SQL)

多表查询

Mysql sql语句优化经验总结

大型单表的 MySQL 查询优化 [关闭]

掌握查询利器 深入理解PostgreSQL索引原理与优化

优化联合 sql 查询