TiDB source code reading series of articles (3) The life of SQL

Overview

The previous article explained the structure of the TiDB project and its three core parts. This article starts from the SQL processing flow, introduces where the entry is, what operations need to be done with SQL, and know where a SQL comes from and where to process it. and return from where.

There are many kinds of SQL, such as read, write, modify, delete, and management SQL. Each SQL has its own execution logic, but the general process is similar, and they all operate under a unified framework.

frame

Let's take a look at the whole thing, what aspects of work a statement needs to go through. If you still remember the three core parts mentioned in the previous article, you can think that you must first go through the protocol analysis and conversion, get the statement content, then process the SQL core layer logic to generate the query plan, and finally go to the storage engine to get the data , perform the calculation, and return the result. This is a rough processing framework, and this article will continue to refine this framework.

For the first part, protocol analysis and conversion, all the logic is in the server package. The main logic is divided into two parts: one is the establishment and management of connections, each connection corresponds to a Session; the other is processing on a single connection logic. The first point is not covered in this article for the time being. Interested students can flip through the code to see how the connection is established, how to shake hands, and how to destroy. There will be special articles to explain later. For the SQL execution process, the second point is more important, that is, the connection has been established, and the operation on this connection will be explained in detail in this article.

For the second part, the processing of the SQL layer is the most complicated part of the whole TiDB. Why is this part complicated? There are three reasons:

  1. The SQL language itself is a complex language. There are many types of statements, many data types, many operators, and many syntax combinations. These "many" will become "many" and "very many" after being arranged and combined, so it is necessary to write a lot of code to handle.

  2. SQL is an ideographic language. It only says "what data is needed", not "how to get data", so some complex logic is needed to choose "how to get data", that is, to choose a good query plan.

  3. The bottom layer is a distributed storage engine, which will face many problems that stand-alone storage engines will not encounter. For example, when making query plans, it is necessary to consider that the data in the lower layer is fragmented and how to deal with the network failure. Therefore, some complexities are required. The logic of handling these cases, and need a good mechanism to encapsulate these processing logic. These complexities are a big obstacle to understanding the source code, so this article will try to eliminate these interferences and explain to you what the core logic is.

There are several core concepts in this layer. If you master these, you will also master the framework of this layer. Please pay attention to the following interfaces:

In the following details, these interfaces will be explained, and the entire logic will be clarified with these interfaces.

The third part can be considered as two parts, the first part is the KV interface layer, the main function is to route the request to the correct KV Server, receive the return message and pass it to the SQL layer, and handle various abnormal logic in this process; The second block is the specific implementation of KV Server. Since TiKV is more complicated, we can first look at the implementation of Mock-TiKV. Here are all the logic related to SQL distributed computing. In the next few sections, the above three blocks will be described in detail.

Protocol layer entry

After the connection with the client is established, there will be a goroutine in TiDB listening on the port, waiting for the packets sent from the client, and processing the packets. This logic is in server/conn.go, which can be considered as the entrance of TiDB. This section introduces this logic. First look at clientConn.Run() , which will continuously read network packets in a loop:

	445:	data, err := cc.readPacket()

Then call the dispatch() method to process the received request:

	465:		if err = cc.dispatch(data); err != nil {

Next, enter the clientConn.dispatch() method:

	func (cc *clientConn) dispatch(data []byte) error {

The package to be processed here is the original byte array, and the content reader can refer to the MySQL protocol . The first byte is the type of Command:

		580: 	cmd := data[0]

Then call the corresponding processing function according to the type of Command. The most commonly used Command is COM_QUERY . For most SQL statements, as long as the prepared method is not used, it is COM_QUERY. This article will only introduce this Command. For other Commands, please refer to MySQL. Documentation see code. For Command Query, the main thing sent from the client is the SQL text, and the processing function is handleQuery() :

	func (cc *clientConn) handleQuery(goCtx goctx.Context, sql string) (err error) {

This function will call the specific execution logic:

	850:  rs, err := cc.ctx.Execute(goCtx, sql)

The implementation of this Execute method is in server/driver_tidb.go,

	func (tc *TiDBContext) Execute(goCtx goctx.Context, sql string) (rs []ResultSet, err error) {
		rsList, err := tc.session.Execute(goCtx, sql)

The most important thing is to call tc.session.Execute. The implementation of this session.Execute is in session.go. From then on, it will enter the SQL core layer. The detailed implementation will be described in the following chapters.

After a series of processing, after getting the result of the SQL statement, the writeResultset method will be called to write the result back to the client:

		857:		err = cc.writeResultset(goCtx, rs[0], false, false)

Protocol layer exit

The export is relatively simple. It is the writeResultset method mentioned above. According to the requirements of the MySQL protocol, the results (including the Field list and each row of data) are written back to the client. Readers can refer to COM_QUERY Response in the MySQL protocol to understand this code.

In the next few sections, we enter the core process and see how a textual SQL is processed. I will introduce all the processes first, and then use a diagram to string them all together.

Session

The most important function in Session is Execute , which will call various modules described below to complete statement execution. Note that in the process of execution, the Session environment variables, such as whether or not AutoCommit, what the time zone is.

Lexer & Yacc

These two components together form the Parser module, which calls Parser to parse the text into structured data, which is an abstract syntax tree (AST):

	session.go 699: 	return s.parser.Parse(sql, charset, collation)

In the parsing process, the lexer will be used to continuously convert the text into tokens and deliver them to the Parser. The Parser is generated according to the yacc grammar . According to the grammar, the token sequence sent from the Lexer can be continuously determined to match which grammar rules, and the final output structure ized nodes. For example, such a statement SELECT * FROM t WHERE c > 1;, which can match the rules of SelectStmt , is converted into the following data structure:

	type SelectStmt struct {
		dmlNode
		resultSetNode
	
		// SelectStmtOpts wraps around select hints and switches.
		*SelectStmtOpts
		// Distinct represents whether the select has distinct option.
		Distinct bool
		// From is the from clause of the query.
		From *TableRefsClause
		// Where is the where clause in select statement.
		Where ExprNode
		// Fields is the select expression list.
		Fields *FieldList
		// GroupBy is the group by expression list.
		GroupBy *GroupByClause
		// Having is the having condition.
		Having *HavingClause
		// OrderBy is the ordering expression list.
		OrderBy *OrderByClause
		// Limit is the limit clause.
		Limit *Limit
		// LockTp is the lock type
		LockTp SelectLockType
		// TableHints represents the level Optimizer Hint
		TableHints []*TableOptimizerHint
	}

Among them, FROM twill be parsed as FROMfield , WHERE c > 1parsed as Wherefield , and *parsed as Fieldsfield . The structure of all statements is abstracted into one ast.StmtNode, and readers of this interface can read the comments themselves to understand. Just to mention one point here, most of the data structures in the ast package implement the ast.Nodeinterface . This interface has a Acceptmethod. Subsequent processing of the AST mainly relies on the Accept method, which traverses all nodes in the Visitor mode and converts the AST structure. .

Query planning and optimization

After getting the AST, you can do various verifications, changes, and optimizations. The entrance to this series of actions is here:

	session.go 805: 			stmt, err := compiler.Compile(goCtx, stmtNode)

When we enter the Compile function , we can see three important steps:

  • plan.Prepprocess: Do some validity checks and name bindings;

  • plan.Optimize: Formulate a query plan and optimize it, this is one of the most core steps, and the following articles will focus on it;

  • Structure executor.ExecStmtStructure : This ExecStmt structure holds the query plan, which is the basis for subsequent execution and is very important, especially the Exec method.

Generate executor

In this process, the plan will be converted into an executor, and the execution engine can execute the query plan previously set through the executor. For the specific code, see ExecStmt.buildExecutor() :

	executor/adpter.go 227:  e, err := a.buildExecutor(ctx)

After the executor is generated, it is encapsulated in a recordSetstruct :

		return &recordSet{
			executor:    e,
			stmt:        a,
			processinfo: pi,
			txnStartTS:  ctx.Txn().StartTS(),
		}, nil

This structure implements the ast.RecordSetinterface . Literally, you can see that this interface represents the abstraction of the query result set. Let's take a look at several of its methods:

	// RecordSet is an abstract result set interface to help get data from Plan.
	type RecordSet interface {
		// Fields gets result fields.
		Fields() []*ResultField
	
		// Next returns the next row, nil row means there is no more to return.
		Next(ctx context.Context) (row types.Row, err error)
	
		// NextChunk reads records into chunk.
		NextChunk(ctx context.Context, chk *chunk.Chunk) error
	
		// NewChunk creates a new chunk with initial capacity.
		NewChunk() *chunk.Chunk
	
		// SupportChunk check if the RecordSet supports Chunk structure.
		SupportChunk() bool
	
		// Close closes the underlying iterator, call Next after Close will
		// restart the iteration.
		Close() error
	}

You can see the function of this interface by commenting. In short, you can call the Fields() method to get the type of each column of the result set, call Next/NextChunk() to get a row or batch of data, and call Close() to close the result set.

run the executor

The execution engine of TiDB runs in the Volcano model. All physical Executors form a tree structure. Each layer obtains the result by calling the Next/NextChunk() method of the next layer. For example, assuming the statement is SELECT c1 FROM t WHERE c2 > 1;, and the query plan selects a full table scan + filter, the executor tree will look like this:

You can see the calling relationship between Executors and the flow of data from the figure. So where is the top-level Next called, that is, where is the starting point of the entire calculation, and who will drive this process? There are two places you need to pay attention to. These two places deal with two types of statements respectively. The first type of statement is a query statement such as Select, which needs to return results to the client. The executor call point of this type of statement is where the data is returned to the client :

			row, err = rs.Next(ctx)

Here rsis an RecordSetinterface, which can be called continuously to Next()get more results and return them to MySQL Client. The second type of statement is the Insert statement that does not need to return data, but only needs to complete the execution of the statement. This type of statement is also executed by the Nextdriver , the driver point is before the construction of the recordSetstructure :

		// If the executor doesn't return any result to the client, we execute it without delay.
		if e.Schema().Len() == 0 {
			return a.handleNoDelayExecutor(goCtx, e, ctx, pi)
		} else if proj, ok := e.(*ProjectionExec); ok && proj.calculateNoDelay {
			// Currently this is only for the "DO" statement. Take "DO 1, @a=2;" as an example:
			// the Projection has two expressions and two columns in the schema, but we should
			// not return the result of the two expressions.
			return a.handleNoDelayExecutor(goCtx, e, ctx, pi)
		}

Summarize

The execution framework of the entire SQL layer is described above. Here is a picture to describe the entire process:

Through this article, I believe that you have understood the execution framework of statements in TiDB. The whole logic is relatively simple. The detailed explanation of the specific modules in the framework will be given in the subsequent chapters. The next article will use specific sentences as examples to help you understand this article.

Author: Shen Li

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324400030&siteId=291194637