Postrgresql: Ускорение Через Intarray

Лет 6 назад, когда слон был только в 8.0, а я сильно застрял на MySql, я часто слышал призывы сменить БД.

Помню, как больно было начинать.

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

Плюсов здесь масса, но пост не об этом.

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

А-ля Фото, Горячая линия.

Ну и стандартная задача для таких сайтов – фильтр.

Как это делается в обычных двигателях? Правильно: параметры = фильтры.

Ну понятно, что это просто, но не всегда красиво, особенно когда нужны фильтры с диапазонами (например: диагональ 10"-11").

И даже тогда придется задуматься о том, что фильтры — это сущность, независимая от параметров.

В моей версии фильтры «привязаны» к параметрам.

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

Я не буду вдаваться в подробности архитектуры; это тема для другого поста.

Но мы рассмотрим упрощенный вариант, чтобы не заблудиться.

Проблема таких фильтров заключается в их конструкции.

Подсчитайте количество товаров с выбранными (или еще не выбранными) фильтрами.

Так… Создайте таблицу для продуктов:

  
  
  
  
  
  
  
  
  
   

CREATE TABLE public.products ( id SERIAL, title VARCHAR NOT NULL, CONSTRAINT products_pkey PRIMARY KEY(id) );

Таблица фильтров:

CREATE TABLE public.filters ( id SERIAL, title VARCHAR NOT NULL, CONSTRAINT filters_pkey PRIMARY KEY(id) );

Таблица соединений между фильтрами и изделиями:

CREATE TABLE public.products_ref_filters ( id_product INTEGER NOT NULL, id_filter INTEGER NOT NULL, CONSTRAINT products_ref_filters_pkey PRIMARY KEY(id_product, id_filter), CONSTRAINT products_ref_filters_fk FOREIGN KEY (id_product) REFERENCES public.products(id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT products_ref_filters_fk1 FOREIGN KEY (id_filter) REFERENCES public.filters(id) ON DELETE CASCADE ON UPDATE CASCADE );

И это оказывается стандартный вариант. Тривиально.

Дальше… Добавьте поле фильтров в таблицу товаров:

ALTER TABLE public.products ADD COLUMN filters INTEGER[] DEFAULT ARRAY[]::integer[] NOT NULL;

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

Для целостности пишем процедуры и прикрепляем их к триггеру.

Вставлять:

CREATE OR REPLACE FUNCTION public.products_ref_filters__insert_tr () RETURNS trigger AS' BEGIN UPDATE products SET filters = filters + NEW.id_filter --push element onto array WHERE id = NEW.id_product; RETURN NEW; END; 'LANGUAGE 'plpgsql'; CREATE TRIGGER products_ref_filters__insert_tr AFTER INSERT ON public.products_ref_filters FOR EACH ROW EXECUTE PROCEDURE public.products_ref_filters__insert_tr();

Удаление:

CREATE OR REPLACE FUNCTION public.products_ref_filters__delete_tr () RETURNS trigger AS' BEGIN UPDATE products SET filters = filters - OLD.id_filter --remove entries matching right argument from array WHERE id = OLD.id_product; RETURN OLD; END; 'LANGUAGE 'plpgsql';

Обновлять:

CREATE OR REPLACE FUNCTION public.products_ref_filters__update_tr () RETURNS trigger AS' BEGIN UPDATE products SET filters = filters - OLD.id_filter WHERE id = OLD.id_product; UPDATE products SET filters = filters + NEW.id_filter WHERE id = NEW.id_product; RETURN NEW; END; 'LANGUAGE 'plpgsql'; CREATE TRIGGER products_ref_filters__update_tr AFTER UPDATE ON public.products_ref_filters FOR EACH ROW EXECUTE PROCEDURE public.products_ref_filters__update_tr();

Очистка:

CREATE OR REPLACE FUNCTION public.products_ref_filters__truncate_tr () RETURNS trigger AS' BEGIN UPDATE products SET filters = ARRAY[]::INTEGER[]; RETURN NULL; END; 'LANGUAGE 'plpgsql'; CREATE TRIGGER products_ref_filters__truncate_tr AFTER TRUNCATE ON public.products_ref_filters FOR EACH STATEMENT EXECUTE PROCEDURE public.products_ref_filters__truncate_tr();

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

И мы избавились от JOIN. Теперь нет необходимости склеивать таблицы в запросе.

Это дает много преимуществ: запросы строить проще, они быстрее, памяти меньше и т. д. Но статья про intarray? Да.

Установите расширение:

CREATE EXTENSION intarray;

После выполнения этой команды в базе данных появятся функции, индексы и операторы для работы с массивами типа INTEGER. Теперь работать будет намного быстрее и удобнее.

Наполняем нашу базу данных.

10 000 фильтров.

100 000 товаров.

Каждый продукт имеет 10 фильтров.

Итого: промежуточная таблица 1 000 000 строк.

На выполнение этого блока запросов у меня ушло 8 минут. Так что ждите.



INSERT INTO filters (title) SELECT 'filter_' || num FROM generate_series(1, 10000) as num; INSERT INTO products (title) SELECT 'product_' || num FROM generate_series(1, 100000) as num; DO $$ DECLARE idp INTEGER; BEGIN FOR idp IN SELECT id FROM products LOOP

Теги: #postgresql #postgresql

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.