Этот пост я пишу как частичную текстовую расшифровку видео с 1:14:42. Не знаю как Вам, но я не особо люблю смотреть видюшки, и легче воспринимаю написанное текстом с примерами. Поэтому делаю заметку для себя, но если оно окажется еще кому-то полезным — буду рад.
Создадим таблички вот такой структуры как на картинке и заполним их данными:
-- Таблица с постами CREATE TABLE post( id bigserial PRIMARY KEY, person_id int8 NOT NULL, created_at timestamptz NOT NULL, something text ); -- Добавляем миллион записей с данными INSERT INTO post( person_id, created_at, something ) SELECT (random()* 10 ^5)::int8 AS person_id, now()- INTERVAL '1 minute' *(random()* 60 * 24 * 365 * 2) AS created_at, ( SELECT string_agg( substr( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW XYZ0123456789 ',( random()* 72 )::INTEGER + 1, 1 ), '' ) FROM generate_series( 1, 100 + i % 10 +(random()* 2000)::INTEGER ) ) AS something FROM generate_series(1, 1000000) AS g(i); -- Удаляем записи записи одного автора у которых совпадает время DELETE FROM post WHERE ( person_id, created_at ) IN( SELECT person_id, created_at FROM post GROUP BY person_id, created_at HAVING COUNT(*) > 1 ); -- Индекс по времени CREATE INDEX i_post_created_at ON post USING btree(created_at); -- Уникальный индекс по дате и автору CREATE UNIQUE INDEX u_post_author_id_created_at ON post( person_id, created_at ); -- Табличка с авторами CREATE TABLE person AS SELECT DISTINCT ON (person_id) person_id AS id, 'person_' || person_id AS name FROM post; -- Первичный ключ ALTER TABLE person ADD PRIMARY KEY(id); -- Внешний ключ. Не верьте тем, кто говорят что не нужно их использовать. С ними гораздо лучше. ALTER TABLE post ADD CONSTRAINT fk_post_person_id FOREIGN KEY(person_id) REFERENCES person(id); -- Собираем статистику analyze post; analyze person; |
Далее пишем запросы которые выбирают какие-то данные из этих таблиц.
Первое и самое простое — посчитать количество постов, которое у нас есть в post
SELECT COUNT(*) FROM post; COUNT --------- 1000000 (1 строка) |
Этот запрос выполняется по следующему плану:
explain analyse select count(*) from post; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------- Aggregate (cost=142283.69..142283.70 rows=1 width=8) (actual time=388.014..388.014 rows=1 loops=1) -> Seq Scan on post (cost=0.00..139784.35 rows=999735 width=0) (actual time=0.032..327.397 rows=1000000 loops=1) Planning time: 0.241 ms Execution time: 388.114 ms (4 строки)
Видим сколько оно выполнялось. Это долго, если учесть что мы просто прочитали миллион строк и подсчитали их количество.
Самый быстрый и простой способ посмотреть сколько строчек в таблице — это заглянуть в статистику. Число будет не точным, но если у нас автовакуум настроен агрессивно и делает свою работу вовремя, то на полученное число можно ориентироваться:
explain select count(*) from post; QUERY PLAN -------------------------------------------------------------------- Aggregate (cost=142281.59..142281.60 rows=1 width=8) -> Seq Scan on post (cost=0.00..139782.67 rows=999567 width=0) (2 строки)
Мы только что запускали analyze post; так что значение 999567 очень близко к точному значению.
В ситуации когда нам необходимо количество постов одного конкретного автора мы можем заставить его делать Index Only Scan и это будет очень быстро
explain analyze SELECT count(person_id) from post where person_id=1; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------ Aggregate (cost=48.64..48.65 rows=1 width=8) (actual time=0.143..0.144 rows=1 loops=1) -> Index Only Scan using u_post_author_id_created_at on post (cost=0.42..48.61 rows=11 width=8) (actual time=0.060..0.115 rows=14 loops=1) Index Cond: (person_id = 1) Heap Fetches: 14 Planning time: 0.395 ms Execution time: 0.277 ms (6 строк)
Здесь мы в качестве параметра count() подставляем то же индексированное поле, по которому ограничиваем выборку в выражении where, таким образом, из индекса мы получаем все необходимое для формирования результата и postgres даже не обращается к страницам самой таблицы. Тут важную роль играет селективность person_id. Если бы выборка получалась больше некоторого значения, то, возможно, было бы дешевле сделать Bitmap Index Scan. В любом случае актуальность статистики и карты видимости очень важны для правильной оценки оптимального плана.
Больше про count() можно почитать в статье Joe Nelson Faster PostgreSQL Counting или тут
В следующем посте поговорим о пагинации и почему offset плохо.