TIMESTAMPTZ 和函数不变性的索引
Posted
技术标签:
【中文标题】TIMESTAMPTZ 和函数不变性的索引【英文标题】:Index for TIMESTAMPTZ and function immutability 【发布时间】:2019-10-15 20:30:16 【问题描述】:我们有一个类似下面的结构:
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
);
表格已简化。
在这里摆弄示例数据: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;
我们还有一个获取公司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;
$$;
另一个在应用 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;
$$;
我们现在遇到的问题是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';
所以,为了加快查询速度,我们需要在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));
为此,我们需要将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';
使用索引:
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';
为什么会这样?
2 - 我们知道,理论上,tz_company
不应该是immutable
,最多稳定。但是,公司 tz 是一个永远不应该改变的信息。是的,它可能会发生,但它是不可能的。三年来,我们从未改变过任何一家公司的tz。那么,tz_company
成为immutable
仍然是个问题吗?如果是,我们如何重写索引?请注意,单个SELECT
可能会带来多个公司的信息并混合不同的时区。
3 - 由于处理timestamptz
列中的索引的复杂性,我们考虑在每个具有ts_tz
的表中添加另一列。此新列将是已应用 tz 的日期。这是一个好方法吗?
此外,我们需要在转换之前应用 tz,因为每个客户(公司)只选择要过滤的日期,并且这些日期是区域设置感知的(tz感知的)。
编辑 1:
使用的查询仅用于演示。但是一个要求是客户端看到事件发生的时区中的时间戳,这是一个重要的要求。我们处理巴西的物流业务,巴西本身在全国有四个不同的时区。 控股公司可能拥有不同的公司,而且每家公司可能位于不同的时区。
因此,许多查询涉及不同时区的不同公司,并应用了一些日期过滤。今天,我们的后端返回所有准备显示的数据,并应用了时区,这很难改变。
我们想要实现的是一种处理那些 timestamptz 列的简单且高效的方式:应用按日期过滤(tz 感知)并使用索引来加速查询。
【问题讨论】:
"为什么会发生 [to use the index]?" - 因为只有当 exact 表达式出现在查询,并且您的索引表达式包含::date
演员表。
我会尝试使tz_company
成为一个稳定的函数并使用language SQL
。理论上,优化器可能会内联它并将其优化为一个连接——它永远不会用 plpgsql 这样做。
另一个想法:尝试将比较的日期放入公司时区,然后与 ts_tz
比较,而不是将 ts_tz
移出时区。也许这允许在ts_tz
上使用普通索引。
还有一个想法,或者实际上更多的黑客:尝试添加cd.ts_tz >= '2019-08-20' - '1 day' AND cd.ts_tz <= '2019-08-22' + '1 day' conditions. They should be able to use a normal index on
ts_tz`,作为更复杂的时区调整比较之前的预过滤器,同时不删除任何正确的匹配给你时区知识。
@Bergi 最后一条评论(可索引的预过滤器)肯定是赢家,但内联函数也有机会。这足以回答问题。
【参考方案1】:
1 - 这是因为tz_date
没有被标记为不可变。如果 postgres 允许在与函数主体相同的表达式上创建索引,则将其标记为不可变是安全的——它只允许在不可变的表达式上创建索引。一些 postgres 日期时间操作函数和类型转换是不可变的,有些则不是。顺便说一句,如果 at time zone
运算符在 tzdata 更改时违反其不变性合同,我不确定索引会发生什么情况——这在 postgres 或操作系统升级时经常发生,具体取决于设置。
2 - 这是一种非常危险的方法,如果您更改数据,索引就会损坏。您可能会丢失数据。如果您绝对需要这个伪不可变函数,我强烈建议您添加一个触发器,该触发器不允许删除、截断和更新company.tz
。如果您需要更改时区数据,请先删除索引。
3 - 关键问题是您是否碰巧跨多个公司查询数据?
a) 如果你这样做了,那只是命理意义上的。来自纽埃 (UTC-11) 的 2011-09-13
事件和来自新西兰 (UTC+13) 的 2019-09-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 `<=`
【讨论】:
对于 1 和 2,好的。对于 3,我用更多信息更新了问题。我们可以从具有多个时区的公司中进行选择。在这种情况下,日期过滤器应考虑事件在特定公司时区发生的日期。如果我过滤2019-09-13
,我对当天发生的事件感兴趣,时区感知(考虑到它发生所在公司的时区),这就是我们在转换到日期之前应用 tz 的原因。【参考方案2】:
我会将所有时区标准化为一个称为数据库或服务器时间的时区。我了解公司位于不同的地方,但这不是在您的数据中设置时区的好理由。使用这种方法将不需要有一个时区参考表。当您从这些公司中的任何一家提取数据时,请编写代码以考虑服务器时区,以便读取您的本地时间。
这将消除大量潜在的混乱。这是世界范围内使用的一种方法,这就是为什么大多数 API 中的数据时间戳只有一个时区。
回应编辑:
嗨@路易斯
让我从没有正确或错误的答案开始,它认为最有效。就我而言,我认为前端视图和数据应该分开管理。根据本主题,在数据方面,我将使用服务器时间处理所有日期戳。需要以一种或另一种方式查看数据是前端问题。
如果您有要求,我会像这样硬编码一个 js 开关。
switch("CampanyA")
case "CompanyA":
return Timezone EST...
// code block
break;
case "CompanyB" :
// code block
break;
default:
// code block
或者如果有很多公司需要处理硬代码,我会制作一个包含“公司 ID”、“公司名称”和“时区代码”的表格。不要将此表链接到您的数据。您应该将“公司 ID”添加到具有服务器时区的事件的主表中。
使用带有公司时区代码的表格来填充将用于运行查询的查找过滤器。当您的脚本事件处理程序对下拉菜单做出反应时,它将保存与该公司关联的当前 TM 区域代码,并在尝试根据您的要求显示时区时使用该值。我还会强制您的代码以异步方式加载数据(每几毫秒大约 1000 条记录),而不是一次全部加载。这将大大提高性能,并且用户将无法判断他们的数据仍在加载中。
这项工作将使您能够操纵时区以满足当前和未来可能出现的要求。
【讨论】:
让我看看我是否明白这一点:使用这种方法我必须在客户端应用时区? (请参阅我添加到问题中的编辑 1) 嗨@Luiz,我试图回复评论,但我的回答太长了。我已经用详细信息编辑了我的答案。请让我知道您的想法。【参考方案3】:我认为您用于您的应用程序的当前架构不是解决此类问题的最佳方案。
在同一张桌子上保存不同的时区会遇到很多问题。 使用 UTC,只在 DB/Schema 级别使用 UTC,你也可以在 Postgres conf 中设置。
根据应用程序,您可以发回 UTC 日期并将其转换为 javascript/server 端的当前本地时间。如果这不可能,请在一个地方指定用户当前的 UTC 偏移量,然后在显示日期/时间之前将其转换为他们的时间。
这将使您的生活变得超级简单,并且您可以在查询级别上获得出色的性能,因为您现在将拥有一个高性能的数据库架构,您拥有的 SQL 函数没有任何意义,因为您可以通过使用获得更好的性能数据库中的索引。
因此,根据您的具体要求,我将拥有与您一样的架构并添加一些内容,我将为表 company 编制索引,并将所有数据以 UTC 格式存储在表 company_data 中以获取时间戳。
如果正在请求公司数据,我们从公司表中获取时区(文本),使用这些数据我们可以让后端代码/JS 执行时区更改魔法。
我们的时区数量有限,理想情况下,您可以在配置中设置这些时区,以便更轻松、更快速地查找。
【讨论】:
以上是关于TIMESTAMPTZ 和函数不变性的索引的主要内容,如果未能解决你的问题,请参考以下文章