我有一个记录有关正在运行的进程的信息的系统。每个运行的进程都包含一系列可能并行运行的步骤。系统将有关进程及其步骤的信息记录到两个单独的表中:
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_step
表包含关于每个步骤的一堆资源使用情况统计信息,我在这里简化为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次单独的尝试,这些尝试都产生了不同性能水平的相同结果。为了简洁起见,我将每个查询及其解释计划放在一个要点(https://gist.github.com/anonymous/3b57f70015b0d234a2de)中,但我会解释每个问题的方法:
这是我的第一次尝试,它绝对是最直观,最容易维护的。它交叉使用generate_series
连接不同的流程标签,为每个标签生成5秒的刻度,然后在pid
和pid_step
表上保留连接。左连接创建了一个"零填充"效果并确保我们不会删除任何没有关联数据的刻度。不幸的是,这种方法表现最差(参见下面的基准链接),我认为这是由于使用了散列连接,其中between t2.start_time and t2.end_time
谓词被处理为连接过滤器而不是连接条件。
这是我的第二次尝试,它表现得更好,但不那么直观和可维护。 "零填充"方法与查询1中的方法相同。但是,在执行pid
和pid_step
的左连接之前,我根据最大流程耗用时间和流程步骤预先计算具有关联数据的滴答开始和结束时间。这允许排序合并连接,其中tick和label谓词都可以表示为连接条件,并且不使用连接过滤器。
这是我的最后一次尝试,它在与查询2具有相同的直观性和可维护性方面表现最佳。这里的优化是我使用最大过程步骤经过的时间,保证小于最大过程经过时间,因此在CTE t3的开头创建一个较小的嵌套循环。
理想情况下,我希望SQL与查询1一样简单和可维护,但执行与查询3一样好。我可以在索引的方式上做什么,或者稍微重写一下查询1提高绩效?
基准测试结果: http://i.imgur.com/yZxdQlM.png
答案 0 :(得分:0)
以下是使用PostgreSQL ranges(SQLFiddle)
的强大功能的解决方案CREATE TABLE pid (
pid integer PRIMARY KEY,
label char(30)
);
CREATE TABLE pid_step (
pid integer,
step serial,
start_time timestamp,
end_time timestamp,
mem bigint,
PRIMARY KEY (pid, step)
);
抽样方法是一个好主意,但在我看来,是一种优化。这是我的解决方案:
假设我们想要绘制一天的数据,我们将这一天分成多个时间片,每个时间片持续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允许indexes on expressions:
-- 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, '()'));
有了这个索引,我们现在能够以很少的处理成本使用各种PostgreSQL range operators,唯一需要注意的是,为了使用这个索引,我们必须使用完全相同的函数我们的询问。
正如您可能已经猜到的那样,索引将用于加入时间片和步骤,因为我们需要加入,如果步骤"虚拟"范围重叠时间片。
以下是最终查询:
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
;
我希望您的生产数据中的性能仍然很好(查询计划公式中有很多细节)。