Блеск и нищета PostgreSQL
Давно сталкиваюсь в продакшене с проблемой, что выборки с подзапросами выполняются неприлично долго. Причем в тестах воспроизвести такое поведение никак не удавалось. И вот наконец получилось!
Оказывается, проблема существует всегда, но становится заметной только на таблицах с миллионами записей - простейшая выборка "тормозит", если увеличиваются _значения_ используемых в выборке идентификаторов, потому в таблицах с десятками тысяч и менее записей баг просто незаметен.
Сначала приведу один из рабочих запросов, над которыми ломал голову:
Upd.Надо же, в форуме указал версию постгреса, а здесь забыл:
Оказывается, проблема существует всегда, но становится заметной только на таблицах с миллионами записей - простейшая выборка "тормозит", если увеличиваются _значения_ используемых в выборке идентификаторов, потому в таблицах с десятками тысяч и менее записей баг просто незаметен.
Сначала приведу один из рабочих запросов, над которыми ломал голову:
SELECT d.id AS document_id,
d.phone_number AS phone,
r.name AS region,
p.code AS point,
p.trademark AS point_trademark,
(case when p.is_priority then '1' else '' end) AS color_it,
c.code AS center_code,
a.value AS status,
u2.name AS user_2,
tp.name AS template,
c.name AS center,
(a.save_date at time zone interval '04:00')::date AS work_date,
date_trunc('second',(a.save_date at time zone interval '04:00')::time) AS work_time
FROM offline.documents AS d
JOIN auth.users AS u ON d.user_id = u.id
JOIN auth.regions AS r ON u.region_id = r.id
JOIN auth.points AS p ON u.point_id = p.id
JOIN auth.centers AS c ON u.center_id = c.id
JOIN offline.attributes AS a ON d.id = a.document_id
AND a.is_last
AND a.save_date BETWEEN (now()::timestamp - interval '2 week') AND (now()::timestamp + interval '1 day')
AND a.document_id IN (
SELECT document_id
FROM offline.attributes
WHERE user_id='1111'::int
)
JOIN auth.users u2 ON a.user_id=u2.id
JOIN offline.document_templates tp ON d.document_template_id = tp.id
JOIN offline.attributes a_s ON d.id = a_s.document_id
AND a_s.is_last_status
AND a_s.value='status1'
WHERE
d.id in (
SELECT document_id
FROM offline.attributes
WHERE user_id='1111'::int
)
ORDER BY p.is_priority DESC, a_s.save_date ASC
OFFSET '0'::int
LIMIT '50'::int;
Последовательное упрощение запроса показало, что проблема скрывается в совершенно элементарном запросе:SELECT id FROM offline.documents where id in (select document_id from tmp);Притом подзапрос возвращает менее сотни записей, а выборка из основной таблицы идет по PRIMARY KEY:
explain analyze select document_id from offline.attributes where user_id=3650;
"Index Scan using attributes_user_id_idx on attributes (cost=0.00..1417.18 rows=546 width=4) (actual time=0.015..0.154 rows=79 loops=1)"
" Index Cond: (user_id = 3650)"
"Total runtime: 0.195 ms"
create temp table tmp as select document_id from offline.attributes where user_id=3650;
select count(*) from tmp;
79
explain analyze SELECT id FROM offline.documents where id in (select document_id from tmp);
"Merge Join (cost=100000046.39..100030925.47 rows=200 width=4) (actual time=797.686..888.461 rows=40 loops=1)"
" Merge Cond: ("outer".id = "inner".document_id)"
" -> Index Scan using documents_pkey on documents (cost=0.00..29407.21 rows=587549 width=4) (actual time=0.010..637.431 rows=587025 loops=1)"
" -> Sort (cost=100000046.39..100000046.89 rows=200 width=4) (actual time=0.150..0.169 rows=40 loops=1)"
" Sort Key: tmp.document_id"
" -> HashAggregate (cost=100000036.75..100000038.75 rows=200 width=4) (actual time=0.092..0.097 rows=40 loops=1)"
" -> Seq Scan on tmp (cost=100000000.00..100000031.40 rows=2140 width=4) (actual time=0.003..0.046 rows=79 loops=1)"
"Total runtime: 888.522 ms"
explain analyze SELECT d.id FROM offline.documents as d, tmp where d.id=tmp.document_id;
"Merge Join (cost=100000149.78..100031057.96 rows=2140 width=4) (actual time=795.564..885.250 rows=79 loops=1)"
" Merge Cond: ("outer".id = "inner".document_id)"
" -> Index Scan using documents_pkey on documents d (cost=0.00..29407.21 rows=587549 width=4) (actual time=0.011..636.265 rows=587025 loops=1)"
" -> Sort (cost=100000149.78..100000155.13 rows=2140 width=4) (actual time=0.098..0.127 rows=79 loops=1)"
" Sort Key: tmp.document_id"
" -> Seq Scan on tmp (cost=100000000.00..100000031.40 rows=2140 width=4) (actual time=0.003..0.043 rows=79 loops=1)"
"Total runtime: 885.329 ms"
explain analyze SELECT id FROM offline.documents where id in (826798,826798,832792,832792,836566,842270,842270,842144,
842144,842034,842037,842037,844288,844288,844658,844658,844640,853201,853201,853313,853549,853313,853549,855102,855102,
855102,855097,855097,855102,856702,856702,869625,869625,869861,869861,869860,869860,870458,870458,870463,870463,871536,
871536,872055,872055,872279,872279,872368,872368,873792,873789,873789,878005,878005,878022,878022,878022,878022,888492,
888492,890071,890071,890061,890061,891715,891677,891483,891715,891483,895619,895619,899233,899205,899160,899160,899205,
899233,899697,899697);
"Bitmap Heap Scan on documents (cost=158.28..466.00 rows=79 width=4) (actual time=0.341..0.394 rows=40 loops=1)"
" Recheck Cond: ((id = 826798) OR (id = 826798) ... OR (id = 899697) OR (id = 899697))"
" -> BitmapOr (cost=158.28..158.28 rows=79 width=0) (actual time=0.330..0.330 rows=0 loops=1)"
" -> Bitmap Index Scan on documents_pkey (cost=0.00..2.00 rows=1 width=0) (actual time=0.013..0.013 rows=1 loops=1)"
" Index Cond: (id = 826798)"
" -> Bitmap Index Scan on documents_pkey (cost=0.00..2.00 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=1)"
" Index Cond: (id = 826798)"
...
" -> Bitmap Index Scan on documents_pkey (cost=0.00..2.00 rows=1 width=0) (actual time=0.005..0.005 rows=1 loops=1)"
" Index Cond: (id = 899697)"
" -> Bitmap Index Scan on documents_pkey (cost=0.00..2.00 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=1)"
" Index Cond: (id = 899697)"
"Total runtime: 0.617 ms"
Еще несколько экспериментов - и готов тестовый сценарий для воспроизведения проблемы:drop table tmp_facts6;
create table tmp_facts6(id serial NOT NULL, CONSTRAINT tmp_facts6_pkey PRIMARY KEY (id)) WITHOUT OIDS;
insert into tmp_facts6 select id from generate_series(1,1000000) as id;
drop table tmp;
create table tmp as select (1000*random())::int as document_id from generate_series(1,100) as document_id;
explain analyze select id from tmp_facts6 where id in (select document_id from tmp);
"Merge Join (cost=100000046.39..100021005.39 rows=200 width=4) (actual time=0.292..1.568 rows=96 loops=1)"
" Merge Cond: ("outer".id = "inner".document_id)"
" -> Index Scan using tmp_facts6_pkey on tmp_facts6 (cost=0.00..18456.00 rows=1000000 width=4) (actual time=0.009..0.726 rows=994 loops=1)"
" -> Sort (cost=100000046.39..100000046.89 rows=200 width=4) (actual time=0.252..0.290 rows=96 loops=1)"
" Sort Key: tmp.document_id"
" -> HashAggregate (cost=100000036.75..100000038.75 rows=200 width=4) (actual time=0.118..0.183 rows=96 loops=1)"
" -> Seq Scan on tmp (cost=100000000.00..100000031.40 rows=2140 width=4) (actual time=0.002..0.057 rows=100 loops=1)"
"Total runtime: 1.645 ms"
drop table tmp;
create table tmp as select (100000*random())::int as document_id from generate_series(1,100) as document_id;
explain analyze select id from tmp_facts6 where id in (select document_id from tmp);
"Merge Join (cost=100000046.39..100021005.39 rows=200 width=4) (actual time=1.759..115.736 rows=100 loops=1)"
" Merge Cond: ("outer".id = "inner".document_id)"
" -> Index Scan using tmp_facts6_pkey on tmp_facts6 (cost=0.00..18456.00 rows=1000000 width=4) (actual time=0.010..71.777 rows=99397 loops=1)"
" -> Sort (cost=100000046.39..100000046.89 rows=200 width=4) (actual time=0.268..0.313 rows=100 loops=1)"
" Sort Key: tmp.document_id"
" -> HashAggregate (cost=100000036.75..100000038.75 rows=200 width=4) (actual time=0.121..0.220 rows=100 loops=1)"
" -> Seq Scan on tmp (cost=100000000.00..100000031.40 rows=2140 width=4) (actual time=0.003..0.057 rows=100 loops=1)"
"Total runtime: 115.824 ms"
drop table tmp;
create table tmp as select (1000000*random())::int as document_id from generate_series(1,100) as document_id;
explain analyze select id from tmp_facts6 where id in (select document_id from tmp);
"Merge Join (cost=100000046.39..100021005.39 rows=200 width=4) (actual time=31.807..1148.310 rows=100 loops=1)"
" Merge Cond: ("outer".id = "inner".document_id)"
" -> Index Scan using tmp_facts6_pkey on tmp_facts6 (cost=0.00..18456.00 rows=1000000 width=4) (actual time=0.011..713.344 rows=995715 loops=1)"
" -> Sort (cost=100000046.39..100000046.89 rows=200 width=4) (actual time=0.263..0.305 rows=100 loops=1)"
" Sort Key: tmp.document_id"
" -> HashAggregate (cost=100000036.75..100000038.75 rows=200 width=4) (actual time=0.121..0.178 rows=100 loops=1)"
" -> Seq Scan on tmp (cost=100000000.00..100000031.40 rows=2140 width=4) (actual time=0.004..0.062 rows=100 loops=1)"
"Total runtime: 1148.408 ms"
Как видим, скорость выборки падает при пропорционально _увеличению значений идентификаторов_, возвращаемых подзапросом! Грабли просто фееричные, пожалуй, разработчикам PostgreSQL пора давать Шнобелевскую премию.Upd.Надо же, в форуме указал версию постгреса, а здесь забыл:
select version();
version
----------------------------------------------------------------------------------------------------------------
PostgreSQL 8.1.15 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)
(1 row)
В 8.4 этого бага нет, но версия 8.1 поддерживаемая, а в ней не поправили. Вечная память...
Comments
Вот у меня 8.4.4 и 9.0beta1 ведут нормально:
в subselect-e 1000 записей
postgres=# explain analyze select id from tmp_facts6 where id in (select document_id from tmp);
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=16.50..7194.99 rows=999 width=4) (actual time=4.425..24.664 rows=999 loops=1)
-> HashAggregate (cost=16.50..26.49 rows=999 width=4) (actual time=4.395..6.175 rows=999 loops=1)
-> Seq Scan on tmp (cost=0.00..14.00 rows=1000 width=4) (actual time=0.022..1.923 rows=1000 loops=1)
-> Index Scan using tmp_facts6_pkey on tmp_facts6 (cost=0.00..7.16 rows=1 width=4) (actual time=0.010..0.012 rows=1 loops=999)
Index Cond: (tmp_facts6.id = tmp.document_id)
Total runtime: 26.185 ms
(6 rows)
Time: 27.338 ms
Информацию о версии добавил в статью, это 8.1.15. В конфиге только лимиты памяти подняты.
Впрочем смысла использовать Постгри ниже 8.4, когда уже 9 на носу особого наверное и нету, кроме истории?
Что касается реакции разработчиков, то выше можете видеть комментарий от Oleg Bartunov.
А сообщение выше от Олега приблизительно то же - юзайте 8.4, там проблема не воспроизводится :)
Впрочем это не есть багрепорт в любом случае.
Цена обновления весьма ощутима, плюс всегда есть опасность огрести какие-то новые баги, в то время как предыдущая версия стабильно работает.
Однако вроде тема началась с того что не все какраз работает :) Точнее не так как хотелось бы.
SELECT id FROM offline.documents where id in (select document_id from tmp);
Что-то неясно, а вышеозначенный запрос чем не эквивалентен самому подзапросу? -
select document_id from tmp