PostgreSQL

индекс
123,82

Подсчет количества найденных записей в PostgreSQL

На работе в новом проекте используется СУБД PostgreSQL. Так как до сих пор я работал с MySQL, сейчас приходится изучать и открывать для себя Постгри. Первая проблема, которая меня заинтересовала — замена мускулевского SQL_CALC_FOUND_ROWS. При использовании этой константы в MySQL можно получить количество всех найденных по запросу записей, даже если запрос с limit'ом — это незаменимо при постраничном выводе поисковых результатов, когда используются "тяжелые" запросы.

Сходу готового решения найти не удалось. На форумах просто констатировали, что SQL_CALC_FOUND_ROWS в Постгри нет. Некоторые писали, что надо юзать count(*). И больше никакой информации. Но еще из MySQL мне было известно, что поиск с count()-запросом работает почти в 2 раза медленнее, чем с SQL_CALC_FOUND_ROWS. Я консультировался у тех, кто пользуется PostgreSQL, день мучал google и в результате получил 4 варианта замены SQL_CALC_FOUND_ROWS в PostgreSQL, один из которых вполне приемлимый по скорости.

Итак, сразу представлю те четыре варианта, о которых пойдет речь. Наш целевой запрос ищет в таблице записи, в которых встречается текст adf в поле `text`. Выбираем id 20 записей начиная от 180.000 по порядку и количество найденных всего.

Вариант 1. Взят из phpPgAdmin. Я просто заглянул в код этого клиента для PostgreSQL и посмотрел как подсчет сделан у них при просмотре данных таблицы. Используется 2 запроса с подзапросами. Удобство в том, что не надо парсить и менять исходный запрос, чтобы подсчитать количество записей, найденных им.

select count(id) from (select id from testing where text like '%adf%') as sub;
select * from (select id from testing where text like '%adf%') as sub limit 20 offset 180000

Вариант 2. Самый простой вариант, который обычно юзают новички как в MySQL, так и в Postgres и других СУБД. 2 запроса.

select count(id) from testing where text like '%adf%';
select id from testing where text like '%adf%' limit 20 offset 180000

Вариант 3. © max_posedon. Это попытка эмуляции мускулевского SQL_CALC_FOUND_ROWS в Postgres по логике. Правда работает только при сортировке по id (в данном случае). Здесь подставляется id последней записи в выборке, т.е. записи под номером 180.000 + 20.

select id from testing where text like '%adf%' limit 20 offset 180000;
select count(id) from testing where text like '%adf%' and id > 132629;

Вариант 4. По советам пользователей irc.freenode.org, опять же max_posedon‘а, и этого ответа на форуме PostgreSQL, который прятался глубоко в гугле. Используется курсор.

DECLARE curs CURSOR FOR select id from testing where text like '%adf%';
MOVE FORWARD 180000 IN curs;
FETCH 20 FROM curs;
MOVE FORWARD ALL IN curs;

+ фунция PQcmdTuples() API Postgres (или $count = pg_cmdtuples($result); в PHP).

Обратите внимание, что все 4 варианта запросов следует выполнять в одной транзакции, тогда они работают быстрее. 4й вариант вовсе не будет работать, если не использовать одну транзакцию: теряется курсор.

Теперь о скоростях. Я провел тестирование скорости работы этих четырех вариантов. Вобщем-то тесты подтвердили ожидания. Но отмечу важный факт. Все запросы запускались на конфигурации PostgreSQL по умолчанию, которая не является оптимизированной на производительность. У меня под рукой просто не было оптимизированного сервера. Так что цифры могут немного корректироваться при запуске с “хорошим” конфигом. Однако суть не изменится.

Тестовые запуски проводились в PHP по 20 повторов 2 раза на каждый вариант. Доступен php-скрипт, который запускал тесты. Кому интересно, есть полная статистика выборок в Excel™. Здесь опубликую лишь сводную таблицу:

Вар 1 Вар 2 Вар 3 Вар 4
Ср. время (мс) 647,41 648,25 450,64 370,67
Отношение к вар 4 1,75 1,75 1,22

Для сравнения время запросов без использования транзакции:

  • Вар 1: 1204 мс,
  • Вар 2: 689 мс,
  • Вар 3: 560 мс,
  • Вар 4 работает только в пределах транзакции.

Итоги. Самый быстрый вариант 4 с использованием курсора. Его скорость обусловлена тем, что “тяжелый” поисковый запрос выполняется только один раз. Далее проводятся операции с курсором. Аналогично работает и SQL_CALC_FOUND_ROWS в MySQL. На 20% от него отстает вариант 3 — попытка эмуляции работы SQL_CALC_FOUND_ROWS в PostgreSQL. Варианты 1 и 2 работат примерно с одинаковой скоростью и на 75% (более чем на 2/3!) уступают по скорости запросу с курсором.

P.S. для pg-гуру. Во-первых, если для вас эта информация покажется очевидной, то поверьте мне — для начинающих пользователей PostgreSQL это совсем не очевидно, и найти эту информацию не так просто. Во-вторых, жду от вас комментариев о тестах на тюнингованной на производительность конфигурации постгри или о вашем опыте, и о других вариантах подсчета.

+33
24 июля 2008, 21:10
52

комментарии (41)

0
TermiT #
Помоему первый и второй у вас идентичны...
0
TermiT #
Первый и второй вариант
0
feedbee #
Да, пардон, промахнулся. Уже поправил. Спасибо.
0
PQR #
Не уловил разницы в коде вариантов 1 и 2 - опечатка?
0
Tr1aL #
разница есть, смотри внимательней :)
0
symbix #
>>из MySQL мне было известно, что поиск с count()-запросом работает почти в 2 раза медленнее, чем с SQL_CALC_FOUND_ROWS

далеко не всегда

http://www.mysqlperformanceblog.com/2007…
0
razawa #
А я вот про MySQL новое узнал отсюда :-). PostgreSQL еще не использовал
+1
romychs #
Пасиб, чую, скоро нужно будет программулинку переделывать под postgre, как раз пригодятся Ваши варианты.
+1
Frank #
Не поверите, ровно 10 минут назад как раз искал эту информацию. Спасибо.
+1
LDEV #
Открыл для себя новый параметр в MySQL - SQL_CALC_FOUND_ROWS. Если честно, никогда его не встречал. Возьму на вооружение!
0
3stark #
кажется, в Варианте 3 описка:
select id from testing where text like '%adf%' limit 20 offset 180000;
0
feedbee #
В данном случае это не существенно, но таки да: там должен быть id.
0
AlienZzzz #
я бы лучше сам запрос оптимизировал. и использовал бы полнотекстовый поиск. который встроен в постгрессе, у меня 200 к записей примерно
нужно постранично выводить.

я делаю так
select count(*) from ( .. запрос .. )

ну и далее лимитом уже, постранично когда показать надо.
0
feedbee #
Я специально сделал поиск через LIKE, чтобы получить "тяжелый" запрос. Понятно, что по тексту лучше искать full text search'ем. А тяжелые запросы не получаются не только при поиске по тексту. На счет варианта "select count(*) from ( .. запрос .. ) ну и далее лимитом уже, постранично когда показать надо. " как раз написано в этой статье, почитайле пожалуйста. Там еще указана разница во времени такого запроса и варианта с курсором — ваш вариант на 75% медленнее при выборках далеких страниц.
0
AlienZzzz #
у меня немного сложно реализовать через курсор, так как сам ".. запрос .." у меня еще парсится в зависимости от фильтров (у мен каждое поле можно отфильтровать и т.д.), поэтому запрос может быть разным.

select count(*) from ( .. запрос .. )

___

на 75 проц. медленнее если лайк , но не фул текстом )
0
feedbee #
Не важно, лайк или полнотекстный поиск. В 2 запроса при использовании сортировки или выборе далекой страницы будет обрабатываться примерно в 1,75 раза дольше. Через лайк речь будет о 0,6 с против 0,4 с; при фултексте речь будет например о 0,2 против 0,12
0
AlienZzzz #
в любом случае респект. я попробую подкрутить это к себе (ессли смогу задавать запрос, как строчку 'DECLARE curs CURSOR FOR 'se ....'; )
0
feedbee #
Спасибо :) Было бы интересно сравнение скоростей в вашем конкретном случае. Толкьо не забудьте, что сравнивать надо на далеких страницах от начала. Чем ближе страница к началу, тем меньше времени работает второй запрос, если нет сортировки.
0
AlienZzzz #
сортировка есть .
+2
zolotukhin #
Отличное сравнение, спасибо за него. Есть еще интересная возможность быстрого определения приблизительного количества рядов, когда не нужна идеальная точность (такое бывает в случае больших чисел): делать explain запросу при обновленной статистике (в свежих версиях PostgreSQL при включенном автовакууме статистика всегда актуальна) и смотреть на оценку рядов, которую делает планировщик запросов. Цена вопроса — миллисекунды.
НЛО прилетело и опубликовало эту надпись здесь
0
AlienZzzz #
не выполняеться а анализируется 1 раз, собствено на анализ то время большое тратится.
НЛО прилетело и опубликовало эту надпись здесь
0
galaxy #
Вот уж неправда. Во-первых, если два одинаковых запроса идут НЕ в одной транзакции с уровнем изоляции SERIALIZABLE, то перечитывать придется, т.к. новые записи могли появится за это время. Во-вторых, представьте себе оверхед от кеширования результатов запросов; постгрес этим не занимается. Возможно, вы путаете с кешированием часто запрашиваемых страниц.
НЛО прилетело и опубликовало эту надпись здесь
0
shuffle #
лучше делать не count(*), а count(case when text like '%adf%' then 1 end) - попробуйте, скорее всего будет быстрее.
НЛО прилетело и опубликовало эту надпись здесь
0
yar #
еще проще select count(1) from test where text like '%adf%', без всяких case :)
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
0
feedbee #
Это если всегда одинаковый запрос. Тогда так и надо делать. А если запросы постоянно разные (фильтры, поиски текста), то не перекешируешь все.
НЛО прилетело и опубликовало эту надпись здесь
0
yar #
делать пейджинг при больших объемах в любом случае бессмысленно
ну какой смысл листать миллион записей?
НЛО прилетело и опубликовало эту надпись здесь
0
Psih #
Использовать SQL_CALC_FOUND_ROWS в MySQL следует только при LIKE или FullTextSearch или нету индексов на поисковых полях, в результате чего запрос работает долго. Если у вас выборка вида
SELECT fields
FROM table1
LEFT JOIN table2 ON table2.id = table2.reference
WHERE table1.reference = INT LIMIT 0, 20

из хотя бы десятков тысячь записей, то SELECT + SELECT COUNT(*) по условию будет сильно быстрее. SQL_CALC_FOUND_ROWS в данном случае заставляет сделать JOIN на все записи доступные в таблицах и только потом отрезает их. Без него это дело сильно оптимизируется и 2 запроса в итоге могут быть в несколько раз быстрее чем один с SQL_CALC_FOUND_ROWS (да, и на памяти вы тоже сильно экономите)
0
feedbee #
Проверял на практике: в запросе нет ни FT, ни LIKE. Запросы с count() работали дольше. А вот в случае с JOIN ваша правда (во всяком случае по логике).
0
Q2W #
Да, вариант с count() чаще всего лучше, ибо:
1. Не нужна сортировка (а это очень тяжёлая операция).
2. Часто можно выключить join'ы из запроса.
3. Можно выключить всякие расчёты и проч. из запроса.

В результате запрос с count() становится гораздо легче и быстрее.
А вот в варианте с SQL_CALC_FOUND_ROWS базе приходится применять всё вышеописанное ко всем строкам таблицы, а не только к выбираемым. И хоть и есть экономия на выводе только того, что нужно, в результате получается медленнее.

Всё вышесказанное неоднократно проверено и используется на более-менее нагруженном проекте, над оптимизацией производительности которого работали очень много.
0
fisher #
>>базе приходится применять всё вышеописанное ко всем строкам таблицы
а это извините из общих физических соображений-то вообще зачем? где такое написано? и не путаете ли Вы с count у innodb vs isam.
0
johnny_bee #
Небольшое замечание, почему в постгре нет SQL_CALC_FOUND_ROWS (что бы его не ругали). Постгря умеет использовать индексы для сортировки и выборок limit/offset, соотв. при выборке он может и не пройтись по всем полям, удовлетворяющим where.

На практике имеет смысл денормализовать по полям, которые используются в where.
0
munkie #
В php лучше использовать функцию pg_affected_rows(), pg_cmdtuples() считается устаревшей.
0
xiWera #
это только у меня так, что после move на начало, move обратно в конец отработывает столько же времени сколько в первый раз? хотя внутри транзакции ничего не меняется

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.