Почему агрегатные функции SQL намного медленнее, чем Python и Java (или OLAP для бедняков)

StackOverflow https://stackoverflow.com/questions/51553

Вопрос

Мне нужно мнение реального администратора базы данных.Postgres 8.3 выполняет этот запрос на моем Macbook Pro за 200 мс, тогда как Java и Python выполняют тот же расчет менее чем за 20 мс (350 000 строк):

SELECT count(id), avg(a), avg(b), avg(c), avg(d) FROM tuples;

Это нормальное поведение при использовании базы данных SQL?

Схема (таблица содержит ответы на опрос):

CREATE TABLE tuples (id integer primary key, a integer, b integer, c integer, d integer);

\copy tuples from '350,000 responses.csv' delimiter as ','

Я написал несколько тестов на Java и Python для контекста, и они сокрушают SQL (кроме чистого Python):

java   1.5 threads ~ 7 ms    
java   1.5         ~ 10 ms    
python 2.5 numpy   ~ 18 ms  
python 2.5         ~ 370 ms

Даже sqlite3 конкурирует с Postgres, несмотря на то, что он предполагает, что все столбцы являются строками (для сравнения:даже использование простого переключения на числовые столбцы вместо целых чисел в Postgres приводит к замедлению в 10 раз)

Настройки, которые я безуспешно пробовал, включают (слепо следуя некоторым советам в Интернете):

increased the shared memory available to Postgres to 256MB    
increased the working memory to 2MB
disabled connection and statement logging
used a stored procedure via CREATE FUNCTION ... LANGUAGE SQL

Итак, мой вопрос: является ли мой опыт здесь нормальным и чего я могу ожидать при использовании базы данных SQL?Я понимаю, что ACID требует затрат, но, на мой взгляд, это какое-то безумие.Я не прошу скорости игры в реальном времени, но, поскольку Java может обрабатывать миллионы двойных операций менее чем за 20 мс, я немного завидую.

Есть ли лучший способ сделать простой OLAP дешевле (как с точки зрения денег, так и с точки зрения сложности сервера)?Я рассматривал Mondrian и Pig + Hadoop, но не очень в восторге от поддержки еще одного серверного приложения и не уверен, что они вообще помогут.


Нет, код Python и код Java выполняют всю работу, так сказать, дома.Я просто генерирую 4 массива по 350 000 случайных значений каждый, а затем беру среднее значение.Я не включаю в тайминги генерацию, только шаг усреднения.При синхронизации потоков Java используются 4 потока (в среднем по одному на массив), что является излишним, но оно определенно самое быстрое.

Тайминг sqlite3 управляется программой Python и запускается с диска (не :memory:).

Я понимаю, что Postgres делает гораздо больше за кулисами, но большая часть этой работы не имеет для меня значения, поскольку это данные только для чтения.

Запрос Postgres не меняет время при последующих запусках.

Я повторно запустил тесты Python, включив в них буферизацию с диска.Время значительно замедляется почти до 4 секунд.Но я предполагаю, что код обработки файлов Python в значительной степени написан на C (хотя, может быть, не в библиотеке csv?), так что это указывает мне на то, что Postgres также не выполняет потоковую передачу с диска (или что вы правы, и я должен поклониться до того, как кто-то написал свой уровень хранения!)

Это было полезно?

Решение

Postgres делает гораздо больше, чем кажется (для начала обеспечивает согласованность данных!)

Если значения не обязательно должны быть точными на 100 % или таблица обновляется редко, но вы часто выполняете этот расчет, возможно, вы захотите изучить материализованные представления, чтобы ускорить его.

(Обратите внимание: я не использовал материализованные представления в Postgres, они выглядят немного хаотично, но могут подойти в вашей ситуации).

Материализованные представления

Также учтите накладные расходы на фактическое подключение к серверу и время туда и обратно, необходимое для отправки запроса на сервер и обратно.

Я бы посчитал 200 мс для чего-то подобного довольно хорошим. Быстрый тест на моем сервере Oracle, та же структура таблицы с примерно 500 тыс. строк и без индексов, занимает около 1–1,5 секунды, что почти все представляет собой просто высасывание данных оракулом. с диска.

Реальный вопрос в том, достаточно ли быстро 200 мс?

-------------- Более --------------------

Мне было интересно решить эту проблему с помощью материализованных представлений, поскольку я никогда особо с ними не играл.Это в оракуле.

Сначала я создал клип, который обновляется каждую минуту.

create materialized view mv_so_x 
build immediate 
refresh complete 
START WITH SYSDATE NEXT SYSDATE + 1/24/60
 as select count(*),avg(a),avg(b),avg(c),avg(d) from so_x;

Во время обновления строки не возвращаются.

SQL> select * from mv_so_x;

no rows selected

Elapsed: 00:00:00.00

После обновления это НАМНОГО быстрее, чем выполнение необработанного запроса.

SQL> select count(*),avg(a),avg(b),avg(c),avg(d) from so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:05.74
SQL> select * from mv_so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:00.00
SQL> 

Если мы вставим в базовую таблицу, результат не будет сразу виден для просмотра MV.

SQL> insert into so_x values (1,2,3,4,5);

1 row created.

Elapsed: 00:00:00.00
SQL> commit;

Commit complete.

Elapsed: 00:00:00.00
SQL> select * from mv_so_x;

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899459 7495.38839 22.2905454 5.00276131 2.13432836

Elapsed: 00:00:00.00
SQL> 

Но подождите минуту или около того, и MV незаметно обновится, и результат будет возвращен так быстро, как вы захотите.

SQL> /

  COUNT(*)     AVG(A)     AVG(B)     AVG(C)     AVG(D)
---------- ---------- ---------- ---------- ----------
   1899460 7495.35823 22.2905352 5.00276078 2.17647059

Elapsed: 00:00:00.00
SQL> 

Это не идеально.во-первых, это не реалтайм, вставки/обновления сразу не будут видны.Кроме того, у вас есть запрос на обновление MV, нужно вам это или нет (это можно настроить на любой временной интервал или по требованию).Но это показывает, насколько быстрее MV может представиться конечному пользователю, если вы можете жить со значениями, которые не совсем точны.

Другие советы

Я бы сказал, что ваша тестовая схема бесполезна.Чтобы выполнить запрос к базе данных, сервер базы данных выполняет несколько шагов:

  1. разобрать SQL
  2. разработать план запроса, т.е.е.решить, какие индексы использовать (если есть), оптимизировать и т. д.
  3. если используется индекс, найдите в нем указатели на фактические данные, затем перейдите к соответствующему месту в данных или
  4. если индекс не используется, сканировать весь стол чтобы определить, какие строки необходимы
  5. загрузить данные с диска во временное место (надеюсь, но не обязательно, в память)
  6. выполнить вычисления count() и avg()

Итак, создание массива в Python и получение среднего значения по сути пропускает все эти шаги, за исключением последнего.Поскольку дисковый ввод-вывод является одной из самых дорогостоящих операций, которые должна выполнять программа, это серьезный недостаток теста (см. также ответы на этот вопрос Я здесь уже спрашивал).Даже если вы читаете данные с диска в другом тесте, процесс совершенно другой, и трудно сказать, насколько релевантны результаты.

Чтобы получить больше информации о том, где Postgres проводит свое время, я бы предложил следующие тесты:

  • Сравните время выполнения вашего запроса со временем выполнения SELECT без агрегирующих функций (т.е.вырезать шаг 5)
  • Если вы обнаружите, что агрегирование приводит к значительному замедлению, попробуйте сделать это быстрее, получая необработанные данные посредством простого SELECT из сравнения.

Чтобы ускорить ваш запрос, сначала уменьшите доступ к диску.Я очень сомневаюсь, что именно агрегация отнимает время.

Есть несколько способов сделать это:

  • Кэшируйте данные (в памяти!) для последующего доступа либо с помощью собственных возможностей механизма БД, либо с помощью таких инструментов, как memcached.
  • Уменьшите размер хранимых данных
  • Оптимизируйте использование индексов.Иногда это может означать полный отказ от использования индекса (в конце концов, это тоже доступ к диску).Насколько я помню, для MySQL рекомендуется пропускать индексы, если вы предполагаете, что запрос извлекает более 10% всех данных в таблице.
  • Если в вашем запросе эффективно используются индексы, я знаю, что для баз данных MySQL полезно размещать индексы и данные на отдельных физических дисках.Однако я не знаю, применимо ли это к Postgres.
  • Также могут возникнуть более сложные проблемы, такие как замена строк на диск, если по какой-то причине набор результатов не может быть полностью обработан в памяти.Но я бы оставил подобные исследования до тех пор, пока не столкнусь с серьезными проблемами производительности, которые я не могу найти другого способа исправить, поскольку они требуют знания множества мелких деталей вашего процесса.

Обновлять:

Я только что понял, что вы, похоже, не используете индексы для приведенного выше запроса и, скорее всего, тоже их не используете, поэтому мой совет по индексам, вероятно, не помог.Извини.Тем не менее, я бы сказал, что проблема не в агрегации, а в доступе к диску.В любом случае я оставлю индексный материал, возможно, он еще пригодится.

Я повторно протестировал MySQL, указав ENGINE = MEMORY, и это ничего не меняет (все еще 200 мс).Sqlite3, использующий базу данных в памяти, также дает аналогичные тайминги (250 мс).

Математика здесь выглядит правильно (по крайней мере, размер, так как размер базы данных sqlite :-)

Я просто не верю в аргумент о том, что диск вызывает медленную работу, поскольку есть все признаки того, что таблицы находятся в памяти (все ребята из postgres предостерегают от слишком усердных попыток закрепить таблицы в памяти, поскольку они клянутся, что ОС сделает это лучше, чем программист) )

Чтобы прояснить тайминги, код Java не читает с диска, что делает совершенно несправедливое сравнение, если Postgres читает с диска и вычисляет сложный запрос, но это не имеет значения, БД должна быть достаточно умна, чтобы выполнить небольшой таблицу в память и предварительно скомпилировать хранимую процедуру ИМХО.

ОБНОВЛЕНИЕ (в ответ на первый комментарий ниже):

Я не уверен, как бы я протестировал запрос без использования функции агрегирования таким образом, чтобы это было справедливо, поскольку, если я выберу все строки, это потратит массу времени на сериализацию и форматирование всего.Я не говорю, что медлительность связана с функцией агрегации, это все равно может быть просто накладными расходами из-за параллелизма, целостности и друзей.Я просто не знаю, как выделить агрегацию как единственную независимую переменную.

Это очень подробные ответы, но в основном они вызывают вопрос: как мне получить эти преимущества, не покидая Postgres, учитывая, что данные легко помещаются в память, требуют одновременного чтения, но не записи, и запрашиваются с помощью одного и того же запроса снова и снова.

Можно ли предварительно скомпилировать запрос и план оптимизации?Я думал, что это сделает хранимая процедура, но на самом деле это не помогает.

Чтобы избежать доступа к диску, необходимо кэшировать всю таблицу в памяти. Могу ли я заставить Postgres сделать это?Я думаю, что он уже это делает, поскольку запрос выполняется всего за 200 мс после повторных запусков.

Могу ли я сообщить Postgres, что таблица доступна только для чтения, чтобы он мог оптимизировать любой код блокировки?

Думаю, можно прикинуть стоимость построения запроса по пустой таблице (тайминги 20-60 мс)

Я до сих пор не понимаю, почему тесты Java/Python недействительны.Postgres просто не выполняет столько работы (хотя я до сих пор не рассмотрел аспект параллелизма, а только кэширование и построение запросов).

ОБНОВЛЯТЬ:Я не думаю, что было бы справедливо сравнивать SELECTS, как это предлагается, протягивая 350 000 через шаги драйвера и сериализации в Python для запуска агрегации, или даже опускать агрегацию, поскольку накладные расходы на форматирование и отображение трудно отделить от времени.Если оба механизма работают с данными памяти, это должно быть сравнение яблок с яблоками, хотя я не уверен, как гарантировать, что это уже происходит.

Не могу разобраться как добавлять комментарии, может мне не хватает репутации?

Я сам занимаюсь MS-SQL, и мы бы использовали DBCC ПИНТАБЛИЦА сохранить таблицу в кэше и НАСТРОЙКА СТАТИСТИКИ В/В чтобы увидеть, что он читает из кеша, а не с диска.

Я не могу найти в Postgres ничего, что могло бы имитировать PINTABLE, но pg_buffercache кажется, дает подробную информацию о том, что находится в кеше - вы можете проверить это и посмотреть, действительно ли ваша таблица кэшируется.

Быстрый расчет конверта заставляет меня заподозрить, что вы выполняете подкачку с диска.Предполагая, что Postgres использует 4-байтовые целые числа, у вас есть (6 * 4) байтов на строку, поэтому ваша таблица имеет минимум (24 * 350 000) байт ~ 8,4 МБ.Предполагая, что постоянная пропускная способность вашего жесткого диска составляет 40 МБ/с, вы рассчитываете примерно на 200 мс для чтения данных (что, как указано, должно быть там, где проводится почти все время).

Если я где-то не облажался с математикой, я не понимаю, как возможно, что вы сможете прочитать 8 МБ в своем Java-приложении и обработать его за то время, которое вы показываете, - если только этот файл уже не кэширован на диске или на вашем компьютере. ОПЕРАЦИОННЫЕ СИСТЕМЫ.

Я не думаю, что ваши результаты настолько уж удивительны — скорее всего, Postgres настолько быстр.

Запрос Postgres выполняется быстрее во второй раз, когда у него есть возможность кэшировать данные?Чтобы быть немного справедливее, ваш тест на Java и Python должен в первую очередь покрывать затраты на получение данных (в идеале - загрузку их с диска).

Если этот уровень производительности является проблемой для вашего приложения на практике, но вам нужна СУБД по другим причинам, вы можете посмотреть кэширование памяти.Тогда вы получите более быстрый кэшированный доступ к необработанным данным и сможете выполнять вычисления в коде.

Используете ли вы TCP для доступа к Postgres?В таком случае Нэгл путает ваше время.

Еще одна вещь, которую обычно делает для вас СУБД, — это обеспечение параллелизма, защищая вас от одновременного доступа со стороны другого процесса.Это делается путем установки блокировок, и это приводит к некоторым накладным расходам.

Если вы имеете дело с полностью статическими данными, которые никогда не меняются, и особенно если вы работаете в основном в «однопользовательском» сценарии, то использование реляционной базы данных не обязательно принесет вам большую выгоду.

Вам необходимо увеличить кэши postgres до такой степени, что весь рабочий набор умещается в памяти, прежде чем вы сможете ожидать производительности, сравнимой с производительностью, выполняемой в памяти с помощью программы.

Спасибо за тайминги Oracle, это именно то, что я ищу (хотя разочаровывает :-)

Материализованные представления, вероятно, заслуживают рассмотрения, поскольку я думаю, что могу заранее вычислить наиболее интересные формы этого запроса для большинства пользователей.

Я не думаю, что время выполнения запроса туда и обратно должно быть очень большим, поскольку я выполняю запросы на том же компьютере, на котором работает Postgres, поэтому оно не может увеличить задержку?

Я также немного проверил размеры кэша, и кажется, что Postgres полагается на операционную систему для управления кэшированием. Они специально упоминают BSD как идеальную ОС для этого, поэтому я думаю, что Mac OS должна быть очень умной, чтобы перенести таблицу в Память.Если кто-то не имеет в виду более конкретные параметры, я думаю, что более конкретное кеширование находится вне моего контроля.

В конце концов, я, вероятно, смогу смириться со временем отклика 200 мс, но знание того, что возможная цель - 7 мс, заставляет меня чувствовать себя неудовлетворенным, поскольку даже время отклика 20-50 мс позволит большему количеству пользователей иметь более актуальные запросы и избавиться от множество кеширующих и предварительно вычисленных хаков.

Я только что проверил тайминги с помощью MySQL 5, и они немного хуже, чем в Postgres.Так что, за исключением некоторых серьезных прорывов в области кэширования, я думаю, это то, чего я могу ожидать, идя по пути реляционных баз данных.

Я бы хотел проголосовать за некоторые из ваших ответов, но у меня пока недостаточно баллов.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top