Проблема:
У меня есть данные, связанные с временем, в моей базе данных, и я изо всех сил стараюсь организовать, структурировать и индексировать эти данные таким образом, чтобы пользователи могли получать их эффективно; даже простые запросы к базе данных занимают больше времени, чем это возможно.
Контекст проекта:
Хотя это вопрос с чистой базой данных, некоторый контекст может помочь понять модель данных:
Проект сосредотачивается вокруг проведения исследований на большой, сложной машине. Я не очень разбираюсь в самой машине, но слух в лаборатории есть там кошка Шрёдингера, которая висела на ней сбоку; -)
Мы измеряем множество различных параметров, когда машина работает с датчиками, расположенными по всему аппарату в разных точках измерения (так называемые пятна) через определенные промежутки времени в течение определенного периода времени. Мы используем не только одно устройство для измерения этих параметров, но и целый ряд из них; они отличаются качеством своих данных измерений (я думаю, что это касается выборочных ставок, качества датчика, цены и многих других аспектов, с которыми меня не интересуют); одной из целей проекта является установление сравнения между этими устройствами. Вы можете визуализировать эти измерительные приборы как кучу лабораторных тележек, каждая из которых имеет множество кабелей, подключенных к машине, каждая из которых предоставляет данные измерений.
Модель данных:
Существуют данные измерений из каждого места и каждого устройства для каждого параметра, например, один раз в минуту в течение 6 дней. Моя задача - хранить эти данные в базе данных и обеспечивать эффективный доступ к ней.
В двух словах:
- устройство имеет уникальное имя
- параметр также имеет имя; они не уникальны, поэтому, у него также есть идентификатор
- у пятна есть идентификатор
База данных проекта, конечно, сложнее, но эти данные не имеют отношения к проблеме.
- индекс данных измерений имеет идентификатор, отметку времени, когда было произведено измерение, и ссылки на устройство и место, на котором выполнялось измерение.
- значение данных измерения имеет ссылку на параметр и на фактическое значение
Первоначально я смоделировал значение данных измерений, чтобы иметь свой собственный идентификатор в качестве первичного ключа; отношение n:m
между индексом данных измерения и значением представляло собой отдельную таблицу, в которой хранились только пары index:value
ID, но поскольку сама таблица потребляла довольно много пространства жесткого диска, мы ее исключили и изменили значение ID как простое целое число который хранит идентификатор индекса данных измерений, которому он принадлежит; первичный ключ значения данных измерения теперь состоит из этого идентификатора и идентификатора параметра.
На стороне примечания. Когда я создал модель данных, я внимательно следил за общими принципами проектирования, такими как 3NF и соответствующими ограничениями таблицы (например, уникальные ключи); другим правилом было создание индекса для каждого внешнего ключа. У меня есть подозрение, что отклонение в таблицах индексов/значений измерительных данных от "строгого" 3NF может быть одной из причин проблем производительности, которые я рассматриваю сейчас, но изменение модели данных назад не решило проблему.
Модель данных в DDL:
ПРИМЕЧАНИЕ. Ниже приведено обновление этого кода.
Ниже представлен список script базы данных и всех таблиц. Обратите внимание, что пока нет явных индексов. Прежде чем запускать это, убедитесь, что у вас нет базы данных под названием 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
);
Я также создал script, чтобы заполнить таблицу некоторыми тестовыми данными:
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 дня. Основной результат заключается в том, что они слишком медленны, но их план запросов слишком сложный для понимания, поэтому я вернулся к действительно простому запросу, использованному выше.
Я просмотрел partitioning таблицу значений. Данные связаны со временем, и разбиение представляется подходящим средством для организации такого рода данных; даже примеры в документации PostgreSQL используют что-то подобное. Тем не менее, я читал в той же статье:
Преимущества обычно будут полезны только тогда, когда таблица в противном случае была бы очень большой. Точная точка, в которой таблица выиграет от секционирования, зависит от приложения, хотя эмпирическое правило заключается в том, что размер таблицы должен превышать физическую память сервера базы данных.
Вся тестовая база данных имеет размер менее 1 ГБ, и я запускаю свои тесты на машине разработки с 8 ГБ ОЗУ и на виртуальной машине с 1 ГБ (см. также среду ниже), поэтому таблица далеко не очень большая или даже превышая физическую память. Я мог бы реализовать разделение в любом случае на каком-то этапе, но я чувствую, что подход не нацелен на проблему производительности.
Кроме того, я рассматриваю cluster таблицу значений. Мне не нравится тот факт, что кластеризация должна быть переделана всякий раз, когда вставляются новые данные, и что для этого дополнительно требуется эксклюзивная блокировка чтения/записи, но, глядя на этот, вопрос SO кажется, что это в любом случае имеет свои преимущества и может быть вариантом. Тем не менее, кластеризация выполняется по индексу, и поскольку в запрос входит (до 4) критериев выбора (устройства, точки, параметры и время), мне придется создавать кластеры для всех из них, что, в свою очередь, создает впечатление, что Я просто не создаю нужные индексы...
Моя среда:
- развитие происходит на MacBook Pro (середина 2009 года) с двухъядерным процессором и 8 ГБ оперативной памяти.
- Я запускаю тесты производительности базы данных на виртуальной машине Debian 6.0 с 1 ГБ ОЗУ, размещенной на MBP
- Версия PostgreSQL - это 9.1, поскольку это была последняя версия, когда я ее установил, возможно обновление до 9.2.
- Я изменил
shared_buffers
с 1600 до 256% оперативной памяти на обеих машинах, как рекомендовано в PostgreSQL docs (что связано с расширение настройки ядра, такие как SHMALL, SHMMAX и т.д.) - Аналогично, я изменил effective_cache_size с 128 МБ по умолчанию на 50% доступной ОЗУ
- Я провел тест производительности с различными настройками work_mem, но не видел каких-либо существенных различий в производительности
Примечание:Один из аспектов, который, как мне кажется, важен, состоит в том, что серия тестов производительности с реальными запросами проекта не отличается по производительности между MacBook с 8 ГБ и виртуальной машиной с 1 ГБ; то есть, если запрос занимает 10 секунд на MacBook, он также занимает 10 секунд на виртуальной машине. Кроме того, я выполнял те же тесты производительности до и после изменения shared_buffers
, effective_cache_size
и work_mem
, а изменения конфигурации не улучшали производительность более чем на 10%; некоторые результаты на самом деле даже ухудшились, поэтому кажется, что любая разница вызвана скорее изменением теста, чем изменением конфигурации. Эти наблюдения заставляют меня думать, что параметры ОЗУ и postgres.conf
пока не являются ограничивающими факторами.
Мои вопросы:
Я не знаю, ускорили бы ли другие или дополнительные индексы запрос, и если они это сделают, какие из них создать. Если посмотреть на размер базы данных и насколько прост мой запрос, у меня сложилось впечатление, что в моей модели данных есть что-то принципиально неправильное или как я выбрал свои индексы до сих пор.
Есть ли у кого-нибудь совет для меня, как структурировать и индексировать связанные с моим временем улучшения для повышения производительности запросов?
В более широком контексте настраивается производительность запросов
- обычно делается "на базе инцидента", т.е. как только запрос не будет удовлетворительно работать? Кажется, что все мои запросы слишком медленные...
- в основном вопрос о поиске (и понимании) планов запросов, затем добавление индексов и измерение, если что-то улучшится, возможно, ускорит процесс, применив один опыт?
Как я могу получить эту базу данных?
Обновление 01:
Глядя на ответы до сих пор, я думаю, что я не объяснил необходимость в таблицах индексов/значений данных измерений, поэтому позвольте мне попробовать еще раз. Место для хранения здесь.
Примечание:
- приведенные здесь цифры являются скорее иллюстративными и предназначены только для сравнения, т.е. сами цифры не имеют отношения к делу, имеет значение перцентальное различие в требованиях к хранению между использованием одной таблицы и использованием индекса и таблицы значений
- Размеры хранилища данных типа PostgreSQL задокументированы в этой главе
- это не претендует на научно обоснованность, например. единицы, вероятно, являются математическим фикцией; цифры должны складываться, хотя
Полагая
- 1 день измерений
- 1 набор измерений в минуту
- 10 устройств
- 10 параметров
- 10 точек
Это добавляет к
1 meas/min x 60 min/hour x 24 hour/day = 1440 meas/day
Каждое измерение имеет данные из каждого места и каждого устройства для каждого параметра, поэтому
10 spots x 10 devices x 10 parameters = 1000 data sets/meas
Итак, всего
1440 meas/day x 1000 data sets/meas = 1 440 000 data sets/day
Если мы сохраним все измерения в одной таблице как 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 bytes/row
в худшем случае, когда все поля varchar полностью заполнены. Это составляет
50 bytes/row x 1 440 000 rows/day = 72 000 000 bytes/day
или ~ 69 МБ в день.
В то время как это не сильно звучит, требование к пространству хранения в реальной базе данных было бы непомерно (опять же, используемые здесь цифры только для иллюстрации). Поэтому мы разделили данные измерений на индекс и таблицу значений, как объяснялось ранее в вопросе:
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...
);
где идентификатор строки значений равен идентификатору индекса, который он принадлежит.
Размеры строки в таблицах индексов и значений
index: 4 + 17 + 4 + 8 = 33 bytes value: 4 + 4 + 17 = 25 bytes
(опять же, наихудший сценарий). Общее количество строк
index: 10 devices x 10 spots x 1440 meas/day = 144 000 rows/day value: 10 parameters x 144 000 rows/day = 1 440 000 rows/day
поэтому общее число
index: 33 bytes/row x 144 000 rows/day = 4 752 000 bytes/day value: 25 bytes/row x 1 440 000 rows/day = 36 000 000 bytes/day total: = 40 752 000 bytes/day
или ~ 39 МБ в день - в отличие от ~ 69 МБ для решения одной таблицы.
Обновить 02 (re: ответ wildplassers):
Этот вопрос довольно длинный, так как я рассматривал возможность обновления кода в исходном вопросе выше, но я думаю, что это могло бы помочь как первым, так и улучшенным решениям здесь, чтобы лучше видеть различия.
Изменения по сравнению с первоначальным подходом (несколько в порядке важности):
- временная метка swap и параметр, т.е. переместите поле
t_stamp
из таблицыmeasurement_data_index
вmeasurement_data_value
и переместите полеfk_parameter_id
из значения в таблицу индексов: при этом изменении все поля в таблице индексов являются постоянными и новыми измерениями данные записываются только в таблицу значений. Я не ожидал значительного улучшения производительности запроса (я был неправ), но я чувствую, что он делает концепцию индекса данных измерений более ясной. Хотя для этого требуется дробно большее пространство для хранения (согласно некоторой довольно грубой оценке), наличие "статической" таблицы индексов также может помочь в развертывании, когда tablespaces перемещаются на разные жесткие диски в соответствии с их требованиями к чтению/записи. - используйте суррогатный ключ в таблице устройств. Из того, что я понимаю, суррогатный ключ является первичным ключом, который строго не требуется с точки зрения дизайна базы данных (например, имя устройства уже уникально, поэтому оно также может служить PK), но может помочь повысить производительность запросов. Я добавил это, потому что, опять же, я чувствую, что это делает концепцию более ясной, если только индексная таблица ссылается только на идентификаторы (вместо некоторых имен и некоторых идентификаторов).
- 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();
На каком-то этапе я изменю свои собственные соглашения на использование операторов inline 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.3s требуется первый подход; учитывая, что я загружаю 432K строк, это результат, с которым я могу жить на данный момент.
ПРИМЕЧАНИЕ: Порядок полей в таблице значений PK равен id, t_stamp
; порядок в ответе wildplassers t_stamp, whw_id
. Я сделал это таким образом, потому что я чувствую, что "регулярный" порядок полей - это тот, в котором поля перечислены в объявлении таблицы (а "обратное" - это наоборот), но это только мое собственное соглашение, которое мешает мне получить смущенный. В любом случае, как сказал Эрвин Брандштеттер, этот порядок является абсолютно критическим для улучшения производительности; если это неправильный путь (и обратный индекс, как в решении 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