Приветствую! Сегодня мы рассмотрим путь выполнения сложного SELECT запроса. Выполнение простого запроса было рассмотрено в предыдущей статье. На этот раз запрос выглядит следующим образом: Различия, при сравнении с выполнением простого запроса, имеются в 3 местах: парсинг, создание плана и выполнение. Поэтому рассмотрим только эти случаи. Различия начинаются на этапе переписывания.  Напомню, что входная точка переписывания запроса - transformStmt (src/backend/parser/analyze.c). Эта функция различает основные типы запросов и запускает соответствующий обработчик. Для SELECT это снова transformSelectStmt (src/backend/parser/analyze.c).  Для обработки выражения FROM вызывается transformFromClause (src/backend/parser/parse_clause.c). Обработка будет вестись точно так же как и в простом запросе. В простом запросе мы получали все столбцы, используя астериск (*). Для этого использовалась отдельная функция  ExpandColumnRefStar (src/backend/parser/parse_target.c). В случае когда в запросе нет астериска, вызывается общая функция transformTargetEntry (src/backend/parser/parse_target.c)  - не важно столбец, выражение, вызов функции или константа. Внутри она состоит из 2 частей (вызовов функций): transformExpr - создание выражения (src/backend/parser/parse_expr.c) FigureColname - определение названия столбца (src/backend/parser/parse_target.c) transformExpr создает объект ParseState и передает ее transformExprRecurse - настоящему парсеру. Внутри он запускает обработчик на основании тэга узла. Для u.name тэг - T_ColumnRef (выражение - ссылка на столбец). Для него запустится обработчик transformColumnRef На вход он получает созданную структуру состояния парсинга ParseState и структуру столбца ColumnRef Внутри проверяется допустимость использования ссылки на столбец. Проверка выполняется с помощью проверки типа выражения узла. Тип выражения определяется перечислением ParseExprKind(src/include/parser/parse_node.h) Единственные недопустимые значения - EXPR_KIND_COLUMN_DEFAULT и EXPR_KIND_PARTITION_BOUND. Стоит обратить внимание на алгоритм парсинга ссылок на столбцы. Для обращений к столбцам таблиц может быть использовано несколько вариантов. Например, A.B.C или A.*  Всего может быть 7 вариантов действий. Каждый описан в комментарии функции Для определения используемого варианта, используется поле fields структуры ColumnRef. Это список содержащий названия A, B, C, D (из комментария).  В зависимости от длины списка определяется и метод обработки. Например, если длина списка 2, то проверяются варианты A.B и A.* Также внутри функции определено вспомогательное перечисление для корректной обработки ошибок. Так в описанном выше случае, если таблица не найдена, то переменной crerr присваивается значение CRERR_NO_RTE После успешного парсинга столбца нужно определить его отображаемое название. Если мы указали псевдоним ( “AS …”), то название уже задано, иначе пытаемся вывести. Для этого используется функция FigureColname (src/backend/parser/parse_target.c) Название создается на основании типа узла. В случае столбца выбирается самое последнее название из списка (за исключением *) Сейчас мы имеем только названия столбцов, но куда они указывают не понятно. Последняя стадия - добавление OID таблицы, на которую столбец указывает. За это отвечает функция markTargetListOrigin  (src/backend/parser/parse_target.c) Дальше идет парсинг предложения WHERE. За это отвечает transformWhereClause В начале происходит парсинг самого выражения. В условии может быть любое выражение, поэтому для обработки вызывается transformExpr (общая функция парсинга выражений) Выражение это такой же узел и для него определены свои тэги. Для выражений с бинарными/унарными операторами или SQL предложениями (LIKE, BETWEEN) тег узла равен T_A_Expr. А чтобы выявить подтип, используется дополнительное поле kind типа A_Expr_Kind (src/include/nodes/parsenodes.h) Для бинарной операции (сравнение) это поле равно AEXPR_OP. За его обработку отвечает функция transformAExprOp (src/backend/parser/parse_expr.c). В итоге получается такой порядок Сам transformAExprOp просматривает несколько краевых случаев: "= NULL" Самый первый обрабатываемый случай - это трансформация “= NULL” в “IS NULL”. В комментарии поясняется, что это сделано для совместимости с другими языками, нарушающими стандарты SQL  ROW op SUBSELECT Другой особый случай - когда один из операндов подзапрос. Этом случае тип выражения меняется с EXPR_SUBLINK на ROWCOMPARE_SUBLINK. Сам тип представляется перечислением SubLinkType Когда обе части - это ROW  выражения тоже отдельный случай (ROW op ROW). Обычный оператор Точнее сказать, это все остальные случаи. Сюда мы попадаем, если другие условия не сработали. При исполнении нашего запроса, мы попадаем в последнюю ветку. Внутри каждая часть выражения рекурсивно разбирается (вызов transformExprRecurse): Левая часть u.name попадает под случай ColumnRef - ссылка на столбец. Она обрабатывается так же как и выше (в SELECT) Правая часть 24 попадает под случай T_Const : узел - константа. Для разбора констант узлов используется функция make_const. Разбор константы - тот же самый switch/case. Причем парсинг числа - самый простой (всего 4 строки) После вызывается функция make_op (src/backend/parser/parse_oper.c) для создания узла операции. Следующий этап - разбор предложения HAVING. Для разбора HAVING используется та же функция, что и при парсинге  WHERE - transformWhereClause Различие начинается в функции transformExprRecurse: левый оператор теперь не ссылка на столбец, а агрегирующая функция. Поэтому тэг не T_ColumnRef, а T_FuncCall - вызов функции. За ее обработку отвечает transformFuncCall(src/backend/parser/parse_expr.c) Сама логика парсинга заключается в функции ParseFuncOrColumn В Postgres обращение к столбцу в таблице может быть записано 2 способами: через точку (table.column) и как вызов функции (column(table)). Это легаси, с которым нужно мириться. Поэтому название функции имеет такое название. В комментарии дано более подробное описание  Информация о функциях (и других . Для получения информации о функции используется func_get_detail(src/backend/parser/parse_func.c). Для поиска используются кандидаты. Поиск производится по названию (разбирается на неймспейс и имя) и аргументам (количеству и именам). Поиск кандидатов реализуется в FuncnameGetCandidates(src/backend/catalog/namespace.c). Вначале из системного кэша получаются все вхождения функций (список) с совпадающим названием. После происходит итерирование по всем элементам этого списка в поиске кандидатов. Функция может стать кандидатом только если: Неймспейс совпадает (поиск происходил только по названию) Произошло совпадение по OUT параметрам Именованные аргументы совпали Совпадения по типам переданных параметров Когда список кандидатов готов, он подвергается дополнительным испытаниям.  Происходит поиск лучшего кандидата: сравниваются типы всех аргументов. Если они все совпадают, то функция найдена Если такой функции не нашлось и при этом у функции только 1 аргумент - делается предположение, что функция - каст к типу. Для этого имя функции интерпретируется как название типа и происходит попытка привести его к касту (coercion). Потом происходит поиск по совпадению аргументов с возможным приведением типов - func_match_argtypes(src/backend/parser/parse_func.c). Если нашлась одна подходящая функция, то возвращается она.  Если под условие подошло сразу несколько то происходит последний рывок вызов func_select_candidate (src/backend/parser/parse_func.c),  которая пытается решить конфликт. Если дошли до этого момента - то функцию не удалось найти. Во время работы нам может понадобиться множество объектов. Например, информация о существующих функциях как в примере выше. Для ускорения работы, подобные “горячие” элементы хранятся в памяти. Доступ к ним производится через системный кэш. За работу с ним отвечает модуль syscache (src/include/utils/syscache.h). В этом заголовочном файле объявлены необходимые для работы перечисление SysCacheIdentifier (src/include/utils/syscache.h), сигнатуры функций для доступа к кэшу и несколько полезных макросов. Перечисление SysCacheIdentifier определяет область данных, которые мы ищем. Как понятно из сигнатуры, функции InitCatalogCache и InitCatalogCachePhase2 инициализируют кэш. Обе эти функции вызываются в InitPostgres на моменте старта базы данных. Используется 2 функции, так как инициализация разбита на 2 фазы: выделение памяти и общая инициализация (не загрузка кэша), соответственно.  На примере поиска необходимой функции, работа с кэшем выглядит следующим образом: Вызываем макрос SearchSysCacheList* для списка значений по ключу. Под * имеется ввиду количество ключей по которым идет поиск. В случае поиска функции, использовалось значение 1 Нам возвращается структура CatCList. Она представляет собой список из элементов кэша. Элементами этого списка являются CatCTup Итерируемся по всем элементам members, получаем хранимые в нем данные и работаем с ними Возвращаем список обратно в кэш - ReleaseSysCacheList Осталось только выяснить как этот кэш заполняется и хранится. Сам кэш хранится в глобальной переменной SysCache. Это массив типа CatCache  Заполнение - лениво:  во время вызова SearchCatCacheList мы итерируемся по имеющимся в памяти данным, но если в результате мы ничего не нашли, то входим в область заполнения кэша. Эта область окаймлена PG_TRY/PG_CATCH так как во время работы с таблицей может возникнуть исключение и тогда нужно удалить все добавленные элементы в хэш таблицу.  После идет обработка ORDER BY u.name.  За это отвечает transformSortClause(src/backend/parser/parse_clause.c) Парсинг заключается в последовательной обработке выражений из списка сортировки (выражений, используемых для сортировки) и добавлению их в результирующий список. За их обработку отвечают 2 функции: findTargetlistEntrySQL99 - стандарт SQL 99 (src/backend/parser/parse_clause.c) findTargetlistEntrySQL92 - стандарт SQL 92 (src/backend/parser/parse_clause.c) Сам разбор состоит из простого итерирования по списку и вызова transformTargetEntry для каждого элемента Одно из различий в стандартах SQL92 и SQL99 заключается в логике обработки списка выражений в ORDER BY и GROUP BY. В стандарте SQL92 этот список состоит из названий столбцов или номера столбца (числа). Например, SELECT name, age FROM users ORDER BY 2 - будет сортировать по возрасту (age) В стандарте SQL99 каждый элемент списка - это выражение, использующее столбцы таблиц. Например,  SELECT length(name), name from users ORDER BY age * 2  в ORDER BY содержится выражение, а не сырой столбец. Важно отметить, что в логике парсинга сделана небольшая оптимизация: при парсинге стандартом SQL92, если проваливается попытка обнаружения ссылки на столбец из SELECT выражения, то запасным вариантом запускается парсинг SQL99 стандартом.  Для определения какой метод разбора выбрать используется фича-флаг useSQL99, который передается в transformSortClause.  GROUP BY u.name После идет обработка GROUP BY. За это отвечает transformGroupClause (src/backend/parser/parse_clause.c). В списке выражений группировки может быть 2 “граничных” случая, которые обрабатываются по разному:  Выражение (столбец, функция) GROUPING SET Все выражения группировки хранятся в общем списке flat_grouplist. В процессе обработки каждого выражения группировки этот список обновляется. За обработку выражения отвечает transformGroupClauseExpr. Разбор выражения ведется точно так же как и выражения сортировки - findTargetlistEntrySQL99 и findTargetlistEntrySQL92. Но дальше идет оптимизация: учет сортировки.  Для обработки таких случаев используется структура SortGroupClause (src/include/nodes/parsenodes.h). Внутри себя она хранит информацию необходимую для сортировки и группировки (операторы сравнения, флаг хэшируемости), а для оптимизации памяти вместо самого выражения - индекс на элемент в списке столбцов SELECT. Работа transformGroupClauseExpr зависит от типа используемой группировки. В случае GROUPING SET действия зависят от подтипа. Подтип определяется перечислением GroupingSetKind (src/include/nodes/parsenodes.h) Остальные случаи используют  transformGroupClauseExpr, для парсинга выражений. Результатом работы этого этапа является список SortGroupClause - ссылок на элементы из результирующего списка выражений. Последним обрабатывается выражение LIMIT. За его обработку отвечает transformLimitClause(src/backend/parser/parse_clause.c) Обработка заключается в парсинге выражения. За это отвечает базовый transformExpr. Дополнительно проводится проверка на то, что выражение не ссылается на переменные в запросе. Для этого используется функция checkExprIsVarFree. Она обходит дерево запроса и проверяет существование переменных в любом из узлов. Этот паттерн посетитель реализуется функцией query_tree_walker(src/backend/nodes/nodeFuncs.c) Сам посетитель - функция, которую передаем аргументом. query_tree_walker проходит по всем узлам запроса и применяет ее к каждому узлу. Посетитель, проверяющий существование переменных, - contain_vars_of_level_walker(src/backend/optimizer/util/var.c). Важно заметить, что transformLimitClause используется для парсинга как и LIMIT так и OFFSET. Разница заключается в дополнительной проверке для LIMIT: в FETCH FIRST … WITH TIES не разрешено использование NULL Следующий важный этап - составление плана запроса. Но перед этим происходит предобработка некоторых выражений.  Первым производится обработка LIMIT/OFFSET выражения. За это отвечает функция preprocess_limit(src/backend/optimizer/plan/planner.c). Эта функция не просто парсит значение узла LIMIT, здесь происходит оценка возможного количества кортежей на выходе - tuple_fraction. Одно из достоинств PostgreSQL - умный планировщик. Один из компонентов такой репутации - логика определения количества кортежей на выходе. В GUC присутствует параметр cursor_tuple_fraction, который представляет оценку количества записей, которые будут получены. По умолчанию, он равен 0,1 (10%) Это значение используется для определения лучшего плана исполнения: если планируется получить мало строк, то выбирается план с самым быстрым стартом. Но cursor_tuple_fraction - не конечное значение. Это значение регулируется - preprocess_limit возвращает новое значение tuple_fraction, которое и будет использоваться в процессе создания плана. Код его расчета - дерево выбора  Дальше происходит обработка выражения GROUP BY - вызов preprocess_groupclause (src/backend/optimizer/plan/planner.c). Главная задача этой функции - синхронизировать GROUP BY и ORDER BY выражения. Если это получится сделать, то сортировку и группировку можно выполнить одной операцией. За предобработку возвращаемого списка значений отвечает preprocess_targetlist (src/backend/optimizer/prep/preptlist.c). Вкратце, на данном этапе добавляются рабочие столбцы, которые будут полезны в дальнейшей работе. В случае, если в запросе было указано FOR UPDATE/SHARE, то полученные кортежи нужно заблокировать, чтобы не дать получить к ним доступ другим пользователям.  Интересен вариант реализации - если в выражении есть помеченные строки, то в результирующий список добавляются новые (мусорные, junk) столбцы, единственная задача которых - помечать строки. Имена этих столбцов создаются динамически и доступа к ним не получить обычным способом. Сами помеченные столбцы представляются структурой PlanRowMark  Когда предобработка завершена, начинается создание путей выполнения.  Путь - это все данные необходимые для создания плана выполнения. Самый простой путь - последовательное сканирование - представляется структурой Path. Он является базовым для всех других. Начальный путь создается вызовом query_planner. Затем, каждый из дополнительных выражений в запросе (GROUP BY, ORDER BY) добавляют свои пути. Для создания путей, учитывающих сортировку, вызывается make_sort_input_target(src/backend/optimizer/plan/planner.c). Вначале, просматривается каждое выражение участвующее в сортировке. В зависимости от его типа/свойств, обновляются переменные контекста.  Например, если в сортировке используются volatile функции или функции, стоимость которых слишком большая, то выставляется флаг have_volatile Дополнительно учитываются столбцы, вычисление которых может быть отложено. Для отслеживания таких выражений используется массив флагов. Это показано выше: postpone_col[i] = true. После создается новый PathTarget специально для сортировки. В процессе выбора лучшего пути, нам нужны не все данные. В частности, больше всего нам полезны стоимость пути или количество используемых выражений. Для этого существует PathTarget На текущем этапе создаются именно они. Сами планы будут созданы после, на основании этих путей  Первым заполняется список выражений, которые будут возвращены запросом - сперва идут простые переменные, а в конце те, что мы указали как отложенные. В конце, вызывается set_pathtarget_cost_width(src/backend/optimizer/path/costsize.c) для определения стоимости пути. Почти все функции, создающие новые пути, подчиняются одному паттерну:  Создание нового пути (PathTarget) Заполнение его данными Вызов set_pathtarget_cost_width  Возврат полученного значения из этой функции  P.S. 3 и 4 объединяются в return set_pathtarget_cost_width Задача этой функции - определить стоимость пути.  Стоимость представляется структурой QualCost Эта функция итерируется по всем выражениям, используемым в пути, и, в зависимости от его типа, обновляет стоимость. Можно заметить, что при расчете стоимости учитывается только 2 типа выражений: переменные и обычные выражения. Причем стоимость пути могут изменить только обычные выражения - считается, что стоимость переменных равна 0 Для определения стоимости выражения используется паттерн Visitor - функция cost_qual_eval_node (src/backend/optimizer/path/costsize.c) запускает посетитель cost_qual_eval_walker который обходит каждый узел выражения.  Например, в случае если выражение - вызов функции, то вызовется функция add_function_cost (src/backend/optimizer/util/plancat.c), которая и подсчитает стоимость функции  Дальше идет вычисление путей для группирующих выражений. За это отвечает make_group_input_target(src/backend/optimizer/plan/planner.c). В начале все результирующие столбцы делятся на 2 группы: участвующие и нет в группировке. После этого в результирующий путь сначала добавляются все столбцы, участвующие в группировке, а затем все остальные. После так же как и в сортировке, вызывается set_pathtarget_cost_width для оценки стоимости. Когда пути созданы, они сохраняются в поле upper_targets. Это массив хранящий в себе созданные пути. Для индексации по нему используется перечисление UpperRelationKind После предобработки, сохраняются следующие планы Сейчас начинается создание дополнительных планов. Первым создается план, учитывающий группировку. Это create_grouping_paths. В Postgres есть понятие degenerate grouping. Это такое выражение, в котором не используются агрегирующие функции и GROUP BY, но при этом есть HAVING или GROUPING SET.  Для таких случаев имеется отдельный поток выполнения.  Проверка на этот случай содержится в is_degenerate_grouping (src/backend/optimizer/plan/planner.c) Для них определен свой собственный узел - GroupResultPath  Создание группирующих планов начинается с определения важных свойств выражения. Например, применимы ли сортировка или хэширование. Эти флаги определены макросами  Например, возможность использования хэширования определяется таким образом Сама логика добавления путей группировки заключена в add_paths_to_grouping_rel (src/backend/optimizer/plan/planner.c). Здесь создаются пути выполнения для вариантов с хэшированием и сортировкой. Для случая GROUP BY u.name работа будет следующей.  Флаг can_hash выставлен, поэтому обрабатывается эта ветка. Берется следующий путь (массив pathlist) GROUPING SET у нас отсутствует, поэтому создаем HashAgg узел (create_agg_path) В конце, вызывается set_cheapest (src/backend/optimizer/util/pathnode.c), чтобы обновить лучшие пути. set_cheapest - функция, которая занимается обновлением информации о лучших путях При создании планов, для хранения информации используется структура RelOptInfo. В ней присутствуют поля, хранящие текущие лучшие пути Но это только слепок - в процессе могут появиться новые более выгодные пути. Для обновления этих полей используется set_cheapest. Внутри  происходит простой проход по всем путям и сравнения с текущим лучшим Сравнение стоимости путей реализуется функцией compare_path_costs (src/backend/optimizer/util/pathnode.c)  Дальше идет создание планов, учитывающих сортировку. За это отвечает create_ordered_paths(src/backend/optimizer/plan/planner.c). За создание сортирующего плана отвечает create_sort_path (src/backend/optimizer/util/pathnode.c), либо create_incremental_sort_path, если необходима инкрементальная сортировка. Просто обходится каждый путь и добавляется узел сортировки  В конце когда результирующий план построен, к нему добавляются узлы LIMIT и/или OFFSET. Это делает create_limit_path (src/backend/optimizer/util/pathnode.c). Спустя несколько возвратов вверх по стеку вызовов, результирующий план вычисляется в standard_planner План построен, пора приступать к его выполнению. Выполнение начинается в функции ExecutePlan. Напомню, что: Выполняется не план, а портал. Он хранит в себе все необходимое для выполнения плана Каждый узел “наследуется” от PlanState  Каждый PlanState “реализует” ExecProcNodeMtd - функцию, которая исполняет работу узла (например, если это LIMIT, то следит за количеством полученных строк) Представим, что для нашего запроса создался план выполнения: LIMIT ORDER BY GROUP BY SEQ SCAN Тогда получение следующего кортежа пройдет следующие узлы (этапы). Корнем нашего дерева запроса будет LIMIT. Узел, его представляющий, - LimitState Реализация ExecProcNodeMtd - ExecLimit (src/backend/executor/nodeLimit.c). Узел работает по принципу машины состояний. Состояния описываются перечислением LimitStateCond Для LIMIT 50 переходы в процессе работы будут следующими: LIMIT_INITIAL Это начальное состояние. Здесь только вызывается recompute_limits, чтобы вычислить значения offset (сколько пропустить) и count (сколько взять). Из этого состояния мы сразу переходим в LIMIT_RESCAN. LIMIT_RESCAN Это основное рабочее состояние. Здесь мы вызываем подплан (получаем от него кортежи) до того момента пока не отправим указанное количество кортежей. Если предела достигли, то переходим в LIMIT_INWINDOW. LIMIT_INWINDOW Поведение в этом состоянии определяется опцией WITH TIES. В нашем случае, если мы попали сюда, то это означает, что мы отправили 50 строк и в таблице есть еще (так как не вернулся null).  WITH TIES мы не указывали, а значит следующее состояние - LIMIT_WINDOWEND. LIMIT_WINDOWEND Для нас это терминальное состояние (из этого состояния еще можно вернуть данные, но только если курсор движется в обратном направлении). * Если случилось так, что из подплана вернулось меньше, чем указано в LIMIT, то следующее после LIMIT_RESCAN состояние - LIMIT_EMPTY. Это также терминальное состояние, но в отличие от LIMIT_WINDOWEND - всегда возвращает NULL (нет варианта передвинуться назад) Следующий подузел - сортировка. Его состояние представляется структурой SortState Реализация ExecProcNodeMtd - ExecSort (src/backend/executor/nodeSort.c). Флаг sort_Done сигнализирует о том, была ли сортировка уже произведена (первый ли запуск). Если нет, то запускается логика сортировки.  Структура Tuplesortstate представляет собой состояние необходимое для выполнения сортировки. Сейчас важно знать, что она хранит в себе все кортежи, которые предстоит отсортировать. Также стоит подметить, что это часть реализации и закрыта для остальных (поэтому поле, в котором она хранится, имеет тип void*, см. выше) Сортировать мы можем либо скалярные (единичные) значения, либо целые кортежи. Для обобщения их логики и была создана абстракция Tuplesortstate. Для каждого типа сравнения, определен свой класс функций выполняющих определенные шаги: добавление и получение данных из состояния. Например, вот так происходит добавление данных  Datum представляет собой обертку над “любым” типом данных, с которым мы можем работать. Он является псевдонимом, поэтому с сырым значением ничего полезного с ним сделать нельзя  Но для работы с ним определена куча макросов. Например, для работы с int есть 2 макроса Вообще макросы определены для всех простых типов + собственные простые типы Postgres, например TransactionId Дополнительно определена отдельная структура NullableDatum, для работы со значениями допускающими null. Но единственные места, где явно она используется - работа с jit функциями и компиляция llvm. Из примера выше, для добавления единичных значений в Tuplesortstate сначала получается Datum из подузла.  Для добавления значения используется tuplesort_putdatum. Как я уже сказал, явный NullableDatum используется не везде. Здесь флаги null сделаны в виде дополнительного массива. Вот так Datum добавляется в Tuplesortstate (в качестве Datum выступают ссылки на кортежи, поэтому используется ссылочная интерпретация значения)  Но вот логика сортировки - единая для всех. Содержится она в tuplesort_performsort(src/backend/utils/sort/tuplesort.c). Внутри происходит выбор между несколькими стратегиями сортировки, определяемыми перечислением TupSortStatus. Например, в начальном состоянии, когда памяти достаточно, мы можем использовать quick sort На этом сортировка окончена и ее состояние сохранилось в SortState. Теперь мы можем начать получать уже отсортированные кортежи. Получение данных из Tuplesortstate также зависит от типа данных. В случае кортежей используется tuplesort_gettupleslot (src/backend/utils/sort/tuplesort.c). Получение кортежа из отсортированного списка довольно тривиальное - возвращается следующий элемент из отсортированного массива (но это в случае если сортировка происходила в памяти) Теперь мы можем читать кортежи из отсортированного массива. Нижележащий узел - группировка. Стратегий группировки много. Предположим что мы используем группировку хэшированием. Структура ее представляющая - AggState Реализация ExecProcNodeMtd - ExecAgg (src/backend/executor/nodeAgg.c). Группировка может производиться несколькими стратегиями. Например, хэширование или сканирование уже отсортированных данных. Такие стратегии определяются перечислением AggStrategy В нашем случае будет использоваться группировка хэшированием - AGG_HASHED. Общая логика получения следующего кортежа находится в agg_retrieve_hash_table_in_memory (src/backend/executor/nodeAgg.c). Для хэштаблицы используемой в хэш агрегировании есть своя структура - TupleHashTable Элементы TupleHashTable - TupleHashEntryData  Для AggState есть несколько типов состояний: AggStatePerAgg - содержит данные для каждого набора кортежей, которые будут использованы для вычисления результирующего кортежа AggStatePerGroup - представляет собой единый блок группировки AggStatePerHash - состояние, используемое при стратегии AGG_HASH (в обычном случае имеется один экземпляр, но если используется GROUPING SET, то для каждой группы свое состояние) Вначале мы итерируемся по всем вхождениям в хэш таблице и пытаемся найти первый TupleHashEntryData, удовлетворяющий HAVING. Для получения следующего кортежа из хэш таблицы используется макрос ScanTupleHashTable.  Хэш-таблицы генерируются во время компиляции и для каждого отдельного варианта использования можно создать свою структуру хэш-таблицы. Причем можно задавать свой префикс для этой структуры. Для хэш таблицы кортежей используется префикс tuplehash и все операции с этой хэш таблицей и имеют префикс tuplehash Когда элемент найден, вызывается project_aggregates. Эта функция заключает в себе проверку условия HAVING и результирующее отображение. За проверку HAVING отвечает ExecQual. По факту она просто вызывает функцию, хранящуюся в поле evalfunc Если условие выполнилось, то вызовом ExecProject создается результирующий кортеж, который и возвращается. Последним этапом будет последовательное сканирование таблицы. Ее реализация была обсуждена в предыдущей статье. Поэтому сейчас просто напомню, что: Реализация ExecProcNodeMtd - ExecSeqScan. Она использует обобщенную функцию ExecScan В начале приобретается ссылка на необходимую таблицу Для чтения данных используется менеджер буферов, из которого поочередно получаются буферы представляющие область памяти на диске Теперь эта цепочка функций будет вызываться до того момента, пока не будут возвращены все кортежи.  Бэкэнд разработчик, но для ПМ могу быть кем угодно