Postgresql 如何获得一个简单的哈希连接查询以及一个复杂的排序合并查询?

Postgresql 如何获得一个简单的哈希连接查询以及一个复杂的排序合并查询?,postgresql,join,query-optimization,generate-series,Postgresql,Join,Query Optimization,Generate Series,我有一个记录运行进程信息的系统。每个正在运行的进程都包含一系列可能并行运行也可能不并行运行的步骤。系统将有关流程及其步骤的信息记录到两个单独的表中: CREATE TABLE pid ( pid integer, start_time timestamp, end_time timestamp, elapsed bigint, aborted integer, label char(30) ); CREATE TABL

我有一个记录运行进程信息的系统。每个正在运行的进程都包含一系列可能并行运行也可能不并行运行的步骤。系统将有关流程及其步骤的信息记录到两个单独的表中:

CREATE TABLE pid (
  pid         integer,
  start_time  timestamp,
  end_time    timestamp,
  elapsed     bigint,
  aborted     integer,
  label       char(30)
);

CREATE TABLE pid_step (
  pid         integer,
  step        integer,
  start_time  timestamp,
  end_time    timestamp,
  elapsed     bigint,
  mem         bigint,
  ...
);
pid_步骤表包含一组关于每个步骤的资源使用统计数据,我在这里将其简化为mem列,该列记录为该步骤分配的内存字节数。我想按进程标签对内存分配进行采样,可能每隔5秒一次,这样我就可以绘制它了。我需要一个类似于以下内容的结果:

tick                    label  mem
----------------------- ------ -----------
2014-11-04 05:37:40.0   foo      328728576
2014-11-04 05:37:40.0   bar         248436
2014-11-04 05:37:40.0   baz        1056144
2014-11-04 05:37:45.0   foo     1158807552
2014-11-04 05:37:45.0   bar         632822
2014-11-04 05:37:45.0   baz         854398
由于日志只提供每个进程和步骤的开始和结束时间戳,而不是5秒间隔的资源使用示例,因此我需要找到最有效的方法来确定哪些进程步骤以每5秒间隔运行,然后聚合它们分配的内存。我已经做了3次尝试,所有这些尝试都产生了相同的结果,但性能水平不同。为了简洁起见,我将把每个查询及其解释计划放在一个要点中,但我将解释每个查询的方法:

这是我的第一次尝试,绝对是最直观和最容易维护的。它将不同的流程标签与generate_series交叉连接,为每个标签生成5秒的刻度,然后在pid和pid_步骤表上左连接。左连接创建零填充效果,并确保不会删除任何没有关联数据的记号。不幸的是,这种方法执行下面最差的see基准链接,我认为这是由于使用了散列连接,其中t2.start_time和t2.end_time谓词作为连接筛选器而不是连接条件处理

这是我的第二次尝试,它的性能更好,但直观性和可维护性要差得多。零填充方法与查询1中的方法相同。但是,在执行pid和pid_步骤的左连接之前,我会根据最大进程运行时间和进程步骤开始和结束时间预先计算具有关联数据的刻度。这允许排序合并联接,其中记号谓词和标签谓词都可以表示为联接条件,并且不使用联接筛选器

这是我最后一次尝试,它的表现最好,与查询2的直观性和可维护性差不多。这里的优化是,我使用最大进程步骤经过时间,它保证小于最大进程经过时间,因此在CTE t3开始时创建一个较小的嵌套循环

理想情况下,我希望SQL与查询1一样简单且可维护,但其性能与查询3一样好。在索引或稍微重写查询1方面,我能做些什么来提高性能吗


基准测试结果:

下面是一个使用

抽样方法是一个好主意,但在我看来是一种优化。以下是我的解决方案:

假设我们要绘制一天的数据,我们将这一天划分为若干时间片,每个时间片持续5秒。对于一个进程和一个时间片,我们希望检索在这5秒内运行的所有步骤的平均内存。 因此,我们不是每5秒采样一次,这会隐藏数据峰值,而是显示这5秒的相关数据的聚合。聚合可以是任何可用的PostgreSQL聚合函数

第一步是生成这些时间片,就像您在不使用范围数据类型的情况下所做的那样:

-- list of time ranges of 5 seconds interval
-- inclusive lower bound, exclusive upper bound
SELECT 
  tsrange(tick, tick + '5 seconds'::interval, '[)') as time_range
FROM generate_series(
  '2001-02-16 21:28:30'::timestamp, 
  '2001-02-16 22:28:30'::timestamp, 
  '5 seconds'::interval
) AS tick
请注意,这些切片不会相互重叠,因为下限是包含的,上限是独占的

这里是棘手的部分,我们不想通过删除start_time和end_time并为此数据创建范围列来更改表模式。幸运的是,PostgreSQL允许:

有了这个索引,我们现在能够以处理成本的一小部分使用各种各样的,唯一需要注意的是,为了使用这个索引,我们必须在查询中使用完全相同的函数

正如您可能已经猜到的,索引将用于连接时间片和步骤,因为如果步骤虚拟范围与时间片重叠,我们需要连接

最后一个问题是:

WITH

time_range AS (
  -- list of time ranges of 5 seconds interval
  -- inclusive lower bound, exclusive upper bound
  SELECT 
    tsrange(tick, tick + '5 seconds'::interval, '[)') as time_range
  FROM generate_series(
    '2001-02-16 21:28:30'::timestamp, 
    '2001-02-16 22:28:30'::timestamp, 
    '5 seconds'::interval
  ) AS tick
),

-- associate each pid_step with the matching time_range
-- aggregate the average memory usage for each pid for each time slice
avg_memory_by_pid_by_time_range AS (
  SELECT 
    time_range,
    pid,
    avg(mem) avg_memory
  FROM 
    time_range
    JOIN pid_step 
      ON tsrange(pid_step.start_time, pid_step.end_time, '()') && time_range.time_range
  GROUP BY
    time_range,
    pid
)

-- embellish the result with some additional data from pid
SELECT 
  lower(time_range) AS tick,
  pid.label AS label,
  trunc(avg_memory) AS mem
FROM
  avg_memory_by_pid_by_time_range
  JOIN pid ON avg_memory_by_pid_by_time_range.pid = pid.pid
ORDER BY
  lower(time_range),
  pid.label
;

我希望您的生产数据的性能仍然很好。查询计划等式中有很多细节。

我想说这与要使用的非常匹配。您能发布一个完整的模式,包括所有索引吗?我感觉你很想念他们。我设置了一个虚拟数据库,在我的生产系统之外重新创建它。这里发布的所有结果都来自运行在AWS RDS m1.small实例上的虚拟数据库,该实例只有这两个表。目前还没有索引,这就是为什么我想知道是否有我可以创建的索引可以使查询1和查询3一样执行?另外,tstzrange看起来很有趣,但我应该提到这一点 s是一个没有范围类型的旧系统,更重要的是,我无法更改创建或写入表的系统。我需要处理那里的内容,我只能修改我的查询或添加索引。
-- create index on range (inclusive on upper and lower) 
CREATE INDEX pid_step_tstzrange_index ON pid_step 
USING gist (tsrange(start_time, end_time, '()'));
WITH

time_range AS (
  -- list of time ranges of 5 seconds interval
  -- inclusive lower bound, exclusive upper bound
  SELECT 
    tsrange(tick, tick + '5 seconds'::interval, '[)') as time_range
  FROM generate_series(
    '2001-02-16 21:28:30'::timestamp, 
    '2001-02-16 22:28:30'::timestamp, 
    '5 seconds'::interval
  ) AS tick
),

-- associate each pid_step with the matching time_range
-- aggregate the average memory usage for each pid for each time slice
avg_memory_by_pid_by_time_range AS (
  SELECT 
    time_range,
    pid,
    avg(mem) avg_memory
  FROM 
    time_range
    JOIN pid_step 
      ON tsrange(pid_step.start_time, pid_step.end_time, '()') && time_range.time_range
  GROUP BY
    time_range,
    pid
)

-- embellish the result with some additional data from pid
SELECT 
  lower(time_range) AS tick,
  pid.label AS label,
  trunc(avg_memory) AS mem
FROM
  avg_memory_by_pid_by_time_range
  JOIN pid ON avg_memory_by_pid_by_time_range.pid = pid.pid
ORDER BY
  lower(time_range),
  pid.label
;