Sum() против count()

Рассмотрим систему голосования, реализованную в PostgreSQL, где каждый пользователь может проголосовать вверх или вниз на "foo". Существует таблица foo, в которой хранятся все "данные foo" и таблица votes, в которой хранятся user_id, foo_id и vote, где vote равно +1 или -1.

Чтобы получить подсчет голосов для каждого foo, следующий запрос будет работать:

SELECT sum(vote) FROM votes WHERE foo.foo_id = votes.foo_id;

Но следующее будет работать так же хорошо:

(SELECT count(vote) FROM votes 
 WHERE foo.foo_id = votes.foo_id 
 AND votes.vote = 1)
- (SELECT count(vote) FROM votes 
   WHERE foo.foo_id = votes.foo_id 
   AND votes.vote = (-1))

В настоящее время у меня есть индекс на votes.foo_id.

Каков более эффективный подход? (Другими словами, что будет работать быстрее?) Меня интересует как ответ на PostgreSQL, так и общий ответ SQL.

ИЗМЕНИТЬ

Многие ответы учитывали случай, когда vote имеет значение NULL. Я забыл упомянуть, что в столбце голосования есть ограничение NOT NULL.

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

Ответ 1

Конечно, первый пример быстрее, проще и легче читать. Должно быть очевидно даже до того, как вы получите удары с водными существами. В то время как sum() немного дороже, чем count(), важно то, что второй пример требует двух сканирований.

Но существует фактическая разница: sum() может возвращать NULL, где count() нет. Я цитирую руководство для агрегатных функций:

Следует отметить, что за исключением count, эти функции возвращают null, когда ни одна строка не выбрана. В частности, сумма строк возвращает null, а не нуль, как можно было бы ожидать,

Поскольку у вас, похоже, слабое место для оптимизации производительности, вот вам подробная информация: count(*) немного быстрее, чем count(vote). Только эквивалент, если голос NOT NULL. Проверка производительности с помощью EXPLAIN ANALYZE.

При ближайшем рассмотрении

Оба запроса - это синтаксическая бессмыслица, стоящая одна. Это имеет смысл, если вы скопировали их из списка SELECT большего запроса типа:

SELECT *, (SELECT sum(vote) FROM votes WHERE votes.foo_id = foo.foo_id)
FROM   foo;

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

В Postgres 9.3 или новее альтернативное, более чистое, эквивалентное 100% -ное решение будет с LEFT JOIN LATERAL ... ON true:

SELECT *
FROM   foo f
LEFT   JOIN LATERAL (
   SELECT sum(vote) FROM votes WHERE foo_id = f.foo_id
   ) v ON true;

Обычно аналогичная производительность. Подробности:

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

SELECT f.*, v.score
FROM   foo f
JOIN   (
   SELECT foo_id, sum(vote) AS score
   FROM   votes
   GROUP  BY 1
   ) v USING (foo_id);

Сначала агрегируйте значения в подзапросе, затем присоедините к результату.
О USING:

Ответ 2

Первый будет быстрее. Вы можете попробовать это простым способом.

Сгенерируйте некоторые данные:

CREATE TABLE votes(foo_id integer, vote integer);
-- Insert 1000000 rows into 100 foos (1 to 100)
INSERT INTO votes SELECT round(random()*99)+1, CASE round(random()) WHEN 0 THEN -1 ELSE 1 END FROM generate_series(1, 1000000);
CREATE INDEX idx_votes_id ON votes (foo_id);

Проверьте оба

EXPLAIN ANALYZE SELECT SUM(vote) FROM votes WHERE foo_id = 5;
EXPLAIN ANALYZE SELECT (SELECT COUNT(*) AS count FROM votes WHERE foo_id=5 AND vote=1) - (SELECT COUNT(*)*-1 AS count FROM votes WHERE foo_id=5 AND vote=-1);

Но правда в том, что они не эквивалентны, чтобы убедиться, что первый будет работать как второй, вам нужно обработать случай null:

SELECT COALESCE(SUM(vote), 0) FROM votes WHERE foo_id = 5;

Еще одна вещь. Если вы используете PostgreSQL 9.2, вы можете создать свой индекс с обоими столбцами в нем, и таким образом вы сможете использовать сканирование только по индексу:

CREATE INDEX idx_votes_id ON votes (foo_id, vote);

НО! В некоторых ситуациях этот индекс может быть худшим, поэтому вы должны попробовать с ними и запустить EXPLAIN ANALYZE, чтобы узнать, какой из них лучше, или даже создать оба, и проверить, какой из них использует PostgreSQL (и исключить другое).

Ответ 3

Я бы ожидал, что первый запрос будет работать быстрее, так как это единственный запрос, и он более читабельный (удобно, если вам придется вернуться к нему через некоторое время).

Второй запрос состоит из двух запросов. Вы получаете только результат, как если бы это был единственный запрос.

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