问题:
我的数据库中有与时间相关的数据,我正在努力以某种方式组织、构造和索引这些数据,以便用户可以有效地检索它;即使是简单的数据库查询也需要比可接受的更长的时间。
项目背景:
虽然这是一个纯粹的数据库问题,但一些上下文可能有助于理解数据模型:
该项目围绕对大型复杂机器进行研究。我对机器本身不太了解,但实验室里有传言说那里的某个地方有一个磁通电容器——我想昨天,我发现薛定谔猫的尾巴挂在旁边;-)
我们在机器运行时使用传感器测量许多不同的参数,这些传感器位于机器各处的不同测量点(所谓的点),在一段时间内以一定的间隔。我们不仅使用一种设备来测量这些参数,而且使用它们的整个范围;它们的测量数据质量不同(我认为这涉及采样率、传感器质量、价格和我不关心的许多其他方面);该项目的一个目标实际上是在这些设备之间进行比较。您可以将这些测量设备想象成一堆实验室手推车,每个手推车都有许多连接到机器的电缆,每个都提供测量数据。
数据模型:
有来自每个地点和每个设备的每个参数的测量数据,例如每分钟一次,为期 6 天。我的工作是将这些数据存储在数据库中并提供对它的有效访问。
简而言之:
- 设备具有唯一名称
- 参数也有名称;它们不是唯一的,所以它也有一个 ID
- 一个点有一个 ID
项目数据库当然更复杂,但这些细节似乎与问题无关。
- 测量数据索引具有 ID、测量完成时间的时间戳以及对设备的引用以及进行测量的地点
- 测量数据值引用参数和实际测量的值
最初,我将测量数据值建模为具有自己的 ID 作为主键;测量数据索引和值之间的n:m
关系是一个单独的表,只存储index:value
ID对,但是由于该表本身占用了相当多的硬盘空间,我们将其删除并将值ID更改为一个简单的整数,存储该ID的ID所属的测量数据指标;测量数据值的主键现在由该 ID 和参数 ID 组成。
附带说明:当我创建数据模型时,我仔细遵循了常见的设计准则,如3NF和适当的表约束(如唯一键);另一个经验法则是为每个外键创建一个索引。我怀疑测量数据索引/值表与“严格”3NF 的偏差可能是我现在正在查看的性能问题的原因之一,但是将数据模型改回来并没有解决问题。
DDL 中的数据模型:
注意:下面有此代码的更新。
下面的脚本创建数据库和所有涉及的表。请注意,目前还没有明确的索引。在你运行这个之前,请确保你没有碰巧已经有一个数据库调用so_test
了任何有价值的数据......
\c postgres
DROP DATABASE IF EXISTS so_test;
CREATE DATABASE so_test;
\c so_test
CREATE TABLE device
(
name VARCHAR(16) NOT NULL,
CONSTRAINT device_pk PRIMARY KEY (name)
);
CREATE TABLE parameter
(
-- must have ID as names are not unique
id SERIAL,
name VARCHAR(64) NOT NULL,
CONSTRAINT parameter_pk PRIMARY KEY (id)
);
CREATE TABLE spot
(
id SERIAL,
CONSTRAINT spot_pk PRIMARY KEY (id)
);
CREATE TABLE measurement_data_index
(
id SERIAL,
fk_device_name VARCHAR(16) NOT NULL,
fk_spot_id INTEGER NOT NULL,
t_stamp TIMESTAMP NOT NULL,
CONSTRAINT measurement_pk PRIMARY KEY (id),
CONSTRAINT measurement_data_index_fk_2_device FOREIGN KEY (fk_device_name)
REFERENCES device (name) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT measurement_data_index_fk_2_spot FOREIGN KEY (fk_spot_id)
REFERENCES spot (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT measurement_data_index_uk_all_cols UNIQUE (fk_device_name, fk_spot_id, t_stamp)
);
CREATE TABLE measurement_data_value
(
id INTEGER NOT NULL,
fk_parameter_id INTEGER NOT NULL,
value VARCHAR(16) NOT NULL,
CONSTRAINT measurement_data_value_pk PRIMARY KEY (id, fk_parameter_id),
CONSTRAINT measurement_data_value_fk_2_parameter FOREIGN KEY (fk_parameter_id)
REFERENCES parameter (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION
);
我还创建了一个脚本来用一些测试数据填充表格:
CREATE OR REPLACE FUNCTION insert_data()
RETURNS VOID
LANGUAGE plpgsql
AS
$BODY$
DECLARE
t_stamp TIMESTAMP := '2012-01-01 00:00:00';
index_id INTEGER;
param_id INTEGER;
dev_name VARCHAR(16);
value VARCHAR(16);
BEGIN
FOR dev IN 1..5
LOOP
INSERT INTO device (name) VALUES ('dev_' || to_char(dev, 'FM00'));
END LOOP;
FOR param IN 1..20
LOOP
INSERT INTO parameter (name) VALUES ('param_' || to_char(param, 'FM00'));
END LOOP;
FOR spot IN 1..10
LOOP
INSERT INTO spot (id) VALUES (spot);
END LOOP;
WHILE t_stamp < '2012-01-07 00:00:00'
LOOP
FOR dev IN 1..5
LOOP
dev_name := 'dev_' || to_char(dev, 'FM00');
FOR spot IN 1..10
LOOP
INSERT INTO measurement_data_index
(fk_device_name, fk_spot_id, t_stamp)
VALUES (dev_name, spot, t_stamp) RETURNING id INTO index_id;
FOR param IN 1..20
LOOP
SELECT id INTO param_id FROM parameter
WHERE name = 'param_' || to_char(param, 'FM00');
value := 'd' || to_char(dev, 'FM00')
|| '_s' || to_char(spot, 'FM00')
|| '_p' || to_char(param, 'FM00');
INSERT INTO measurement_data_value (id, fk_parameter_id, value)
VALUES (index_id, param_id, value);
END LOOP;
END LOOP;
END LOOP;
t_stamp := t_stamp + '1 minute'::INTERVAL;
END LOOP;
END;
$BODY$;
SELECT insert_data();
PostgreSQL 查询计划器需要最新的统计信息,因此请分析所有表。可能不需要吸尘,但无论如何都要这样做:
VACUUM ANALYZE device;
VACUUM ANALYZE measurement_data_index;
VACUUM ANALYZE measurement_data_value;
VACUUM ANALYZE parameter;
VACUUM ANALYZE spot;
示例查询:
如果我现在运行一个非常简单的查询来例如获取某个参数的所有值,它已经需要几秒钟,尽管数据库还不是很大:
EXPLAIN (ANALYZE ON, BUFFERS ON)
SELECT measurement_data_value.value
FROM measurement_data_value, parameter
WHERE measurement_data_value.fk_parameter_id = parameter.id
AND parameter.name = 'param_01';
我的开发机器上的示例结果(有关我的环境的一些详细信息,请参见下文):
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
Hash Join (cost=1.26..178153.26 rows=432000 width=12) (actual time=0.046..2281.281 rows=432000 loops=1)
Hash Cond: (measurement_data_value.fk_parameter_id = parameter.id)
Buffers: shared hit=55035
-> Seq Scan on measurement_data_value (cost=0.00..141432.00 rows=8640000 width=16) (actual time=0.004..963.999 rows=8640000 loops=1)
Buffers: shared hit=55032
-> Hash (cost=1.25..1.25 rows=1 width=4) (actual time=0.010..0.010 rows=1 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 1kB
Buffers: shared hit=1
-> Seq Scan on parameter (cost=0.00..1.25 rows=1 width=4) (actual time=0.004..0.008 rows=1 loops=1)
Filter: ((name)::text = 'param_01'::text)
Buffers: shared hit=1
Total runtime: 2313.615 ms
(12 rows)
除了隐式索引之外,数据库中没有索引,因此规划器仅执行顺序扫描也就不足为奇了。如果我遵循似乎是经验法则并为每个外键添加 btree 索引,例如
CREATE INDEX measurement_data_index_idx_fk_device_name
ON measurement_data_index (fk_device_name);
CREATE INDEX measurement_data_index_idx_fk_spot_id
ON measurement_data_index (fk_spot_id);
CREATE INDEX measurement_data_value_idx_fk_parameter_id
ON measurement_data_value (fk_parameter_id);
然后再做一次真空分析(为了安全起见)并重新运行查询,规划器使用位图堆和位图索引扫描,总查询时间有所改善:
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=8089.19..72842.42 rows=431999 width=12) (actual time=66.773..1336.517 rows=432000 loops=1)
Buffers: shared hit=55033 read=1184
-> Seq Scan on parameter (cost=0.00..1.25 rows=1 width=4) (actual time=0.005..0.012 rows=1 loops=1)
Filter: ((name)::text = 'param_01'::text)
Buffers: shared hit=1
-> Bitmap Heap Scan on measurement_data_value (cost=8089.19..67441.18 rows=431999 width=16) (actual time=66.762..1237.488 rows=432000 loops=1)
Recheck Cond: (fk_parameter_id = parameter.id)
Buffers: shared hit=55032 read=1184
-> Bitmap Index Scan on measurement_data_value_idx_fk_parameter_id (cost=0.00..7981.19 rows=431999 width=0) (actual time=65.222..65.222 rows=432000 loops=1)
Index Cond: (fk_parameter_id = parameter.id)
Buffers: shared read=1184
Total runtime: 1371.716 ms
(12 rows)
但是,对于一个非常简单的查询,这仍然是超过一秒的执行时间。
到目前为止我做了什么:
- 给自己买了一本PostgreSQL 9.0 High Performance - 好书!
- 做了一些基本的 PostgreSQL 服务器配置,见下面的环境
- 创建了一个框架来使用项目中的真实查询运行一系列性能测试并以图形方式显示结果;这些查询使用设备、点、参数和时间间隔作为输入参数,并且测试系列运行例如 5、10 个设备、5、10 个点、5、10、15、20 个参数和 1..7 天。基本结果是它们都太慢了,但是它们的查询计划太复杂了,我无法理解,所以我回到上面使用的非常简单的查询。
我已经研究过对值表进行分区。数据与时间相关,分区似乎是组织此类数据的合适方法;甚至 PostgreSQL 文档中的示例也使用了类似的东西。但是,我在同一篇文章中读到:
这些好处通常只有在表格非常大的情况下才值得。表从分区中受益的确切点取决于应用程序,但经验法则是表的大小应该超过数据库服务器的物理内存。
整个测试数据库的大小小于 1GB,我在具有 8GB RAM 的开发机器和具有 1GB 的虚拟机上运行测试(另请参见下面的环境),因此该表远非很大甚至超过物理内存。无论如何,我可能会在某个阶段实现分区,但我觉得这种方法并不针对性能问题本身。
此外,我正在考虑对值表进行聚类。我不喜欢这样一个事实,即每当插入新数据时都必须重新进行集群,而且它还需要一个独占的读/写锁,但是看看这个SO question,它似乎无论如何都有它的好处,并且可能是一种选择。但是,集群是在索引上完成的,由于查询中最多有 4 个选择条件(设备、点、参数和时间),我必须为所有这些条件创建集群——这反过来给我的印象是我只是没有创建正确的索引......
我的环境:
- 正在开发具有双核 CPU 和 8GB RAM 的 MacBook Pro(2009 年中)
- 我正在 MBP 上托管的具有 1GB RAM 的虚拟 Debian 6.0 机器上运行数据库性能测试
- PostgreSQL 版本是 9.1,因为这是我安装时的最新版本,可以升级到 9.2
- 按照PostgreSQL 文档中的建议,我已将
shared_buffers
两台机器上的默认 1600kB 更改为 25% 的 RAM (这涉及扩大SHMALL、SHMMAX 等内核设置) - 同样,我已将有效缓存大小从默认的 128MB 更改为可用 RAM 的 50%
- 我使用不同的work_mem设置进行了性能测试,但没有发现性能上有任何重大差异
注意:我认为重要的一个方面是,具有来自项目的真实查询的性能测试系列在 8GB 的 MacBook 和 1GB 的虚拟机之间在性能方面没有差异;即,如果在 MacBook 上查询需要 10 秒,那么在 VM 上也需要 10 秒。此外,我在更改前后进行了相同的性能测试shared_buffers
,effective_cache_size
并且work_mem
配置更改并没有将性能提高 10% 以上;实际上,有些结果甚至变得更糟,因此似乎任何差异都是由测试变化而不是由配置更改引起的。这些观察让我相信 RAM 和postgres.conf
设置还不是这里的限制因素。
我的问题:
我不知道不同的或额外的索引是否会加快查询速度,如果有的话,要创建哪些。查看数据库的大小和查询的简单程度,我的印象是我的数据模型或到目前为止我选择索引的方式存在根本性错误。
有没有人对我如何构建和索引与时间相关的我以提高查询性能有一些建议?
更广泛地问,是调优查询性能
- 通常是“在事件基础上”完成的,即一旦查询不能令人满意地执行?看来我所有的查询都太慢了......
- 主要是查看(和理解)查询计划的问题,然后添加索引并衡量情况是否有所改善,可能会通过应用自己的经验来加速流程?
我如何让这个数据库飞起来?
更新01:
看看到目前为止的回复,我想我没有正确解释测量数据索引/值表的必要性,所以让我再试一次。存储空间是这里的问题。
笔记:
- 此处使用的数字更多地用于说明目的,仅用于比较,即数字本身不相关,重要的是使用单个表与使用索引和值表之间存储要求的百分比差异
- 本章记录了PostgreSQL数据类型存储大小
- 这并没有声称在科学上是正确的,例如,这些单位可能是数学假的;这些数字应该加起来
假设
- 1天的测量
- 每分钟 1 组测量
- 10 台设备
- 10个参数
- 10个点
这加起来
1 测量/分钟 x 60 分钟/小时 x 24 小时/天 = 1440 测量/天
每次测量都有来自每个地点和每个设备的每个参数的数据,所以
10 个点 x 10 个设备 x 10 个参数 = 1000 个数据集/测量
所以总的来说
1440 个测量值/天 x 1000 个数据集/测量值 = 1 440 000 个数据集/天
如果我们按照Catcall 的建议将所有测量值存储在一个表中,例如
CREATE TABLE measurement_data
(
device_name character varying(16) NOT NULL,
spot_id integer NOT NULL,
parameter_id integer NOT NULL,
t_stamp timestamp without time zone NOT NULL,
value character varying(16) NOT NULL,
-- constraints...
);
单行加起来
17 + 4 + 4 + 8 + 17 = 50 字节/行
在最坏的情况下,所有 varchar 字段都已完全填充。这相当于
50 字节/行 x 1 440 000 行/天 = 72 000 000 字节/天
或每天约 69 MB。
虽然这听起来并不多,但真实数据库中的存储空间要求会令人望而却步(再次重申,此处使用的数字仅用于说明)。因此,我们将测量数据拆分为索引和值表,如问题前面所述:
CREATE TABLE measurement_data_index
(
id SERIAL,
fk_device_name VARCHAR(16) NOT NULL,
fk_spot_id INTEGER NOT NULL,
t_stamp TIMESTAMP NOT NULL,
-- constraints...
);
CREATE TABLE measurement_data_value
(
id INTEGER NOT NULL,
fk_parameter_id INTEGER NOT NULL,
value VARCHAR(16) NOT NULL,
-- constraints...
);
其中值行的 ID 等于它所属的索引的 ID。
索引表和值表中一行的大小是
索引:4 + 17 + 4 + 8 = 33 字节 值:4 + 4 + 17 = 25 个字节
(再次,最坏的情况)。总行数为
索引:10 个设备 x 10 个点 x 1440 次测量/天 = 144 000 行/天 值:10 个参数 x 144 000 行/天 = 1 440 000 行/天
所以总数是
索引:33 字节/行 x 144 000 行/天 = 4 752 000 字节/天 值:25 字节/行 x 1 440 000 行/天 = 36 000 000 字节/天 总计:= 40 752 000 字节/天
或每天约 39 MB - 而单表解决方案约 69 MB。
更新 02(回复:wildplassers 响应):
这个问题已经很长了,所以我正在考虑更新上面原始问题中的代码,但我认为在这里同时拥有第一个和改进的解决方案可能有助于更好地看到差异。
与原始方法相比的变化(按重要性排序):
- 交换时间戳和参数,即将
t_stamp
字段从measurement_data_index
表移到measurement_data_value
和fk_parameter_id
从值移到索引表:通过这种更改,索引表中的所有字段都是不变的,新的测量数据只写入值表。我没想到这会带来任何重大的查询性能改进(我错了),但我觉得它使测量数据索引的概念更加清晰。虽然它需要更多的存储空间(根据一些相当粗略的估计),但当根据读/写要求将表空间移动到不同的硬盘驱动器时,拥有一个“静态”索引表也可能有助于部署。 - 在设备表中使用代理键:据我了解,代理键是从数据库设计的角度来看并非严格要求的主键(例如,设备名称已经是唯一的,因此它也可以用作 PK),但是可能有助于提高查询性能。我再次添加它是因为,如果索引表仅引用 ID(而不是某些名称和某些 ID),我觉得它会使概念更清晰。
- rewrite
insert_data()
:使用generate_series()
而不是嵌套FOR
循环;使代码更加“活泼”。 - 作为这些更改的副作用,插入测试数据只需要第一个解决方案所需时间的大约 50%。
- 我没有按照 wildplasser 的建议添加视图;不需要向后兼容。
- 查询计划器似乎忽略了索引表中 FK 的附加索引,并且对查询计划或性能没有影响。
(似乎没有这一行,下面的代码没有正确显示为SO页面上的代码......)
\c postgres
DROP DATABASE IF EXISTS so_test_03;
CREATE DATABASE so_test_03;
\c so_test_03
CREATE TABLE device
(
id SERIAL,
name VARCHAR(16) NOT NULL,
CONSTRAINT device_pk PRIMARY KEY (id),
CONSTRAINT device_uk_name UNIQUE (name)
);
CREATE TABLE parameter
(
id SERIAL,
name VARCHAR(64) NOT NULL,
CONSTRAINT parameter_pk PRIMARY KEY (id)
);
CREATE TABLE spot
(
id SERIAL,
name VARCHAR(16) NOT NULL,
CONSTRAINT spot_pk PRIMARY KEY (id)
);
CREATE TABLE measurement_data_index
(
id SERIAL,
fk_device_id INTEGER NOT NULL,
fk_parameter_id INTEGER NOT NULL,
fk_spot_id INTEGER NOT NULL,
CONSTRAINT measurement_pk PRIMARY KEY (id),
CONSTRAINT measurement_data_index_fk_2_device FOREIGN KEY (fk_device_id)
REFERENCES device (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT measurement_data_index_fk_2_parameter FOREIGN KEY (fk_parameter_id)
REFERENCES parameter (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT measurement_data_index_fk_2_spot FOREIGN KEY (fk_spot_id)
REFERENCES spot (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT measurement_data_index_uk_all_cols UNIQUE (fk_device_id, fk_parameter_id, fk_spot_id)
);
CREATE TABLE measurement_data_value
(
id INTEGER NOT NULL,
t_stamp TIMESTAMP NOT NULL,
value VARCHAR(16) NOT NULL,
-- NOTE: inverse field order compared to wildplassers version
CONSTRAINT measurement_data_value_pk PRIMARY KEY (id, t_stamp),
CONSTRAINT measurement_data_value_fk_2_index FOREIGN KEY (id)
REFERENCES measurement_data_index (id) MATCH FULL
ON UPDATE NO ACTION ON DELETE NO ACTION
);
CREATE OR REPLACE FUNCTION insert_data()
RETURNS VOID
LANGUAGE plpgsql
AS
$BODY$
BEGIN
INSERT INTO device (name)
SELECT 'dev_' || to_char(item, 'FM00')
FROM generate_series(1, 5) item;
INSERT INTO parameter (name)
SELECT 'param_' || to_char(item, 'FM00')
FROM generate_series(1, 20) item;
INSERT INTO spot (name)
SELECT 'spot_' || to_char(item, 'FM00')
FROM generate_series(1, 10) item;
INSERT INTO measurement_data_index (fk_device_id, fk_parameter_id, fk_spot_id)
SELECT device.id, parameter.id, spot.id
FROM device, parameter, spot;
INSERT INTO measurement_data_value(id, t_stamp, value)
SELECT index.id,
item,
'd' || to_char(index.fk_device_id, 'FM00') ||
'_s' || to_char(index.fk_spot_id, 'FM00') ||
'_p' || to_char(index.fk_parameter_id, 'FM00')
FROM measurement_data_index index,
generate_series('2012-01-01 00:00:00', '2012-01-06 23:59:59', interval '1 min') item;
END;
$BODY$;
SELECT insert_data();
在某个阶段,我将改变我自己的约定,使用内联PRIMARY KEY
和REFERENCES
语句而不是显式CONSTRAINT
s;目前,我认为保持这种方式可以更容易地比较两种解决方案。
不要忘记更新查询计划器的统计信息:
VACUUM ANALYZE device;
VACUUM ANALYZE measurement_data_index;
VACUUM ANALYZE measurement_data_value;
VACUUM ANALYZE parameter;
VACUUM ANALYZE spot;
运行一个查询,它应该产生与第一种方法中的结果相同的结果:
EXPLAIN (ANALYZE ON, BUFFERS ON)
SELECT measurement_data_value.value
FROM measurement_data_index,
measurement_data_value,
parameter
WHERE measurement_data_index.fk_parameter_id = parameter.id
AND measurement_data_index.id = measurement_data_value.id
AND parameter.name = 'param_01';
结果:
Nested Loop (cost=0.00..34218.28 rows=431998 width=12) (actual time=0.026..696.349 rows=432000 loops=1)
Buffers: shared hit=435332
-> Nested Loop (cost=0.00..29.75 rows=50 width=4) (actual time=0.012..0.453 rows=50 loops=1)
Join Filter: (measurement_data_index.fk_parameter_id = parameter.id)
Buffers: shared hit=7
-> Seq Scan on parameter (cost=0.00..1.25 rows=1 width=4) (actual time=0.005..0.010 rows=1 loops=1)
Filter: ((name)::text = 'param_01'::text)
Buffers: shared hit=1
-> Seq Scan on measurement_data_index (cost=0.00..16.00 rows=1000 width=8) (actual time=0.003..0.187 rows=1000 loops=1)
Buffers: shared hit=6
-> Index Scan using measurement_data_value_pk on measurement_data_value (cost=0.00..575.77 rows=8640 width=16) (actual time=0.013..12.157 rows=8640 loops=50)
Index Cond: (id = measurement_data_index.id)
Buffers: shared hit=435325
Total runtime: 726.125 ms
这几乎是第一种方法所需的约 1.3 秒的一半;考虑到我正在加载 432K 行,这是我目前可以接受的结果。
注意:值表 PK 中的字段顺序为id, t_stamp
; wildplassers 响应的顺序是t_stamp, whw_id
. 我这样做是因为我觉得“常规”字段顺序是在表声明中列出字段的顺序(而“反向”则是另一种方式),但这只是我自己的约定,让我无法获得使困惑。无论哪种方式,正如Erwin Brandstetter所指出的,这个顺序对于性能提升绝对是至关重要的;如果是错误的方法(并且缺少 wildplassers 解决方案中的反向索引),查询计划如下所示,性能会差 3 倍以上:
Hash Join (cost=22.14..186671.54 rows=431998 width=12) (actual time=0.460..2570.941 rows=432000 loops=1)
Hash Cond: (measurement_data_value.id = measurement_data_index.id)
Buffers: shared hit=63537
-> Seq Scan on measurement_data_value (cost=0.00..149929.58 rows=8639958 width=16) (actual time=0.004..1095.606 rows=8640000 loops=1)
Buffers: shared hit=63530
-> Hash (cost=21.51..21.51 rows=50 width=4) (actual time=0.446..0.446 rows=50 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 2kB
Buffers: shared hit=7
-> Hash Join (cost=1.26..21.51 rows=50 width=4) (actual time=0.015..0.359 rows=50 loops=1)
Hash Cond: (measurement_data_index.fk_parameter_id = parameter.id)
Buffers: shared hit=7
-> Seq Scan on measurement_data_index (cost=0.00..16.00 rows=1000 width=8) (actual time=0.002..0.135 rows=1000 loops=1)
Buffers: shared hit=6
-> Hash (cost=1.25..1.25 rows=1 width=4) (actual time=0.008..0.008 rows=1 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 1kB
Buffers: shared hit=1
-> Seq Scan on parameter (cost=0.00..1.25 rows=1 width=4) (actual time=0.004..0.007 rows=1 loops=1)
Filter: ((name)::text = 'param_01'::text)
Buffers: shared hit=1
Total runtime: 2605.277 ms