【问题标题】:Database design for time series时间序列的数据库设计
【发布时间】:2019-11-13 17:50:01
【问题描述】:

我大约每 10 分钟插入约 50 条具有相同时间戳的记录。
这意味着每小时约 600 条记录或每天 7.200 条记录或每年 2.592.000 条记录。
用户想要检索最接近请求时间的时间戳的所有记录。

设计#1 - 一张在时间戳列上有索引的表:

    CREATE TABLE A (t timestamp, value int);
    CREATE a_idx ON A (t);

单个插入语句创建约 50 条具有相同时间戳的记录:

    INSERT INTO A VALUES (
      (‘2019-01-02 10:00’, 5),
      (‘2019-01-02 10:00’, 12),
      (‘2019-01-02 10:00’, 7),
       ….
    )

获取最接近询问时间的所有记录
(我使用 PostgreSQL 中可用的函数 best()):

    SELECT * FROM A WHERE t =
(SELECT t FROM A ORDER BY greatest(t - asked_time, asked_time - t) LIMIT 1)

我认为这个查询效率不高,因为它需要全表扫描。
我计划按时间戳对 A 表进行分区,以便每年有 1 个分区,但上面的近似匹配仍然会很慢。

设计#2 - 创建 2 个表格:
第一张表:保持唯一的时间戳和自动递增的 PK,
第二张表:在第一张表PK上保存数据和外键

    CREATE TABLE UNIQ_TIMESTAMP (id SERIAL PRIMARY KEY, t timestamp);
    CREATE TABLE DATA (id INTEGER REFERENCES UNIQ_TIMESTAMP (id), value int);
    CREATE INDEX data_time_idx ON DATA (id);

获取最接近询问时间的所有记录:

SELECT * FROM DATA WHERE id =
(SELECT id FROM UNIQ_TIMESTAMP ORDER BY greatest(t - asked_time, asked_time - t) LIMIT 1)

与设计 #1 相比,它应该运行得更快,因为嵌套选择会扫描较小的表。
这种方法的缺点:
- 我必须插入 2 个表而不是一个
- 我失去了按时间戳对 DATA 表进行分区的能力

你可以推荐什么?

【问题讨论】:

  • 如果用户请求的时间戳可能不完全存在于数据集中,那么我将采用您的第一种方法。您可以使用 RANK 作为替代方案,但我看不出有任何方法可以避免某种子查询。

标签: postgresql database-design relational-database


【解决方案1】:

我会采用单表方法,可能按年份分区,以便轻松删除旧数据。

创建一个类似的索引

CREATE INDEX ON a (date_trunc('hour', t + INTERVAL '30 minutes'));

然后像你写的那样使用你的查询,但是添加

AND date_trunc('hour', t + INTERVAL '30 minutes')
  = date_trunc('hour', asked_time + INTERVAL '30 minutes')

附加条件充当过滤器,可以使用索引。

【讨论】:

    【解决方案2】:

    您可以使用两个查询的 UNION 来查找最接近给定时间戳的所有时间戳:

    (
      select t
      from a
      where t >= timestamp '2019-03-01 17:00:00'
      order by t
      limit 1
    )
    union all
    (
      select t
      from a
      where t <= timestamp '2019-03-01 17:00:00'
      order by t desc
      limit 1
    )
    

    这将有效地利用t 上的索引。在有 1000 万行(大约 3 年的数据)的表上,我得到以下执行计划:

    Append  (cost=0.57..1.16 rows=2 width=8) (actual time=0.381..0.407 rows=2 loops=1)
      Buffers: shared hit=6 read=4
      I/O Timings: read=0.050
      ->  Limit  (cost=0.57..0.58 rows=1 width=8) (actual time=0.380..0.381 rows=1 loops=1)
            Output: a.t
            Buffers: shared hit=1 read=4
            I/O Timings: read=0.050
            ->  Index Only Scan using a_t_idx on stuff.a  (cost=0.57..253023.35 rows=30699415 width=8) (actual time=0.380..0.380 rows=1 loops=1)
                  Output: a.t
                  Index Cond: (a.t >= '2019-03-01 17:00:00'::timestamp without time zone)
                  Heap Fetches: 0
                  Buffers: shared hit=1 read=4
                  I/O Timings: read=0.050
      ->  Limit  (cost=0.57..0.58 rows=1 width=8) (actual time=0.024..0.025 rows=1 loops=1)
            Output: a_1.t
            Buffers: shared hit=5
            ->  Index Only Scan Backward using a_t_idx on stuff.a a_1  (cost=0.57..649469.88 rows=78800603 width=8) (actual time=0.024..0.024 rows=1 loops=1)
                  Output: a_1.t
                  Index Cond: (a_1.t <= '2019-03-01 17:00:00'::timestamp without time zone)
                  Heap Fetches: 0
                  Buffers: shared hit=5
    Planning Time: 1.823 ms
    Execution Time: 0.425 ms
    

    如您所见,它只需要很少的 I/O 操作,而且几乎与表大小无关。

    以上可用于IN条件:

    select *
    from a
    where t in ( 
      (select t
       from a
       where t >= timestamp '2019-03-01 17:00:00'
       order by t
       limit 1)
      union all
      (select t
       from a
       where t <= timestamp '2019-03-01 17:00:00'
       order by t desc
       limit 1)
    );
    

    如果您知道接近请求的时间戳的值永远不会超过 100 个,则可以完全删除 IN 查询,只需在联合的两个部分中使用 limit 100。这使得查询更有效率,因为没有第二步来评估 IN 条件,但可能会返回比您想要的更多的行。

    如果您总是在同一年寻找时间戳,那么按年分区确实会有所帮助。

    如果查询太复杂,可以将其放入函数中:

    create or replace function get_closest(p_tocheck timestamp)
      returns timestamp
    as
    $$
      select *
      from (
         (select t
         from a
         where t >= p_tocheck
         order by t
         limit 1)
        union all
        (select t
         from a
         where t <= p_tocheck
         order by t desc
         limit 1)
      ) x
      order by greatest(t - p_tocheck, p_tocheck - t)
      limit 1;
    $$
    language sql stable;
    

    查询变得如此简单:

    select *
    from a
    where t = get_closest(timestamp '2019-03-01 17:00:00');
    

    另一种解决方案是使用btree_gist 扩展,它提供“距离”运算符&lt;-&gt;

    然后你可以在时间戳上创建一个 GiST 索引:

    create index on a using gist (t) ;
    

    并使用以下查询:

    select *
    from a where t in (select t
                      from a
                      order by t <-> timestamp '2019-03-01 17:00:00'
                      limit 1);
    

    【讨论】:

      猜你喜欢
      • 2019-10-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-22
      相关资源
      最近更新 更多