Как ускорить запрос в PostgreSQL

У меня есть БД в PostgreSQL с большими данными (сейчас это где-то около 46 ГБ, и БД будет расти). Я создал индексы для часто используемых столбцов и адаптировал файл конфигурации:

shared_buffers = 1GB
temp_buffers = 256MB
work_mem = 512MB

Но этот запрос все еще медленный:

select distinct us_category_id as cat, count(h_user_id) as res from web_hits 
inner join users on h_user_id = us_id 
where (h_datetime)::date = ('2015-06-26')::date and us_category_id != ''
group by us_category_id

Объясните, проанализируйте:

HashAggregate (cost=2870958.72..2870958.93 rows=21 width=9) (actual time=899141.683..899141.683 rows=0 loops=1)

Group Key: users.us_category_id, count(web_hits.h_user_id)
-> HashAggregate (cost=2870958.41..2870958.62 rows=21 width=9) (actual time=899141.681..899141.681 rows=0 loops=1)

Group Key: users.us_category_id
 -> Hash Join (cost=5974.98..2869632.11 rows=265259 width=9) (actual time=899141.679..899141.679 rows=0 loops=1)

Hash Cond: ((web_hits.h_user_id)::text = (users.us_id)::text)
-> Seq Scan on web_hits (cost=0.00..2857563.80 rows=275260 width=7) (actual time=899141.676..899141.676 rows=0 loops=1)
-> Seq Scan on web_hits (cost=0.00..2857563.80 rows=275260 width=7) (actual time=899141.676..899141.676 rows=0 loops=1)
Filter: ((h_datetime)::date = '2015-06-26'::date)

Rows Removed by Filter: 55051918
-> Hash (cost=4292.99..4292.99 rows=134559 width=10) (never executed)
-> Seq Scan on users (cost=0.00..4292.99 rows=134559 width=10) (never executed)
Filter: ((us_category_id)::text <> ''::text)

"Planning time: 1.309 ms"
"Execution time: 899141.789 ms"

Дата изменена. Как ускорить запрос?

Создание таблиц и индексов

CREATE TABLE web_hits (
  h_id integer NOT NULL DEFAULT nextval('w_h_seq'::regclass),
  h_user_id character varying,
  h_datetime timestamp without time zone,
  h_db_id character varying,
  h_voc_prefix character varying,
  ...
  h_bot_chek integer, -- 1-бот...
  CONSTRAINT w_h_pk PRIMARY KEY (h_id)
);
ALTER TABLE web_hits OWNER TO postgres;
COMMENT ON COLUMN web_hits.h_bot_chek IS '1-бот, 0-не бот';

CREATE INDEX h_datetime ON web_hits (h_datetime);
CREATE INDEX h_db_index ON web_hits (h_db_id COLLATE pg_catalog."default");
CREATE INDEX h_pref_index ON web_hits (h_voc_prefix COLLATE pg_catalog."default" text_pattern_ops);
CREATE INDEX h_user_index ON web_hits (h_user_id text_pattern_ops);

 CREATE TABLE users (
  us_id character varying NOT NULL,
  us_category_id character varying,
  ...
  CONSTRAINT user_pk PRIMARY KEY (us_id),
  CONSTRAINT cities_users_fk FOREIGN KEY (us_city_home)
      REFERENCES cities (city_id),
  CONSTRAINT countries_users_fk FOREIGN KEY (us_country_home)
      REFERENCES countries (country_id),
  CONSTRAINT organizations_users_fk FOREIGN KEY (us_institution_id)
      REFERENCES organizations (org_id),
  CONSTRAINT specialities_users_fk FOREIGN KEY (us_speciality_id)
      REFERENCES specialities (speciality_id),
  CONSTRAINT us_affiliation FOREIGN KEY (us_org_id)
      REFERENCES organizations (org_id),
  CONSTRAINT us_category FOREIGN KEY (us_category_id)
      REFERENCES categories (cat_id),
  CONSTRAINT us_reading_room FOREIGN KEY (us_reading_room_id)
      REFERENCES reading_rooms (rr_id)
);
ALTER TABLE users OWNER TO sveta;
COMMENT ON COLUMN users.us_type IS '0-аноним, 1-читатель, 2-удаленный';

CREATE INDEX us_cat_index ON users (us_category_id);
CREATE INDEX us_user_index ON users (us_id text_pattern_ops);

person Svetlana    schedule 28.06.2015    source источник
comment
Просто примечание: вы можете удалить ключевое слово DISTINCT, потому что результат уже различен благодаря вашей GROUP BY.   -  person jarlh    schedule 28.06.2015
comment
Пожалуйста, опубликуйте определение таблицы и индексов.   -  person Jakub Kania    schedule 28.06.2015
comment
Можете ли вы подробно описать ваши индексы, которые уже существуют? Обе таблицы выглядят так, как будто к ним обращались при последовательном сканировании.   -  person Jeremy Fortune    schedule 28.06.2015
comment
Я удалил шум из вашей установки (настройки по умолчанию). С другой стороны, важная информация отсутствует. Обратите внимание на инструкции в информации тега для postgresql-performance. Почему одна таблица принадлежит postgres, а другая — sveta? По какой-то конкретной причине вы используете символьные типы данных для нескольких столбцов идентификаторов вместо простых integer (или bigint)?   -  person Erwin Brandstetter    schedule 29.06.2015
comment
И почему web_hits.h_user_id не определяется NOT NULL? В столбце есть значения NULL? Если да, то как вы собираетесь их считать? Вроде действительно должно быть ограничение FK от web_hits.h_user_id до users. us_id...   -  person Erwin Brandstetter    schedule 29.06.2015
comment
Why is one table owned by postgres and the other table by sveta? Просто опечатка, исправляю в базе. Не все идентификаторы являются целыми числами. И да, web_hits.h_user_id может быть NULL. Я не считаю их.   -  person Svetlana    schedule 29.06.2015


Ответы (2)


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

Запрос

В принципе, запрос можно упростить/улучшить до этого в любом случае:

SELECT u.us_category_id AS cat, count(*) AS res
FROM   users    u
JOIN   web_hits w ON w.h_user_id = u.us_id
WHERE  w.h_datetime >= '2015-06-26 0:0'::timestamp
AND    w.h_datetime <  '2015-06-27 0:0'::timestamp
AND    w.h_user_id IS NOT NULL  -- remove irrelevant rows, match index
AND    u.us_category_id <> ''
GROUP  BY 1;
  • DISTINCT явно не нужен дорогой способ, поскольку вы уже group by us_category_id (например, @Gordon уже упоминал).

  • Сделайте условия sargable, чтобы можно было использовать индексы:

  • Поскольку вы соединились в столбце w.h_user_id, логически следует, что результирующие строки NOT NULL в этом столбце. count(*) в этом случае эквивалентен и немного быстрее.

  • Условие h_user_id IS NOT NULL кажется избыточным, поскольку NULL в любом случае исключается в JOIN, но позволяет использовать частичный индекс с условием соответствия (см. ниже).

  • users.us_id (и, следовательно, web_hits.h_user_id), вероятно, не должен иметь тип данных varchar (character varying). Это неэффективный тип данных для столбца PK/FK в огромной таблице. Используйте числовой тип данных, например int или bigint (или < a href="http://www.postgresql.org/docs/current/interactive/datatype-uuid.html" rel="nofollow noreferrer">uuid, если необходимо). Аналогичные соображения для us_category_id: должно быть integer или родственным.

  • Стандартный оператор неравенства SQL — <>. Используйте это вместо также поддерживаемого !=.

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

Оптимизировать

Далее предполагая, что:

  • users.us_category_id <> '' верно для большинства строк.
  • Подсчитываются большинство или все строки с web_hits.h_user_id IS NOT NULL.

Тогда это будет быстрее, но:

SELECT u.us_category_id AS cat, sum(ct) AS res
FROM   users u
JOIN  (
   SELECT h_user_id, count(*) AS ct
   FROM   web_hits
   WHERE  h_datetime >= '2015-06-26 0:0'::timestamp
   AND    h_datetime <  '2015-06-27 0:0'::timestamp
   AND    h_user_id IS NOT NULL  -- remove irrelevant rows, match index
   GROUP  BY 1
   ) w ON w.h_user_id = u.us_id
AND    u.us_category_id <> ''
GROUP  BY 1;

Индексы

В любом случае частичные индексы лучше всего подходят для вашего случая:

1.

CREATE INDEX wh_usid_datetime_idx ON web_hits(h_user_id, h_datetime)
WHERE  h_user_id IS NOT NULL;

Удалите строки, где web_hits.h_user_id IS NULL из индекса.

Столбцы в таком порядке, а не наоборот, как было предложено. Детальное объяснение:

2.

CREATE INDEX us_usid_cat_not_empty_idx ON users(us_id)
WHERE  us_category_id <> '';

Это будет значительно меньше, потому что мы не храним потенциально длинный varchar столбец us_category_id в индексе, который нам в любом случае не нужен. Нам просто нужно знать, что это <> ''. Если бы у вас было integer столбцов, это соображение не применялось бы.

И мы также исключаем строки с '' или NULL в us_category_id, еще больше уменьшая индекс.

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


Разумеется, все обычные советы по оптимизации производительности тоже применимы.

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

person Erwin Brandstetter    schedule 29.06.2015
comment
К сожалению, web_hits.h_user_id может быть NULL или иметь буквенное значение. Но в остальном вы дали мне подробную информацию, поэтому большое вам спасибо! - person Svetlana; 29.06.2015
comment
@Svetlana: Обновил свой ответ на ваше обновление в комментарии. Пожалуйста, не забудьте предоставить всю необходимую информацию в своем следующем вопросе. - person Erwin Brandstetter; 30.06.2015

Во-первых, различать не нужно:

select u.us_category_id as cat, count(h_user_id) as res
from web_hits h inner join
     users u
     on h.h_user_id = u.us_id 
where (h.h_datetime)::date = '2015-06-26'::date and
      u.us_category_id <> ''
group by u.us_category_id

Во-вторых, вы хотите удалить конверсию в столбце. Так:

select u.us_category_id as cat, count(h_user_id) as res
from web_hits h inner join
     users u
     on h.h_user_id = u.us_id 
where (h.h_datetime >= '2015-06-26' and h.h_datetime < '2015-06-27) and
      u.us_category_id <> ''
group by u.us_category_id;

Тогда следующий индекс должен помочь запросу: web_hits(h_datetime, h_user_id). Также может быть полезно иметь индекс для users(us_id, us_category_id).

person Gordon Linoff    schedule 28.06.2015
comment
Преобразование ::date удалить нельзя, т.к. h_datetime имеет формат yyyyMMdd dd:mm:ss - person Svetlana; 28.06.2015
comment
Каков тип данных h_datetime? - person Dan Bracuk; 28.06.2015
comment
временная метка без часового пояса - person Svetlana; 28.06.2015
comment
Тогда вам не придется беспокоиться о формате. Временная метка — это, по сути, число с плавающей запятой. Различные клиенты отображают его по-разному. - person Dan Bracuk; 28.06.2015
comment
@DanBracuk: На самом деле timestamp - это integer, представляющий 6 дробных цифр внутри современного Postgres: stackoverflow.com/a/9576170/939860. - person Erwin Brandstetter; 29.06.2015