Как стать автором
Обновить

PostgreSQL под капотом. Часть 5. Простой SELECT запрос

Время на прочтение42 мин
Количество просмотров7.4K

Приветствую!

На прошлом шаге мы изучили цикл бэкэнда в общих черта. В этой части рассмотрим простой SELECT запрос. Представим у нас есть таблица users

create table users(
  id integer generated always as identity,
  email varchar not null,
  name varchar
);

и нам нужно получить все записи из нее:

select * from users;

Подготовка к выполнению запроса

Путь запроса начинается в функции exec_simple_query (src/backend/tcop/postgres.c), так как запрос будет передаваться через протокол простой передачи.

Статистика

В самом начале находится блок обновления статистики. Статус текущего бэкэнда обновляется в соответствии с запросом. Для этого используется функция pgstat_report_activity (src/backend/utils/activity/backend_status.c), которой передаются текущий статус (STATE_RUNNING) и сырая строка запроса.

Само обновление статистики заключается в обновлении:

  • Статуса

  • Времени вхождения в этот статус

  • Времени нахождения в предыдущем статусе

  • Выполняемой строки запроса

Дополнительно вызывается функция ResetUsage для сбрасывания накопленной статистики использования ресурсов, для правильного учета дальнейшей статистики.

Статистика использования ресурсов

В процессе работы системы периодически требуется отображать потребление ресурсов: отработанное время, использование памяти, сигналы. Для работы с такой информацией существует отдельный заголовочный файл <sys/resource.h>. В нем определяются структуры, функции и макросы для работы со статистикой программы.

Так, статистика представляется структурой rusage:

struct rusage
{
    /* Total amount of user time used.  */
    struct timeval ru_utime;
    /* Total amount of system time used.  */
    struct timeval ru_stime;
    /* Maximum resident set size (in kilobytes).  */
    union
    {
        long int ru_maxrss;
        __syscall_slong_t __ru_maxrss_word;
    };
    /* Amount of sharing of text segment memory
       with other processes (kilobyte-seconds).  */
    union
    {
        long int ru_ixrss;
        __syscall_slong_t __ru_ixrss_word;
    };
    /* Amount of data segment memory used (kilobyte-seconds).  */
    union
    {
        long int ru_idrss;
        __syscall_slong_t __ru_idrss_word;
    };
    /* Amount of stack memory used (kilobyte-seconds).  */
    
    union
    {
        long int ru_isrss;
        __syscall_slong_t __ru_isrss_word;
    };
    /* Number of soft page faults (i.e. those serviced by reclaiming
       a page from the list of pages awaiting reallocation.  */
    union
    {
        long int ru_minflt;
        __syscall_slong_t __ru_minflt_word;
    };
    /* Number of hard page faults (i.e. those that required I/O).  */
    union
    {
        long int ru_majflt;
        __syscall_slong_t __ru_majflt_word;
    };
    /* Number of times a process was swapped out of physical memory.  */
    union
    {
        long int ru_nswap;
        __syscall_slong_t __ru_nswap_word;
    };
    /* Number of input operations via the file system.  Note: This
       and `ru_oublock' do not include operations with the cache.  */
    union
    {
        long int ru_inblock;
        __syscall_slong_t __ru_inblock_word;
    };
    /* Number of output operations via the file system.  */
    union
    {
        long int ru_oublock;
        __syscall_slong_t __ru_oublock_word;
    };
    /* Number of IPC messages sent.  */
    union
    {
        long int ru_msgsnd;
        __syscall_slong_t __ru_msgsnd_word;
    };
    /* Number of IPC messages received.  */
    union
    {
        long int ru_msgrcv;
        __syscall_slong_t __ru_msgrcv_word;
    };
    /* Number of signals delivered.  */
    union
    {
        long int ru_nsignals;
        __syscall_slong_t __ru_nsignals_word;
    };
    /* Number of voluntary context switches, i.e. because the process
       gave up the process before it had to (usually to wait for some
       resource to be available).  */
    union
    {
        long int ru_nvcsw;
        __syscall_slong_t __ru_nvcsw_word;
    };
    /* Number of involuntary context switches, i.e. a higher priority process
       became runnable or the current process used up its time slice.  */
   union
    {
        long int ru_nivcsw;
        __syscall_slong_t __ru_nivcsw_word;
    };
};

Для работы с ней имеется несколько функций. Например, getrusage - получение этой статистики (https://man7.org/linux/man-pages/man2/getrusage.2.html). 

В Postgres имеется семейство GUC настроек log_*_stats. Если они выставлены, то по достижении определенного этапа логируются потребленные ресурсы. Например, при выставленном log_statement_stats после выполнения отправленного выражения (или нескольких, если в одном запросе) эта статистика будет выводиться в лог. 

За логирование использования ресурсов отвечает функция ShowUsage

void ShowUsage(const char *title);

На вход ей передается название области, для которой подсчитано потребление ресурсов. Например, для выражений передается “QUERY STATISTICS”

ShowUsage("QUERY STATISTICS");

Внутри она вычисляет разницу между последней сохраненной статистикой и текущей и выводит отформатированное значение в лог. 

Чтобы сохранить статистику перед входом в какую-то область, используется ResetUsage. Она сохраняет текущие значения потребления ресурсов как точку отсчета. При следующем использовании ShowUsage использоваться будет сохраненное значение.

Для exec_simple_query использование следующее:

if (save_log_statement_stats)
    ResetUsage();

//// Выполнение запроса

if (save_log_statement_stats)
	ShowUsage("QUERY STATISTICS");

Начало транзакции

На этом этапе инициируется транзакция.

Для управления транзакциями есть отдельный модуль xact (src/backend/access/transam/xact.c). Но внутри файла с точкой входа для Postgres определены свои функции: start_xact_command и finish_xact_command.

Первая отвечает за начало транзакции, последняя — за окончание. Дополнительно в них присутствует логика проверки действующей транзакции. Как побочные эффекты, они управляют (начинают/сбрасывают) таймаутами подключения клиента и времени выполнения выражения.

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

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

Подготовка выражения к парсингу

Планы запросов могут сохраняться между запросами к серверу. Сохраненный запрос представляется структурой CachedPlanSource, а сами структуры хранятся в глобальном (для бэкэнда) ассоциативном массиве. 

typedef struct CachedPlanSource
{
	int			magic;			/* should equal CACHEDPLANSOURCE_MAGIC */
	struct RawStmt *raw_parse_tree; /* output of raw_parser(), or NULL */
	const char *query_string;	/* source text of query */
	CommandTag	commandTag;		/* 'nuff said */
	Oid		   *param_types;	/* array of parameter type OIDs, or NULL */
	int			num_params;		/* length of param_types array */
	ParserSetupHook parserSetup;	/* alternative parameter spec method */
	void	   *parserSetupArg;
	int			cursor_options; /* cursor options used for planning */
	bool		fixed_result;	/* disallow change in result tupdesc? */
	TupleDesc	resultDesc;		/* result type; NULL = doesn't return tuples */
	MemoryContext context;		/* memory context holding all above */
	/* These fields describe the current analyzed-and-rewritten query tree: */
	List	   *query_list;		/* list of Query nodes, or NIL if not valid */
	List	   *relationOids;	/* OIDs of relations the queries depend on */
	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
	struct OverrideSearchPath *search_path; /* search_path used for parsing
											 * and planning */
	MemoryContext query_context;	/* context holding the above, or NULL */
	Oid			rewriteRoleId;	/* Role ID we did rewriting for */
	bool		rewriteRowSecurity; /* row_security used during rewrite */
	bool		dependsOnRLS;	/* is rewritten query specific to the above? */
	/* If we have a generic plan, this is a reference-counted link to it: */
	struct CachedPlan *gplan;	/* generic plan, or NULL if not valid */
	/* Some state flags: */
	bool		is_oneshot;		/* is it a "oneshot" plan? */
	bool		is_complete;	/* has CompleteCachedPlan been done? */
	bool		is_saved;		/* has CachedPlanSource been "saved"? */
	bool		is_valid;		/* is the query_list currently valid? */
	int			generation;		/* increments each time we create a plan */
	/* If CachedPlanSource has been saved, it is a member of a global list */
	dlist_node	node;			/* list link, if is_saved */
	/* State kept to help decide whether to use custom or generic plans: */
	double		generic_cost;	/* cost of generic plan, or -1 if not known */
	double		total_custom_cost;	/* total cost of custom plans so far */
	int64		num_custom_plans;	/* # of custom plans included in total */
	int64		num_generic_plans;	/* # of generic plans */
} CachedPlanSource;

1 выражение в рамках простого запроса имеет время жизни в рамках 1 итерации. Поэтому нет необходимости тратить ресурсы на его поиск в словаре. Поэтому для них (безымянных) запросов существует отдельная переменная unnnamed_stmt_psrc:

/*
 * If an unnamed prepared statement exists, it's stored here.
 * We keep it separate from the hashtable kept by commands/prepare.c
 * in order to reduce overhead for short-lived queries.
 */
static CachedPlanSource *unnamed_stmt_psrc = NULL;

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

Так как начинается новый запрос, то значение в переменной удаляется.

Дополнительно происходит переключение контекста памяти на MessageContext. В нем будут храниться данные необходимые на этапе парсинга и переписывания.

Парсинг запроса

После настройки контекста памяти начинается парсинг выражений из запроса.

За парсинг отвечает модуль parser (src/backend/parser/parser.c). Логика парсинга заключена в функции raw_parser

/*
 * raw_parser
 *		Given a query in string form, do lexical and grammatical analysis.
 *
 * Returns a list of raw (un-analyzed) parse trees.  The contents of the
 * list have the form required by the specified RawParseMode.
 */
List *
raw_parser(const char *str, RawParseMode mode);

На вход передается строка запроса и режим парсинга - RawParseMethod (src/include/parser/parser.h). Так как нам нужно спарсить SQL запрос, то передается RAW_PARSE_DEFAULT. Всего есть 6 режимов парсинга

/*
 * RawParseMode determines the form of the string that raw_parser() accepts:
 *
 * RAW_PARSE_DEFAULT: parse a semicolon-separated list of SQL commands,
 * and return a List of RawStmt nodes.
 *
 * RAW_PARSE_TYPE_NAME: parse a type name, and return a one-element List
 * containing a TypeName node.
 *
 * RAW_PARSE_PLPGSQL_EXPR: parse a PL/pgSQL expression, and return
 * a one-element List containing a RawStmt node.
 *
 * RAW_PARSE_PLPGSQL_ASSIGNn: parse a PL/pgSQL assignment statement,
 * and return a one-element List containing a RawStmt node.  "n"
 * gives the number of dotted names comprising the target ColumnRef.
 */
typedef enum
{
	RAW_PARSE_DEFAULT = 0,
	RAW_PARSE_TYPE_NAME,
	RAW_PARSE_PLPGSQL_EXPR,
	RAW_PARSE_PLPGSQL_ASSIGN1,
	RAW_PARSE_PLPGSQL_ASSIGN2,
	RAW_PARSE_PLPGSQL_ASSIGN3
} RawParseMode;

За парсинг отвечает LEX. Код парсинга лежит в src/backend/parser/scan.l

LEX работает в связке с YACC. Он описывает грамматику. Файл с грамматикой лежит в src/backend/parser/gram.y

Результатом работы парсера является список из RawStmt - сырых (as-is) деревьев парсинга:

pg_list - работа со списками

Помимо StringInfo, есть еще один инфраструктурный модуль - pg_list (src/include/nodes/pg_list.h). Он содержит тип динамического массива и функции для работы с ним.

Сам динамический массив определен структурой List. Для хранения данных в элементах используется объединение ListCell

typedef union ListCell
{
	void	   *ptr_value;
	int			int_value;
	Oid			oid_value;
} ListCell;

typedef struct List
{
	NodeTag		type;			/* T_List, T_IntList, or T_OidList */
	int			length;			/* number of elements currently present */
	int			max_length;		/* allocated length of elements[] */
	ListCell   *elements;		/* re-allocatable array of cells */
	/* We may allocate some cells along with the List header: */
	ListCell	initial_elements[FLEXIBLE_ARRAY_MEMBER];
	/* If elements == initial_elements, it's not a separate allocation */
} List;

Чтобы определить, какого типа хранятся данные в списке используются тэги T_List, T_IntList и T_OidList, хранящиеся в поле type:

typedef enum NodeTag
{
// ...
    /*
	 * TAGS FOR LIST NODES (pg_list.h)
	 */
	T_List,
	T_IntList,
	T_OidList    
// ...
}

Через макрос foreach и другие вспомогательные методы реализуется паттерн Итератор:

#define foreach(cell, lst)	\
	for (ForEachState cell##__state = {(lst), 0}; \
		 (cell##__state.l != NIL && \
		  cell##__state.i < cell##__state.l->length) ? \
		 (cell = &cell##__state.l->elements[cell##__state.i], true) : \
		 (cell = NULL, false); \
		 cell##__state.i++)

Для управления списками определены следующие функции:

  • forboth, forthree, forfour, forfive - итерация по 2, 3, 4 или 5 спискам одновременно;

  • lfirst- получить значение из текущего элемента внутри foreach;

  • linitial, lsecond, lthird, lfourth, llast - получить первый, второй, третий, четвертый, последний элемент из списка;

  • lappend - создать новый список, в который нужно включить переданный элемент.

Для оптимизации, пустой список представляется через NULL:

/*
 * The *only* valid representation of an empty list is NIL; in other
 * words, a non-NIL list is guaranteed to have length >= 1.
 */
#define NIL	((List *) NULL)

Изначально, часть кода для работы со списками была написана на Lisp. Поэтому названия некоторых методов имеют Lisp-like названия. Например, lcons (аналог cons), добавляет элемент в голову списка.

Сегодня весь код на LISP переписан на C.

Сырое дерево парсинга представляется структурой RawStmt. 

/*
 *		RawStmt --- container for any one statement's raw parse tree
 *
 * Parse analysis converts a raw parse tree headed by a RawStmt node into
 * an analyzed statement headed by a Query node.  For optimizable statements,
 * the conversion is complex.  For utility statements, the parser usually just
 * transfers the raw parse tree (sans RawStmt) into the utilityStmt field of
 * the Query node, and all the useful work happens at execution time.
 *
 * stmt_location/stmt_len identify the portion of the source text string
 * containing this raw statement (useful for multi-statement strings).
 */
typedef struct RawStmt
{
	NodeTag		type;
	Node	   *stmt;			/* raw parse tree */
	int			stmt_location;	/* start location, or -1 if unknown */
	int			stmt_len;		/* length in bytes; 0 means "rest of string" */
} RawStmt;

Корень этого дерева хранится в поле stmt и представляется структурой Node

/*
 * The first field of a node of any type is guaranteed to be the NodeTag.
 * Hence the type of any node can be gotten by casting it to Node. Declaring
 * a variable to be of Node * (instead of void *) can also facilitate
 * debugging.
 */
typedef struct Node
{
	NodeTag		type;
} Node;
"Базовое" наследование

Для Node можно заметить “наследование”.

Первым полем в структуре RawStmt идет поле type типа NodeTag

NodeTag
/*
 * The first field of every node is NodeTag. Each node created (with makeNode)
 * will have one of the following tags as the value of its first field.
 *
 * Note that inserting or deleting node types changes the numbers of other
 * node types later in the list.  This is no problem during development, since
 * the node numbers are never stored on disk.  But don't do it in a released
 * branch, because that would represent an ABI break for extensions.
 */
typedef enum NodeTag
{
	T_Invalid = 0,
/*
 * TAGS FOR EXECUTOR NODES (execnodes.h)
 */
T_IndexInfo,
T_ExprContext,
T_ProjectionInfo,
T_JunkFilter,
T_OnConflictSetState,
T_MergeActionState,
T_ResultRelInfo,
T_EState,
T_TupleTableSlot,

/*
 * TAGS FOR PLAN NODES (plannodes.h)
 */
T_Plan,
T_Result,
T_ProjectSet,
T_ModifyTable,
T_Append,
T_MergeAppend,
T_RecursiveUnion,
T_BitmapAnd,
T_BitmapOr,
T_Scan,
T_SeqScan,
T_SampleScan,
T_IndexScan,
T_IndexOnlyScan,
T_BitmapIndexScan,
T_BitmapHeapScan,
T_TidScan,
T_TidRangeScan,
T_SubqueryScan,
T_FunctionScan,
T_ValuesScan,
T_TableFuncScan,
T_CteScan,
T_NamedTuplestoreScan,
T_WorkTableScan,
T_ForeignScan,
T_CustomScan,
T_Join,
T_NestLoop,
T_MergeJoin,
T_HashJoin,
T_Material,
T_Memoize,
T_Sort,
T_IncrementalSort,
T_Group,
T_Agg,
T_WindowAgg,
T_Unique,
T_Gather,
T_GatherMerge,
T_Hash,
T_SetOp,
T_LockRows,
T_Limit,
/* these aren't subclasses of Plan: */
T_NestLoopParam,
T_PlanRowMark,
T_PartitionPruneInfo,
T_PartitionedRelPruneInfo,
T_PartitionPruneStepOp,
T_PartitionPruneStepCombine,
T_PlanInvalItem,

/*
 * TAGS FOR PLAN STATE NODES (execnodes.h)
 *
 * These should correspond one-to-one with Plan node types.
 */
T_PlanState,
T_ResultState,
T_ProjectSetState,
T_ModifyTableState,
T_AppendState,
T_MergeAppendState,
T_RecursiveUnionState,
T_BitmapAndState,
T_BitmapOrState,
T_ScanState,
T_SeqScanState,
T_SampleScanState,
T_IndexScanState,
T_IndexOnlyScanState,
T_BitmapIndexScanState,
T_BitmapHeapScanState,
T_TidScanState,
T_TidRangeScanState,
T_SubqueryScanState,
T_FunctionScanState,
T_TableFuncScanState,
T_ValuesScanState,
T_CteScanState,
T_NamedTuplestoreScanState,
T_WorkTableScanState,
T_ForeignScanState,
T_CustomScanState,
T_JoinState,
T_NestLoopState,
T_MergeJoinState,
T_HashJoinState,
T_MaterialState,
T_MemoizeState,
T_SortState,
T_IncrementalSortState,
T_GroupState,
T_AggState,
T_WindowAggState,
T_UniqueState,
T_GatherState,
T_GatherMergeState,
T_HashState,
T_SetOpState,
T_LockRowsState,
T_LimitState,

/*
 * TAGS FOR PRIMITIVE NODES (primnodes.h)
 */
T_Alias,
T_RangeVar,
T_TableFunc,
T_Var,
T_Const,
T_Param,
T_Aggref,
T_GroupingFunc,
T_WindowFunc,
T_SubscriptingRef,
T_FuncExpr,
T_NamedArgExpr,
T_OpExpr,
T_DistinctExpr,
T_NullIfExpr,
T_ScalarArrayOpExpr,
T_BoolExpr,
T_SubLink,
T_SubPlan,
T_AlternativeSubPlan,
T_FieldSelect,
T_FieldStore,
T_RelabelType,
T_CoerceViaIO,
T_ArrayCoerceExpr,
T_ConvertRowtypeExpr,
T_CollateExpr,
T_CaseExpr,
T_CaseWhen,
T_CaseTestExpr,
T_ArrayExpr,
T_RowExpr,
T_RowCompareExpr,
T_CoalesceExpr,
T_MinMaxExpr,
T_SQLValueFunction,
T_XmlExpr,
T_NullTest,
T_BooleanTest,
T_CoerceToDomain,
T_CoerceToDomainValue,
T_SetToDefault,
T_CurrentOfExpr,
T_NextValueExpr,
T_InferenceElem,
T_TargetEntry,
T_RangeTblRef,
T_JoinExpr,
T_FromExpr,
T_OnConflictExpr,
T_IntoClause,

/*
 * TAGS FOR EXPRESSION STATE NODES (execnodes.h)
 *
 * ExprState represents the evaluation state for a whole expression tree.
 * Most Expr-based plan nodes do not have a corresponding expression state
 * node, they're fully handled within execExpr* - but sometimes the state
 * needs to be shared with other parts of the executor, as for example
 * with SubPlanState, which nodeSubplan.c has to modify.
 */
T_ExprState,
T_WindowFuncExprState,
T_SetExprState,
T_SubPlanState,
T_DomainConstraintState,

/*
 * TAGS FOR PLANNER NODES (pathnodes.h)
 */
T_PlannerInfo,
T_PlannerGlobal,
T_RelOptInfo,
T_IndexOptInfo,
T_ForeignKeyOptInfo,
T_ParamPathInfo,
T_Path,
T_IndexPath,
T_BitmapHeapPath,
T_BitmapAndPath,
T_BitmapOrPath,
T_TidPath,
T_TidRangePath,
T_SubqueryScanPath,
T_ForeignPath,
T_CustomPath,
T_NestPath,
T_MergePath,
T_HashPath,
T_AppendPath,
T_MergeAppendPath,
T_GroupResultPath,
T_MaterialPath,
T_MemoizePath,
T_UniquePath,
T_GatherPath,
T_GatherMergePath,
T_ProjectionPath,
T_ProjectSetPath,
T_SortPath,
T_IncrementalSortPath,
T_GroupPath,
T_UpperUniquePath,
T_AggPath,
T_GroupingSetsPath,
T_MinMaxAggPath,
T_WindowAggPath,
T_SetOpPath,
T_RecursiveUnionPath,
T_LockRowsPath,
T_ModifyTablePath,
T_LimitPath,
/* these aren't subclasses of Path: */
T_EquivalenceClass,
T_EquivalenceMember,
T_PathKey,
T_PathKeyInfo,
T_PathTarget,
T_RestrictInfo,
T_IndexClause,
T_PlaceHolderVar,
T_SpecialJoinInfo,
T_AppendRelInfo,
T_RowIdentityVarInfo,
T_PlaceHolderInfo,
T_MinMaxAggInfo,
T_PlannerParamItem,
T_RollupData,
T_GroupingSetData,
T_StatisticExtInfo,
T_MergeAction,

/*
 * TAGS FOR MEMORY NODES (memnodes.h)
 */
T_AllocSetContext,
T_SlabContext,
T_GenerationContext,

/*
 * TAGS FOR VALUE NODES (value.h)
 */
T_Integer,
T_Float,
T_Boolean,
T_String,
T_BitString,

/*
 * TAGS FOR LIST NODES (pg_list.h)
 */
T_List,
T_IntList,
T_OidList,

/*
 * TAGS FOR EXTENSIBLE NODES (extensible.h)
 */
T_ExtensibleNode,

/*
 * TAGS FOR STATEMENT NODES (mostly in parsenodes.h)
 */
T_RawStmt,
T_Query,
T_PlannedStmt,
T_InsertStmt,
T_DeleteStmt,
T_UpdateStmt,
T_MergeStmt,
T_SelectStmt,
T_ReturnStmt,
T_PLAssignStmt,
T_AlterTableStmt,
T_AlterTableCmd,
T_AlterDomainStmt,
T_SetOperationStmt,
T_GrantStmt,
T_GrantRoleStmt,
T_AlterDefaultPrivilegesStmt,
T_ClosePortalStmt,
T_ClusterStmt,
T_CopyStmt,
T_CreateStmt,
T_DefineStmt,
T_DropStmt,
T_TruncateStmt,
T_CommentStmt,
T_FetchStmt,
T_IndexStmt,
T_CreateFunctionStmt,
T_AlterFunctionStmt,
T_DoStmt,
T_RenameStmt,
T_RuleStmt,
T_NotifyStmt,
T_ListenStmt,
T_UnlistenStmt,
T_TransactionStmt,
T_ViewStmt,
T_LoadStmt,
T_CreateDomainStmt,
T_CreatedbStmt,
T_DropdbStmt,
T_VacuumStmt,
T_ExplainStmt,
T_CreateTableAsStmt,
T_CreateSeqStmt,
T_AlterSeqStmt,
T_VariableSetStmt,
T_VariableShowStmt,
T_DiscardStmt,
T_CreateTrigStmt,
T_CreatePLangStmt,
T_CreateRoleStmt,
T_AlterRoleStmt,
T_DropRoleStmt,
T_LockStmt,
T_ConstraintsSetStmt,
T_ReindexStmt,
T_CheckPointStmt,
T_CreateSchemaStmt,
T_AlterDatabaseStmt,
T_AlterDatabaseRefreshCollStmt,
T_AlterDatabaseSetStmt,
T_AlterRoleSetStmt,
T_CreateConversionStmt,
T_CreateCastStmt,
T_CreateOpClassStmt,
T_CreateOpFamilyStmt,
T_AlterOpFamilyStmt,
T_PrepareStmt,
T_ExecuteStmt,
T_DeallocateStmt,
T_DeclareCursorStmt,
T_CreateTableSpaceStmt,
T_DropTableSpaceStmt,
T_AlterObjectDependsStmt,
T_AlterObjectSchemaStmt,
T_AlterOwnerStmt,
T_AlterOperatorStmt,
T_AlterTypeStmt,
T_DropOwnedStmt,
T_ReassignOwnedStmt,
T_CompositeTypeStmt,
T_CreateEnumStmt,
T_CreateRangeStmt,
T_AlterEnumStmt,
T_AlterTSDictionaryStmt,
T_AlterTSConfigurationStmt,
T_CreateFdwStmt,
T_AlterFdwStmt,
T_CreateForeignServerStmt,
T_AlterForeignServerStmt,
T_CreateUserMappingStmt,
T_AlterUserMappingStmt,
T_DropUserMappingStmt,
T_AlterTableSpaceOptionsStmt,
T_AlterTableMoveAllStmt,
T_SecLabelStmt,
T_CreateForeignTableStmt,
T_ImportForeignSchemaStmt,
T_CreateExtensionStmt,
T_AlterExtensionStmt,
T_AlterExtensionContentsStmt,
T_CreateEventTrigStmt,
T_AlterEventTrigStmt,
T_RefreshMatViewStmt,
T_ReplicaIdentityStmt,
T_AlterSystemStmt,
T_CreatePolicyStmt,
T_AlterPolicyStmt,
T_CreateTransformStmt,
T_CreateAmStmt,
T_CreatePublicationStmt,
T_AlterPublicationStmt,
T_CreateSubscriptionStmt,
T_AlterSubscriptionStmt,
T_DropSubscriptionStmt,
T_CreateStatsStmt,
T_AlterCollationStmt,
T_CallStmt,
T_AlterStatsStmt,

/*
 * TAGS FOR PARSE TREE NODES (parsenodes.h)
 */
T_A_Expr,
T_ColumnRef,
T_ParamRef,
T_A_Const,
T_FuncCall,
T_A_Star,
T_A_Indices,
T_A_Indirection,
T_A_ArrayExpr,
T_ResTarget,
T_MultiAssignRef,
T_TypeCast,
T_CollateClause,
T_SortBy,
T_WindowDef,
T_RangeSubselect,
T_RangeFunction,
T_RangeTableSample,
T_RangeTableFunc,
T_RangeTableFuncCol,
T_TypeName,
T_ColumnDef,
T_IndexElem,
T_StatsElem,
T_Constraint,
T_DefElem,
T_RangeTblEntry,
T_RangeTblFunction,
T_TableSampleClause,
T_WithCheckOption,
T_SortGroupClause,
T_GroupingSet,
T_WindowClause,
T_ObjectWithArgs,
T_AccessPriv,
T_CreateOpClassItem,
T_TableLikeClause,
T_FunctionParameter,
T_LockingClause,
T_RowMarkClause,
T_XmlSerialize,
T_WithClause,
T_InferClause,
T_OnConflictClause,
T_CTESearchClause,
T_CTECycleClause,
T_CommonTableExpr,
T_MergeWhenClause,
T_RoleSpec,
T_TriggerTransition,
T_PartitionElem,
T_PartitionSpec,
T_PartitionBoundSpec,
T_PartitionRangeDatum,
T_PartitionCmd,
T_VacuumRelation,
T_PublicationObjSpec,
T_PublicationTable,

/*
 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
 */
T_IdentifySystemCmd,
T_BaseBackupCmd,
T_CreateReplicationSlotCmd,
T_DropReplicationSlotCmd,
T_ReadReplicationSlotCmd,
T_StartReplicationCmd,
T_TimeLineHistoryCmd,

/*
 * TAGS FOR RANDOM OTHER STUFF
 *
 * These are objects that aren't part of parse/plan/execute node tree
 * structures, but we give them NodeTags anyway for identification
 * purposes (usually because they are involved in APIs where we want to
 * pass multiple object types through the same pointer).
 */
T_TriggerData,				/* in commands/trigger.h */
T_EventTriggerData,			/* in commands/event_trigger.h */
T_ReturnSetInfo,			/* in nodes/execnodes.h */
T_WindowObjectData,			/* private in nodeWindowAgg.c */
T_TIDBitmap,				/* in nodes/tidbitmap.h */
T_InlineCodeBlock,			/* in nodes/parsenodes.h */
T_FdwRoutine,				/* in foreign/fdwapi.h */
T_IndexAmRoutine,			/* in access/amapi.h */
T_TableAmRoutine,			/* in access/tableam.h */
T_TsmRoutine,				/* in access/tsmapi.h */
T_ForeignKeyCacheInfo,		/* in utils/rel.h */
T_CallContext,				/* in nodes/parsenodes.h */
T_SupportRequestSimplify,	/* in nodes/supportnodes.h */
T_SupportRequestSelectivity,	/* in nodes/supportnodes.h */
T_SupportRequestCost,		/* in nodes/supportnodes.h */
T_SupportRequestRows,		/* in nodes/supportnodes.h */
T_SupportRequestIndexCondition, /* in nodes/supportnodes.h */
T_SupportRequestWFuncMonotonic	/* in nodes/supportnodes.h */

} NodeTag;

И так, для использования "наследования":

  1. Получаем объект Node

  2. Определяем тип, используя значение из поля type

  3. Кастуем полученный объект к указанному

  4. Используем по назначению

Например, в случае RawStmt мы

  1. Получаем указатель на Node

  2. Читаем значение stmt

  3. Определяем его значение T_RawStmt

  4. Кастуем указатель к RawStmt

// src/backend/tcop/utility.c
LogStmtLevel
GetCommandLogLevel(Node *parsetree)
{
LogStmtLevel lev;
switch (nodeTag(parsetree))
{
		/* recurse if we're given a RawStmt */
	case T_RawStmt:
		lev = GetCommandLogLevel(((RawStmt *) parsetree)-&gt;stmt);
		break;
    // ...
}

}

Итерация по полученным выражениям

Настройка транзакции

В начале выполнения каждого выражения настраивается транзакция.

В процессе выполнения может случится ошибка в транзакции. Этот случай обрабатывается первым.

Делается проверка, что если в текущей транзакции произошла ошибка то, текущая команда — это ROLLBACK. Если нет, то выполнение обрывается.

В противном случае, начинается новая транзакция — вызывается start_xact_command.

Создание снапшота

Перед исполнением выражения необходимо сделать снапшот. Снапшот представляет собой запись, описывающую какие записи для нас валидны, а какие нет.

Это необходимо, когда:

  • Некоторые выражения имеют последствия. Например, DELETE удаляет запись.

  • В процессе выполнения данные могут измениться. Например, долгий SELECT может затронуть как старые, так и новые записи.

  • Или все вместе. Например, SELECT FOR UPDATE.

Для управления конкурентным доступом к данным Postgres использует MVCC. В частности, для исполняемой команды нужен слепок. Этот слепок представляет структура Snapshot:

typedef struct SnapshotData *Snapshot;

#define InvalidSnapshot		((Snapshot) NULL)

/*
 * Struct representing all kind of possible snapshots.
 *
 * There are several different kinds of snapshots:
 * * Normal MVCC snapshots
 * * MVCC snapshots taken during recovery (in Hot-Standby mode)
 * * Historic MVCC snapshots used during logical decoding
 * * snapshots passed to HeapTupleSatisfiesDirty()
 * * snapshots passed to HeapTupleSatisfiesNonVacuumable()
 * * snapshots used for SatisfiesAny, Toast, Self where no members are
 *	 accessed.
 */
typedef struct SnapshotData
{
	SnapshotType snapshot_type; /* type of snapshot */

	TransactionId xmin;			/* all XID < xmin are visible to me */
	TransactionId xmax;			/* all XID >= xmax are invisible to me */

	TransactionId *xip;
	uint32		xcnt;			/* # of xact ids in xip[] */

	TransactionId *subxip;
	int32		subxcnt;		/* # of xact ids in subxip[] */
	bool		suboverflowed;	/* has the subxip array overflowed? */

	bool		takenDuringRecovery;	/* recovery-shaped snapshot? */
	bool		copied;			/* false if it's a static snapshot */

	CommandId	curcid;			/* in my xact, CID < curcid are visible */

	uint32		speculativeToken;

	struct GlobalVisState *vistest;

	uint32		active_count;	/* refcount on ActiveSnapshot stack */
	uint32		regd_count;		/* refcount on RegisteredSnapshots */
	pairingheap_node ph_node;	/* link in the RegisteredSnapshots heap */

	TimestampTz whenTaken;		/* timestamp when snapshot was taken */
	XLogRecPtr	lsn;			/* position in the WAL stream when taken */

	uint64		snapXactCompletionCount;
} SnapshotData;

Получение снапшота для текущего момента осуществляется функцией GetTransactionSnapshot(). Она либо создает новый, либо возвращает существующий снапшот.

В процессе работы может быть несколько вложенных транзакций, и для хранения всех снапшотов используется стек. Вершина стека хранится в переменной ActiveSnapshot. Для стека снапшотов определена отдельная структура:

/*
 * Elements of the active snapshot stack.
 *
 * Each element here accounts for exactly one active_count on SnapshotData.
 *
 * NB: the code assumes that elements in this list are in non-increasing
 * order of as_level; also, the list must be NULL-terminated.
 */
typedef struct ActiveSnapshotElt
{
	Snapshot	as_snap;
	int			as_level;
	struct ActiveSnapshotElt *as_next;
} ActiveSnapshotElt;

/* Top of the stack of active snapshots */
static ActiveSnapshotElt *ActiveSnapshot = NULL;

/* Bottom of the stack of active snapshots */
static ActiveSnapshotElt *OldestActiveSnapshot = NULL;

Настройка контекста памяти

После снапшота, настраивается контекст памяти. 

Если было передано несколько выражений, то для выполнения каждого выражения, создается отдельный контекст памяти - per_parsetree_context. Время его жизни ограничено одной итерацией. 

P.S. Для каждого кроме последнего. Для оптимизации используется MessageContext, который будет удален после выхода завершения обработки запроса.

Создание структуры запроса из токенов

На этом моменте начинается “настоящая” работа. Запрос разобран и получено синтаксическое дерево. Далее, необходимо создать объект, представляющий запрос. Именно с ним будет дальше происходить работа: оптимизация и переписывание. 

Объект запроса представляет структура Query:

typedef struct Query
{
	NodeTag		type;

	CmdType		commandType;	/* select|insert|update|delete|merge|utility */

	QuerySource querySource;	/* where did I come from? */

	uint64		queryId;		/* query identifier (can be set by plugins) */

	bool		canSetTag;		/* do I set the command result tag? */

	Node	   *utilityStmt;	/* non-null if commandType == CMD_UTILITY */

	int			resultRelation; /* rtable index of target relation for
								 * INSERT/UPDATE/DELETE/MERGE; 0 for SELECT */

	bool		hasAggs;		/* has aggregates in tlist or havingQual */
	bool		hasWindowFuncs; /* has window functions in tlist */
	bool		hasTargetSRFs;	/* has set-returning functions in tlist */
	bool		hasSubLinks;	/* has subquery SubLink */
	bool		hasDistinctOn;	/* distinctClause is from DISTINCT ON */
	bool		hasRecursive;	/* WITH RECURSIVE was specified */
	bool		hasModifyingCTE;	/* has INSERT/UPDATE/DELETE in WITH */
	bool		hasForUpdate;	/* FOR [KEY] UPDATE/SHARE was specified */
	bool		hasRowSecurity; /* rewriter has applied some RLS policy */

	bool		isReturn;		/* is a RETURN statement */

	List	   *cteList;		/* WITH list (of CommonTableExpr's) */

	List	   *rtable;			/* list of range table entries */
	FromExpr   *jointree;		/* table join tree (FROM and WHERE clauses);
								 * also USING clause for MERGE */

	List	   *mergeActionList;	/* list of actions for MERGE (only) */
	bool		mergeUseOuterJoin;	/* whether to use outer join */

	List	   *targetList;		/* target list (of TargetEntry) */

	OverridingKind override;	/* OVERRIDING clause */

	OnConflictExpr *onConflict; /* ON CONFLICT DO [NOTHING | UPDATE] */

	List	   *returningList;	/* return-values list (of TargetEntry) */

	List	   *groupClause;	/* a list of SortGroupClause's */
	bool		groupDistinct;	/* is the group by clause distinct? */

	List	   *groupingSets;	/* a list of GroupingSet's if present */

	Node	   *havingQual;		/* qualifications applied to groups */

	List	   *windowClause;	/* a list of WindowClause's */

	List	   *distinctClause; /* a list of SortGroupClause's */

	List	   *sortClause;		/* a list of SortGroupClause's */

	Node	   *limitOffset;	/* # of result tuples to skip (int8 expr) */
	Node	   *limitCount;		/* # of result tuples to return (int8 expr) */
	LimitOption limitOption;	/* limit type */

	List	   *rowMarks;		/* a list of RowMarkClause's */

	Node	   *setOperations;	/* set-operation tree if this is top level of
								 * a UNION/INTERSECT/EXCEPT query */

	List	   *constraintDeps; /* a list of pg_constraint OIDs that the query
								 * depends on to be semantically valid */

	List	   *withCheckOptions;	/* a list of WithCheckOption's (added
									 * during rewrite) */

	/*
	 * The following two fields identify the portion of the source text string
	 * containing this query.  They are typically only populated in top-level
	 * Queries, not in sub-queries.  When not set, they might both be zero, or
	 * both be -1 meaning "unknown".
	 */
	int			stmt_location;	/* start location, or -1 if unknown */
	int			stmt_len;		/* length in bytes; 0 means "rest of string" */
} Query;

Парсинг реализуется модулем analyze (src/backend/parser/analyze.c).

За парсинг и превращение дерева запроса в Query отвечает функция transformTopLevelStmt (src/backend/parser/analyze.c). На вход она принимает:

  • RawStmt — дерево парсинга. Получили на предыдущих шагах.

  • ParseState — состояние парсинга. Структура, представляющая информацию по текущему обрабатываемому выражению.

struct ParseState
{
	ParseState *parentParseState;	/* stack link */
	const char *p_sourcetext;	/* source text, or NULL if not available */
	List	   *p_rtable;		/* range table so far */
	List	   *p_joinexprs;	/* JoinExprs for RTE_JOIN p_rtable entries */
	List	   *p_joinlist;		/* join items so far (will become FromExpr
								 * node's fromlist) */
	List	   *p_namespace;	/* currently-referenceable RTEs (List of
								 * ParseNamespaceItem) */
	bool		p_lateral_active;	/* p_lateral_only items visible? */
	List	   *p_ctenamespace; /* current namespace for common table exprs */
	List	   *p_future_ctes;	/* common table exprs not yet in namespace */
	CommonTableExpr *p_parent_cte;	/* this query's containing CTE */
	Relation	p_target_relation;	/* INSERT/UPDATE/DELETE/MERGE target rel */
	ParseNamespaceItem *p_target_nsitem;	/* target rel's NSItem, or NULL */
	bool		p_is_insert;	/* process assignment like INSERT not UPDATE */
	List	   *p_windowdefs;	/* raw representations of window clauses */
	ParseExprKind p_expr_kind;	/* what kind of expression we're parsing */
	int			p_next_resno;	/* next targetlist resno to assign */
	List	   *p_multiassign_exprs;	/* junk tlist entries for multiassign */
	List	   *p_locking_clause;	/* raw FOR UPDATE/FOR SHARE info */
	bool		p_locked_from_parent;	/* parent has marked this subquery
										 * with FOR UPDATE/FOR SHARE */
	bool		p_resolve_unknowns; /* resolve unknown-type SELECT outputs as
									 * type text */

	QueryEnvironment *p_queryEnv;	/* curr env, incl refs to enclosing env */

	/* Flags telling about things found in the query: */
	bool		p_hasAggs;
	bool		p_hasWindowFuncs;
	bool		p_hasTargetSRFs;
	bool		p_hasSubLinks;
	bool		p_hasModifyingCTE;

	Node	   *p_last_srf;		/* most recent set-returning func/op found */

	/*
	 * Optional hook functions for parser callbacks.  These are null unless
	 * set up by the caller of make_parsestate.
	 */
	PreParseColumnRefHook p_pre_columnref_hook;
	PostParseColumnRefHook p_post_columnref_hook;
	ParseParamRefHook p_paramref_hook;
	CoerceParamHook p_coerce_param_hook;
	void	   *p_ref_hook_state;	/* common passthrough link for above */
};

Входная точка для превращения RawStmt в Query - функция transformStmt

/*
 * transformStmt -
 *	  recursively transform a Parse tree into a Query tree.
 */
Query*
transformStmt(ParseState *pstate, Node *parseTree);

Для парсинга выражений определено семейство transform* функций. Парсинг происходит путем определения типа выражения (проверка поля type структуры Node) и применения к нему необходимой функции обработки. 

Также передается (проталкивается) объект ParseState. По мере обработки выражения, ParseState заполняется данными, которые могут быть использованы на внутренних узлах парсинга.

Это и делает функция transformStmt. Внутри нее расположен switch/case. Он поочередно проверяет тэг пришедшего выражения. Если выражение допускает оптимизацию, то для него вызывается соответствующий обработчик. 

Например, в случае нашего выражения поле type будет T_SelectStmt, и обработчик - transformSelectStmt

Query *
transformStmt(ParseState *pstate, Node *parseTree)
{
	Query	   *result;

	/*
	 * We apply RAW_EXPRESSION_COVERAGE_TEST testing to basic DML statements;
	 * we can't just run it on everything because raw_expression_tree_walker()
	 * doesn't claim to handle utility statements.
	 */
    switch (nodeTag(parseTree))
	{
          // ...
          case T_SelectStmt:
			{
				SelectStmt *n = (SelectStmt *) parseTree;

				if (n->valuesLists)
					result = transformValuesClause(pstate, n);
				else if (n->op == SETOP_NONE)
					result = transformSelectStmt(pstate, n);
				else
					result = transformSetOperationStmt(pstate, n);
			}
			break;
        // ...
    }
    return result;
}

Для нашего запроса (SELECT * FROM users) мы переходим в функцию transformSelectStmt

Логика этой функции состоит из последовательных шагов обработки различных частей запроса: WITH, FROM, WHERE, HAVING и т.д. Так как в запросе нет никаких фильтрующих/агрегирующих/усложняющих жизнь парсинг элементов, то работа будет заключаться в следующем:

  1. Обработка FROM.

Для обработки FROM вызывается функция transformFromClause. Так как во FROM может быть несколько источников (таблица, функция, другой SELECT), то для каждого определена своя функция. 

Пропуская все косвенные вызовы, конечная функция обработчик - transformTableEntry.

Для представления таблицы, как источника, используется структура RangeTblEntry.

typedef struct RangeTblEntry
{
	NodeTag		type;
	RTEKind		rtekind;
	Oid			relid;			/* OID of the relation */
	char		relkind;		/* relation kind (see pg_class.relkind) */
	int			rellockmode;	/* lock level that query requires on the rel */
	struct TableSampleClause *tablesample;	/* sampling info, or NULL */
	
    Query	   *subquery;		/* the sub-query */
	bool		security_barrier;	/* is from security_barrier view? */

	JoinType	jointype;		/* type of join */
	int			joinmergedcols; /* number of merged (JOIN USING) columns */
	List	   *joinaliasvars;	/* list of alias-var expansions */
	List	   *joinleftcols;	/* left-side input column numbers */
	List	   *joinrightcols;	/* right-side input column numbers */

	Alias	   *join_using_alias;

	List	   *functions;		/* list of RangeTblFunction nodes */
	bool		funcordinality; /* is this called WITH ORDINALITY? */

	TableFunc  *tablefunc;

	List	   *values_lists;	/* list of expression lists */

	char	   *ctename;		/* name of the WITH list item */
	Index		ctelevelsup;	/* number of query levels up */
	bool		self_reference; /* is this a recursive self-reference? */

	List	   *coltypes;		/* OID list of column type OIDs */
	List	   *coltypmods;		/* integer list of column typmods */
	List	   *colcollations;	/* OID list of column collation OIDs */

	char	   *enrname;		/* name of ephemeral named relation */
	Cardinality enrtuples;		/* estimated or actual from caller */

	Alias	   *alias;			/* user-written alias clause, if any */
	Alias	   *eref;			/* expanded reference names */
	bool		lateral;		/* subquery, function, or values is LATERAL? */
	bool		inh;			/* inheritance requested? */
	bool		inFromCl;		/* present in FROM clause? */
	AclMode		requiredPerms;	/* bitmask of required access permissions */
	Oid			checkAsUser;	/* if valid, check access as this role */
	Bitmapset  *selectedCols;	/* columns needing SELECT permission */
	Bitmapset  *insertedCols;	/* columns needing INSERT permission */
	Bitmapset  *updatedCols;	/* columns needing UPDATE permission */
	Bitmapset  *extraUpdatedCols;	/* generated columns being updated */
	List	   *securityQuals;	/* security barrier quals to apply, if any */
} RangeTblEntry;
  1. Обработка правил переписывания SELECT запросов.

Далее идет переписывание SELECT выражений. Для обработки всех правил в запросе определена функция fireRIRrules.

/*
 * fireRIRrules -
 *	Apply all RIR rules on each rangetable entry in the given query
 *
 * activeRIRs is a list of the OIDs of views we're already processing RIR
 * rules for, used to detect/reject recursion.
 */
static Query *
fireRIRrules(Query *parsetree, List *activeRIRs)

Она выявляет все правила, которые необходимо применить, и для каждого вызывает ApplyRetrieveRule.

/*
 * ApplyRetrieveRule - expand an ON SELECT rule
 */
static Query *
ApplyRetrieveRule(Query *parsetree,
				  RewriteRule *rule,
				  int rt_index,
				  Relation relation,
				  List *activeRIRs)

При необходимости из ApplyRetrieveRule рекурсивно вызывается fireRIRrules, на случай ссылки на другое представление.

RIR - Retrieve-Instead-Retrieve

Первым языком запросов в Postgres был PostQUEL (https://ru.wikipedia.org/wiki/POSTQUEL). Синтаксис PostgreSQL пришел позднее. Но с тех пор осталось много легаси как, например, аббревиатура RIR. 

RIR (Retrieve Instead Retrieve) означает правило переписывания при обработке запроса выборки. Например, 2 идентичных запроса.

retrieve (STAFF.pay) from STAFF where STAFF.name = "Ivan"
select STAFF.pay from STAFF where STAFF.name = 'Ivan'

Как можно догадаться, ключевое слово retrieve переименовалось в select.

Сегодня, представления (VIEW) реализованы через правила переписывания. В файле rewriteHandler.cприсутствует функция fireRIRrules (src/backend/rewrite/rewriteHandler.c). Ее основная задача применить правила переписывания SELECT в SELECT (в частном случае VIEW) для всего запроса.

Система переписывания описана в документации https://www.postgresql.org/docs/6.4/rules13914.htm 

  1. Определение тэга команды.

Тэг команды (CommandTag) — это строка результата исполнившегося выражения, которая возвращается после выполнения выражения. Например, для INSERT это «INSERT 0 1» при вставке единственной записи.

Он посылается вместе с сообщением CommandComplete https://www.postgresql.org/docs/current/protocol‑message‑formats.html#PROTOCOL‑MESSAGE‑FORMATS‑COMMANDCOMPLETE

Создание планов запросов

Следующий этап — создание плана запроса.

За создание планов запросов отвечает модуль planner (src/backend/optimizer/plan/planner.c).

Входная точка для планирования запроса — функция planner.

PlannedStmt *
planner(Query *parse, const char *query_string, int cursorOptions,
		ParamListInfo boundParams)

По факту, эта функция входная точка, которая в зависимости от существования хука (кастомного планировщика) вызывает либо его, либо стандартный планировщик (standard_planner). Дальше предполагается использование стандартного планировщика.

По аналогии с проталкиванием контекста (ParseState) при создании объекта Query, здесь присутствует такой же контекст - PlannerGlobal.

/*----------
 * PlannerGlobal
 *		Global information for planning/optimization
 *
 * PlannerGlobal holds state for an entire planner invocation; this state
 * is shared across all levels of sub-Queries that exist in the command being
 * planned.
 *----------
 */
typedef struct PlannerGlobal
{
	NodeTag		type;
	ParamListInfo boundParams;	/* Param values provided to planner() */
	List	   *subplans;		/* Plans for SubPlan nodes */
	List	   *subroots;		/* PlannerInfos for SubPlan nodes */
	Bitmapset  *rewindPlanIDs;	/* indices of subplans that require REWIND */
	List	   *finalrtable;	/* "flat" rangetable for executor */
	List	   *finalrowmarks;	/* "flat" list of PlanRowMarks */
	List	   *resultRelations;	/* "flat" list of integer RT indexes */
	List	   *appendRelations;	/* "flat" list of AppendRelInfos */
	List	   *relationOids;	/* OIDs of relations the plan depends on */
	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
	List	   *paramExecTypes; /* type OIDs for PARAM_EXEC Params */
	Index		lastPHId;		/* highest PlaceHolderVar ID assigned */
	Index		lastRowMarkId;	/* highest PlanRowMark ID assigned */
	int			lastPlanNodeId; /* highest plan node ID assigned */
	bool		transientPlan;	/* redo plan when TransactionXmin changes? */
	bool		dependsOnRole;	/* is plan specific to current role? */
	bool		parallelModeOK; /* parallel mode potentially OK? */
	bool		parallelModeNeeded; /* parallel mode actually required? */
	char		maxParallelHazard;	/* worst PROPARALLEL hazard level */
	PartitionDirectory partition_directory; /* partition descriptors */
} PlannerGlobal;

Перед началом планирования применяется ряд оптимизаций. Каждая представляется отдельной функцией. Например, pull_up_sublinks – превращает ANY или EXISTS в JOIN.

Когда с оптимизациями закончено, вызывается grouping_planner. Именно он создает план выполнения. 

Начальный этап планирования различается в зависимости от типа выражения. Если выражение определено как операция над множеством (UNION/INTERSECT/EXCEPT), то запускается другой планировщик.

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

/*
 * If we have grouping and/or aggregation, consider ways to implement
 * that.  We build a new upperrel representing the output of this
 * phase.
 */
if (have_grouping)
{
    current_rel = create_grouping_paths(root,
                                        current_rel,
                                        grouping_target,
                                        grouping_target_parallel_safe,
                                        gset_data);
    /* Fix things up if grouping_target contains SRFs */
    if (parse->hasTargetSRFs)
        adjust_paths_for_srfs(root, current_rel,
                              grouping_targets,
                              grouping_targets_contain_srfs);
}

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

Если вернуть надо все строки (или просто неизвестно сколько будет возвращено), то результат — сохраненный в поле cheapest_total_path путь, иначе в массиве path_list находится самый дешевый.

Сравнение стоимости путей реализовано в функции compare_fractional_path_costs.

/*
 * compare_fractional_path_costs
 *	  Return -1, 0, or +1 according as path1 is cheaper, the same cost,
 *	  or more expensive than path2 for fetching the specified fraction
 *	  of the total tuples.
 *
 * If fraction is <= 0 or > 1, we interpret it as 1, ie, we select the
 * path with the cheaper total_cost.
 */
int
compare_fractional_path_costs(Path *path1, Path *path2,
							  double fraction)

Результатом работы планировщика является объект PlannedStmt

typedef struct PlannedStmt
{
	NodeTag		type;
	CmdType		commandType;	/* select|insert|update|delete|merge|utility */
	uint64		queryId;		/* query identifier (copied from Query) */
	bool		hasReturning;	/* is it insert|update|delete RETURNING? */
	bool		hasModifyingCTE;	/* has insert|update|delete in WITH? */
	bool		canSetTag;		/* do I set the command result tag? */
	bool		transientPlan;	/* redo plan when TransactionXmin changes? */
	bool		dependsOnRole;	/* is plan specific to current role? */
	bool		parallelModeNeeded; /* parallel mode required to execute? */
	int			jitFlags;		/* which forms of JIT should be performed */
	struct Plan *planTree;		/* tree of Plan nodes */
	List	   *rtable;			/* list of RangeTblEntry nodes */
  
	/* rtable indexes of target relations for INSERT/UPDATE/DELETE/MERGE */
	List	   *resultRelations;	/* integer list of RT indexes, or NIL */
	List	   *appendRelations;	/* list of AppendRelInfo nodes */
	List	   *subplans;		/* Plan trees for SubPlan expressions; note
								 * that some could be NULL */
	Bitmapset  *rewindPlanIDs;	/* indices of subplans that require REWIND */
	List	   *rowMarks;		/* a list of PlanRowMark's */
	List	   *relationOids;	/* OIDs of relations the plan depends on */
	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
	List	   *paramExecTypes; /* type OIDs for PARAM_EXEC Params */
	Node	   *utilityStmt;	/* non-null if this is utility stmt */
  
	/* statement location in source string (copied from Query) */
	int			stmt_location;	/* start location, or -1 if unknown */
	int			stmt_len;		/* length in bytes; 0 means "rest of string" */
} PlannedStmt;

Представим, что на вход подается наше выражение. Тогда выполнение будет следующим:

  1. Инициализация контекста запроса (PlannerInfo).

  2. Добавление в контекст информации о единственной таблице — build_simple_rel (src/backend/optimizer/util/relnode.c).

  3. Создание плана запроса — make_one_rel (src/backend/optimizer/path/allpaths.c).

  4. Создание пути выполнения.

  5. Выбор созданного пути как самого дешевого.

Удаление снапшота

Ранее, мы сохранили снапшот. После создания запроса в нем нет необходимости. Поэтому он удаляется с вершины стека.

Это выполняется из‑за конкурентного изменения данных различными пользователями в перерыве между планированием запроса и непосредственным выполнением выражений.

Эта проблема уже закреплена в рассылке и изначально касалась проблем с поведением LOCK при переходе от версии 9.1 к 9.2.

Описание проблемы из рассылки
Hi,

I've just noticed a change of LOCK command behavior between 9.1 and 9.2,
and I'm not sure whether this is expected or not.

Let's use a very simple table

  CREATE TABLE x (id INT);

Say there are two sessions - A and B, where A performs some operations
on "x" and needs to protect them with an "ACCESS EXCLUSIVE" lock (e.g.
it might be a pg_bulkload that acquires such locks, and we need to do
that explicitly on one or two places).

Session B is attempting to read the data, but is blocked and waits. On
9.1 it sees the commited data (which is what we need) but on 9.2 it sees
only data commited at the time of the lock attemt.

Example:

A: BEGIN;
A: LOCK x IN ACCESS EXCLUSIVE MODE;
A: INSERT INTO x VALUES (100);
B: SELECT * FROM x;
A: COMMIT;

Now on 9.1, B receives the value "100" while on 9.2 it gets no rows.
Is this expected? I suspect the snapshot is read at different time or
something, but I've checked release notes but I haven't seen anything
relevant.

Without getting the commited version of data, the locking is somehow
pointless for us (unless using a different lock, not the table itself).

regards
Tomas

Настройка портала

План у нас на руках, осталось только его выполнить. Для этого используется портал. В Simple Query все выражения безымянные. Для них создаются безымянные порталы.

/*
 * Create unnamed portal to run the query or queries in. If there
 * already is one, silently drop it.
 */
portal = CreatePortal("", true, true);

После создания и настройки идет подготовка к выполнению. Для этого имеется функция PortalStart. Выполнение портала зависит от его стратегии — PortalStrategy.

/*
 * We have several execution strategies for Portals, depending on what
 * query or queries are to be executed.  (Note: in all cases, a Portal
 * executes just a single source-SQL query, and thus produces just a
 * single result from the user's viewpoint.  However, the rule rewriter
 * may expand the single source query to zero or many actual queries.)
 */
typedef enum PortalStrategy
{
    /* single SELECT query */
  	PORTAL_ONE_SELECT,   
	/* single INSERT/UPDATE/DELETE query with RETURNING clause */
    PORTAL_ONE_RETURNING,
	/* single SELECT query, but has modifying CTEs */
    PORTAL_ONE_MOD_WITH,  
	/* contains UTILITY statement that returns a SELECT-like result (e.g. EXPLAIN, SHOW) */
    PORTAL_UTIL_SELECT,   
	/* all other cases */
    PORTAL_MULTI_QUERY   
} PortalStrategy;

Этап подготовки к выполнению зависит от PortalStrategy. В общем случае, все сводится к определению вида возвращаемых кортежей. Для описания этих метаданных существует структура TupleDesc.

typedef struct TupleDescData
{
	int			natts;			/* number of attributes in the tuple */
	Oid			tdtypeid;		/* composite type ID for tuple type */
	int32		tdtypmod;		/* typmod for tuple type */
	int			tdrefcount;		/* reference count, or -1 if not counting */
	TupleConstr *constr;		/* constraints, or NULL if none */
	/* attrs[N] is the description of Attribute Number N+1 */
	FormData_pg_attribute attrs[FLEXIBLE_ARRAY_MEMBER];
}			TupleDescData;
typedef struct TupleDescData *TupleDesc;

В случае единственного SELECT — стратегия PORTAL_ONE_SELECT.

Настройка портала в данном случае сводится к настройке исполнителя (Executor). Именно он отвечает за выполнение запроса.

Для этого создается QueryDesc (src/include/executor/execnodes.h)‑ объект описывающий запрос (TupleDesc в том числе).

typedef struct QueryDesc
{
	/* These fields are provided by CreateQueryDesc */
	CmdType		operation;		/* CMD_SELECT, CMD_UPDATE, etc. */
	PlannedStmt *plannedstmt;	/* planner's output (could be utility, too) */
	const char *sourceText;		/* source text of the query */
	Snapshot	snapshot;		/* snapshot to use for query */
	Snapshot	crosscheck_snapshot;	/* crosscheck for RI update/delete */
	DestReceiver *dest;			/* the destination for tuple output */
	ParamListInfo params;		/* param values being passed in */
	QueryEnvironment *queryEnv; /* query environment passed in */
	int			instrument_options; /* OR of InstrumentOption flags */

	/* These fields are set by ExecutorStart */
	TupleDesc	tupDesc;		/* descriptor for result tuples */
	EState	   *estate;			/* executor's query-wide state */
	PlanState  *planstate;		/* tree of per-plan-node state */

	/* This field is set by ExecutorRun */
	bool		already_executed;	/* true if previously executed */

	/* This is always set NULL by the core system, but plugins can change it */
	struct Instrumentation *totaltime;	/* total time spent in ExecutorRun */
} QueryDesc;

Само состояние Executor'а представляется структурой EState. Она хранится в поле estate QueryDesc (src/include/nodes/execnodes.h).

typedef struct EState
{
	NodeTag		type;

	ScanDirection es_direction; /* current scan direction */
	Snapshot	es_snapshot;	/* time qual to use */
	Snapshot	es_crosscheck_snapshot; /* crosscheck time qual for RI */
	List	   *es_range_table; /* List of RangeTblEntry */
	Index		es_range_table_size;	/* size of the range table arrays */
	Relation   *es_relations;	/* Array of per-range-table-entry Relation
								 * pointers, or NULL if not yet opened */
	struct ExecRowMark **es_rowmarks;	/* Array of per-range-table-entry
										 * ExecRowMarks, or NULL if none */
	PlannedStmt *es_plannedstmt;	/* link to top of plan tree */
	const char *es_sourceText;	/* Source text from QueryDesc */

	JunkFilter *es_junkFilter;	/* top-level junk filter, if any */

	/* If query can insert/delete tuples, the command ID to mark them with */
	CommandId	es_output_cid;

	/* Info about target table(s) for insert/update/delete queries: */
	ResultRelInfo **es_result_relations;	/* Array of per-range-table-entry
											 * ResultRelInfo pointers, or NULL
											 * if not a target table */
	List	   *es_opened_result_relations; /* List of non-NULL entries in
											 * es_result_relations in no
											 * specific order */

	PartitionDirectory es_partition_directory;	/* for PartitionDesc lookup */

	List	   *es_tuple_routing_result_relations;

	List	   *es_trig_target_relations;	/* trigger-only ResultRelInfos */

	ParamListInfo es_param_list_info;	/* values of external params */
	ParamExecData *es_param_exec_vals;	/* values of internal params */

	QueryEnvironment *es_queryEnv;	/* query environment */
	MemoryContext es_query_cxt; /* per-query context in which EState lives */
	List	   *es_tupleTable;	/* List of TupleTableSlots */
	uint64		es_processed;	/* # of tuples processed */
	int			es_top_eflags;	/* eflags passed to ExecutorStart */
	int			es_instrument;	/* OR of InstrumentOption flags */
	bool		es_finished;	/* true when ExecutorFinish is done */
	List	   *es_exprcontexts;	/* List of ExprContexts within EState */
	List	   *es_subplanstates;	/* List of PlanState for SubPlans */
	List	   *es_auxmodifytables; /* List of secondary ModifyTableStates */
	ExprContext *es_per_tuple_exprcontext;

	struct EPQState *es_epq_active;

	bool		es_use_parallel_mode;	/* can we use parallel workers? */

	struct dsa_area *es_query_dsa;

	int			es_jit_flags;
	struct JitContext *es_jit;
	struct JitInstrumentation *es_jit_worker_instr;

	List	   *es_insert_pending_result_relations;
	List	   *es_insert_pending_modifytables;

	List	   *es_resultrelinfo_extra;
} EState;

Настройка получателя

Следующий этап — определение получателя результата. Получатель описывается структурой DestReceiver (src/include/tcop/dest.h). По факту это интерфейс — в нем присутствуют только указатели на функции.

typedef struct _DestReceiver DestReceiver;

struct _DestReceiver
{
	/* Called for each tuple to be output: */
	bool		(*receiveSlot) (TupleTableSlot *slot,
								DestReceiver *self);
	/* Per-executor-run initialization and shutdown: */
	void		(*rStartup) (DestReceiver *self,
							 int operation,
							 TupleDesc typeinfo);
	void		(*rShutdown) (DestReceiver *self);
	/* Destroy the receiver object itself (if dynamically allocated) */
	void		(*rDestroy) (DestReceiver *self);
	/* CommandDest code for this receiver */
	CommandDest mydest;
	/* Private fields might appear beyond this point... */
};

В коде определены основные часто используемые варианты получателей:

  • DR_printtup для DestRemote — выслать ответ фронтэнду, используя libpq.

  • donothingDR для DestNone — ничего не делать, заглушка.

  • debugtupDR для DestDebug — отобразить результат на экране (интерактивный бэкэнд).

В случае DestRemote создается DR_printtup. После его создания должна быть произведена дополнительная настройка — сохранение указателя на портал. Это необходимо для форматирования результатов.

Выполнение портала

На этом этапе начинается самая главная часть — выполнение запроса.

Как мы помним выполняется не сам запрос, а портал. Входная точка выполнения портала — PortalRun (src/backend/tcop/pquery.c)

Внутри присутствует своя точка входа. Она зависит от стратегии. Для SELECT запроса вызывается PortalRunSelect. После проверки и валидации входных параметров (направление курсора, количество строк в ответе, стартовая позиция) выполнение делегируется исполнителю.

ExecutorRun — входная точка для исполнителя запроса.

void
ExecutorRun(QueryDesc *queryDesc,
			ScanDirection direction, uint64 count,
			bool execute_once)

Он, так же как и планировщик имеет стандартную точку входа (standard_ExecutorRun) и кастомную (ExecutorRun_hook)

Логика работы исполнителя заключена в бесконечном цикле. Он прерывается, когда обработано требуемое количество кортежей. Каждая итерация:

  1. Сброс контекста памяти кортежа (находится в поле es_per_tuple_exprcontext)

  2. Получение следующего кортежа — ExecProcNode

    1. Если вернулся NULL, значит кортежей не осталось — закончить выполнение.

  3. Применить «junk» фильтр.

  4. Отправить кортеж получателю.

  5. Если достигнут лимит обработанных кортежей — закончить выполнение.

junk фильтр

На ряду с пользовательскими данными в таблице хранятся и системные — ctid, xmin, xmax, cmin, cmax, tableoid; или это вспомогательные данные только для исполнителя. Такие «никому не нужные» данные называются «junk» атрибутами. (Junk — пустой). Следовательно junk фильтр применяется для отбрасывания таких данных.

Логика фильтрации находится в функции ExecFilterJunk (src/backend/executor/execJunk.c). 

Кстати, здесь находится один из наиболее старых TODO

/*-------------------------------------------------------------------------
 *		XXX this stuff should be rewritten to take advantage
 *			of ExecProject() and the ProjectionInfo node.
 *			-cim 6/3/91
 * 
 * ...
 *
 */

Для получения следующего кортежа вызывается функция, хранящаяся в поле ExecProcNode

// src/include/nodes/execnodes.h
typedef struct PlanState
{
// ...

	ExecProcNodeMtd ExecProcNode;	/* function to return next tuple */
	ExecProcNodeMtd ExecProcNodeReal;	/* actual function, if above is a
                                         * wrapper */
// ...
} PlanState;


// src/include/executor/executor.h
static inline TupleTableSlot *
ExecProcNode(PlanState *node)
{
	if (node->chgParam != NULL) /* something changed? */
		ExecReScan(node);		/* let ReScan handle this */

	return node->ExecProcNode(node);
}

Именно в ней хранится логика выполнения методов доступа. Реализации хранятся в исходных файлах папки src/backend/executor/node*.c.

Например, для оператора LIMIT создается узел LimitState (src/include/nodes/execnodes.h). Функция, реализующая его логику, - ExecLimit в src/backend/executor/nodeLimit.c

typedef struct LimitState
{
	PlanState	ps;				/* its first field is NodeTag */
	ExprState  *limitOffset;	/* OFFSET parameter, or NULL if none */
	ExprState  *limitCount;		/* COUNT parameter, or NULL if none */
	LimitOption limitOption;	/* limit specification type */
	int64		offset;			/* current OFFSET value */
	int64		count;			/* current COUNT, if any */
	bool		noCount;		/* if true, ignore count */
	LimitStateCond lstate;		/* state machine status, as above */
	int64		position;		/* 1-based index of last tuple returned */
	TupleTableSlot *subSlot;	/* tuple last obtained from subplan */
	ExprState  *eqfunction;		/* tuple equality qual in case of WITH TIES
								 * option */
	TupleTableSlot *last_slot;	/* slot for evaluation of ties */
} LimitState;

Вернемся к нашему запросу. Использоваться будет последовательное сканирование. За этот метод доступа отвечает ExecSeqScan. Его состояние определяется структурой SeqScanState.

typedef struct SeqScanState
{
	ScanState	ss;				/* its first field is NodeTag */
	Size		pscan_len;		/* size of parallel heap scan descriptor */
} SeqScanState;

Так как логика получения следующих элементов из последовательности повторяется (выборка из таблицы, CTE, индекса, материализованного представления; с применением функции), то реализация сделана общей. Подобные методы доступа, которые возвращают кортежи используют ExecScan (src/backend/executor/execScan.c).

TupleTableSlot *
ExecScan(ScanState *node,
		 ExecScanAccessMtd accessMtd,	/* function returning a tuple */
		 ExecScanRecheckMtd recheckMtd)

На вход она получает состояние запроса, функцию для получения следующего кортежа и функцию проверки кортежа на валидность. Для последовательного сканирования это SeqNext и SeqRecheck, соответственно (src/backend/executor/nodeSeqscan.c).

/* ----------------------------------------------------------------
 *		SeqNext
 *
 *		This is a workhorse for ExecSeqScan
 * ----------------------------------------------------------------
 */
static TupleTableSlot *
SeqNext(SeqScanState *node);

/*
 * SeqRecheck -- access method routine to recheck a tuple in EvalPlanQual
 */
static bool
SeqRecheck(SeqScanState *node, TupleTableSlot *slot)
{
	/*
	 * Note that unlike IndexScan, SeqScan never use keys in heap_beginscan
	 * (and this is very bad) - so, here we do not check are keys ok or not.
	 */
	return true;
}

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

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

  1. Вызывается ExecProcNode — входная точка для выполнения плана, хранящегося в запросе. Внутри, вызывается функция, хранящаяся в поле ExecProcNode. Это корень дерева плана. Там хранится ExecSeqScan.

  2. Внутри вызов делегируется общей функции — ExecScan. На вход она принимает состояние запроса, функцию метода доступа и функцию перепроверки — SeqNext, SeqRecheck.

  3. Вызывается функция, хранящаяся в поле scan_getnextslot, для получения следующего кортежа из таблицы. В ней хранится функция heap_getnextslot.

  4. Вызывается heapgettup — функция для получения следующего кортежа из памяти.

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

  6. Буфер с кортежем закрепляется, чтобы никто его не выгрузил.

  7. Вызывается «junk» фильтр для отсеивания ненужных столбцов.

  8. Кортеж отправляется получателю.

Реализация heapgettup и логика работы с диском

Основная логика получения записей из таблицы располагается в функции heapgettup (src/backend/access/heap/heapam.c)

В начале происходит поиск позиции старта чтения. Она зависит от направления: вперед или назад. 

Для ее определения берется последняя сохраненная (из предыдущего запроса) страница. Из нее рассчитываются:

  • Смещение следующего кортежа

  • Буфер, хранящий страницу

  • Количество оставшихся записей на странице

Работа с диском - это краеугольный камень всех баз данных. Причины:

  • Низкая скорость работы операций ввода/вывода - IO-bound задача

  • Буферизация ОС при работе с диском - нет гарантии записи на диск

  • Неуправляемый сброс страниц в память при нехватке последней - если виртуальной больше, чем физической

Решением этих проблем стали следующие принципы:

  • Минимизация операций ввода/вывода

  • Ручное управление загруженными в память страницами

При работе с данными используется “3-слойная архитектура”

  • Слой отображения тэга буфера на его Id

  • Слой отображения Id буфера на указатель на него в памяти

  • Слой буферов

Изображение с сайта https://www.interdb.jp/pg/pgsql08.html
Изображение с сайта https://www.interdb.jp/pg/pgsql08.html

Также при работе с данными используется несколько сущностей:

  • Страница - единица чтения с диска. По умолчанию, в Postgres равна 8 Кб. Представляется типом BlockNumber (src/include/storage/block.h)

typedef uint32 BlockNumber;

#define InvalidBlockNumber		((BlockNumber) 0xFFFFFFFF)

#define MaxBlockNumber			((BlockNumber) 0xFFFFFFFE)
  • Буфер - единица работы с данными. Каждый буфера имеет размер диской страницы. В отличие от последней имеет свою компоновку. Представляется структурой Page (src/include/storage/bufpage.h)

typedef Pointer Page;
  • Менеджер буферов - модуль, инкапсулирующий в себе всю логику работы с буферами - src/backend/storage/buffer/bufmgr.c

  • Тэг буфера - идентификатор требуемой страницы. Состоит из тройки  (адрес таблицы, тип форка, номер страницы). С помощью нее происходит доступ к страницам. Используется только менеджером буферов. Представляется структурой BufferTag (src/include/storage/buf_internals.h)

typedef struct buftag
{
	RelFileNode rnode;			/* physical relation identifier */
	ForkNumber	forkNum;
	BlockNumber blockNum;		/* blknum relative to begin of reln */
} BufferTag;

Работа с памятью ведется следующим образом:

  • Клиент (бэкэнд) запрашивает требуемый буфер. В запросе указывает какую страницу он хочет получить с помощью тэга буфера

  • Менеджер буферов ищет Id буфера по переданному тегу. Если не найден, то загружает с память требуемый буфер

  • Для найденного/созданного буфера создается Id

  • Id буфера возвращается клиенту

Дальше клиент работает с буфером только через переданный Id

Получение следующего кортежа выглядит следующим образом:

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

// Настройка при условии направления вперед

if (!scan->rs_inited)
{
    /*
     * return null immediately if relation is empty
     */
    if (scan->rs_nblocks == 0 || scan->rs_numblocks == 0)
    {
        Assert(!BufferIsValid(scan->rs_cbuf));
        tuple->t_data = NULL;
        return;
    }
    if (scan->rs_base.rs_parallel != NULL)
    {
        ParallelBlockTableScanDesc pbscan =
        (ParallelBlockTableScanDesc) scan->rs_base.rs_parallel;
        ParallelBlockTableScanWorker pbscanwork =
        scan->rs_parallelworkerdata;

        table_block_parallelscan_startblock_init(scan->rs_base.rs_rd,
                                                 pbscanwork, pbscan);

        page = table_block_parallelscan_nextpage(scan->rs_base.rs_rd,
                                                 pbscanwork, pbscan);

        /* Other processes might have already finished the scan. */
        if (page == InvalidBlockNumber)
        {
            Assert(!BufferIsValid(scan->rs_cbuf));
            tuple->t_data = NULL;
            return;
        }
    }
    else
        page = scan->rs_startblock; /* first page */
    heapgetpage((TableScanDesc) scan, page);
    lineoff = FirstOffsetNumber;	/* first offnum */
    scan->rs_inited = true;
    }
    else
    {
        /* continue from previously returned page/tuple */
        page = scan->rs_cblock; /* current page */
        lineoff =			/* next offnum */
            OffsetNumberNext(ItemPointerGetOffsetNumber(&(tuple->t_self)));
    }
    
    LockBuffer(scan->rs_cbuf, BUFFER_LOCK_SHARE);
    
    dp = BufferGetPage(scan->rs_cbuf);
    TestForOldSnapshot(snapshot, scan->rs_base.rs_rd, dp);
    lines = PageGetMaxOffsetNumber(dp);
    /* page and lineoff now reference the physically next tid */
    
    linesleft = lines - lineoff + 1;
}
  1. Из страницы последовательно считываются все кортежи, пока не найдется удовлетворяющая снапшоту

while (linesleft > 0)
{
    if (ItemIdIsNormal(lpp))
    {
        bool		valid;

        tuple->t_data = (HeapTupleHeader) PageGetItem((Page) dp, lpp);
        tuple->t_len = ItemIdGetLength(lpp);
        ItemPointerSet(&(tuple->t_self), page, lineoff);

        /*
         * if current tuple qualifies, return it.
         */
        valid = HeapTupleSatisfiesVisibility(tuple,
                                             snapshot,
                                             scan->rs_cbuf);

        HeapCheckForSerializableConflictOut(valid, scan->rs_base.rs_rd,
                                            tuple, scan->rs_cbuf,
                                            snapshot);

        if (valid && key != NULL)
            HeapKeyTest(tuple, RelationGetDescr(scan->rs_base.rs_rd),
                        nkeys, key, valid);

        if (valid)
        {
            LockBuffer(scan->rs_cbuf, BUFFER_LOCK_UNLOCK);
            return;
        }
    }

    /*
     * otherwise move to the next item on the page
     */
    --linesleft;
    if (backward)
    {
        --lpp;			/* move back in this page's ItemId array */
        --lineoff;
    }
    else
    {
        ++lpp;			/* move forward in this page's ItemId array */
        ++lineoff;
    }
}
  1. Если кортеж не был найден, то загружается новая страница и повторяется 2 шаг. Если страниц больше нет возвращается NULL - данных больше нет

/*
 * if we get here, it means we've exhausted the items on this page and
 * it's time to move to the next.
 */
LockBuffer(scan->rs_cbuf, BUFFER_LOCK_UNLOCK);

/*
 * advance to next/prior page and detect end of scan
 */
if (backward)
{
    finished = (page == scan->rs_startblock) ||
        (scan->rs_numblocks != InvalidBlockNumber ? --scan->rs_numblocks == 0 : false);
    if (page == 0)
        page = scan->rs_nblocks;
    page--;
}
else if (scan->rs_base.rs_parallel != NULL)
{
    ParallelBlockTableScanDesc pbscan =
    (ParallelBlockTableScanDesc) scan->rs_base.rs_parallel;
    ParallelBlockTableScanWorker pbscanwork =
    scan->rs_parallelworkerdata;

    page = table_block_parallelscan_nextpage(scan->rs_base.rs_rd,
                                             pbscanwork, pbscan);
    finished = (page == InvalidBlockNumber);
}
else
{
    page++;
    if (page >= scan->rs_nblocks)
        page = 0;
    finished = (page == scan->rs_startblock) ||
        (scan->rs_numblocks != InvalidBlockNumber ? --scan->rs_numblocks == 0 : false);

    /*
     * Report our new scan position for synchronization purposes. We
     * don't do that when moving backwards, however. That would just
     * mess up any other forward-moving scanners.
     *
     * Note: we do this before checking for end of scan so that the
     * final state of the position hint is back at the start of the
     * rel.  That's not strictly necessary, but otherwise when you run
     * the same query multiple times the starting position would shift
     * a little bit backwards on every invocation, which is confusing.
     * We don't guarantee any specific ordering in general, though.
     */
    if (scan->rs_base.rs_flags & SO_ALLOW_SYNC)
        ss_report_location(scan->rs_base.rs_rd, page);
}

/*
 * return NULL if we've exhausted all the pages
 */
if (finished)
{
    if (BufferIsValid(scan->rs_cbuf))
        ReleaseBuffer(scan->rs_cbuf);
    scan->rs_cbuf = InvalidBuffer;
    scan->rs_cblock = InvalidBlockNumber;
    tuple->t_data = NULL;
    scan->rs_inited = false;
    return;
}

heapgetpage((TableScanDesc) scan, page);

LockBuffer(scan->rs_cbuf, BUFFER_LOCK_SHARE);

dp = BufferGetPage(scan->rs_cbuf);
TestForOldSnapshot(snapshot, scan->rs_base.rs_rd, dp);
lines = PageGetMaxOffsetNumber((Page) dp);
linesleft = lines;
if (backward)
{
    lineoff = lines;
    lpp = PageGetItemId(dp, lines);
}
else
{
    lineoff = FirstOffsetNumber;
    lpp = PageGetItemId(dp, FirstOffsetNumber);
}

Завершение итерации

После выполнения портала он удаляется. Для завершения работы портала вызывается функция PortalDrop (src/backend/utils/mmgr/portalmem.c):

  • Освобождение использованных ресурсов.

  • Удаление хранилища для кортежей (для прокручиваемых курсоров созданные записи хранятся в нем).

  • Удаление контекста памяти текущего портала.

  • Освобождение выделенной для самого портала памяти.

Также удаляется объект получателя ответа. Для этого в объекте получателя присутствует поле rDestroy, указывающее на функцию деструктора. В случае с получателем фронтендом, вызывается printtup_destroy. Она освобождает выделенную под него память.

Если текущее выражение последнее в цепочке присланных или само выражение связанно с транзакцией (например, BEGIN или SAVEPOINT) то происходит коммит.

После успешного коммита, клиенту посылается пакет CommandComplete. Он сигнализирует о выполненном выражении.

Завершение выполнения запроса

Может случиться так, что после парсинга, список выражений оказался пустым. Этот случай обрабатывается после цикла. В этом случае открытая вначале транзакция коммитится и клиенту посылается EmptyQueryResponse пакет.

Конец

На этом работа по выполнению простого запроса закончена и сервер начинает обрабатывать следующий.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 13: ↑13 и ↓0+13
Комментарии3

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань