Open5

PostgreSQL 14.x のソースコードを眺めてみる

hassaku63hassaku63

なんとなくの読解テーマにしたいこと

  • クエリを受け付けてから実際にデータを返すまでの主要な流れ
    • "典型的な" 簡単なクエリを受け付ける流れを読む
    • クエリの解析とか書き換えの部分 (analyze, rewrite のような、生のクエリを実際の問い合わせ内容に変換する方面)
    • Seq Scan とか Index Scan を用いるシンプルなクエリで、実際にどうテーブルアクセスされるか
    • テーブルアクセスメソッド
    • インデックスの構造

ざっと眺めてみることにする。

hassaku63hassaku63

src/backend/tcop/postgres.cPostgresMain 関数がバックエンドのエントリポイントになるのでここから

PostgresMain
postgres main loop -- all backends, interactive or otherwise start here

プロトコル上では "simple query" という種別がシンプルなクエリ発行時の応答に対応するっぽい。それと、ソースコード上では直下の "parse" も若干興味範囲に被っている

// src/backend/tcop/postgres.c
		switch (firstchar)
		{
			case 'Q':			/* simple query */
				{
					const char *query_string;

					/* Set statement_timestamp() */
					SetCurrentStatementStartTimestamp();

					query_string = pq_getmsgstring(&input_message);
					pq_getmsgend(&input_message);

					if (am_walsender)
					{
						if (!exec_replication_command(query_string))
							exec_simple_query(query_string);
					}
					else
						exec_simple_query(query_string);

					send_ready_for_query = true;
				}
				break;

			case 'P':			/* parse */
				{
					const char *stmt_name;
					const char *query_string;
					int			numParams;
					Oid		   *paramTypes = NULL;

					forbidden_in_wal_sender(firstchar);

					/* Set statement_timestamp() */
					SetCurrentStatementStartTimestamp();

					stmt_name = pq_getmsgstring(&input_message);
					query_string = pq_getmsgstring(&input_message);
					numParams = pq_getmsgint(&input_message, 2);
					if (numParams > 0)
					{
						paramTypes = (Oid *) palloc(numParams * sizeof(Oid));
						for (int i = 0; i < numParams; i++)
							paramTypes[i] = pq_getmsgint(&input_message, 4);
					}
					pq_getmsgend(&input_message);

					exec_parse_message(query_string, stmt_name,
									   paramTypes, numParams);
				}
				break;
// ...

テーブルアクセスメソッドのあたりを見てみようと思うので、"simple query" の exec_simple_query を見ていく

https://github.com/postgres/postgres/blob/REL_14_STABLE/src/backend/tcop/postgres.c#L953-L1326

static void exec_simple_query(const char *query_string)

なんとなくの処理の流れを追ってみる(拾うのは興味持ちそうなところだけ)

hassaku63hassaku63

xact (transaction) に関係する情報は src/backend/access/transam/README に補足情報が書かれている
実装は src/backend/access/transam/xact.c に関係しそうなモノがまとまっている

transam の README によるとトランザクションシステムを構成する機能で特に postgres.c から呼ばれることになる中間層の機能は次の3つとある

  • StartTransactionCommand
  • CommitTransactionCommand
  • AbortCurrentTransaction

start_xact_command は内部で StartTransactionCommand を呼び出ししていて、関係性がある


(1) start_xact_command

トランザクションの(コマンド)ブロックを開始するらしい。通常のケースだと "simple query" プロトコルでは、 BEGIN COMMIT ABORT が検出されない限りは同一ブロックとして扱われる。これらのコマンドが見つかったら新しいトランザクションコマンドを開始しなければならないらしい

	/*
	 * Start up a transaction command.  All queries generated by the
	 * query_string will be in this same command block, *unless* we find a
	 * BEGIN/COMMIT/ABORT statement; we have to force a new xact command after
	 * one of those, else bad things will happen in xact.c. (Note that this
	 * will normally change current memory context.)
	 */
	start_xact_command();

(2) "MessageContext" への遷移

	/*
	 * Switch to appropriate context for constructing parsetrees.
	 */
	oldcontext = MemoryContextSwitchTo(MessageContext);

いったん理解は省略する。"simple query" は「メッセージ」プロトコルの種別の一つなので、メッセージを解釈する処理文脈を固定する役割と思っておくことにする。

(3) クエリ文字列の「パース」

	/*
	 * Do basic parsing of the query or queries (this should be safe even if
	 * we are in aborted transaction state!)
	 */
	parsetree_list = pg_parse_query(query_string);

メインディッシュっぽいので、個別で掘り下げて見ていくことにする。この場では後回し。

"Parse tree" という概念が登場するらしいので、そのへんは覚えておく

(4) Log statement のチェック

	/* Log immediately if dictated by log_statement */
	if (check_log_statement(parsetree_list))
	{
		ereport(LOG,
				(errmsg("statement: %s", query_string),
				 errhidestmt(true),
				 errdetail_execute(parsetree_list)));
		was_logged = true;
	}

省略する

(5) セットした MemoryContext を復元する

	/*
	 * Switch back to transaction context to enter the loop.
	 */
	MemoryContextSwitchTo(oldcontext);

この場では読解を省略する。

(6) 「暗黙的なブロック」の判定

複数の SQL 文が含まれてた場合の処理、らしい

	/*
	 * For historical reasons, if multiple SQL statements are given in a
	 * single "simple Query" message, we execute them as a single transaction,
	 * unless explicit transaction control commands are included to make
	 * portions of the list be separate transactions.  To represent this
	 * behavior properly in the transaction machinery, we use an "implicit"
	 * transaction block.
	 */
	use_implicit_block = (list_length(parsetree_list) > 1);

transam の README にも書かれているように、BEGIN/COMMIT/ABORT がない限りは(複数個の SQL 文があった場合でも)1個のトランザクションブロックで囲われる。これを表現するために Implicit block という概念を使うらしい

(7) foreach 文の中身

parse tree の中を1個1個処理するらしいが、parse tree がわからないので理解を後回しにする

(8) finish_xact_command

	/*
	 * Close down transaction statement, if one is open.  (This will only do
	 * something if the parsetree list was empty; otherwise the last loop
	 * iteration already did it.)
	 */
	finish_xact_command();

トランザクションを閉じる。内部的に CommitTransactionCommand が呼ばれている。詳細を追うのは省く

hassaku63hassaku63

pg_parse_query と、その結果となる parse tree を受け取って行う foreach 文の中身の話をこれから読むことになるが、その前に parse tree という概念の解説をあたってみる。

https://www.postgresql.org/docs/14/parser-stage.html
https://www.postgresql.jp/document/14/html/parser-stage.html

ここに書いてある概要を見てみると、オプティマイザ(クエリ最適化)とか呼ばれるような処理とはまた話が違ってそうな雰囲気がする。とりあえず英文を眺める

"parser stage" という段階は2段階で構成されていて、単純な文字列の解析という意味でのパースと、パーサによって返されたデータを変更したり、負荷したりする "transformation" というフェーズがある。

parse stage はシステムテーブルなどは一切見ないので、文の意味に立ち入ることはない。transform で意味に立ち入ることになる。transformation で書いてあることは全部重要かつ短いので、全文原文を載せて訳すことにする

The parser stage creates a parse tree using only fixed rules about the syntactic structure of SQL. It does not make any lookups in the system catalogs, so there is no possibility to understand the detailed semantics of the requested operations. After the parser completes, the transformation process takes the tree handed back by the parser as input and does the semantic interpretation needed to understand which tables, functions, and operators are referenced by the query. The data structure that is built to represent this information is called the query tree.

クエリ内で使われているテーブル、関数、演算子といった要素の意味を解釈するところを transformation で行われる。この結果としてできあがるツリーは "query tree" と呼ばれるらしい。

The reason for separating raw parsing from semantic analysis is that system catalog lookups can only be done within a transaction, and we do not wish to start a transaction immediately upon receiving a query string. The raw parsing stage is sufficient to identify the transaction control commands (BEGIN, ROLLBACK, etc), and these can then be correctly executed without any further analysis. Once we know that we are dealing with an actual query (such as SELECT or UPDATE), it is okay to start a transaction if we're not already in one. Only then can the transformation process be invoked.

parse の段階で意味解析をしない(意味解析を分割している)理由は、システムカタログの検索はトランザクションの中でしか行えないため。クエリ文字列を受け取った時点で、直ちにトランザクションを開始したいわけではない。

トランザクションコマンド (BEGIN, ROLLBACK, etc) が識別するのは parse ステージで十分。実際のクエリ (SELECT, UPDATE) を実行していることがわかれば、またすでにトランザクションが開始されていなければ、新たにトランザクションを開始しても問題ない。

The query tree created by the transformation process is structurally similar to the raw parse tree in most places, but it has many differences in detail. For example, a FuncCall node in the parse tree represents something that looks syntactically like a function call. This might be transformed to either a FuncExpr or Aggref node depending on whether the referenced name turns out to be an ordinary function or an aggregate function. Also, information about the actual data types of columns and expression results is added to the query tree.

"query tree" は transformation プロセスで構築される。ほとんどの場合では生の parse tree と似た構造をしている(しかし、多くの詳細は異なる)。

※ "parse tree" という用語のリンク先のドキュメントに説明はなさそうだが、多分 parse フェーズで文字列的な解析だけ行われた結果のツリーをそう呼んでいるように見える。

例えば、parse tree における "FuncCall" ノードは、単に文法的に「関数呼び出し」に見えるものを指す。transformation の後では、これは "FuncExpr" または "Aggref" ノードになる可能性がある。どちらのノードになるのかは、参照される名前が通常の関数であるか、集約関数であるかによる。

また、カラムの実際のデータ型や式の結果に関係する情報は、"query tree" に追加される。