3
头图

introduction

​ The content is the study notes content of the "High Concurrency, High Performance and High Availability MySQL Practice" video of MOOC.com and the notes after personal organization and expansion. The content of this section describes the content of index optimization. In addition, this part of the content involves a lot of optimization content. Therefore, when studying, it is recommended to open Chapter 6 of "High Performance Mysql" for review and understanding. It is necessary for students who develop Mysql data to have a general understanding of the internal working mechanism.

​ Because the content of the article is too long, it is divided into two parts, and the content of the upper and lower parts uses sakila-db , which is the official case of mysql. The first part describes the theory of optimization and the flaws of the optimizer design of Mysql in the past. At the same time, it will introduce how to fix and improve these problems in later versions (but from my personal point of view, those optimizations in the new version are not optimizations at all, and even some optimizations It is still copied from the implementation of the original author of Mysql. After so many years of development, this achievement is still due to the credit of an extreme commercial company such as Oracle).

If the content is difficult, you can follow the "How Mysql Works" personal reading notes column to make up lessons, and individuals are also learning and updating synchronously.

The address is as follows: https://juejin.cn/column/7024363476663730207 .

【Knowledge points】

  • Introduction to Mysql Index Content
  • Index usage policy and usage rules
  • Query optimization and troubleshooting, a simple understanding of the responsibilities of each component of Mysql

pre-preparation

sakila-db

​ What is sakila-db? A concept that is very popular abroad, refers to the fact that the foreign movie rental market uses the rental method to watch movies, which is very popular in foreign countries. It is introduced here because this case is used in the subsequent content. So we need to prepare the relevant environment in advance and download it from the following address:

​ Download address: https://dev.mysql.com/doc/index-other.html

The SQL case of "High Performance Mysql" also uses the official example

work-bench

​ work-bench is an officially developed visualization tool for database relationship diagrams. The specific relationship diagram of the official case is displayed as follows. Through these diagrams, you can see the general relationship between Sakila-db:

Work-bench is also open source free software, the download address is as follows:

https://dev.mysql.com/downloads/workbench/

sakila-db示意图

​ The way of installing workbench and downloading sakila-db is not recorded here. When running, you need to create a database to run the sheme file first, then execute the sql file of data, and finally view the data in navicat:

数据库关系图

body part

index type

​ The first is the characteristics and functions of the index:

  1. The purpose of indexing is to improve the efficiency of data.
  2. The use of indexes is very important for the ORM framework, but the optimization of ORM is often difficult to take into account all business situations and is gradually abandoned.
  3. Different index types are suitable for different scenarios.
  4. The key to indexing is to reduce the amount of data that needs to be scanned, while avoiding the internal sorting of content and temporary tables in the server (because the temporary table will fail the index), random IO to sequential IO, etc.

The following describes the types of indexes related to Mysql:

  • Hash index: Hash index is suitable for full value matching and precise search, and the query speed is very fast. In MySQL, only the memory storage engine explicitly supports this index, and memory also supports non-unique hash index, which is compared in the design of hash index special.
  • Spatial index: Spatial index is supported by myisam table and is mainly used for geographic data storage. There is a thing called GIS here, but GIS is much better in Postgre than MySQL, so spatial index in MySQL is irrelevant.
  • Full-text indexing: Full-text indexing is also an index type that is uniquely supported by myisam. The suitable scenarios are full-value matching scenarios and keyword queries, which can effectively handle keyword matching of large texts.
  • Clustered index: Clustered index is the default storage engine of the innodb storage engine.
  • Prefix compression index: Note that this index is aimed at the myisam storage engine. The purpose is to put the index into memory for sorting. The method of prefix compression is to first save the first value of the index block, and then save the second value. The second value stores the prefix index in the form of (length, index value).

Additional Index Type Notes:

​ Archive only supports single-column auto-incrementing indexes after 5.1.

​ MyISAM supports compressed prefix indexes, making the data structure smaller.

hash index

​ The only storage engine that explicitly implements hash indexes in Mysql is Memory. Memory has a non-unique hash index. At the same time, BTree also supports the "adaptive hash index method" compatible with hash indexes.

The following are hash index features:

  • The key stores the index hash value, note that it is not the index value itself, but the value stores the pointer to the row
  • Note that this hash index cannot avoid row scans, but in memory pointers are very fast usually negligible
  • Note that only the hash values ​​are sorted in order, but the row pointers are not in order
  • Hash does not support: partial index coverage, only full index coverage is supported, because the hash value is calculated using all index columns
  • Hash indexes support equal-value matching operations but do not support range queries, such as equals, in subqueries, incomplete, etc.
  • If there is a hash conflict, the hash index will degenerate into a linked list sequential query, and the cost of maintaining the index will also increase.

clustered index

Clustering means that the values ​​of data rows are stored compactly together. The value of the innodb cluster is the value of the primary key, so the index on the primary key is usually used, and the selection of the primary key index is very important. Since this section focuses on index optimization, clustered indexes are not described here.

The difference between the primary key index of MyISam and Innodb is that the index of MyISam is very simple, because the data row only contains the row number, so the index directly stores the column value and row number , and the data is stored separately in another place, similar to a unique non-empty index, the index and The data is not in one place. The index design of MyISam is much simpler than that of InnoDB, which is directly related to the fact that MyIsam does not need to support transactions, while innodb puts the index and row data into a data structure, and compactly stores the columns.

Clustered indexes have the following advantages

  • Stores rows of data compactly, so data can be retrieved with only a few disk scans
  • The speed of data access is very fast. The index and data are placed in the same BTree, which is much faster than non-clustered index queries.
  • Covering indexes can directly reduce back to the table

Of course, indexes also have the following disadvantages:

  • For non-IO-intensive applications, clustered index optimization is meaningless.
  • The insertion speed depends on the insertion order, but if it is not an auto-incrementing insertion, you need to optimize the table to reorganize the table.
  • The update cost is very high, because the BTree needs to move the data page position and pointer to ensure the ordering.
  • If the primary key data is too full, the data page will be split, and the row overflow will increase the storage pressure.
  • Clustered indexes cause slow full table scans, page splits cause data problems, etc.
  • The secondary index needs to query the clustered index back to the table to query the data.
  • The secondary index will have more overhead due to the need to store the primary key. At least the overhead of maintaining a secondary index in InnoDb is quite large.

compressed index

​ Compressed index is characterized by using less space to store as much content as possible, but this processing method is only suitable for IO-intensive systems. The biggest drawback of the compressed prefix storage form is that it cannot be searched using the binary method. The reverse order index method such as the order by desc method may be stuck due to the problem of compressing the index.

Features of Bree Index

  • There are two types of leaf nodes: logical pages and index pages. Usually, the non-bottom-level leaf nodes are index pages, and the bottom-level index pages are concatenated by a linked list.
  • The Btree index will sort the index values ​​according to the table creation order . It is recommended to move the frequently queried fields forward when the index table is created.
  • Btree indexes are suitable for query types: prefix query, range query, key-value query (hash index) .

Adaptive Hash Index

​ When innodb finds that some index columns and values ​​are frequently used, BTree will automatically create a hash index to assist optimization on this basis, but this behavior is not controlled by external, it is completely internal optimization behavior, if not needed, you can Consider closing.

Btree query type

​ For the Btree index of Innodb, there are the following common query methods:

  • Full value matching: the method of equal value matching, full value matching is suitable for hash index query
  • The leftmost matching principle: the query conditions of the secondary index are placed on the leftmost where
  • Prefix matching: only use the first column of the index, and like 'xxx%'
  • Range Matching: Range matches values ​​between an indexed column and another column
  • Range query combined with exact match, one full value match, one range match
  • Covering index query: Covering index is also a query method.

index strategy

Here are some common strategies for indexing:

  1. The first thing to consider is to predict which data are hot data or hot columns. According to the introduction of "High Performance Mysql", for hot columns, the principle of maximum selectivity is sometimes violated. By establishing an index that is frequently searched as the leftmost The default setting for the prefix. Optimizing a query at the same time needs to consider all the columns, and if the optimization of one query will break another query, then the structure of the index needs to be optimized.
  2. The second thing is to consider the combination of where conditions. By combining multiple where conditions, what needs to be considered is to make the query reuse the index as much as possible instead of creating a new index on a large scale.
  3. Avoid scanning in multiple ranges. On the one hand, range queries will cause problems, but for multiple equal-value conditional queries, the best way is to control the search range as much as possible.

​ For the indexing strategy, we also need to understand the following details

  • Single-row access is very slow, especially random access is much slower than sequential access, and loading many data pages at one time will cause performance waste.
  • Sequential access to range data is fast, and the speed of sequential IO does not require multi-track search, which is much more than random access to IO blocks. Sequential access can also be aggregated by group by.
  • The index coverage is very fast, and if the query field contains the index column, there is no need to return the table.

Index Fragmentation Optimization

The data structure and characteristics of Innodb will lead to data fragmentation in the index. For any storage structure, sequential storage structure is the most suitable, and index sequential access is much faster than random access. Fragmentation of data storage is much more complicated than the index itself. , index fragmentation usually includes the following situations:

  • Row fragmentation: The data of the data row is stored in multiple data pages, and fragmentation may cause performance degradation.
  • Inter-row fragmentation: Pages in logical order, rows are not stored sequentially on disk, and inter-row data fragmentation will cause a full table scan.
  • Remaining space fragmentation: A large amount of garbage data is wasted in the gaps of data pages.

​ For the above points, it may appear for myisam, but the row fragmentation of innodb will not appear, and the fragments will be moved internally and rewritten to a fragment.

​ Processing of index fragmentation: In Mysql, you can rearrange data by importing and exporting optimize table to prevent data fragmentation.

index rule

  • Indices must match from left to right in index order
  • If a range occurs in the middle of the query, the index after the range query is invalid
  • The query method that cannot skip the index column (related to the design of the B+tree index data structure)

​ Then there is the issue of index order. Due to the structural characteristics of BTree, indexes are searched according to the order in which they are established. Usually, when sorting and grouping are not included, placing the index with the highest selectivity in the leftmost column is a generally correct strategy.

​ How to check the index cardinality: show index from sakila.actor , and another way is to query this information through the information_schema.statistics table, which can be written as a query to give a less selective index.

​ When innodb opens some tables, it will trigger the statistics of index information, such as opening information_schema table or using show table status and show index , so if the system Try to avoid these operations during stressful business times.

redundant duplicate index

​ Mysql allows multiple types of indexes to be created on the same column. Sometimes, due to the characteristics of table building, repeated indexing of fields will cause unnecessary performance waste. What is the difference between a redundant index and a duplicate index?

​ Redundant index: Repeatedly indexing the same column in accordance with the leftmost matching rule.

​ Duplicate index: It is possible to create an index repeatedly for an index that is not created in the best way.

​ For example, a joint index: (A, B) If creating (A) or (A, B) is a duplicate index, but creating (B) is not a duplicate index but a redundant index. In addition, redundant indexes may be used in some very special cases, but this will greatly increase the cost of index maintenance. The most intuitive feeling is that the cost of inserting, updating, and deleting becomes very large.

multi-column index

​ First of all, the multi-column index does not mean that the where field needs to be added where it appears. Secondly, although the multi-column index has been used in the current mainstream version (after version 5.1), the internal merging of the index is implemented, that is, the use of and or or and and or merge using the index, but it has the following disadvantages

  • The merging and calculation of the internal optimizer consumes a lot of CPU performance, and the index increases the complexity of data query, and the efficiency is not good
  • There are often cases of over-optimization, resulting in a performance that is not as good as a full table scan
  • The occurrence of multi-column index merging usually means that the way of index building is wrong, and there is suspicion of reverse optimization

file sorting

​ File sorting follows the most basic principle of Innodb's Btree index: the leftmost prefix principle . If the order of the index columns is consistent with the order by, and the query columns are the same as the sorting column, the index will be used instead of sorting. For multi-table queries, The sorting field is all the first table in order to perform index sorting. But there is a special case that index sorting can still be used when the leading column of the sorting field is constant .

​ Case: Sort the joint index column of the rental table

Backward index scan is a special optimization item of MySQL-8.0.x for the above scenario. It can read from the back of the index to the front, and its performance is much better than adding index hints.
 EXPLAIN select rental_id,staff_id from rental where rental_date = '2005-05-25' order by inventory_id desc, customer_id asc;
-- 1 SIMPLE rental ref rental_date rental_date 5 const 1 100.00 Using filesort

EXPLAIN select rental_id,staff_id from rental where rental_date = '2005-05-25' order by inventory_id desc;
-- Backward-index-scan
-- Backward index scan 是 MySQL-8.0.x 针对上面场景的一个专用优化项,它可以从索引的后面往前面读,性能上比加索引提示要好的多
-- 1 SIMPLE rental ref rental_date rental_date 5 const 1 100.00 Backward index scan

EXPLAIN select rental_id,staff_id from rental where rental_date = '2005-05-25' order by inventory_id, staff_id;
-- 1 SIMPLE rental ref rental_date rental_date 5 const 1 100.00 Using filesort
-- 无法使用索引
EXPLAIN select rental_id,staff_id from rental where rental_date > '2005-05-25' order by inventory_id, customer_id;
-- 1 SIMPLE rental ALL rental_date 16008 50.00 Using where; Using filesort

EXPLAIN select rental_id,staff_id from rental where rental_date = '2005-05-25' and inventory_id in (1,2) order by customer_id;
-- 1 SIMPLE rental range rental_date,idx_fk_inventory_id rental_date 8 2 100.00 Using index condition; Using filesort

explain select actor_id, title from film_actor inner join film using(film_id) order by actor_id;
-- 1 SIMPLE film index PRIMARY idx_title 514 1000 100.00 Using index; Using temporary; Using filesort
-- 1 SIMPLE film_actor ref idx_fk_film_id idx_fk_film_id 2 sakila.film.film_id 5 100.00 Using index

Query optimization troubleshooting

​ Query optimization investigation means that we need to understand what each component of Mysql does in each step. The following picture is from "High Performance Mysql". For a client request, it is roughly divided into the following processes:

  1. Client sends request
  2. Server query execution cache

    • Not important, it has been removed after 8.0
  3. Server-side SQL parsing and preprocessing

    • permission check
    • Lexical analysis
    • syntax tree
  4. The optimizer generates an execution plan

    • Problems with the optimizer?
    • How does the optimizer work?
  5. Call the APi interface of the storage engine to execute the query according to the execution plan
  6. The result is returned to the client

​ For relational databases, the core part lies in the query optimizer and execution plan, because no matter how we write SQL statements, if there is no powerful optimizer and execution plan, everything is empty talk, so the focus of this part is also It will be explained around the optimizer, before we look at the work of other components:

​ First of all, the query cache does not need to be explained too much. Its function is to cache the results internally when the user repeatedly executes a query, but once the user modifies the query conditions, the cache will be invalid. In the early Internet environment, this kind of processing Very good, it can reduce the pressure on disk IO and CPU, but it is obviously not suitable in the current environment, so it is understandable to delete 8.0.

​ Next is the parser. The main job of the parser is to form a parse tree and preprocess the statement by parsing the grammar. The preprocessing can be regarded as the process of our compiler "translating" the programming statement we wrote into machine code, so that The next step optimizer can recognize this parse tree to parse,

​ If you want to understand the underlying process of SQL parsing optimization, you can start with this article:

​The application of SQL parsing in Meituan - Meituan Technical Team (meituan.com)

​ In the above blog, I mentioned a tool pt-query-digest that DBAs must master to analyze slow query logs. The following article provides an actual case for troubleshooting and optimization. The case is relatively simple and suitable for people who are new to this tool. Learn and think, all listed here.

​Using pt-query-digest to analyze RDS MySQL slow query logs | Amazon AWS official blog (amazon.com)

Notes on SQL parsing:

Lexical analysis: The core code is in the sql/sql_lex.c file, MySQLLex→lex_one_Token

MySQL parse tree generation process : all the source code is in sql/sql_yacc.yy , and there are about 17K lines of code in MySQL5.6

The core structure is SELECT_LEX, which is defined in sql/sql_lex.h

Let's take a deeper look at some of the work of the optimizer and the history of Mysql optimization:

​ Since there is less content about the optimizer, the content of "High Performance Mysql" is directly summarized here, and the optimizer does not need to be studied and memorized, because the optimizer will continue to be adjusted as the version iteratively updated, and everything should be based on real experiments. allow:

1. Subquery association :

​ The following query is usually considered to be a subquery first, and then scan the film table through a for loop for matching operations, and then from the explain result, we can see that the query line here performs a full table scan, and then passes the associated index. Perform the for loop query of the second layer, which is similar to exists .

 explain select * from sakila.film where film_id in (select film_id from film_actor where actor_id)
-- 1    SIMPLE    film        ALL    PRIMARY                1000    100.00    
-- 1    SIMPLE    film_actor        ref    idx_fk_film_id    idx_fk_film_id    2    sakila.film.film_id    5    90.00    Using where; Using index; FirstMatch(film)

​ The way to optimize this subquery is to use an associated query instead of a subquery, but it should be noted that there is a where condition to go to the index, otherwise it is no different from the above results:

 explain select film.* from sakila.film film  join film_actor actor using (film_id) where actor.actor_id = 1

The other is to use the exists method for association matching.

 explain select * from film where exists (select * from film_actor actor where actor.film_id =  film.film_id and actor.actor_id = 1);

​ It can be seen that even in version 5.8, since the sub-query optimization of Mysql has not been greatly improved, in general, if you are not sure about the content of the in query, it is recommended to use exists or join to query, and don’t trust any in query. It must be slow to say that it may have different effects in different mysql optimizer versions.

2. union query

​ Although we will replace or with union in most cases, we should avoid using union as much as possible, because union queries will generate temporary tables and intermediate result sets, which can easily lead to the failure of optimized indexes. It should be noted that union will trigger internal Sorting action, that is to say, union will be equivalent to the sorting of order by . If the data is not strongly required and cannot be repeated, it is more recommended to use union all. The result set is just grouped together, and it will not be sorted.

​ The union query can be used without it, unless it is necessary to use it when it is used to replace the or query.

​ Finally, note that the ordering of union generation is not controlled, and unexpected results may occur.

3. Parallel query optimization

​ Parallel query optimization has finally been implemented in 8.0, which can be verified according to the parameters: innodb_parallel_read_threads =并行数 .

Since the individual is the CPU of M1, readers can conduct experiments according to their actual situation.

 set local innodb_parallel_read_threads = 1;
select count(*) from payment;
set local innodb_parallel_read_threads = 6;
select count(*) from payment;

From the execution results, we can see that there is an obvious and intuitive gap in the count(*) query of more than 10,000 pieces of data:

4. Hash association

​ Official document introduction address: Mysql official document hash association

​ In MySQL 8.0.18, Mysql finally added the function of hash association. In the previous version, the optimizer of Mysql usually only supported nested associations of for loops. The way to save the country is to build a hash index or use the Memory storage engine. The hash association provided by the new version provides a new The pair association method, the hash association method is as follows:

​ Store the data of a small table in the hash table in memory , calculate the hash value by matching the data in the large table, and return the qualified data from the memory to the client.

​ For the hash association of Mysql, we directly use the official example:

 CREATE TABLE t1 (c1 INT, c2 INT);
CREATE TABLE t2 (c1 INT, c2 INT);
CREATE TABLE t3 (c1 INT, c2 INT);

EXPLAIN
     SELECT * FROM t1
         JOIN t2 ON t1.c1=t2.c1;
-- Using where; Using join buffer (hash join)

In addition to the equivalent query, Mysql provides more support after 8.0.20. For example, in MySQL 8.0.20 and later, the connection no longer needs to contain at least one equal join condition to use the hash join, in addition to In addition it includes the following:

 -- 8.0.20 支持范围查询哈希关联
EXPLAIN  SELECT * FROM t1 JOIN t2 ON t1.c1 < t2.c1;
-- 8.0.20 支持 in关联
EXPLAIN  SELECT * FROM t1 
        WHERE t1.c1 IN (SELECT t2.c2 FROM t2);
-- 8.0.20 支持 not exists 关联
EXPLAIN  SELECT * FROM t2 
         WHERE NOT EXISTS (SELECT * FROM t1 WHERE t1.c1 = t2.c2);
-- 8.0.20 支持 左右外部连接
EXPLAIN SELECT * FROM t1 LEFT JOIN t2 ON t1.c1 = t2.c1;
EXPLAIN SELECT * FROM t1 RIGHT JOIN t2 ON t1.c1 = t2.c1;

Note that the hash association in version 8.0.18 only supports join queries , and does not support left and right join queries that may result in Cartesian products. However, more query condition support is provided in subsequent versions

In addition, before version 8.0.20, if you want to check whether to use hash join, you need to combine format=tree option.

哈希关联

​ In the end, Mysql in version 8.0.18 once provided switching hash indexes and setting optimizer prompts optimizer_switch and other parameters to determine whether to give hash join prompts, which is really a pain in the ass (the official thinks so too. ), so these parameters were discarded immediately in 8.0.19.

​ Note that the hash connection is not unlimited. If you understand the hash association process, you will find that if the hash table is too large, the entire hash association process will be completed in the disk. The speed can be imagined, so the official provides the following suggestion:

  • Increase join_buffer_size , that is, increase the size of the hash table cache associated with the hash to prevent entering the disk association.
  • Increase the number of open_files_limit , the meaning of this parameter will not be introduced here, the meaning is that increasing this parameter can increase the number of associations during association.

Tucao: To be honest, since Mysql was acquired by Oracle, it has become more and more commercialized and the progress has become smaller and smaller. In fact, in query optimization has been solved by many open source libraries and even the original author of Mysql, but Mysql has reached 8.0. It is still the same as the results of "High Performance Mysql" many years ago. Hey. . . . .

The development of Mysql database also tells us to keep an open mind at all times, learn lessons and face up to shortcomings and improve, so as not to be gradually eliminated by the times.

5. Loose indexing

​ Loose indexes have been supported after Mysql 5.6. A simple understanding of loose indexes is that when performing multi-column index scans, even if the secondary index is not ordered, the skip index is ordered, and the index can be used to quickly match data. .

  松散索引的优化细节放到了下半部分的文章,这里简单讲述一下大致的工作原理。

  1. Query while updating data

​ In Postgresql, the following syntax is supported:

 update tbl_info
set name = tmp.name
from 
(select name from tbl_user where name ='xxx')
tmp
[where ....]

-- 比如下面的写法:
UPDATE `sakila`.`actor` SET `first_name` = 'PENELOPE'
from 
(select address,address_id from address where address_id = 1) tmp
 WHERE `actor_id` = 1 and actor.actor_id = tmp.address_id;

​ But unfortunately, this syntax cannot be implemented or supported in Mysql. Even if it reaches 8.0.26, it is still not supported, which has an essential relationship with the optimizer design of Mysql.

  1. Optimizer Hint Settings

Optimizer hints don't make much sense, so I just skip them here.

  1. Maximum and minimum optimization

​ From the actual situation, the two functions of Mysql maximum and minimum are not used a lot, so they will not be introduced. In addition, no matter what kind of database, it is not recommended to use functions frequently, but to use business + simple SQL instead. Implement efficient index optimization.

Other slow query optimizations

​ For the optimization of slow queries, we need to be clear that optimization is divided into several categories. In Mysql, optimization strategies are divided into dynamic optimization and static optimization : static optimization is mainly to optimize better writing methods, such as constant sorting and some fixed optimizations policies, etc., these actions can usually be completed in one optimization process. The dynamic optimization strategy is much more complicated, it may be optimized during the execution process, and the execution plan may be re-evaluated after execution.

​ Static optimization is affected by the optimizer, and different versions have different situations, so here is the situation of dynamic optimization, and dynamic optimization mainly includes the following contents:

  • The order of the association table, sometimes the order of the association table and the query order are not necessarily the same.
  • Rewrite outer join to inner join: If an outer join association is unnecessary, optimize out the outer join association.
  • Equivalent substitutions, such as a>5 and a= 5 are optimized to a >= 5 , similar to mathematical logic formula simplification
  • Optimize count(), max(), min() and other functions: sometimes you only need to find the largest and smallest index records to find the maximum and minimum values. At this time, because you do not need to traverse, you can think that the way to obtain records is directly for the hash. , so it is reflected in the extra of the query analysis (Select tables optimized away), for example: explain select max(actor_id) from actor;
  • Estimation and conversion constants: Take join query as an example, if the number of associated records can be estimated in the query condition, then it may be optimized by the optimizer as a constant for an associated query, because the records are retrieved in advance. The number of bars is known by the optimizer. So optimization is very simple.
  • Subquery optimization: Although subqueries may be optimized by indexes, they should be avoided as much as possible.
  • Covering index scan: Make the index and query columns consistent, which is a very efficient optimization and execution method
  • Early termination of the query: Early termination of the query means that when some query conditions are encountered, the query will be completed in advance, and the optimizer will judge in advance to speed up data matching and search speed
  • Equal value pass, if the range query can be optimized according to the relation table query, then the data can be searched directly without explicit hints.

References:

Here is a summary of some of the references that appear in the article:

write at the end

​ The first half is based on theory, and the second half will focus on practical content.


Xander
201 声望53 粉丝