TIMESTAMPTZ 索引和函数不变性

Lui*_*uiz 10 postgresql

我们有一个类似于以下的结构:

create table company
(
    id bigint not null,
    tz text not null
);

create table company_data
(
    company_id bigint not null,
    ts_tz timestamp with time zone not null
);
Run Code Online (Sandbox Code Playgroud)

表格已简化。

在这里摆弄示例数据:SQL Fiddle

每个公司都有一个固定的 TZ。因此,当我们需要从中提取一些信息时,company_data我们使用类似于以下内容的查询:

select
       cd.company_id,
       cd.ts_tz at time zone c.tz
from company_data cd
join company c on c.id = cd.company_id;
Run Code Online (Sandbox Code Playgroud)

我们还有一个函数来获取公司 tz:

create or replace function tz_company(f_company_id bigint) returns text
    language plpgsql
as
$$
declare
    f_tz text;
begin
    select c.tz from company c where c.id = f_company_id into f_tz;
    return f_tz;
end;
$$;
Run Code Online (Sandbox Code Playgroud)

另一个在应用 tz 的日期中转换 ts:

create or replace function tz_date(timestamp with time zone, text) returns date
    language plpgsql
    immutable strict
as
$$
begin
    return ($1 at time zone $2) :: date;
end;
$$;
Run Code Online (Sandbox Code Playgroud)

我们现在遇到的问题是company_data(和其他类似的表)是一个大且经常使用的表。该SELECTs表中的大部分使用DATE.

例如:

select cd.company_id,
       cd.ts_tz at time zone tz_company(cd.company_id)
from company_data cd
where tz_date(cd.ts_tz, tz_company(cd.company_id)) >= '2019-08-20'
  and tz_date(cd.ts_tz, tz_company(cd.company_id)) <= '2019-08-22';
Run Code Online (Sandbox Code Playgroud)

因此,为了加快查询速度,我们需要在company_data.ts_tz列中添加索引。我们发现这样做的唯一方法如下:

create index idx_company_data_ts_tz on company_data
    (((company_data.ts_tz at time zone tz_company(company_data.company_id))::date));
Run Code Online (Sandbox Code Playgroud)

为此,我们需要使tz_company函数immutable.

出现了一些其他问题(和想法):

1 - 使用tz_date函数的查询版本不使用索引。

不使用索引:

explain analyse
select cd.company_id,
       cd.ts_tz at time zone tz_company(cd.company_id)
from company_data cd
where tz_date(cd.ts_tz, tz_company(cd.company_id)) >= '2019-08-20'
  and tz_date(cd.ts_tz, tz_company(cd.company_id)) <= '2019-08-22';
Run Code Online (Sandbox Code Playgroud)

使用指数:

explain analyse
select cd.company_id,
       cd.ts_tz at time zone tz_company(cd.company_id)
from company_data cd
where (cd.ts_tz at time zone tz_company(cd.company_id))::date >= '2019-08-20'
  and (cd.ts_tz at time zone tz_company(cd.company_id))::date <= '2019-08-22';
Run Code Online (Sandbox Code Playgroud)

为什么会这样?

2 - 我们知道,理论上,最多tz_company不应该是immutable稳定的。但是,公司 tz 是一个永远不应该改变的信息。是的,它可能发生,但这是不可能的。在过去的三年里,我们从未改变任何公司的 tz。因此,仍然是一个问题tz_companyimmutable?如果是,我们如何重写索引?请注意,单个SELECT可能会带来多个公司的信息并混合不同的时区。

3 - 由于处理timestamptz列中索引的复杂性,我们考虑在每个具有ts_tz. 此新列将是已应用 tz 的日期。这是一个好方法吗?

此外,我们需要在转换之前应用 tz,因为每个客户(公司)只选择要过滤的日期,并且这些日期是区域设置感知(tz 感知)的。

编辑 1:

使用的查询仅用于演示。但是一个要求是客户端看到事件发生所在时区的时间戳,这是一个重要的要求。我们在巴西处理物流业务,而巴西本身在全国有四个不同的时区。一个控股公司可以拥有不同的公司,每家公司可能处于不同的时区。

因此,许多查询涉及不同时区的不同公司并应用一些日期过滤。今天,我们的后端返回所有准备显示的数据,并应用时区,这将很难更改。

我们想要实现的是一种处理这些时间戳列的简单且高效的方法:应用按日期过滤(tz 感知)并使用索引来加速查询。

Ale*_*nov 3

1 - 那是因为tz_date没有标记为不可变。如果 postgres 允许在与函数体相同的表达式上创建索引,那么将其标记为不可变是安全的——它只允许在不可变表达式上执行此操作。有些 postgres 日期时间操作函数和类型转换是不可变的,有些则不是。顺便说一句,我不确定如果at time zone操作符在 tzdata 更改时破坏其不变性契约,索引会发生什么——这种情况在 postgres 或操作系统升级时经常发生,具体取决于设置。

2 - 这是一种非常危险的方法,如果更改数据,索引就会损坏。您可能会丢失数据。如果您绝对需要这个伪不可变函数,我强烈建议添加一个不允许删除、截断和更新company.tz. 如果您需要更改时区数据,请先删除索引。

3 - 关键问题是您是否碰巧跨多个公司查询数据?

a) 如果你这样做,那只是数字意义上的。2011-09-13来自纽埃 (UTC-11) 的事件和2019-09-13来自新西兰 (UTC+13) 的事件永远不会同时发生。这些事件的唯一共同点是它们都发生在 13 号星期五。这只是一种表示法,它从来没有2019-09-13同时出现在两个国家。因此,请确保您的查询确实有意义。在这种不太可能的情况下,将日期符号非规范化为单独的timestamp without time zone列是有意义的,因为您是按时间符号而不是时间时刻进行过滤。我会推荐一个触发器来填充它。

b) 您的所有查询均针对单一公司。在这种情况下,我将仅在不带表达式的列上创建一个普通索引,并创建一个函数并进行如下查询:

create index on company_data(company_id, ts_tz);

create function midnight_at_company(p_date date, p_company_id bigint) strict returns timestamp with time zone as $$
select p_date::timestamp at time zone tz from company where id = p_company_id;
$$ language sql;

-- put your company id instead of $1
explain analyse
select cd.company_id,
       cd.ts_tz at time zone tz_company(cd.company_id)
from company_data cd
where company_id = $1
  and cd.ts_tz >= midnight_at_company('2019-08-20', $1)
  and cd.ts_tz < midnight_at_company('2019-08-23', $1); --note exact `<`, not `<=`
Run Code Online (Sandbox Code Playgroud)