Агрегатные функции не разрешены в WHERE - при объединении таблиц PostgreSQL

В игре, использующей PostgreSQL 9.3.10, некоторые игроки заплатили за "VIP-статус", который указывается в столбце vip, содержащем дату из будущего:

# \d pref_users

   Column   |            Type             |     Modifiers      
------------+-----------------------------+--------------------
 id         | character varying(32)       | not null
 first_name | character varying(64)       | not null
 last_name  | character varying(64)       | 
 vip        | timestamp without time zone | 

Также игроки могут оценивать других игроков, установив для столбца nice значение true, false или оставив значение null:

 # \d pref_rep

  Column   |            Type             |                         Modifiers                         
-----------+-----------------------------+-----------------------------------------------------------
 id        | character varying(32)       | not null
 author    | character varying(32)       | not null
 nice      | boolean                     | 

Я рассчитываю "репутацию" VIP-игроков, выдавая SQL JOIN:

# select u.id, u.first_name, u.last_name, 
  count(nullif(r.nice, false))-count(nullif(r.nice, true)) as rep 
  from pref_users u, pref_rep r 
  where u.vip>now()and u.id=r.id group by u.id order by rep asc;


           id            |           first_name           | last_name | rep  
-------------------------+--------------------------------+--------------------
 OK413274501330          | ali                            | salimov   | -193
 OK357353924092          | viktor                         | litovka   | -137
 DE20287                 | sergej warapow                 |              

Мой вопрос, пожалуйста, следующий:

Как найти всех игроков с отрицательным рейтингом, которые оценили других игроков?

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

Я пробовал следующее, но получаю ошибку ниже:

# select count(*) from pref_rep r, pref_users u 
where r.author = u.id and u.vip > now() and 
u.id in (select id from pref_rep 
where (count(nullif(nice, false)) -count(nullif(nice, true))) < 0);

ERROR:  aggregate functions are not allowed in WHERE
LINE 1: ...now() and u.id in (select id from pref_rep where (count(null...
                                                             ^

ОБНОВИТЬ:

Я пытаюсь это с временной таблицей сейчас -

Сначала я заполняю его всеми VIP-пользователями с отрицательной оценкой, и это хорошо работает:

# create temp table my_temp as select u.id, u.first_name, u.last_name,
  count(nullif(r.nice, false))-count(nullif(r.nice, true)) as rep 
  from pref_users u, pref_rep r 
  where u.vip>now() and u.id=r.id group by u.id;

 SELECT 362

Но тогда мой SQL JOIN возвращает слишком много одинаковых строк, и я не могу найти, какое условие там отсутствует:

 # select u.id, u.first_name, u.last_name 
   from pref_rep r, pref_users u, my_temp t 
   where r.author=u.id and u.vip>now() 
   and u.id=t.id and t.rep<0;

           id            |           first_name           |         last_name          
-------------------------+--------------------------------+----------------------------
 OK400153108439          | Vladimir                       | Pelix
 OK123283032465          | Edik                           | Lehtik
 OK123283032465          | Edik                           | Lehtik
 OK123283032465          | Edik                           | Lehtik
 OK123283032465          | Edik                           | Lehtik
 OK123283032465          | Edik                           | Lehtik
 OK123283032465          | Edik                           | Lehtik

Та же проблема (несколько строк с одинаковыми данными) я получаю для утверждения:

# select u.id, u.first_name, u.last_name 
  from pref_rep r, pref_users u 
  where r.author = u.id and u.vip>now() 
  and u.id in (select id from my_temp where rep < 0);

Интересно, какое условие здесь может отсутствовать?

2 ответа

Решение

Прежде всего, я бы написал ваш первый запрос так:

select
  u.id, u.first_name, u.last_name,
  sum(case
        when r.nice=true then 1
        when r.nice=false then -1
      end) as rep 
from
  pref_users u inner join pref_rep r on u.id=r.id 
where
  u.vip>now()
group by
  u.id, u.first_name, u.last_name;

(он такой же, как и ваш, но мне он понятнее).

Чтобы найти игроков с отрицательным рейтингом, вы можете использовать тот же запрос, что и раньше, просто добавив предложение HAVING:

having
  sum(case
        when r.nice=true then 1
        when r.nice=false then -1
      end)<0

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

select
  s.id, s.first_name, s.last_name, s.rep
from (
  select
    u.id, u.first_name, u.last_name,
    sum(case
          when r.nice=true then 1
          when r.nice=false then -1
        end) as rep 
  from
    pref_users u inner join pref_rep r on u.id=r.id 
  where
    u.vip>now()
  group by
    u.id, u.first_name, u.last_name
  having
    sum(case
          when r.nice=true then 1
          when r.nice=false then -1
        end)<0
  ) s
where
  exists (select * from pref_rep p where p.author = s.id)

в конце концов, предложение has может быть удалено из внутреннего запроса, и вы можете просто использовать это выражение where для внешнего запроса:

where
  rep<0
  and exists (select * from pref_rep p where p.author = s.id)

Вы забыли упомянуть, что pref_users.id определяется как PRIMARY KEY - иначе ваш первый запрос не будет работать. Это также означает, что id уже проиндексирован.

Лучший запрос во многом зависит от типичного распределения данных.

При условии, что:

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

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

SELECT *
FROM  (  -- filter candidates in a subquery
   SELECT *
   FROM   pref_users u
   WHERE  u.vip > now()
   AND    EXISTS (
      SELECT 1
      FROM   pref_rep
      WHERE  author = u.id  -- at least one rating given
      )
   AND    EXISTS (
      SELECT 1
      FROM   pref_rep
      WHERE  id = u.id 
      AND    NOT nice  -- at least one neg. rating received
      )
   ) u
JOIN   LATERAL (  -- calculate total only for identified candidates
   SELECT sum(CASE nice WHEN true THEN 1 WHEN false THEN -1 END) AS rep 
   FROM   pref_rep
   WHERE  id = u.id
   ) r ON r.rep < 0;

Индексы

Очевидно, вам нужен индекс на pref_rep.author помимо (также предполагается!) PRIMARY KEY индексы на обоих id колонны.

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

С одной стороны, вас интересуют только VIP-пользователи (u.vip > now()). Простой индекс на vip пошел бы долгий путь. Или даже частичный многоколонный индекс, который включает в себя id и усекает более старые кортежи из индекса:

CREATE INDEX pref_users_index_name ON pref_users (vip, id)
WHERE vip > '2015-04-21 18:00';

Рассмотрим детали:

Если (и только если) отрицательные голоса составляют меньшинство, частичный индекс pref_rep может также оплатить:

CREATE INDEX pref_rep_downvote_idx ON pref_rep (id)
WHERE NOT nice;

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

Другие вопросы по тегам