WITH RECURSIVE
появилась еще в незапамятные
времена версии 8.4, но до сих пор можно регулярно встретить
потенциально-уязвимые беззащитные запросы. Как избавить себя от
проблем подобного рода?
WITH RECURSIVE src AS ( SELECT '{2,3,5,7,11,13,17,19}'::integer[] arr), T(i, val) AS ( SELECT 1::bigint , 1UNION ALL SELECT i + 1 , val * arr[i] FROM T , src WHERE i <= array_length(arr, 1))SELECT valFROM TORDER BY -- отбор финального результата i DESCLIMIT 1;
WITH src AS ( SELECT unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime)SELECT exp(sum(ln(prime)))::integer valFROM src;
'abcdefgh'
:
WITH RECURSIVE T AS ( SELECT 'abcdefgh' strUNION ALL SELECT substr(str, 1, length(str) - 1) FROM T WHERE length(str) > 1)TABLE T;
LATERAL
и generate_series
, то даже CTE не
понадобятся:
SELECT substr(str, 1, ln) strFROM (VALUES('abcdefgh')) T(str), LATERAL( SELECT generate_series(length(str), 1, -1) ln ) X;
CREATE TABLE message( message_id uuid PRIMARY KEY, reply_to uuid REFERENCES message, body text);CREATE INDEX ON message(reply_to);
WITH RECURSIVE T AS ( SELECT * FROM message WHERE message_id = $1UNION ALL SELECT m.* FROM T JOIN message m ON m.reply_to = T.message_id)TABLE T;
-- добавим поле с общим идентификатором темы и индекс на негоALTER TABLE message ADD COLUMN theme_id uuid;CREATE INDEX ON message(theme_id);-- инициализируем идентификатор темы в триггере при вставкеCREATE OR REPLACE FUNCTION ins() RETURNS TRIGGER AS $$BEGIN NEW.theme_id = CASE WHEN NEW.reply_to IS NULL THEN NEW.message_id -- берем из стартового события ELSE ( -- или из сообщения, на которое отвечаем SELECT theme_id FROM message WHERE message_id = NEW.reply_to ) END; RETURN NEW;END;$$ LANGUAGE plpgsql;CREATE TRIGGER ins BEFORE INSERT ON message FOR EACH ROW EXECUTE PROCEDURE ins();
SELECT *FROM messageWHERE theme_id = $1;
WITH RECURSIVE T AS ( SELECT 0 i ...UNION ALL SELECT i + 1 ... WHERE T.i < 64 -- предел)
WITH RECURSIVE T AS ( SELECT ARRAY[id] path ...UNION ALL SELECT path || id ... WHERE id <> ALL(T.path) -- не совпадает ни с одним из)
WITH RECURSIVE T AS ( SELECT ARRAY[id] path ...UNION ALL SELECT path || id ... WHERE id <> ALL(T.path) AND array_length(T.path, 1) < 10)
Задача классификации одна из самых известных в машинном обучении. Очень многие проблемы, решаемые с помощью ML, так или иначе сводятся к классификации распознавание изображений, например. И все выглядит просто и понятно, когда нам нужно определить объект в один из нескольких классов. А что если у нас не плоская структура из нескольких классов, а сложная разветвленная иерархия на 683 категории? Именно о таком случае мы сегодня и поговорим. Под катом рассказ о том, зачем в задачах классификации нужны сложные иерархии и как с ними жить.
В любой задаче классификации есть набор классов и есть модель, которая умеет предсказывать класс для новых данных. Допустим, модель бинарной классификации Спам/не спам, модель классификации настроения текста Хорошо/нейтрально/плохо. Классы обычно являются абстракцией реальности, а абстракция это всегда упрощение.
Однако реальность может быть гораздо сложнее, ведь имеется некоторая структура, взаимосвязи между классами. Одна из часто встречаемых в жизни структур это дерево иерархии. Существует множество вещей, которые можно классифицировать по одному принципу, но на разных уровнях абстракции или обобщения. Например, известная со школы классификация животных. На первом уровне это могут быть Млекопитающие и Рептилии, на втором Дельфины и Ящерицы. Определенный объект можно отнести к одному из классов на каждом уровне, причем все объекты, относящиеся к некоторому классу, также относятся и к родителю этого класса.
Это звучит понятно в школьном учебнике, но в контексте машинного обучения возникает множество вопросов:
Как с этим всем обращаться?
Какие классы предсказывать?
Сколько моделей тренировать для решения задачи?
Как работать с данными?
Как вносить изменения в иерархию классов и как реагировать на эти изменения с точки зрения модели?
Все эти проблемы мы разберем на примере задачи классификации, которую мы решаем в Контуре.
Мы работаем с чеками. В каждом чеке может встретиться много разных товаров, которые можно сгруппировать множеством разных способов. На данный момент нам интересно группировать эти товары с помощью дерева категорий, которое мы будем называть KPC (Khajiit Product Classification). Это здоровенное дерево, состоящее из 683 категорий.
Для этих категорий у нас есть Golden Set, наш размеченный набор данных (штрихкод категория KPC) для обучения. В датасете почти три миллиона записей и мы постоянно работаем над его дополнением и улучшением разметки.
Именно в этом суть проблемы. Представьте три миллиона записей, 683 класса. Иерархия очень непостоянная одни классы добавляются, другие удаляются, а третьи обрастают дочерними категориями. Поддерживать все это в порядке очень непросто.
Давайте разберемся, как выглядят разные этапы классификации с иерархической структурой классов.
Как уже упоминалось выше, вся эта огромная структура классов очень волатильна. Классы обновляются, удаляются, разрастаются в сложное дерево или превращаются в свалку из трудноопределяемых в другие категории товаров. Из-за этих постоянных изменений появилась необходимость обрабатывать разные категории по-разному.
Так появились статусы категорий. Они помогают понять, в каком состоянии категория прямо сейчас. Благодаря статусам мы можем отдельно обрабатывать только что появившиеся категории или категории, которые скоро будут удалены. На текущий момент мы выделяем четыре статуса категорий:
Активный.
Запланированный категория, на которую мы хотим классифицировать, но пока не можем (не хватает данных или не почистили разметку).
Архивный категория, которую решено было удалить, но товары из категории еще не переразметили.
Удаленный.
В зависимости от статуса мы решаем, что делать с конкретной категорией на каждом из этапов классификации.
Также стоит упомянуть две отдельные группы категорий:
свалка (например Одежда (свалка)) группа для логического удаления некоторых товаров, которые невозможно категоризировать. Например, у нас есть товар Полное тестирование Тест 2 10шт, у которого из какого-то источника данных стоит категория Одежда. Наш аналитик данных понимает, что по факту категории у такого товара нет, поэтому такой товар отправляется в свалку, которая при обучении не рассматривается.
другое/другие (например Молочные товары, сыры и яйцо (другое)) группа для товаров, которые относятся к родительской категории, но не относятся (возможно, пока) ни к одной из дочерних.
А теперь давайте разберемся, как выглядит собственно обновление нашего датасета. Мы можем добавлять категории, удалять, а также разбивать уже существующие категории на несколько новых.
Добавить категорию мы можем в любое время, но для того, чтобы товары начали в неё попадать, необходимо:
Добавить категорию в KPC.
Переразметить обучающую выборку в соответствии с новой категорией (если товары новой категории раньше относились к другой категории проверить, что теперь они относятся к правильной).
Переобучить модель, чтобы она научилась классифицировать товары в новую категорию.
Как можно догадаться по статусам, удаление категорий в нашем случае исключительно логическое. Мы меняем статус категории сначала на архивный, а затем на удаленный.
Для удаления категории необходимо:
Перевести категорию в статус Архивная.
Решить, что мы делаем с товарами, которые относились к удаляемой и дочерним категориям.
Заменить удаляемую категорию у товаров в Golden Set.
Указать дочерним категориям новую родительскую категорию или её отсутствие (если дочерняя категория должна стать категорией верхнего уровня).
Переобучить модель, чтобы она перестала классифицировать товары в удаляемую категорию (и начала классифицировать эти товары в новые категории).
Перевести категорию в статус Удаленная.
Если появляется необходимость разбить одну категорию на несколько новых, нужно:
Обновить категории в Golden Set, чтобы отнести товары к новым категориям.
Переобучить модель, чтобы она научилась классифицировать товары в новые категории.
Суммируя написанное выше, основные изменения при разметке это необходимость учитывать родительские и дочерние классы. Например, при удалении мы должны смотреть, чтобы у всех дочерних категорий нашлись новые родительские.
На этапе обучения все сводится к обучению одного простого классификатора и иерархичность структуры, на первый взгляд, не добавляет никаких проблем. Однако перед обучением мы должны привести наши классы в порядок, чтобы в обучающей выборке не встречались одновременно дочерние и родительские классы (например, родительская категория Молочные продукты, яйца и сыры и дочерняя категория Яйца).
Такие коллизии плохо влияют на обучение товар может одновременно оказаться в обеих категориях и это может смутить нашу модель. Чтобы избежать таких случаев, перед обучением добавлен этап разрешения конфликтов дерева категорий (KPC collisions resolving).
Вспоминая о введенных ранее категориях хочется упомянуть, что в контексте резолвинга архивные категории обрабатываются также, как и активные, а удаленные не обрабатываются вовсе.
Для начала разберемся с тем, что такое конфликт или коллизия. Коллизией мы считаем ситуацию, когда пара категорий ребенок/родитель одновременно представлена в KPC. То есть часть товаров размечена родительской категорией, часть дочерней. Как уже упоминалось выше, такие ситуации плохо влияют на обучение.
Для разрешения этих ситуаций мы будем маппить категории (то есть переносить все товары из одной категории в другую), а после обновлять наше дерево категорий (оставлять в рассмотрении только те категории, которые представлены в Golden set), проверяя, остались ли коллизии.
Теперь важно понять желаемый конечный результат. Выписать несколько возможных ситуаций из дерева категорий и понять, как мы хотим эти ситуации решать. Понять, какие категории в какую смаппятся, а какие категории в итоге останутся и будут учтены классификатором.
Например, на какой-то итерации работы с алгоритмом мы разбирали вот такую ситуацию. Желаемый результат разрешения конфликта здесь не очевиден и требует продуманного аналитического ответа. При некоторых очевидных решениях этот случай может быть упущен, что впоследствии приведет к неожиданному результату вроде потери категорий.
Future/Active на схеме это статусы категорий Запланированная/Активная, а present/NOT present in GS представлена ли категория в Golden set.
Еще один вопрос, который важно разобрать что мы хотим делать с Запланированными категориями? И что мы хотим делать с их детьми?
Есть несколько вариантов. Мы можем:
Использовать эти категории в классификации.
Не классифицировать и выбросить категории из GS.
Переразмечать эти категории в категорию-родителя.
Переразмечать эти товары в категорию другое/другие (например Молочные продукты, сыры и яйцо (другое))
После разбора таких случаев, ответов на архитектурные вопросы и итеративной разработки мы пришли к следующему алгоритму.
Убрать удаленные, редко встречающиеся (меньше 10 товаров в golden set) и те категории, у которых в названии есть свалка.
Если все дети категории в статусе Запланированный, то дочерние категории маппятся в родителя. Это происходит итеративно, так как после первого маппинга может возникнуть аналогичная ситуация на другом уровне дерева.
Смаппить запланированные категории в sibling-категорию с другое/другие в названии, если такая есть.
Удалить из Golden Set категории, у которых есть категории-потомки с товарами в Golden Set. Здесь происходит то самое разрешение конфликтов.
На каждом этапе мы рассматриваем уже обновленное дерево категорий. Важность этого пункта вытекла из разбора неочевидных ситуаций. Например, если не обновлять дерево (не убирать категории без товаров в GS и без потомков с товарами), то правило Смаппить всех Запланированных детей к родителю может не сработать из-за пустого, но активного ребенка.
На этапе валидации возникает еще один важный вопрос. Как сравнивать разные версии модели, если они обучались на разных наборах классов и разных наборах данных? Какие категории нуждаются в развитии? Как оценить проделанную работу по обогащению, чистке или уточнению категории?
В первую очередь мы сравниваем базовые метрики accuracy (по всем классам) и accuracy/coverage. Необходимо следить за тем, чтобы баланс покрытия и точности не нарушался, а также за возможностью подобрать threshold для новой модели, при котором этот баланс соответствует нашим требованиям.
Во вторую очередь будем смотреть на метрики отдельно по каждому классу. Сначала на те, с которыми модель непосредственно знакома. Затем на родительские классы, путем агрегации (взвешенное среднее).
Таким образом, мы сможем сравнить качество по любому классу, даже если новая модель уже его не классифицирует, а, например, классифицирует его детей.
В третью очередь мы строим confusion matrix для всех категорий, чтобы понять между какими классами больше всего путается модель. Эти классы помогают задавать направление для дальнейшего развития.
'errors' - sum of errors of confusing two labels,
'label_1_confuse' - count(true=label_1, pred=label_2) /
'errors',
'label_2_confuse' - count(true=label_2, pred=label_1) /
'errors',
'fraction_of_error_to_label_1' - count(true=label_1, pred=label_2)
/ total_label_1,
'fraction_of_error_to_label_2' - count(true=label_2, pred=label_1)
/ total_label_2,
'fraction_of_all_errors' - 'errors' / total_errors,
'fraction_of_all_errors_cumulative'
Для удобной итеративной работы с деревом категорий полезно ввести статусы категорий. Такие статусы позволят обрабатывать категории в разном состоянии разными способами.
При валидации стоит агрегировать метрики для родительских категорий, чтобы иметь возможность сравнить две модели с разными наборами классов.
Важно следить, чтобы в обучающей выборке не смешивались сэмплы из родительского и из дочернего классов. Также важно продумать желаемый результат разрешения таких конфликтов в разных конфигурациях дерева, статусов и состояния разметки категорий.
JOIN
и предложим альтернативный вариант решения на ней
той же задачи.
WITH RECURSIVE h AS ( SELECT n."@Номенклатура" id , ARRAY[ coalesce( ( SELECT ne."Info" FROM "NomenclatureExt" ne WHERE ne."@Номенклатура" = n."@Номенклатура" LIMIT 1 ) , '{}' ) ] res , n."Раздел" -- предок по иерархии FROM "Номенклатура" n WHERE n."@Номенклатура" = ANY($1::integer[])UNION -- уникализация SELECT h.id , array_append( h.res , coalesce( ( SELECT ne."Info" FROM "NomenclatureExt" ne WHERE ne."@Номенклатура" = n."@Номенклатура" LIMIT 1 ) , '{}' ) ) -- расширение массива , n."Раздел" FROM "Номенклатура" n , h WHERE n."@Номенклатура" = h."Раздел" -- двигаемся вверх по иерархии в сторону предков)SELECT h.id, h.resFROM hWHERE h."Раздел" IS NULL;
Если нечто выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка.
WITH RECURSIVE / Path
Subquery
NomenclatureExt
. Явно это какая-то расширенная
информация по номенклатурной карточке, связанная 1-в-1.UNION
UNION
, а не UNION
ALL
, то записи будут уникализироваться на каждой рекурсивной
итерации.Path Filter
Bitmap Scan
на стартовой
итерации рекурсии и плюсом к ним 107 индексных поисков
связанныхSeq Scan
. В результате, из 22мс
выполнения запроса 12мс мы потратили на чтение всей таблицы и еще
5мс на ее хэширование, итого больше 77%.Hash
Join
окажутся только 475 штук и теперь добавим к ним еще 475
Index Scan
по связанным записям.(tableAlias)
.=
ANY(ARRAY(...))
, исключая возможность возникновения
неудобных JOIN
.(row)::text
.ARRAY(TABLE X)
до скалярного
значения-массива. А если в ней и так одна запись, то использовать
ее с нужной раскастовкой (TABLE
X)::integer[]
.
-- рекурсивный подъем вверх до корня с поиском только уникальных записей, it AS ( SELECT it::text -- иначе не работает уникализация через UNION FROM "Номенклатура" it WHERE "@Номенклатура" = ANY((TABLE src)::integer[])UNION ( WITH X AS ( SELECT DISTINCT (it::"Номенклатура")."Раздел" FROM it WHERE (it::"Номенклатура")."Раздел" IS NOT NULL ) SELECT it2::text FROM "Номенклатура" it2 WHERE "@Номенклатура" = ANY(ARRAY(TABLE X)) ))
-- рекурсивный спуск вниз для формирования "пути" к каждой карточке, itr AS ( SELECT ARRAY[(it::"Номенклатура")."@Номенклатура"] path , it::"Номенклатура" -- запись исходной таблицы FROM it WHERE (it::"Номенклатура")."Раздел" IS NULL -- стартуем от "корневых" записейUNION ALL SELECT ARRAY[((_it.it)::"Номенклатура")."@Номенклатура"] || itr.path -- наращиваем "путь" спереди , (_it.it)::"Номенклатура" FROM itr JOIN it _it ON ((_it.it)::"Номенклатура")."Раздел@" IS NOT FALSE AND ((_it.it)::"Номенклатура")."Раздел" = (itr.it)."@Номенклатура")
Info
из связанной таблицы. Но бегать так по каждому ID
несколько раз для преобразования каждого отдельного пути будет
очень долго, поэтому:= ANY(ARRAY(...))
.hstore
-словарик.
-- формируем словарь info для каждого ключа, чтобы не бегать по записям CTE, hs AS ( SELECT hstore( array_agg("@Номенклатура"::text) , array_agg(coalesce("Info", '{}')) ) FROM "NomenclatureExt" WHERE "@Номенклатура" = ANY(ARRAY( SELECT (it)."@Номенклатура" FROM itr )))
Info
с помощью ARRAY(SELECT ...
unnest(...))
:
, ARRAY( SELECT (TABLE hs) -> id::text -- извлекаем данные из "словаря" FROM unnest(path) id ) res
-- список всех исходных IDWITH RECURSIVE src AS ( SELECT $1::integer[] -- набор ID в виде сериализованного массива)-- рекурсивный подъем вверх до корня с поиском только уникальных записей, it AS ( SELECT it::text -- иначе не работает уникализация через UNION FROM "Номенклатура" it WHERE "@Номенклатура" = ANY((TABLE src)::integer[])UNION ( WITH X AS ( SELECT DISTINCT (it::"Номенклатура")."Раздел" FROM it WHERE (it::"Номенклатура")."Раздел" IS NOT NULL ) SELECT it2::text FROM "Номенклатура" it2 WHERE "@Номенклатура" = ANY(ARRAY(TABLE X)) ))-- рекурсивный спуск вниз для формирования "пути" к каждой карточке, itr AS ( SELECT ARRAY[(it::"Номенклатура")."@Номенклатура"] path , it::"Номенклатура" FROM it WHERE WHERE (it::"Номенклатура")."Раздел" IS NULL -- стартуем от "корневых" записейUNION ALL SELECT ARRAY[((_it.it)::"Номенклатура")."@Номенклатура"] || itr.path -- наращиваем "путь" спереди , (_it.it)::"Номенклатура" FROM itr JOIN it _it ON ((_it.it)::"Номенклатура")."Раздел@" IS NOT FALSE AND ((_it.it)::"Номенклатура")."Раздел" = (itr.it)."@Номенклатура")-- формируем словарь info для каждого ключа, чтобы не бегать по записям CTE, hs AS ( SELECT hstore( array_agg("@Номенклатура"::text) , array_agg(coalesce("Info", '{}')) ) FROM "NomenclatureExt" WHERE "@Номенклатура" = ANY(ARRAY( SELECT (it)."@Номенклатура" FROM itr )))-- строим цепочку info для каждого id из оригинального набораSELECT path[1] id, ARRAY( SELECT (TABLE hs) -> id::text -- извлекаем данные из "словаря" FROM unnest(path) id ) resFROM itrWHERE path[1] = ANY((TABLE src)::integer[]); -- ограничиваемся только стартовым набором
Регулярно возникают реплики типа "Вот ты ускорил запрос в 10 раз, но всего на 10мс оно же того не стоит! Мы лучше поставим еще пару реплик! Вместо 100MB памяти получилось 1MB? Да нам проще памяти на сервер добавить!"
Тут какой момент разработчик, вооруженный набором стандартных приемов, на оптимизацию запроса тратит константное время (= деньги), а с увеличением функционала и количества пользователей нагрузка на БД растет примерно как N(logN), а даже не линейно. То есть если сейчас ваш проект ест CPU базы на 50%, готовьтесь к тому, что уже через год вам придется ставить еще один такой же сервер (= деньги), потом еще и еще
Оптимизация запросов не избавляет от добавления мощностей, но сильно отодвигает их в будущее. Добившись вместо нагрузки в 50% всего 10%, вы сможете не расширять железо еще года 2-3, а вложить те же деньги, например, в увеличение штата или чьей-то зарплаты.
WITH personIds("Персона") AS ( SELECT $1::uuid[]), persons AS ( SELECT P."Персона" , coalesce(P."Фамилия", '') "Фамилия" , coalesce(P."Имя", '') "Имя" , coalesce(P."Отчество", '') "Отчество" , coalesce( CASE WHEN nullif(P."ФамилияЛица", '') IS NOT NULL THEN P."ФамилияЛица" ELSE P."Фамилия" END , '' ) "ФамилияЛица" , coalesce( CASE WHEN nullif(P."ФамилияЛица", '') IS NOT NULL THEN P."ИмяЛица" ELSE P."Имя" END , '' ) "ИмяЛица" , coalesce( CASE WHEN nullif(P."ФамилияЛица", '') IS NOT NULL THEN P."ОтчествоЛица" ELSE P."Отчество" END , '' ) "ОтчествоЛица" , P."Примечание" , P."Обновлено" , P."Уволен" , P."Группа" , P."Пол" , P."Логин" , P."Город" , P."ДатаРождения" , P."$Создано"::date "ДатаРегистрации" , coalesce(P."ОтдельныйПрофиль", FALSE) "ОтдельныйПрофиль" , coalesce(P."ЕстьКорпАккаунт", FALSE) "ЕстьКорпАккаунт" FROM "Персона" P WHERE "Персона" = ANY((TABLE personids)::uuid[])), counts AS ( SELECT NULL c), users AS ( SELECT hstore( array_agg("Персона"::text) , array_agg(udata::text) ) FROM ( SELECT "Персона"::text , array_agg(u::text) udata FROM "Пользователь" u WHERE "Персона" IN ( SELECT "Персона" FROM persons ) AND ( "Главный" OR ( "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE ) ) GROUP BY 1 ) u2), T1 AS ( SELECT persons."Персона" , persons."Фамилия" , persons."Имя" , persons."Отчество" , persons."ФамилияЛица" , persons."ИмяЛица" , persons."ОтчествоЛица" , persons."Примечание" , persons."Обновлено" , persons."Город" , coalesce(persons."ОтдельныйПрофиль", FALSE) "ОтдельныйПрофиль" , coalesce(persons."ЕстьКорпАккаунт", FALSE) "ЕстьКорпАккаунт" , counts.c "Всего" , persons."Группа" , ( SELECT ARRAY( SELECT row_to_json(t2) FROM ( SELECT "Пользователь" >> 32 as "Account" , "Пользователь" & x'FFFFFFFF'::bigint "Face" , coalesce("ЕстьПользователь", TRUE) "HasUser" , coalesce("ЕстьПользователь", TRUE) AND coalesce("Входил", TRUE) "HasLoggedIn" , coalesce("Уволен", persons."Уволен") "Fired" FROM ( SELECT * FROM ( SELECT (udata::"Пользователь").* FROM unnest(((TABLE users) -> "Персона"::text)::text[]) udata ) udata15 WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE ) udata2 ) t2 ) )::text[] "Users" , coalesce( ( SELECT row_to_json(t3) FROM ( SELECT "Пользователь" >> 32 as "Account" , "Пользователь" & x'FFFFFFFF'::bigint "Face" FROM ( SELECT (udata::"Пользователь").* FROM unnest(((TABLE users) -> "Персона"::text)::text[]) udata ) udata2 WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE AND "Пользователь" >> 32 = 5313189::int ORDER BY "ЕстьПользователь" DESC, "Входил" DESC LIMIT 1 ) t3 ) , ( SELECT row_to_json(t4) FROM ( SELECT "Пользователь" >> 32 as "Account" , "Пользователь" & x'FFFFFFFF'::bigint "Face" FROM ( SELECT (udata::"Пользователь").* FROM unnest(((TABLE users) -> "Персона"::text)::text[]) udata ) udata2 WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE AND "Главный" ORDER BY "ЕстьПользователь" DESC, "Входил" DESC LIMIT 1 ) t4 ) , ( SELECT row_to_json(t5) FROM ( SELECT "Пользователь" >> 32 as "Account" , "Пользователь" & x'FFFFFFFF'::bigint "Face" FROM ( SELECT (udata::"Пользователь").* FROM unnest(((TABLE users) -> "Персона"::text)::text[]) udata ) udata2 WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE LIMIT 1 ) t5 ) ) "PrimaryFaceAccount" , ( SELECT "Пользователь" >> 32 FROM ( SELECT "Пользователь" FROM ( SELECT (udata::"Пользователь").* FROM unnest(((TABLE users) -> "Персона"::text)::text[]) udata ) udata2 WHERE "Главный" ) t3 LIMIT 1 ) "MainAccount" , ARRAY( SELECT "Значение"::int FROM "КонтактныеДанные" WHERE persons."Группа" AND "Персона" = persons."Персона" AND "Тип" = 'account' ) "АккаунтыГруппы" , persons."Пол" , persons."Логин" , persons."ДатаРождения" , persons."ДатаРегистрации" FROM persons , counts)SELECT CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ФамилияЛица" ELSE "Фамилия" END "LastName", CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ИмяЛица" ELSE "Имя" END "FirstName", CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ОтчествоЛица" ELSE "Отчество" END "PatronymicName", *FROM T1;
SubPlan 8, SubPlan 10, SubPlan 12, SubPlan 14, SubPlan
16
), внутри которых время тратится на unnest
записей из массива внутри CTE.hstore
по ключу каждой отдельной
персоны:
, coalesce( ( SELECT row_to_json(T) FROM ( SELECT ... FROM ( SELECT (udata::"Пользователь").* FROM unnest(((TABLE users) -> "Персона"::text)::text[]) udata ) udata2 WHERE ... ORDER BY ... LIMIT 1 ) T )
FILTER
(9.4+) и
LATERAL
(9.3+).LIMIT 1
можно успешно заменить на
извлечение первого элемента массива: arr[1]
.
Так что собираем по каждому условию именно массивы.ARRAY[aggx::text,
aggy::text]
.
WITH personIds("Персона") AS ( SELECT $1::uuid[]), persons AS ( SELECT P."Персона" , coalesce(P."Фамилия", '') "Фамилия" , coalesce(P."Имя", '') "Имя" , coalesce(P."Отчество", '') "Отчество" , coalesce( CASE WHEN nullif(P."ФамилияЛица", '') IS NOT NULL THEN P."ФамилияЛица" ELSE P."Фамилия" END , '' ) "ФамилияЛица" , coalesce( CASE WHEN nullif(P."ФамилияЛица", '') IS NOT NULL THEN P."ИмяЛица" ELSE P."Имя" END , '' ) "ИмяЛица" , coalesce( CASE WHEN nullif(P."ФамилияЛица", '') IS NOT NULL THEN P."ОтчествоЛица" ELSE P."Отчество" END , '' ) "ОтчествоЛица" , P."Примечание" , P."Обновлено" , P."Уволен" , P."Группа" , P."Пол" , P."Логин" , P."Город" , P."ДатаРождения" , P."$Создано"::date "ДатаРегистрации" , coalesce(P."ОтдельныйПрофиль", FALSE) "ОтдельныйПрофиль" , coalesce(P."ЕстьКорпАккаунт", FALSE) "ЕстьКорпАккаунт" FROM "Персона" P WHERE "Персона" = ANY((TABLE personids)::uuid[])), counts AS ( SELECT NULL c), users AS ( SELECT hstore( array_agg("Персона"::text) , array_agg(udata::text) ) FROM ( SELECT "Персона"::text , array_agg(u::text) udata FROM "Пользователь" u WHERE "Персона" IN ( SELECT "Персона" FROM persons ) AND ( "Главный" OR ( "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE ) ) GROUP BY 1 ) u2), T1 AS ( SELECT persons."Персона" , persons."Фамилия" , persons."Имя" , persons."Отчество" , persons."ФамилияЛица" , persons."ИмяЛица" , persons."ОтчествоЛица" , persons."Примечание" , persons."Обновлено" , persons."Город" , coalesce(persons."ОтдельныйПрофиль", FALSE) "ОтдельныйПрофиль" , coalesce(persons."ЕстьКорпАккаунт", FALSE) "ЕстьКорпАккаунт" , counts.c "Всего" , persons."Группа"-- 8< -- , coalesce(usjs[1]::text[], '{}') "Users" , coalesce( (usjs[2]::json[])[1] , (usjs[3]::json[])[1] , (usjs[4]::json[])[1] ) "PrimaryFaceAccount" , ((usjs[5]::json[])[1] ->> 'Account')::bigint "MainAccount"-- 8< -- , ARRAY( SELECT "Значение"::int FROM "КонтактныеДанные" WHERE persons."Группа" AND "Персона" = persons."Персона" AND "Тип" = 'account' ) "АккаунтыГруппы" , persons."Пол" , persons."Логин" , persons."ДатаРождения" , persons."ДатаРегистрации" FROM persons , counts-- 8< -- , LATERAL ( SELECT ARRAY[ -- массив сериализованных json[] array_agg(json_f) FILTER (WHERE -- фильтрация по необходимому условию "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE AND "Пользователь" >> 32 = 5313189::int )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE AND "Главный" )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE )::text , array_agg(json_s) FILTER (WHERE "Главный" )::text ] usjs FROM ( SELECT -- готовим JSON для каждой записи сразу, потом только используем готовый json_build_object( 'Account' , "Пользователь" >> 32 , 'Face' , "Пользователь" & x'FFFFFFFF'::bigint , 'HasUser' , coalesce("ЕстьПользователь", TRUE) , 'HasLoggedIn' , coalesce("ЕстьПользователь", TRUE) AND coalesce("Входил", TRUE) , 'Fired' , coalesce("Уволен", persons."Уволен") ) json_f , json_build_object( 'Account' , "Пользователь" >> 32 , 'Face' , "Пользователь" & x'FFFFFFFF'::bigint ) json_s , * FROM ( SELECT (unnest).* FROM unnest(((TABLE users) -> "Персона"::text)::"Пользователь"[]) ) T ORDER BY -- сортировка одна на всех "ЕстьПользователь" DESC, "Входил" DESC ) T ) usjs-- 8< --)SELECT CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ФамилияЛица" ELSE "Фамилия" END "LastName", CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ИмяЛица" ELSE "Имя" END "FirstName", CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ОтчествоЛица" ELSE "Отчество" END "PatronymicName", *FROM T1;
unnest
!unnest
по каждой персоне разворачиваем
массив, который ранее засунули в hstore
с ключом этой
же персоны? А физически-то мы все равно отбираем в
hstore
независимо по каждой персоне.
WITH personIds("Персона") AS ( SELECT $1::uuid[]), persons AS ( SELECT P."Персона" , coalesce(P."Фамилия", '') "Фамилия" , coalesce(P."Имя", '') "Имя" , coalesce(P."Отчество", '') "Отчество" , coalesce( CASE WHEN nullif(P."ФамилияЛица", '') IS NOT NULL THEN P."ФамилияЛица" ELSE P."Фамилия" END , '' ) "ФамилияЛица" , coalesce( CASE WHEN nullif(P."ФамилияЛица", '') IS NOT NULL THEN P."ИмяЛица" ELSE P."Имя" END , '' ) "ИмяЛица" , coalesce( CASE WHEN nullif(P."ФамилияЛица", '') IS NOT NULL THEN P."ОтчествоЛица" ELSE P."Отчество" END , '' ) "ОтчествоЛица" , P."Примечание" , P."Обновлено" , P."Уволен" , P."Группа" , P."Пол" , P."Логин" , P."Город" , P."ДатаРождения" , P."$Создано"::date "ДатаРегистрации" , coalesce(P."ОтдельныйПрофиль", FALSE) "ОтдельныйПрофиль" , coalesce(P."ЕстьКорпАккаунт", FALSE) "ЕстьКорпАккаунт"-- 8< -- , ( SELECT ARRAY[ -- массив сериализованных json[] array_agg(json_f) FILTER (WHERE -- фильтрация по необходимому условию "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE AND "Пользователь" >> 32 = 5313189::int )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE AND "Главный" )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE )::text , array_agg(json_s) FILTER (WHERE "Главный" )::text ] usjs FROM ( SELECT -- готовим JSON для каждой записи сразу, потом только используем готовый json_build_object( 'Account' , "Пользователь" >> 32 , 'Face' , "Пользователь" & x'FFFFFFFF'::bigint , 'HasUser' , coalesce("ЕстьПользователь", TRUE) , 'HasLoggedIn' , coalesce("ЕстьПользователь", TRUE) AND coalesce("Входил", TRUE) , 'Fired' , coalesce("Уволен", P."Уволен") ) json_f , json_build_object( 'Account' , "Пользователь" >> 32 , 'Face' , "Пользователь" & x'FFFFFFFF'::bigint ) json_s , * FROM "Пользователь" WHERE "Персона" = P."Персона" AND ( "Главный" OR ( "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE ) ) ORDER BY -- сортировка одна на всех "ЕстьПользователь" DESC, "Входил" DESC ) T ) usjs-- 8< -- FROM "Персона" P WHERE "Персона" = ANY((TABLE personids)::uuid[])), counts AS ( SELECT NULL c), T1 AS ( SELECT persons."Персона" , persons."Фамилия" , persons."Имя" , persons."Отчество" , persons."ФамилияЛица" , persons."ИмяЛица" , persons."ОтчествоЛица" , persons."Примечание" , persons."Обновлено" , persons."Город" , coalesce(persons."ОтдельныйПрофиль", FALSE) "ОтдельныйПрофиль" , coalesce(persons."ЕстьКорпАккаунт", FALSE) "ЕстьКорпАккаунт" , counts.c "Всего" , persons."Группа" , coalesce(usjs[1]::text[], '{}') "Users" , coalesce( (usjs[2]::json[])[1] , (usjs[3]::json[])[1] , (usjs[4]::json[])[1] ) "PrimaryFaceAccount" , ((usjs[5]::json[])[1] ->> 'Account')::bigint "MainAccount" , ARRAY( SELECT "Значение"::int FROM "КонтактныеДанные" WHERE persons."Группа" AND "Персона" = persons."Персона" AND "Тип" = 'account' ) "АккаунтыГруппы" , persons."Пол" , persons."Логин" , persons."ДатаРождения" , persons."ДатаРегистрации" FROM persons , counts)SELECT CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ФамилияЛица" ELSE "Фамилия" END "LastName", CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ИмяЛица" ELSE "Имя" END "FirstName", CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ОтчествоЛица" ELSE "Отчество" END "PatronymicName", *FROM T1;
CTE personids
(заменяем на
inline-параметр с раскастовкой) и CTE counts
(вообще какой-то странный атавизм, возвращающий один NULL).
WITH persons AS ( SELECT "Персона" , coalesce("Фамилия", '') "Фамилия" , coalesce("Имя", '') "Имя" , coalesce("Отчество", '') "Отчество" , coalesce( CASE WHEN nullif("ФамилияЛица", '') IS NOT NULL THEN "ФамилияЛица" ELSE "Фамилия" END , '' ) "ФамилияЛица" , coalesce( CASE WHEN nullif("ФамилияЛица", '') IS NOT NULL THEN "ИмяЛица" ELSE "Имя" END , '' ) "ИмяЛица" , coalesce( CASE WHEN nullif("ФамилияЛица", '') IS NOT NULL THEN "ОтчествоЛица" ELSE "Отчество" END , '' ) "ОтчествоЛица" , "Примечание" , "Обновлено" , "Уволен" , "Группа" , "Пол" , "Логин" , "Город" , "ДатаРождения" , "$Создано"::date "ДатаРегистрации" , coalesce("ОтдельныйПрофиль", FALSE) "ОтдельныйПрофиль" , coalesce("ЕстьКорпАккаунт", FALSE) "ЕстьКорпАккаунт" , ( SELECT ARRAY[ -- массив сериализованных json[] array_agg(json_f) FILTER (WHERE -- фильтрация по необходимому условию "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE AND "Пользователь" >> 32 = 5313189::int )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE AND "Главный" )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE )::text , array_agg(json_s) FILTER (WHERE "Главный" )::text ] usjs FROM ( SELECT -- готовим JSON для каждой записи сразу, потом только используем готовый json_build_object( 'Account' , "Пользователь" >> 32 , 'Face' , "Пользователь" & x'FFFFFFFF'::bigint , 'HasUser' , coalesce("ЕстьПользователь", TRUE) , 'HasLoggedIn' , coalesce("ЕстьПользователь", TRUE) AND coalesce("Входил", TRUE) , 'Fired' , coalesce("Уволен", p."Уволен") ) json_f , json_build_object( 'Account' , "Пользователь" >> 32 , 'Face' , "Пользователь" & x'FFFFFFFF'::bigint ) json_s , * FROM "Пользователь" WHERE "Персона" = p."Персона" AND ( "Главный" OR ( "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE ) ) ORDER BY -- сортировка одна на всех "ЕстьПользователь" DESC, "Входил" DESC ) T ) usjs FROM "Персона" p WHERE-- 8< -- "Персона" = ANY($1::uuid[])-- 8< --), T1 AS ( SELECT "Персона" , "Фамилия" , "Имя" , "Отчество" , "ФамилияЛица" , "ИмяЛица" , "ОтчествоЛица" , "Примечание" , "Обновлено" , "Город" , coalesce("ОтдельныйПрофиль", FALSE) "ОтдельныйПрофиль" , coalesce("ЕстьКорпАккаунт", FALSE) "ЕстьКорпАккаунт" , NULL::bigint "Всего" , "Группа" , coalesce(usjs[1]::text[], '{}') "Users" , coalesce( (usjs[2]::json[])[1] , (usjs[3]::json[])[1] , (usjs[4]::json[])[1] ) "PrimaryFaceAccount" , ((usjs[5]::json[])[1] ->> 'Account')::bigint "MainAccount" , ARRAY( SELECT "Значение"::int FROM "КонтактныеДанные" WHERE persons."Группа" AND "Персона" = persons."Персона" AND "Тип" = 'account' ) "АккаунтыГруппы" , "Пол" , "Логин" , "ДатаРождения" , "ДатаРегистрации" FROM persons)SELECT CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ФамилияЛица" ELSE "Фамилия" END "LastName", CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ИмяЛица" ELSE "Имя" END "FirstName", CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ОтчествоЛица" ELSE "Отчество" END "PatronymicName", *FROM T1;
CTE Scan
стоит ресурсов)?coalesce
на одни и
те же поля?
WITH p AS ( SELECT *-- 8< -- , CASE WHEN nullif("ФамилияЛица", '') IS NOT NULL THEN ARRAY[ coalesce("ФамилияЛица", '') , coalesce("ИмяЛица", '') , coalesce("ОтчествоЛица", '') ] ELSE ARRAY[ coalesce("Фамилия", '') , coalesce("Имя", '') , coalesce("Отчество", '') ] END fio-- 8< -- , ( SELECT ARRAY[ -- массив сериализованных json[] array_agg(json_f) FILTER (WHERE -- фильтрация по необходимому условию "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE AND "Пользователь" >> 32 = 5313189::int )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE AND "Главный" )::text , array_agg(json_s) FILTER (WHERE "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE )::text , array_agg(json_s) FILTER (WHERE "Главный" )::text ] usjs FROM ( SELECT -- готовим JSON для каждой записи сразу, потом только используем готовый json_build_object( 'Account' , "Пользователь" >> 32 , 'Face' , "Пользователь" & x'FFFFFFFF'::bigint , 'HasUser' , coalesce("ЕстьПользователь", TRUE) , 'HasLoggedIn' , coalesce("ЕстьПользователь", TRUE) AND coalesce("Входил", TRUE) , 'Fired' , coalesce("Уволен", p."Уволен") ) json_f , json_build_object( 'Account' , "Пользователь" >> 32 , 'Face' , "Пользователь" & x'FFFFFFFF'::bigint ) json_s , * FROM "Пользователь" WHERE "Персона" = p."Персона" AND ( "Главный" OR ( "Уволен" IS DISTINCT FROM TRUE AND "Удален" IS DISTINCT FROM TRUE ) ) ORDER BY -- сортировка одна на всех "ЕстьПользователь" DESC, "Входил" DESC ) T ) usjs FROM "Персона" p WHERE "Персона" = ANY($1::uuid[]))-- 8< --SELECT CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ФамилияЛица" ELSE "Фамилия" END "LastName", CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ИмяЛица" ELSE "Имя" END "FirstName", CASE WHEN "ЕстьКорпАккаунт" OR "MainAccount" IS NOT NULL THEN "ОтчествоЛица" ELSE "Отчество" END "PatronymicName", *FROM ( SELECT "Персона" , coalesce("Фамилия", '') "Фамилия" , coalesce("Имя", '') "Имя" , coalesce("Отчество", '') "Отчество"-- 8< -- , fio[1] "ФамилияЛица" , fio[2] "ИмяЛица" , fio[3] "ОтчествоЛица"-- 8< -- , "Примечание" , "Обновлено" , "Город" , coalesce("ОтдельныйПрофиль", FALSE) "ОтдельныйПрофиль" , coalesce("ЕстьКорпАккаунт", FALSE) "ЕстьКорпАккаунт" , NULL::bigint "Всего" , "Группа" , coalesce(usjs[1]::text[], '{}') "Users" , coalesce( (usjs[2]::json[])[1] , (usjs[3]::json[])[1] , (usjs[4]::json[])[1] ) "PrimaryFaceAccount" , ((usjs[5]::json[])[1] ->> 'Account')::bigint "MainAccount" , ARRAY( SELECT "Значение"::int FROM "КонтактныеДанные" WHERE p."Группа" AND-- 8< -- ("Персона", "Тип") = (p."Персона", 'account')-- 8< -- ) "АккаунтыГруппы" , "Пол" , "Логин" , "ДатаРождения" , "$Создано"::date "ДатаРегистрации" FROM p ) T;-- 8< --
В своем докладе на конференции я говорил, что самая большая проблема, с которой приходится сталкиваться при оптимизации запросов алгоритмическая. То есть когда разработчик не заморачивается пониманием того, как конкретно БД должна/будет выполнять запрос.Именно таким оказался запрос из последнего сегодняшнего дела:
Классический пример цепочкаJOIN
'ов, приводящая к развесистой клюкве изNested Loop/Hash Join/Merge Join
в плане. В особо клинических случаях к ней добавляется схлапывание полученной матрицы с помощьюDISTINCT/GROUP BY
.
SELECT DISTINCT ON (db."@ПулСерверов") group_id."@ПулСерверов" "ИдГруппы", group_id."Название" "ИмяГруппы", CASE WHEN group_id."Название" = 'Управление облаком' THEN TRUE ELSE FALSE END "ЭтоУправлениеОблаком", group_id."Тип" "Тип", group_id."Заблокирован" "Заблокирован", CASE WHEN group_id."Тип" = 15 THEN app."Код" ELSE group_id."Код" END "Код", is_demo."@ПулСерверов" is not null "Демо", group_ext_id."ДопустимоеЧислоПользователей" "ДопустимоеЧислоПользователей", group_ext_id."Состояние" "Состояние", db."@ПулСерверов" "ИдБД", db_name."ИмяБД" "ИмяБД", hosts."Название" "ХостБД", db_name."Порт" "ПортБД", group_id. "Отстойник" "Отстойник", ( WITH params AS( SELECT cpv."Значение" , cpv."Сайт" FROM "ОбщиеПараметры" cp INNER JOIN "ЗначенияОбщихПараметров" cpv ON cp."@ОбщиеПараметры" = cpv."ОбщиеПараметры" WHERE cp."Название" = 'session_cache_time' AND (cpv."Сайт" = 9 or cpv."Сайт" is null) ) SELECT coalesce( (SELECT "Значение" FROM params WHERE "Сайт" = 9) , (SELECT "Значение" FROM params WHERE "Сайт" IS NULL) , (SELECT "ЗначениеПоУмолчанию" FROM "ОбщиеПараметры" WHERE "Название" = 'session_cache_time') , 60::text )::integer ) "ТаймаутКэша", CASE WHEN nullif(111, 0) IS NULL THEN NULL WHEN 111 = group_id."@ПулСерверов" THEN TRUE ELSE FALSE END "Эталонная", site."@Сайт" "ИдСайта", site."Адрес" "ИмяСайта"FROM "ПулСерверов" group_idJOIN "ПулРасширение" group_ext_id ON group_id."@ПулСерверов" = group_ext_id."@ПулСерверов" AND NOT (group_id."@ПулСерверов" = ANY('{}'::integer[]))JOIN "ПулСерверов" folder_db ON group_id."@ПулСерверов" = folder_db."Раздел"JOIN "ПулСерверов" db ON folder_db."@ПулСерверов" = db."Раздел"LEFT JOIN "Сервер" hosts ON db."Сервер" = hosts."@Сервер"JOIN "БазаДанных" db_name ON db."@ПулСерверов" = db_name."@ПулСерверов"LEFT JOIN ( WITH list_demo_app AS ( SELECT ps0."ПулСерверов" FROM "ОбщиеПараметры" p0 INNER JOIN "ОбщиеПараметры" p1 ON p1."Раздел" = p0."@ОбщиеПараметры" AND p0."Название" = 'Управление облаком' INNER JOIN "ОбщиеПараметры" p2 ON p2."Раздел" = p1."@ОбщиеПараметры" AND p1."Название" = 'Шайтан' AND p2."Название" = 'ЭтоДемонстрационнаяГруппа' INNER JOIN "ОбщиеПараметрыСервис" ps0 ON ps0."ОбщиеПараметры" = p2."@ОбщиеПараметры" ) , list_demo_srv AS ( SELECT pool1."@ПулСерверов" FROM list_demo_app ls INNER JOIN "ПулСерверов" pool0 ON ls."ПулСерверов" = pool0."@ПулСерверов" INNER JOIN "ПулСерверов" pool1 ON pool1."Раздел" = pool0."@ПулСерверов" AND pool1."Тип" = 15 ) SELECT "@ПулСерверов" FROM list_demo_srv ) is_demo ON is_demo."@ПулСерверов" = group_id."@ПулСерверов"JOIN "ПулСерверов" app ON group_id."Раздел" = app."@ПулСерверов"LEFT JOIN "Приложение" service ON service."ПулСерверов" = group_id."@ПулСерверов"LEFT JOIN "СайтПриложение" site_app ON site_app."Приложение" = service."Раздел"LEFT JOIN "Сайт" site ON site."@Сайт" = site_app."Сайт"WHERE group_id."Тип" = 15 AND folder_db."Тип" = 8 AND db."Тип" = 4 AND db_name."ИмяБД" IS NOT NULL AND ( (1 = 1 AND is_demo."@ПулСерверов" IS NOT NULL) OR (1 = 2 AND is_demo."@ПулСерверов" IS NULL) OR 1 NOT IN (1, 2) );
DISTINCT ON(db."@ПулСерверов")
мы
ожидаем результат, уникализованный по записи db
, в
нашей схеме она вон аж в каком низу Но посмотрим на условия запроса
в самом низу они из каждой сущности (group_id, folder_db,
db)
отсекают сверху вниз по значению типа существенные
куски.is_demo
. Заметим, что его тело не зависит ни
от чего то есть его можно смело поднять в самое начало
основного WITH-блока. То есть лишнее выделение в подзапрос тут
только усложняет все без какого-либо профита.is_demo."@ПулСерверов" =
group_id."@ПулСерверов"
и is_demo."@ПулСерверов" IS
NOT NULL
при LEFT JOIN
этих таблиц,
фактически, означает необходимость присутствия PK group_id среди
идентификаторов в is_demo.group_id
по набору идентификаторов
is_demo
.ПулСерверов(Тип, Раздел)
. Причем эти типы
константны с точки зрения приложения, поэтому лучше триплет
индексов ПулСерверов(Раздел) WHERE Тип = ...
.ПулСерверов
.ТаймаутКэша
просто переписываем, избавляя от ненужных CTE.
WITH demo_app AS ( SELECT ps0."ПулСерверов" FROM "ОбщиеПараметры" p0 JOIN "ОбщиеПараметры" p1 ON (p1."Раздел", p1."Название") = (p0."@ОбщиеПараметры", 'Шайтан') JOIN "ОбщиеПараметры" p2 ON (p2."Раздел", p2."Название") = (p1."@ОбщиеПараметры", 'ЭтоДемонстрационнаяГруппа') JOIN "ОбщиеПараметрыСервис" ps0 ON ps0."ОбщиеПараметры" = p2."@ОбщиеПараметры" WHERE p0."Название" = 'Управление облаком'), demo_srv as( SELECT pool1."@ПулСерверов" FROM demo_app ls JOIN "ПулСерверов" pool0 ON ls."ПулСерверов" = pool0."@ПулСерверов" JOIN "ПулСерверов" pool1 ON (pool1."Тип", pool1."Раздел") = (15, pool0."@ПулСерверов") -- CREATE INDEX CONCURRENTLY "iПС-tmp0-t15" ON "ПулСерверов"("Раздел") WHERE "Тип" = 15;), grp AS ( SELECT grp FROM "ПулСерверов" grp WHERE "Тип" = 15 AND "@ПулСерверов" = ANY(ARRAY( SELECT "@ПулСерверов" FROM demo_srv ))), fld AS ( SELECT fld FROM "ПулСерверов" fld WHERE "Раздел" = ANY(ARRAY( SELECT (grp)."@ПулСерверов" FROM grp )) AND "Тип" = 8 -- CREATE INDEX CONCURRENTLY "iПС-tmp0-t8" ON "ПулСерверов"("Раздел") WHERE "Тип" = 8;), dbs AS ( SELECT dbs FROM "ПулСерверов" dbs WHERE "Раздел" = ANY(ARRAY( SELECT (fld)."@ПулСерверов" FROM fld )) AND "Тип" = 4 -- CREATE INDEX CONCURRENTLY "iПС-tmp0-t4" ON "ПулСерверов"("Раздел") WHERE "Тип" = 4;), srvhs AS ( SELECT hstore( array_agg((dbs)."@ПулСерверов"::text) , array_agg((dbs)::text) ) FROM ( TABLE dbs UNION ALL TABLE fld UNION ALL TABLE grp ) T)SELECT (grp)."@ПулСерверов" "ИдГруппы", (grp)."Название" "ИмяГруппы", (grp)."Название" IS NOT DISTINCT FROM 'Управление облаком' "ЭтоУправлениеОблаком", (grp)."Тип", (grp)."Заблокирован", CASE WHEN (grp)."Тип" = 15 THEN app."Код" ELSE (grp)."Код" END "Код", TRUE "Демо", grpe."ДопустимоеЧислоПользователей", grpe."Состояние", (dbn)."@ПулСерверов" "ИдБД", dbn."ИмяБД", dbh."Название" "ХостБД", dbn."Порт" "ПортБД", (grp)."Отстойник", ( SELECT coalesce( ( SELECT "Значение" FROM "ЗначенияОбщихПараметров" WHERE "ОбщиеПараметры" = cp."@ОбщиеПараметры" AND coalesce("Сайт", 9) = 9 ORDER BY "Сайт" NULLS LAST LIMIT 1 ) , "ЗначениеПоУмолчанию" , '60' )::integer FROM "ОбщиеПараметры" cp WHERE "Название" = 'session_cache_time' ) "ТаймаутКэша", CASE WHEN nullif(111, 0) IS NULL THEN NULL WHEN (grp)."@ПулСерверов" = 111 THEN TRUE ELSE FALSE END "Эталонная", site."@Сайт" "ИдСайта", site."Адрес" "ИмяСайта"--, *FROM dbsJOIN "БазаДанных" dbn ON dbn."@ПулСерверов" = (dbs.dbs)."@ПулСерверов"JOIN LATERAL ( SELECT ((TABLE srvhs)->((dbs)."Раздел"::text))::"ПулСерверов" fld ) fld ON TRUEJOIN LATERAL ( SELECT ((TABLE srvhs)->((fld)."Раздел"::text))::"ПулСерверов" grp ) grp ON TRUEJOIN "ПулРасширение" grpe ON grpe."@ПулСерверов" = (grp)."@ПулСерверов"JOIN "ПулСерверов" app ON app."@ПулСерверов" = (grp)."Раздел"JOIN "Сервер" dbh ON dbh."@Сервер" = (dbs)."Сервер"LEFT JOIN "Приложение" srv ON srv."ПулСерверов" = (grp)."@ПулСерверов"LEFT JOIN "СайтПриложение" site_app ON site_app."Приложение" = srv."Раздел"LEFT JOIN "Сайт" site ON site."@Сайт" = site_app."Сайт"WHERE dbn."ИмяБД" IS NOT NULL;