Teroy’s Blog


  • 首页

  • 归档

浏览器如何验证https证书的可信度

发表于 2016-11-24 |

部署https站点的时候,服务器通常需要两个东西:https证书和私钥。在浏览器与服务器发起https通讯的时候,服务器会返回部署的https证书给浏览器,那么浏览器是如何确定这个https证书是可以信任的呢?

关于https证书格式

https证书是带有站点信息的x509证书,在浏览器中可以导出后缀名为crt证书,携带的信息通常有:唯一序列号、证书发行机构、发行机构公钥、有效时间段、证书签名算法、证书签名等。

浏览器的检验过程

一般情况下,浏览器会内置受信任证书发布结构的公钥,所以就可以用这个公钥来校验证书的可信度。首先来看一张图,看一下https证书生成签名的过程:

证书机构拥有自己才知道的私钥,在数字签名函数中加入这个私钥,就生成了这个证书的签名。再来看一张浏览器验证签名过程的图:

由于采用了非对称加解密算法,浏览器用公钥对签名进行解密就可以得到摘要,在与证书生成的摘要进行对比,如果一样就表示这个证书是该机构签发的,因为假如证书是伪造的,那么伪造方是肯定不知道正确的私钥,这样用公钥解密出来的摘要肯定与证书生成的摘要不一致。

.NET源码Queue的实现

发表于 2015-04-03 |

Queue(队列)是一种先进先出的数据结构,其中最核心的两个方法是Enqueue(入队)和Dequeue(出队)两个操作。Queue的实现与Stack也有相似的地方,例如底层的数据结构同样是靠T[] _array数组对象维系着,也是使用了2倍数组扩容的方式。不过,由于队列具有先进先出的特性,它决定了不能像Stack那样只用一个_size来维系栈尾的下标,队列必须有一个队头_head下标和一个队尾_tail下标来保证先进先出的特性。考虑到队列的存储效率,还必须涉及到循环队列的问题,所以Queue的实现会比Stack更为复杂一些,同样来看一个简化版本的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
using System;

namespace OriginalCode
{
/// <summary>
/// 基于.NET源码的简化版实现
/// </summary>
public class Queue<T>
{
private static T[] EMPTY_ARRAY = new T[0];
private const int _defaultCapacity = 4;
private T[] _array;
private int _head; //头位置
private int _tail; //尾位置
private int _size; //队列元素个数

public Queue()
{
_array = EMPTY_ARRAY;
_head = 0;
_tail = 0;
_size = 0;
}

public Queue(int capacity)
{
_array = new T[capacity];
_head = 0;
_tail = 0;
_size = 0;
}

/// <summary>
/// 入队操作
/// </summary>
/// <param name="item">待入队元素</param>
public void Enqueue(T item)
{
if (_size == _array.Length)
{
//确定扩充的容量大小
int capacity = _array.Length * 2;
if (capacity < _array.Length + _defaultCapacity)
{
//.NET源码这样实现的一些基本猜想
//由于可以通过调用Queue(int capacity)实例化队列 capacity可以=1 | 2 | 3
//这里做与+4做判断 应该是为了提高基本性能 比如当capacity = 1的时候 *2 = 2 这样2很快容易有下一次扩充
//不过其实感觉效果并不大 有点设计过度的嫌疑
capacity = _array.Length + _defaultCapacity;
}

//实例化一个容量更大的数组
T[] array = new T[capacity];
if (_size > 0)
{
//当需要重新分配数组内存的时候 根据循环队列的特性 这时的_head一定等于_tail
//从旧数组_array[_head]到_array[_size-1] 复制到 新数组array[0]...[_size - _head - 1]
ArrayCopy(_array, array, 0, _head, _size - _head);
//从旧数组_array[0]到_array[_head-1] 复制到 新数组array[_size - _head]...[_size - 1]
ArrayCopy(_array, array, _size - _head, 0, _head);
}

_array = array; //将旧数组指向新数组
_head = 0; //重新将头位置定格为0
_tail = _size; //重新将尾位置定格为_size
}
_array[_tail] = item;
_tail = (_tail + 1) % _array.Length;
_size += 1;
}

/// <summary>
/// 出队操作
/// </summary>
/// <returns>出队元素</returns>
public T Dequeue()
{
if (_size == 0)
{
throw new Exception("当前队列为空 不能执行出队操作");
}
T result = _array[_head];
_array[_head] = default(T);
_head = (_head + 1) % _array.Length;
_size -= 1;
return result;
}

/// <summary>
/// 将旧数组的项复制到新数组(这个方法是一个模拟实现,实际情况.NET源码底层用C++实现了更高效的复制)
/// </summary>
/// <param name="oldArray">旧数组</param>
/// <param name="newArray">新数组</param>
/// <param name="newArrayBeginIndex">新数组开始项下标</param>
/// <param name="oldArrayBeginIndex">旧数组开始项下标</param>
/// <param name="copyCount">复制个数</param>
private void ArrayCopy(T[] oldArray, T[] newArray, int newArrBeginIndex, int oldArrBeginIndex, int copyCount)
{
for (int i = oldArrBeginIndex, j = newArrBeginIndex; i < oldArrBeginIndex + copyCount; i++,j++)
{
newArray[j] = oldArray[i];
}
}
}
}

首先通过下面的图来看一下数组容量足够的时候,循环队列的执行过程:

基于上面这张图的执行过程,来看一下Dequeue函数的实现。第一步判断的是_size是否为0,是的话就抛出异常。如果当前入队个数大于0,则获取_array[_head]元素作为出队元素,之后就调用default(T)填充_array[_head]的位置。由于是一个循环队列的设计,所以不能简单地将_head+=1,而必须这样_head=(_head+1)%_array.Length,如上图所示,_head有可能指向下标为3的位置,假如这时直接_head += 1变为4的话,就跳出了数组的小标范围,而_head=(_head+1)%_array.Length变为0,则指向了数组最前的位置,实现了循环队列的功能,更好地利用了内存。

接下来看一下Enqueue(T item)函数的实现。承接上图的Queue的状态,假如现在要执行q.Enqueue(“f”)的入队操作,但是很明显数组_array已经满了,那么要怎么办呢?其实原理和Stack的实现类似,也是要通过数组扩容的方式,不过比Stack的数组复制要复杂一些。来继续看图:

与Stack一样,影响Queue性能最大因素是数组扩容以及相应的数组复制操作,同样Queue也提供了一个带初始化容量的构造函数Queue(int capacity),如果我们能估算到队列可能同时存在元素的最大值,就尽量调用这个带capacity的构造函数。

.NET源码Stack的实现

发表于 2015-04-01 |

Stack(栈)是一种后进先出的数据结构,其中最核心的两个方法分别为Push(入栈)和Pop(出栈)两个操作,那么.NET类库是如何实现这种数据结构呢?为了降低学习成本,这里将根据.NET源码的实现,结合其中的核心设计思想,得出一个简化版本的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using System;

namespace OriginalCode
{
/// <summary>
/// 基于.NET源码的简化版实现
/// </summary>
public class Stack<T>
{
private const int _defaultCapacity = 4;
private T[] _array;
private int _size;

public Stack()
{
//默认初始化数组的数量为空
_array = new T[0];
//初始化数组的数量为0
_size = 0;
}

/// <summary>
/// 入栈
/// </summary>
/// <param name="item">入栈的元素</param>
public void Push(T item)
{
if (_size == _array.Length)
{
//数组存储已经满了,需重新分配数组大小
//分配的数组大小为原来的两倍
T[] array = new T[_array.Length == 0 ? _defaultCapacity : 2 * _array.Length];

//将原来的数组Copy到新数组中
Copy(_array, array);

//_array指向新数组
_array = array;
}
_array[_size] = item;
_size += 1;
}

/// <summary>
/// 出栈
/// </summary>
/// <returns>出栈的元素</returns>
public T Pop()
{
if (_size == 0)
{
throw new Exception("栈为空,当前不能执行出栈操作");
}
_size -= 1;
T result = _array[_size];
_array[_size] = default(T);
return result;
}

/// <summary>
/// 将旧数组赋值到新数组(这个方法是一个模拟实现,实际情况.NET源码底层用C++实现了更高效的复制)
/// </summary>
/// <param name="oldArray">旧数组</param>
/// <param name="newArray">新数组</param>
private void Copy(T[] oldArray, T[] newArray)
{
for (int i = 0; i < oldArray.Length; i++)
{
newArray[i] = oldArray[i];
}
}
}
}

必须明确的一点是Stack的底层是靠T[] _array数组对象维系着。首先来看构造函数Stack(),这里做的事情无非就是一些基本的初始化工作,当调用这个无参构造函数的时候,会将_array数组实例化为T[0],同时将一个_size初始化为0。这个_size主要是用来表示当前栈中存在的元素个数,同时也承担起类似数组下标的作用,标识下一个元素入栈的数组位置。

接下来来看一下Push(T item)函数的实现。这里的第一步操作其实就是执行一次判断,判断当前_array数组的元素个数是否已经满了,假如满了的话,就要对数组进行扩充。.NET源码对于数组扩充的设计还是比较巧妙的,当_array为空的时候,默认开始分配的数组个数为4,既new T[4],假如要插入的是第5个元素的时候,这时数组的个数不足,就声明一个新的T[] array,并将个数扩充为_array个数的2倍,之后再将_array元素一个个复制到新的array中,最后将_array字段指向array,就完成了数组扩充的工作。这一步在前面的代码中的实现应该是很清晰的,不过需要注意的一点是这里的Copy(_array,array)函数是我自己的一个简单的实现,跟.NET源码中的实现是很不一样的,.NET源码是调用一个Array.Copy(this._array, 0, array, 0, this._size)的函数,它的底层应该是用C++实现了数组复制的更好的优化。通过一张图来看一下数组扩容的过程:

最后来看一下Pop()函数的实现。首先先判断当前数组的个数是否大于0,小于等于0的话就会抛出异常。之后就将_size-=1,得到要Pop的对象在数组的位置。取出_array[_size]后,就调用default(T)填充_array[_size]的位置,这样做的一个好处是取消对原来的对象的引用,是其能够成为垃圾回收的对象,更好地减少内存的占用。总体而言Pop()实现还是比较简单的。

从前面我们知道,使用Stack数据结构,数组扩容应该是影响性能最大的一个因素。默认情况下,假如要往栈中插入100个对象,意味着数组就要经过4->8->16->32->64->128总共5次的数组扩容,那么有没有什么办法可以改善性能呢?答案是有的,.NET源码Stack对象除了提供默认的无参构造函数外,还提供了一个Stack(int capacity)的构造函数,capacity参数其实就是用表示来初始化数组的个数,假如我们能预料到这次插入栈的对象个数的最大值的话(以100为例),就直接这样调用new Stack(100),这样就能减少不必要的数组扩容,从而提高了Stack的使用性能。

阅读《重构》的一些思考

发表于 2015-02-12 |

终于在断断续续的情况下把这本经典巨作看完了。

这本书的全名叫做《重构-改善既有代码的设计》,原有的代码设计存在不足的地方让人感到不好维护,所以才需要去改善既有代码的设计,其实听起来会不会有点亡羊补牢的感觉?这里也提醒了我们一点:从设计代码的初期就要深思熟虑,虽然后续的改动基本无法避免,但良好的初期设计将对后续维护提供帮助。重构不是最终目的,我们的目的是让代码变得更好。

关于书的内容组织

不得不称赞的一点是作者的写作思路是非常清晰的。整本书的开始是由一个小型案例讲起,让读者了解重构是一项什么样的工作,作者的案例设计也是挺巧妙的,值得不熟悉面向对象编程的读者反复揣摩。之后便开始讲诉重构的一些概念,包括什么是重构(在不改变软件可观察行为的前提下,调整其内部结构,整理代码的一个过程。注意与开发新功能的一个区分,开发新功能是在改变软件可观察行为)、为什么要重构(重构可以提高代码的可维护性)以及什么时候重构(添加功能、修复错误、复审代码的时候)。

接下来就列举了一系列要点讲解不好的代码设计的一些症状。中间携带了关于测试体系的介绍,不过这一部分更多的是稍微提点一下,并没有做比较深入的分析。之后就迎来了整本书的重头戏,关于重构手法的介绍,包括如何组织函数、在对象之间搬移特性(字段、函数)、重新组织数据、简化条件表达式、简化函数调用和处理继承关系。随后也顺带介绍了大型重构的四个手法,包括梳理并分解继承体系、将过程化设计转化为对象设计、将领域与表述分离和提炼继承体系。

本书的最后是对于重构的一个概括,作者认为真正懂得重构的一个衡量标准是”你可以自信地停止重构“,学会重构手法只是一个起点,把握何时使用、何时开始、何时停止的节奏才是使重构走向成功的关键。

重构手法

书中介绍了多种重构手法,这里也不打算将其一一列举出来,而是挑选了部分让我醍醐灌顶的重构手法,以及阅读中自己的一些思考。说实在话,自己在阅读本书的时候并没有做到非常细致,特别是对于具体重构的步骤几乎是一扫而过,读得非常”粗“,可能跟我自己没有真正地处理起大型的遗留代码有关联,思维里总是回响着一股这样的声音”这个重构手法是处理这样的问题,恩,在最开始进行开发的时候就要留意这一点“,从而让我觉得这些重构的具体步骤很繁琐无趣,对于现阶段也没有较大的实用价值。或许等我真正接触到非常多的遗留代码问题,在来重读本书会有新的体会。

1. 把关好命名原则

良好的命名是可读代码的基础,书中对于这个概念也是非常重视的,甚至鼓励开发者在没有找到一个合适的命名前不要继续往下开发。虽然书中并没有讨论如何把握命名方式,但是这里我就班门弄斧一回,简单地谈一下自己的一些看法。

首先要注意的第一个环节是要把握C#开发的基本命名规范,例如私有字段用‘_’开头、函数名称的首个单词最好能是一个动词、不要将变量名称命名为“temp”等,如果我们能做好这最基本的一点,让别人来看你的代码也是那么地清晰,这是不是一件美好的事情呢?所以,假如对于C#开发的基本命名规范还没有概念的话,是时候需要去恶补一下了。

多数情况下开发都离不开业务,所以良好的命名也要结合具体业务。在分析业务的同时也要积累业务相关的词汇,例如Order表示订单,Sku表示库存量单位等,这些词汇在命名的时候可以提供支持。

最后提及的一点策略是借鉴设计模式的词汇。虽然有时候我们不一定会在代码中使用设计模式,但是借鉴某些词汇来表达意图也是可以的,例如Factory、Adapter、Template、Bulider、Singleton、Proxy等,前人已经将经验汇集提供给了我们,我们需要做的就是好好利用它们。

总体而言,就连作者本人也承认的一个观点是“经验会带来更多的帮助”,所以在开发中不仅要重视名称的选择,也要注重经验的积累。

2.使用return跳出多条件表达式

说实在话,这是阅读本书最让我眼前一亮的重构手法了。不知道大家有没有跟我有这样的习惯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public bool IsEnable()
{
bool result = false;
if (条件1)
{
result = true;
}
else
{
if (条件2)
{
result = true;
}
else
{
for (int i = 0; i < length; i++)
{
if (条件3)
{
result = true;
break;
}
}
}
}
return result;
}

这里用伪代码模拟了一下场景,其实有可能实际的函数比这个还复杂,在函数的开头就声明一个result变量,接下来就是各种条件判断对result赋值,最后在统一返回这个result,这就是开发人员对于“统一出口”的思想。其实有时候会不会觉得这样的代码看起来很绕?来看下使用这个手法重构后的代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
public bool IsEnable()
{
if (条件1) return true;

if (条件2) return true;

for (int i = 0; i < length; i++)
{
if (条件3) return true;
}
return false;
}

重构后带来的最大变化是减少了else逻辑,及时地return避免引导读者去看另外一个没有用的else区域,代码变得更加清晰了。

3. 没有一成不变的规则,具体情况具体分析

重构手法更多地像一个工具箱,里面放满了十字螺丝刀,钳子,扳手等,处理代码遗留问题就像修理东西一样,遇到不同的问题就要使用不同的工具。其实书中有时候作者介绍的重构手法是一个对立面,例如将提炼函数和内联函数,一个主张提炼出一个独立的函数,一个提倡不使用独立的函数,所以涉及到具体情况就要具体分析,要从理解重构手法的角度出发,没有绝对的使用定律。

我所认为的书中的不足之处

有些讨论的话题比较泛化,例如6.9节替换算法,“将函数本体替换为另一个算法”,内容描述起来比较空洞,有点类似于告诉我们“嗯,这个函数这样写不好,换一种实现方式更好”,并不能合格地称为一种重构手法。

对于某些重构手法我是保持着中立态度(意思是这种重构手法我不太喜欢这样做),例如6.4节的“以查询取代临时变量”,来看一段书中给出的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 重构前的代码
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.85;

// 重构后的代码
if (CaculateBasePrice() > 1000)
return CaculateBasePrice() * 0.95;
else
return CaculateBasePrice() * 0.85;

double CaculateBasePrice()
{
return _quantity * _itemPrice;
}

有时候为了重用代码,将表达式提炼到一个函数中,可以更好地提高代码复用率。但这个重构手法还有一个着重点:用提炼的函数去替代原来的变量。其实我觉得这里用一个临时变量也并没有什么问题,替换为函数反而可能会出现一些问题:如果表达式是一个复杂的计算,每个使用的地方都调用一遍会降低性能;在阅读代码的时候,如果遇到函数调用的情况,都会将关注点调到函数中去,反而会降低了代码可读的流畅性。基于前面诉说的理由,我个人是不太喜欢实践这个重构手法。

不得不承认的一点,就是这本书的写作时间是比较早的,以至于书中介绍的重构手法的实现步骤是建立于当时开发环境下的,部分做法可以说是已经“过时”了。例如10.1节函数改名,书中介绍了较为复杂的做法,但放在当今的开发环境下,IDE基本已经集成了函数改名这一重构功能,只要输入新的函数名称就能自动替换所有引用到旧函数的地方,已经大大提高了重构效率;6.1节提炼函数也是类似的情况。

不过,这并不阻碍这本书的历史地位。推荐给各位读者,读完后也许你也会对代码重构有新的感悟。

SQLServer覆盖索引

发表于 2014-12-13 |

为了更好地理解覆盖索引,在正式介绍覆盖索引之前,首先稍微来谈一谈有关索引的一些基础知识。

数据页和索引页

在SQLServer中,数据存储的基本单位是页,一页的大小为8KB,分别由页首,数据行和行偏移量组成,如下图结构:

页首固定占用96个字节,用来存储相关的页面系统信息,例如所属的数据库表对象Id等。数据行是真实数据的存储区域,每一行的大小是不固定的。行偏移量是一个数组,数组的每个位置占2个字节,用来存储数据行距离开头的位置偏移量,主要是用来做快速定位,例如想要查找第N行,只要访问行偏移量数组的第N项,就能快速找到数据行所在的位置。索引页和数据页的结构类似,所不同的是索引页的数据行存储的是和索引相关的信息。

聚集索引和非聚集索引

聚集索引定义了表中数据存储的真实物理位置,它是按照指定列的顺序来存储数据的,类比于新华字典中的汉字是按照拼音顺序排列的,所以每张表只能建立一个聚集索引。聚集索引是一棵B+树结构,包含索引页和数据页,最底下的一排叶子节点是数据页,往上则为索引页,来看一张图应该更清晰一些:

非聚集索引是独立于数据真实存储顺序逻辑而存在的,类比于新华字典中按偏旁部首查找汉字的方式。与聚集索引对比,非聚集索引也是B+树的数据结构,但却只包含索引页,而且在一张表中可以建立多个非聚集索引,有关索引的深入分析可以查看这篇文章。同样来看一张非聚集索引的图:

什么是覆盖索引

覆盖索引是在SQLServer2005中引入的概念,只能建立在非聚集索引的基础上,通常情况下,非聚集索引的索引页是不包含真实数据的,只存储着指向数据页中数据行的指针,而覆盖索引则是通过将数据存储在索引页上,从而在查找对应数据的时候,只要找到索引页就可以访问到数据,无需再去查询数据页,所以说这个索引是数据“覆盖”的。

1
2
3
4
--覆盖索引的创建是在非聚集索引创建的基础上增加INCLUDE语句
CREATE NONCLUSTERED INDEX {index_name}
ON {table_name}(column_name...) --非聚集索引可以声明指定多个列作为索引项
INCLUDE(column_name...) --覆盖索引可以指定多个列存储在索引页上

覆盖索引分析

这一小节将通过创建覆盖索引以及使用DBCC命令查看索引的方式进行介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
IF DB_ID('Test') IS NULL
BEGIN
CREATE DATABASE Test;
END
GO

USE Test;
GO

IF OBJECT_ID('t1','U') IS NULL
BEGIN
CREATE TABLE t1
(
t1_id INT NOT NULL IDENTITY(1,1),
t1_name VARCHAR(20) NOT NULL,
t1_name1 VARCHAR(20) NOT NULL,
t1_name2 VARCHAR(20) NOT NULL,
t1_name3 VARCHAR(20) NOT NULL,
t1_name4 VARCHAR(20) NOT NULL,
t1_name5 VARCHAR(20) NOT NULL
);
END

/*
*插入测试数据
*/
INSERT INTO t1
( t1_name,
t1_name1,
t1_name2,
t1_name3,
t1_name4,
t1_name5 )
SELECT 'name',
'name',
'name',
'name',
'name',
'name'
FROM sysobjects o1
CROSS JOIN sysobjects o2;

/*
*创建覆盖索引
*/
IF NOT EXISTS(SELECT 1
FROM sysindexes
WHERE name='idx_t1_id')
BEGIN
CREATE NONCLUSTERED INDEX idx_t1_id
ON t1(t1_id)
INCLUDE(t1_name);
END

执行CROSS JOIN插入的测试数据有4000条左右,现在可以使用DBCC命令来查看表的数据页和索引页的情况。

1
2
3
4
5
6
/*
*查看页的基本信息
*前提条件:表中必须插入了数据
*所需参数:(数据库名,表名,-1表示显示全部IAM页,数据页, 索引页)
*/
DBCC IND (Test,t1,-1);

执行完这条命令后,应该可以看到显示的页信息,其中PageType=1的行表示数据页,PageType=2的行表示索引页,任意选择一条PageType=2的行,找到PageFID和PagePID,就可以使用DBCC命令来查看索引页的具体信息。

1
2
3
4
5
/*
*查看索引页的基本信息
*所需参数:(数据库名,PageFID,PagePID,3表示输出每行每列的信息)
*/
DBCC PAGE(Test,1,7732,3);

执行完这条命令后,应该可以看到t1_name这一列的信息是包含在这个索引页中的。现在可以通过执行不同的查询SQL来查看覆盖索引所带来的性能提升,在执行SQL的同时开启显示实际的执行计划,从而可以清楚得看到对比结果。

1
2
3
4
5
6
7
SELECT t1_name
FROM t1
WHERE t1_id = 500;

SELECT t1_name1
FROM t1
WHERE t1_id =500;


查询1开销为33%,而查询2的开销为67%,对比可以看到查询t1_name的开销比查询t1_name1的开销小很多,因为查询t1_name只需要执行索引,就可以在索引页上找到数据,而查询t1_name1还要去查找数据页。

覆盖索引的思考

创建索引能带来查询的优化,但却带来了更改数据的负担,覆盖索引也不意外。由上面的分析我们知道,覆盖索引是非聚集索引的进一步细化,在更新数据的时候,如果涉及到覆盖索引INCLUDE的列,除了更改数据页之外还要更改索引页,比单纯使用非聚集索引增添了额外的工作。所以,在设计覆盖索引的时候,要综合考虑应该覆盖的列,确保INCLUDE的列能带来最佳的性能优化。

12
Teroy

Teroy

15 日志
10 标签
GitHub 邮箱
© 2018 Teroy