本文选自《Pro SQL Server Internals》
作者: Dmitri Korotkevitch
出版社: Apress
出版年: 2016-12-29
作者简介:Dmitri Korotkevitchis是微软SQL Server MVP和微软认证大师。作为应用程序和数据库开发人员、数据库管理员和数据库架构师,他具有多年使用SQL Server的经验。他专门从事OLTP系统在高负载下的设计、开发和性能调优。Dmitri经常在各种Microsoft和SQL PASS活动上发言,他为世界各地的客户提供SQL Server培训。
原文链接:http://www.doc88.com/p-4042504089228.html
CHAPTER 2 ■ TABLES AND INDEXES: INTERNAL STRUCTURE AND ACCESS METHODS
第2章■表与索引:内部结构和访问方法
Figure 2-4. Forwarding pointers and I/O: Reading data when forwarding pointers exist
图形2-4。转发指针和I/O:在转发指针时读取数据
As you can see, the large number of forwarding pointers leads to extra I/O operations and significantly reduces the performance of the queries accessing the data. Companion materials for this book include the script that demonstrates this problem in a large scope with a table that includes a large amount of data. When the size of the forwarded row is reduced by another update and the data page with forwarding pointer has enough space to accommodate the updated version of the row, SQL Server may move it back to its original data page and remove the forwarding pointer row. Nevertheless, the only reliable way to get rid of all of the forwarding pointers is by rebuilding the heap table. You can do that by using an ALTER TABLE REBUILD statement. Heap tables can be useful in staging environments, where you want to import a large amount of data into the system as fast as possible. Inserting data into heap tables can often be faster than inserting it into tables with clustered indexes. Nevertheless, during a regular workload, tables with clustered indexes usually outperform heap tables, which have suboptimal space control and extra I/O operations introduced by forwarding pointers.
正如你所看到的,大量的转发指针会导致额外的I/O操作,并显著地降低访问数据的查询的性能。本书的附带材料包括脚本,该脚本通过一个包含大量数据的表在大范围内演示这个问题。当通过另一次更新减小了转发行的大小并且具有转发指针的数据页有足够的空间来容纳行的更新版本时,SQL Server可以将其移动回其原始数据页并删除转发指针行。然而,消除所有转发指针的唯一可靠方法是重新构建堆表。可以通过使用ALTER表重建语句来实现这一点。堆表在希望尽快将大量数据导入系统的暂存环境中非常有用。将数据插入堆表通常比将其插入具有集群索引的表更快。然而,在常规工作负载期间,具有集群索引的表通常优于具有次优空间控制和由转发指针引入的额外I/O操作的堆表。
Clustered Indexes
聚集索引
A clustered index dictates the physical order of the data in a table, which is sorted according to the clustered index key. The table can have only one clustered index defined. Let’s assume that you want to create a clustered index on the heap table with the data. As a first step, which is shown in Figure 2-5 , SQL Server creates another copy of the data that is then sorted based on the value of the clustered key. The data pages are linked in a double-linked list where every page contains pointers to the next and previous pages in the chain. This list is called the leaf level of the index, and it contains the actual table data.
聚集索引指示表中数据的物理顺序,该表根据聚集索引键进行排序。该表只能定义一个聚集索引。假设您想要在具有数据的堆表上创建集群索引。作为第一步,如图2-5所示,SQL Server创建数据的另一个副本,然后根据集群密钥的值对其进行排序。数据页链接在双链接列表中,其中每个页都包含指向链中的下一页和前一页的指针。这个列表称为索引的叶子级别,它包含实际的表数据。
CHAPTER 2 ■ TABLES AND INDEXES: INTERNAL STRUCTURE AND ACCESS METHODS
第2章表和索引:内部结构和访问方法
Figure 2-5. Clustered index structure: Leaf level
图2-5。聚集索引结构:叶级
Note The sort order on the page is controlled by a slot array. Actual data on the page is unsorted.
注释 页上的排序顺序由时隙数组控制。页面上的实际数据未排序。
When the leaf level consists of multiple pages, SQL Server starts to build an intermediate level of the index, as shown in Figure 2-6 .
当叶级包含多个页面时,SQL Server开始构建索引的中间级别,如图2-6所示。
Figure 2-6. Clustered index structure: Intermediate and leaf levels
图2-6。聚集索引结构:中间和叶级
The intermediate level stores one row per leaf-level page. It stores two pieces of information: the physical address and the minimum value of the index key from the page it references. The only exception is the very first row on the first page, where SQL Server stores NULL rather than the minimum index key value. With such optimization, SQL Server does not need to update non-leaf-level rows when you insert the row with the lowest key value in the table. The pages on the intermediate levels are also linked to the double-linked list. SQL Server adds more and more intermediate levels until there is a level that includes just the single page. This level is called the root level , and it becomes the entry point to the index, as shown in Figure 2-7 .
中间级存储每个叶级页的一行。它存储两段信息:物理地址和它所引用的页面中的索引键的最小值。唯一的例外是第一页的第一行,其中SQL Server存储NULL而不是最小索引键值。通过这种优化,当在表中插入键值最低的行时,SQL Server不需要更新非叶级行。中间层次的页面也链接到双链表。SQL Server添加了越来越多的中间级别,直到有一个仅包含单个页面的级别。这个级别称为根级别,并且它成为索引的入口点,如图2-7所示。
CHAPTER 2 ■ TABLES AND INDEXES: INTERNAL STRUCTURE AND ACCESS METHODS
第2章 ■ 表和索引:内部结构和访问方法
Figure 2-7. Clustered index structure: Root level
图2-7。聚集索引结构:根级
As you can see, the index always has one leaf level, one root level, and zero or more intermediate levels. The only exception is when the index data fits into a single page. In that case, SQL Server does not create the separate root-level page, and the index consists of just the single leaf-level page. The number of levels in the index largely depends on the row and index key sizes. For example, the index on the 4-byte integer column will require 13 bytes per row on the intermediate and root levels. Those 13 bytes consist of a 2-byte slot-array entry, a 4-byte index-key value, a 6-byte page pointer, and a 1-byte row overhead, which is adequate because the index key does not contain variable-length and NULL columns. As a result, you can accommodate 8,060 bytes / 13 bytes per row = 620 rows per page. This means that, with the one intermediate level, you can store information about up to 620 * 620 = 384,400 leaf-level pages. If your data row size is 200 bytes, you can store 40 rows per leaf-level page and up to 15,376,000 rows in the index with just three levels. Adding another intermediate level to the index would essentially cover all possible integer values.
如您所见,索引总是具有一个叶子级别、一个根级别和零个或多个中间级别。唯一的例外是当索引数据适合单个页面时。在这种情况下,SQL Server不创建单独的根级页面,并且索引仅由单个叶级页面组成。索引中的级别数量很大程度上取决于行和索引键的大小。例如,4字节整数列上的索引在中间层和根层上每行需要13个字节。这13个字节由一个2字节的时隙数组条目、一个4字节的索引键值、一个6字节的页指针和一个1字节的行开销组成,这足够了,因为索引键不包含可变长度和NULL列。因此,您可以容纳每行8060字节/13字节=每页620行。这意味着,使用一个中间级别,可以存储最多620*620=384400个叶级页面的信息。如果数据行大小是200字节,那么可以在索引中存储每页40行,最多15376000行,只有三个级别。向索引中添加另一个中间级别将基本上覆盖所有可能的整数值。
Note In real life, index fragmentation would reduce those numbers. We will talk about index fragmentation in Chapter 6 .
注意 在实际生活中,索引碎片会减少这些数字。我们将在第6章中讨论索引碎片。
There are three different ways in which SQL Server can read data from the index. The first one is by an ordered scan. Let’s assume that we want to run the SELECT Name FROM dbo.Customers ORDER BY CustomerId query. The data on the leaf level of the index is already sorted based on the CustomerId column value. As a result, SQL Server can scan the leaf level of the index from the first to the last page and return the rows in the order in which they were stored. SQL Server starts with the root page of the index and reads the first row from there. That row references the intermediate page with the minimum key value from the table. SQL Server reads that page and repeats the process until it finds the first page on the leaf level. Then, SQL Server starts to read rows one by one, moving through the linked list of the pages until all rows have been read. Figure 2-8 illustrates this process.
SQL Server可以以三种不同的方式从索引读取数据。第一个是通过有序扫描。假设我们要运行SELECTNamefromdbo.Customers ORDER BY CustomerId查询。索引的叶级数据已经基于CustomerId列值进行了排序。因此,SQL Server可以扫描索引从第一页到最后一页的叶子级别,并按照存储行的顺序返回这些行。SQL Server从索引的根页开始,然后从那里读取第一行。该行引用具有最小键值的中间页从表中引用。SQLServer读取该页并重复该过程,直到找到叶级上的第一页为止。然后,SQLServer开始逐行读取行,遍历页的链表,直到读取所有行为止。图2-8说明了这一过程。
CHAPTER 2 ■ TABLES AND INDEXES: INTERNAL STRUCTURE AND ACCESS METHODS
第2章表和索引:内部结构和访问方法
Figure 2-8. Ordered index scan
图2-8。有序索引扫描
The execution plan for the preceding query shows the Clustered Index Scan operator with the Ordered property set to true, as shown in Figure 2-9 .
前面查询的执行计划显示了具有Ordered属性设置为true的集群索引扫描操作符,如图2-9所示。
Figure 2-9. Ordered index scan execution plan
图2-9。有序索引扫描执行计划
It is worth mentioning that the order by clause is not required for an ordered scan to be triggered. An ordered scan just means that SQL Server reads the data based on the order of the index key. SQL Server can navigate through indexes in both directions, forward and backward. However, there is one important aspect that you must keep in mind: SQL Server does not use parallelism during backward index scans.
值得一提的是,触发有序扫描不需要order by子句。有序扫描仅仅意味着SQL Server根据索引键的顺序读取数据。SQL Server可以在两个方向上(向前和向后)浏览索引。但是,您必须记住一个重要方面:SQL Server在反向索引扫描期间不使用并行性。
CHAPTER 2 ■ TABLES AND INDEXES: INTERNAL STRUCTURE AND ACCESS METHODS
第2章 ■ 表和索引:内部结构和访问方法
■ Tip You can check scan direction by examining the INDEX SCAN or INDEX SEEK operator properties in the execution plan. Keep in mind, however, that Management Studio does not display these properties in the graphical representation of the execution plan. You need to open the Properties window to see it by selecting the operator in the execution plan and choosing the View/Properties Window menu item or by pressing the F4 key.
■提示 您可以通过检查执行计划中的INDEX SCAN或INDEX SEEK操作符属性来检查扫描方向。但是,请记住,Management Studio不会在执行计划的图形表示中显示这些属性。您需要打开“属性”窗口以通过在执行计划中选择运算符并选择“查看/属性”窗口菜单项或按F4键来查看该属性窗口。
The Enterprise Edition of SQL Server has an optimization feature called merry-go-round scan that allows multiple tasks to share the same index scan. Let’s assume that you have session S1, which is scanning the index. At some point in the middle of the scan, another session, S2, runs a query that needs to scan the same index. With a merry-go-round scan, S2 joins S1 at its current scan location. SQL Server reads each page only once, passing rows to both sessions. When the S1 scan reaches the end of the index, S2 starts scanning data from the beginning of the index until the point where the S2 scan started. A merry-go-round scan is another example of why you cannot rely on the order of the index keys and why you should always specify an ORDER BY clause when it matters. The next access method after the ordered scan is called an allocation order scan. S QL Server accesses the table data through the IAM pages, similar to how it does so with heap tables. The SELECT Name FROM dbo.Customers WITH (NOLOCK) query and Figure 2-10 illustrate this method. Figure 2-11 shows the query execution plan.
SQL Server的企业版有一个名为merry-go-.scan的优化特性,允许多个任务共享相同的索引扫描。假设您有会话S1,它正在扫描索引。在扫描中间的某个时刻,另一个会话S2运行需要扫描相同索引的查询。通过旋转木马扫描,S2在其当前扫描位置连接S1。SQLServer只读取每个页面一次,将行传递给两个会话。当S1扫描到达索引的结束时,S2从索引的开始开始扫描数据,直到S2扫描开始的点。旋转木马扫描是另一个示例,它说明了为什么您不能依赖于索引键的顺序,以及为什么在需要时应该始终指定ORDER BY子句。排序扫描之后的下一个访问方法称为分配顺序扫描。S QL Server通过IAM页面访问表数据,类似于使用堆表的方式。SELECTNamefromdbo.Customers WITH(NOLOCK)查询和图2-10说明了这种方法。图2-11显示了查询执行计划。
Figure 2-10. Allocation order scan
图2-10。分配顺序扫描
CHAPTER 2 ■ TABLES AND INDEXES: INTERNAL STRUCTURE AND ACCESS METHODS
第2章 ■ 表和索引:内部结构和访问方法
Figure 2-11. Allocation order scan execution plan
图2-11。分配顺序扫描执行计划
Unfortunately, it is not easy to detect when SQL Server uses an allocation order scan. Even though the Ordered property in the execution plan shows false , it indicates that SQL Server does not care whether the rows were read in the order of the index key, not that an allocation order scan was used.
不幸的是,很难检测SQL Server何时使用分配顺序扫描。尽管执行计划中的Ordered属性显示为false,但它表明SQL Server并不关心行是否按索引键的顺序读取,而不关心使用分配顺序扫描。分配顺序扫描可以更快地扫描大表,尽管它有更高的启动成本。
An allocation order scan can be faster for scanning large tables, although it has a higher startup cost. SQL Server does not use this access method when the table is small. Another important consideration is data consistency. SQL Server does not use forwarding pointers in tables that have a clustered index, and an allocation order scan can produce inconsistent results. Rows can be skipped or read multiple times due to the data movement caused by page splits. As a result, SQL Server usually avoids using allocation order scans unless it reads the data in READ UNCOMMITTED or SERIALIZABLE transaction-isolation levels.
当表小时,SQLServer不使用此访问方法。另一个重要的考虑因素是数据一致性。SQL Server在具有集群索引的表中不使用转发指针,并且分配顺序扫描可能产生不一致的结果。由于分页导致的数据移动,可以多次跳过或读取行。因此,SQLServer通常避免使用分配顺序扫描。除非它以READ UNCOMMITTED或SERIALIZABLE事务隔离级别读取数据。
■ Note We will talk about page splits and fragmentation in Chapter 6 , “Index Fragmentation,” and discuss locking and data consistency in Part III, “Locking, Blocking, and Concurrency.”
■注意 我们将在第6章“索引分段”中讨论分页和分段,在第三部分“锁定、阻塞和并发”中讨论锁定和数据一致性。
The last index access method is called index seek . The SELECT Name FROM dbo.Customers WHERE CustomerId BETWEEN 4 AND 7 query and Figure 2-12 illustrate the operation.
最后一个索引访问方法称为索引查找。从dbo.Customers WHERE CustomerId BETWEEN 4和7查询的SELECT Name以及图2-12说明了该操作。
Figure 2-12. Index seek
图2-12。索引查找
CHAPTER 2 ■ TABLES AND INDEXES: INTERNAL STRUCTURE AND ACCESS METHODS
第2章 ■ 表和索引:内部结构和访问方法
In order to read the range of rows from the table, SQL Server needs to find the row with the minimum value of the key from the range, which is 4. SQL Server starts with the root page, where the second row references the page with the minimum key value of 350. It is greater than the key value that we are looking for (4), and SQL Server reads the intermediate-level data page (1:170) referenced by the first row on the root page.
为了从表中读取行的范围,SQL Server需要从范围中找到具有最小键值的行,即4。SQL Server从根页面开始,其中第二行引用具有最小键值350的页面。它大于我们正在寻找的键值(4),并且SQL Server读取由根页面上的第一行引用的中间级数据页(1:170)。
Similarly, the intermediate page leads SQL Server to the first leaf-level page (1:176). SQL Server reads that page, then it reads the rows with CustomerIds equal to 4 and 5, and, finally, it reads the two remaining rows from the second page.
类似地,中间页面将SQL Server引导到第一个叶级页面(1:176)。SQLServer读取然后,它读取CustomerIds等于4和5的行,最后,它从第二页读取剩余的两行。
The execution plan is shown in Figure 2-13 .
执行计划如图2-13所示。
Figure 2-13. Index seek execution plan
图2-13。索引查找执行计划
As you can guess, index seek is more efficient than index scan, because SQL Server processes just the subset of rows and data pages rather than scanning the entire table.
可以猜到,索引查找比索引扫描更有效,因为SQL Server只处理行和数据页的子集,而不扫描整个表。
Technically speaking, there are two kinds of index seek operations. The first is called a singleton lookup , or sometimes point-lookup , where SQL Server seeks and returns a single row. You can think about WHERE CustomerId = 2 predicate as an example. The other type of index seek operation is called a range scan , and it requires SQL Server to find the lowest or highest value of the key and scan (either forward or backward) the set of rows until it reaches the end of scan range. The predicate WHERE CustomerId BETWEEN 4 AND 7 leads to the range scan. Both cases are shown as INDEX SEEK operations in the execution plans.
从技术上讲,有两种索引查找操作。第一个称为单例查找,有时称为点查找,其中SQL Server查找并返回一行。您可以想到Cuffer-Id= 2谓词在哪里。另一种类型的索引查找操作称为范围扫描,它要求SQL Server查找键的最低值或最高值,并扫描(向前或向后)行集合,直到到达扫描范围的末尾。客户机ID介于4和7之间的谓词导致范围扫描。两种情况都显示为执行计划中的索引查找操作。
As you can guess, it is entirely possible for range scans to force SQL Server to process a large number or even all data pages from the index. For example, if you changed the query to use a WHERE CustomerId > 0 predicate, SQL Server would read all rows/pages, even though you would have an Index Seek operator displayed in the execution plan. You must keep this behavior in mind and always analyze the efficiency of range scans during query performance tuning.
正如您所猜到的,范围扫描完全可能迫使SQL Server处理索引中的大量数据页甚至所有数据页。例如,如果将查询更改为使用WHERE CustomerId>0谓词,SQL Server将读取所有行/页,尽管在执行计划中将显示Index Seek操作符。您必须牢记这种行为,并在查询性能优化期间始终分析范围扫描的效率。
There is a concept in relational databases called SARGable predicates , which stands for Search Argument able . The predicate is SARGable if SQL Server can utilize an index seek operation, if an index exists. In a nutshell, predicates are SARGable when SQL Server can isolate the single value or range of index key values to process, thus limiting the search during predicate evaluation. Obviously, it is beneficial to write queries using SARGable predicates and utilize index seek whenever possible.
在关系数据库中有一个称为SARGable谓词的概念,它代表Search Argument able。如果SQL Server可以利用索引查找操作(如果存在索引),则谓词是SARGable。简言之,当SQL Server可以隔离要处理的索引键值的单个值或范围时,谓词就是SARGable,从而限制了谓词评估期间的搜索。显然,使用SARGable谓词编写查询并尽可能利用索引查找是有益的。
SARGable predicates include the following operators: = , > , >= , < , <= , IN , BETWEEN , and LIKE (in case of prefix matching). Non-SARGable operators include NOT , <> , LIKE (in case of non-prefix matching), and NOT IN .
SARGable谓词包括下列操作符:=、>、>=、<、<=、IN、BETWEEN和LIKE(在前缀匹配的情况下)。非SARGable操作符包括NOT、<>、LIKE(在非前缀匹配的情况下)和NOT IN。
Another circumstance for making predicates non-SARGable is using functions or mathematical calculations against the table columns. SQL Server has to call the function or perform the calculation for every row it processes. Fortunately, in some of cases you can refactor the queries to make such predicates SARGable. Table 2-1 shows a few examples of this.
使谓词不可SARGable的另一种情况是对表列使用函数或数学计算。SQL Server必须调用该函数或对其处理的每行执行计算。幸运的是,在某些情况下,您可以重构查询以使得这样的谓词SARGable。表2-1显示了一些例子。
CHAPTER 2 ■ TABLES AND INDEXES: INTERNAL STRUCTURE AND ACCESS METHODS
第2章 ■ 表和索引:内部结构和访问方法
Another important factor that you must keep in mind is type conversion . In some cases, you can make predicates non-SARGable by using incorrect data types. Let’s create a table with a varchar column and populate it with some data, as shown in Listing 2-6 .
你必须记住的另一个重要因素是类型转换。在某些情况下,可以使用不正确的数据类型使谓词不可SARGable。让我们创建一个包含varchar列的表,并用一些数据填充它,如清单2-6所示。
Listing 2-6. SARG predicates and data types: Test table creation
清单2-6。SARG谓词和数据类型:测试表创建
The clustered index key column is defined as varchar, even though it stores integer values. Now, let’s run two selects, as shown in Listing 2-7 , and look at the execution plans.
集群索引键列被定义为varchar,即使它存储整数值。现在,让我们运行两个选择,如清单2-7所示,并查看执行计划。
CHAPTER 2 ■ TABLES AND INDEXES: INTERNAL STRUCTURE AND ACCESS METHODS
第2章 ■ 表和索引:内部结构和访问方法
Listing 2-7. SARG predicates and data types: Select with integer parameter
清单2-7。SARG谓词和数据类型:用整数参数选择
declare
@IntParam int = ‘200‘
select * from dbo.Data where VarcharKey = @IntParam;
Select * from dbo.Data where VarcharKey = convert(varchar(10),@IntParam);
As you can see in Figure 2-14 , in the case of the integer parameter, SQL Server scans the clustered index, converting varchar to an integer for every row. In the second case, SQL Server converts the integer parameter to a varchar at the beginning and utilizes a much more efficient clustered index seek operation.
如图2-14所示,在整数参数的情况下,SQL Server扫描集群索引,将每行的varchar转换为整数。在第二种情况下,SQL Server在开始时将整数参数转换为varchar,并利用更有效的集群索引查找操作。
Figure 2-14. SARG predicates and data types: Execution plans with integer parameter
图2-14。SARG谓词和数据类型:整数参数的执行方案
■ Tip Pay attention to the column data types in the join predicates. Implicit or explicit data type conversions can significantly decrease the performance of the queries.
■ 提示 注意连接谓词中的列数据类型。隐式或显式数据类型转换可以显著降低查询的性能。
You will observe very similar behavior in the case of unicode string parameters. Let’s run the queries shown in Listing 2-8 . Figure 2-15 shows the execution plans for the statements.
在unicode字符串参数的情况下,您将观察到非常类似的行为。让我们运行清单2-8所示的查询。图2-15显示了语句的执行计划。
Listing 2-8. SARG predicates and data types: Select with string parameter
清单2-8。SARG谓词和数据类型:用字符串参数选择
select * from dbo.Data where VarcharKey = ‘200‘;
select * from dbo.Data where VarcharKey = N‘200‘; -- unicode parameter
CHAPTER 2 ■ TABLES AND INDEXES: INTERNAL STRUCTURE AND ACCESS METHODS
第2章■ 表和索引:内部结构和访问方法
Figure 2-15. SARG predicates and data types: Execution plans with string parameter
图2-15。SARG谓词和数据类型:具有字符串参数的执行计划
As you can see, a unicode string parameter is non-SARGable for varchar columns. This is a much bigger issue than it appears to be. While you rarely write queries in this way, as shown in Listing 2-8 , most application development environments nowadays treat strings as unicode. As a result, SQL Server client libraries generate unicode ( nvarchar ) parameters for string objects unless the parameter data type is explicitly specified as varchar . This makes the predicates non-SARGable, and it can lead to major performance hits due to unnecessary scans, even when varchar columns are indexed.
正如您所看到的,Unicode字符串参数对于VARCHAR列是非SGARABLE。这是一个比看上去更大的问题。虽然很少以这种方式编写查询,如清单2-8所示,但是现在大多数应用程序开发环境将字符串视为unicode。因此,SQL Server客户端库为字符串对象生成unicode(nvarchar)参数,除非参数数据类型明确指定为varchar。这就使得谓语不可理解,它可以导致主语。即使对varchar列进行索引,由于不必要的扫描也会导致性能下降。
■ Important Always specify parameter data types in client applications. For example, in ADO.Net, use Parameters.Add("@ParamName",SqlDbType.Varchar, <Size>).Value = stringVariable instead of Parameters.Add("@ParamName").Value = stringVariable overload. Use mapping in ORM frameworks to explicitly specify non-unicode attributes in the classes.
■ 重要的是始终在客户端应用程序中指定参数数据类型。例如,在ADO.NET中,使用Parameters.Add("@ParamName",SqlDbType.Varchar, <Size>).Value = stringVariable instead of
Parameters.Add("@ParamName").Value = stringVariable overload. 在ORM框架中使用映射来显式地指定类中的非unicode属性。
It is also worth mentioning that varchar parameters are SARGable for nvarchar unicode data columns.
还值得一提的是,对于nvarchar unicode数据列,varchar参数是SARGable。
Composite Indexes
综合指数
Indexes with multiple key columns are called composite (or compound) indexes . The data in the composite indexes is sorted on a per-column basis from leftmost to rightmost columns. Figure 2-16 shows the structure of a composite index.
具有多个键列的索引称为复合(或复合)索引。复合索引中的数据按每列从最左到最右的列进行排序。图2-16显示了复合索引的结构。
原文地址:https://www.cnblogs.com/hsp9/p/10048242.html