红俊's profile蝈蝈俊的共享空间PhotosBlogListsMore Tools Help

Blog


    January 24

    八年

           昨天,由Sisley Lin 处得知,我将被推荐成了“十大杰出开发技术英雄”候选人之一,有点激动。推荐的理由是我为CSDN 这个最大的中文技术社区的成长所贡献的这些年。当我回首看自己在CSDN经历时,发现自己已为CSDN公司工作了7年半,在CSDN 论坛活动了8年。蓦然回首,八年光阴已匆匆而过,心中真是感慨万千呀。

           记得那是在99年底,2000年初时,我从大学毕业刚半年,跟随一个我的铁哥们跳槽到一家新成立的电子商务的网站(http://www.ego88.com/, 不过这个网站现在已经无法访问了)。由于之前我是在一家使用Delphi开发ERP,医院信息系统的公司,我对Delphi非常感兴趣,当时最常去的就是大富翁论坛(http://www.delphibbs.com/)。经这次跳槽后,由于转行做网站,就开始学习ASP技术。在大富翁论坛时,听说新开了一个程序员的论坛CSDN,而CSDN有个板块在讨论ASP,于是乎,就整天泡CSDN论坛的ASP版,边学习,边帮别人解答问题。慢慢的,自己的ASP水平也提高起来了,后来就成了ASP版的最早一任版主。那时候,也达到了自己在CSDN专家榜的最高排名——CSDN总专家榜排名13。

           2000年6月时,由于我在EGO88网站经常跟技术经理发生技术争执,被开除了。痛定思痛之后,我决定离开杭州,来北京发展,没想到这次离开成了我人生的转折点。恰巧当时CSDN网站刚刚被百联集团收购,准备扩展业务,正在不断招纳新人,于是我便和曾登高联系了一下, 7月15日就来到了北京,从此就开始了我为CSDN网站工作的历程。

           还记得当时CSDN公司在亚运村,就是现在鸟巢的位置,而我就租住在旁边的惠忠北里小区。每天早上,我都从小区出来,向北跑步,就是现在的大屯路一带,全是麦田和菜地。不想经过这五六年的发展,现在这一片的房价均价已达到一万元以上…….

           我在CSDN负责的第一个项目是文档中心(http://dev.csdn.net/ 现在这里已经不归我负责了,也不是我当初做的那个风格了),就是现在博客的前身,不知道还有多少人记得这个产品。当时为了测试和运作文档中心,我从2000年7月开始,疯狂转贴了很多文章。而文档中心文章在升级成博客后,全部迁移到博客中了,这还一度造成我的博客发文数是第一。

           随后,在从事了其他一些小的工作后,我就接手了CSDN论坛,当时那一版本的论坛仍然是ASP开发的,采用了双缓存机制,同时静态帖子采用了XML+XSL技术。说起来你可能不相信,那一个版本的论坛是完全由我一个人开发完成的。 那可是单枪匹马闯五关的时代呀,由于我最早使用XML技术,最初时有些技术难题没能解决,一度被Boss批判说,不要使用不成熟的技术。2003年时,就是我创建的这个版本的论坛达到了历史最高在线人数,同时发帖量,回复量都相当高。要知道,当时的论坛只是两台服务器,一台WEB,一台数据库。当时服务器的配置也只是2G内存+双CPU,志强2.4G,能做到那个成绩实属不易。

           04年是非常郁闷的一年,现在看来,04年忙忙碌碌的开发了很多项目,但是这些项目都荒废了,不再看得到了。03年下半年开始,我开发的项目都转到ASP.net 技术了,虽然这些项目最终由于运维等原因,流产了,但是至少也算是从那时候开始对.net 技术有了较深的了解吧。

           05年的时候,已经开始基于.net技术开发新版论坛了。但是,由于不受公司重视,开发到一半竟被中止,把人员抽调到其他项目,使得无法继续。直到06年的时候,由于论坛在03年9月达到高峰后,一直走下坡路,公司终于决定在论坛方面增大力度。而新的论坛开发已经不再是单枪匹马就可以搞定的了,需要团队协作,而组建的团队又问题多多,磕磕碰碰后,终于在06年底发布了群组。群组使用的是跟论坛一样的底层架构。之后采用里程碑增量开发模式,终于在07年9月28日,把所有老论坛升级到了新版论坛。之后,就是一系列功能的补充完善,优化。随着公司投入力量的加强,我预计,也希望08年论坛又会象03年一样,达到一个新的高峰。

           回首过去这八年,我很感谢CSDN为我提供了一片成长的平台。CSDN伴着我成熟,我也见证了CSDN的成长。随着CSDN的不断改进,我自己也从菜鸟成长起来。我的角色也从最初的程序员变成高级程序员,再变成开发经理。(我在CSDN注册时的ID是4807,也就是我之前CSDN用户只有 不到5000个,而截至2007年底,CSDN注册用户已经达到250万多了。)这些成长都是通过自己的努力来实现的。一分耕耘,一分收获,我在CSDN实现了我的第一个“孩子之梦”……

           CSDN 论坛有很多有意思的故事。比如:女英雄无为;彭大力追旋玑;拉萨mm挖EDYang的故事等等,而这一个个故事都发生在自己一手搭建的论坛平台上,其中的成就感不言而喻。这也是我能在CSDN这家公司工作7年多的根本原因,我已经把它当成了我生活的一部分,无法割舍。

           八年时光,弹指即过,人生能有几个八年?八年时间我做了不少事情,但仅凭一手搭建了CSDN论坛这个平台,此八年不算虚度。希望未来的八年,我的程序员人生将更加精彩。

           仅以本文来怀念我患得患失的八年。

                                       郭红俊

                                                                                    2008年1月10日 星期四   于北京

     

    附,微软开发技术英雄,IT技术英雄的投票地址在 http://www.microsoft.com/zh/cn/default.aspx  的最大的那个flash 广告。 具体地址就是:http://www.hhhchina.net/    。 需要在中国地图上找到某个人才可以投票,郁闷的是,竟然地图上把我丢到内蒙了。

    拉票了,大家都来投我一票啦!^&^

    January 23

    SQL Server 索引基础知识(10)----Joins 时的三种算法简介

    我们书写查询语句的时候,Join 参数之前可以是下面三个 { LOOP | MERGE | HASH } JOIN  。 如果不使用,则系统自己分析那种方式快,使用那种方式。

    这其实是SQL Server 联结时候使用的三种算法。尽管每种算法都并不是很复杂,但考虑到性能优化,在产品级的优化器实现时往往使用的是改进过的变种算法。譬如SQL Server 支持block nested loops、index nexted loops、sort-merge、hash join以及hash team。我们在这里只对上述三种基本算法的原型做一个简单的介绍。

     

    知识点:

          Tables join总是两个两个进行的。所以下面的算法都是两个表的联结。

    • Hash Join (哈希联结)
      • 下图是 SQL Server 标示这种联结的图标,从图标我们就可以看到这个查询的逻辑。
      • HASH  JOIN
      • 逻辑步骤,如下图:
        • 以数据少的数据表的 Join 字段建立 Hash 值。
        • 对应的数据表计算 Join 字段的 Hash ,再与前一个数据表做比对。
        • Hash Join
      • 特点:
        • 处理大量、未排序、无索引的数据
        • 一般来说,查询优化器会首先考虑Nested Loop和Sort-Merge,但如果两个集合量都不小且没有合适的索引时,才会考虑使用Hash Join。
        • Hash Join也用于许多集合比较操作,inner join、left/right/full outer join、intersect、difference等等,当然了,需要保证都是等值联结。
        • Hash Join一个较大限制是它只能应用于等值联结(equality join),这主要是由于哈希函数及其桶的确定性及无序性所导致的。

     

    • Nested Loop Join (嵌套循环联结)
      • 下图是 SQL Server 标示这种联结的图标,从图标我们就可以看到这个查询的逻辑。
      • NESTED-LOOP  JOIN
      • 逻辑步骤,如下图:
        • 从外层的数据表取出一笔记录
        • 使用这个记录扫描内层的数据表
        • 再回到外层的数据表,重复上述的步骤。
        • Join Operation--Nested-Loop Join
      • 特点:
        • 适用于一个集合大而另一个集合小的情况(将小集合做为外循环),I/O性能不错。
        • 当外循环输入相当小而内循环非常大且有索引建立在JOIN字段上时,I/O性能相当不错。
        • 当两个集合中只有一个在JOIN字段上建立索引时,一定要将该集合作为内循环。
        • 对于一对一的匹配关系(两个具有唯一约束字段的联结),可以在找到匹配元组后跳过该次内循环的剩余部分(类似于编程语言循环语句中的continue)。
        • 可以不是等值联结

     

    • Merge Join (合并联结)
      • 下图是 SQL Server 标示这种联结的图标,从图标我们就可以看到这个查询的逻辑。
      • MERGE  JOIN
      • 逻辑步骤。如下图:
        • 使用两个数据表用来 Join 的字段既有的索引
        • 两边的数据表以光标由小到大比较,一边移动到比另一个数据表大时,换移另一个数据表。
        • Join Operation--Merge Join
      • 特点:
        • 可以不是等值联结
        • MERGE JOIN 必须等待两个单独的SORT JOIN完成(如果使用索引作为数据源,可以跳过SORT JOIN这个步骤), 如果两个表的规模相差很大, 会很影响查询的性能。
        • 当查询的两个表都很大或者都很小的时候, 则应该只用MERGE JOIN,如果两个表都很小,则表扫描和分类将进行的很快;

     

    分别使用这三种 Join 的例子:

    create table t1 ( i int not null )
    create table t2 ( i int not null )
    go
    set showplan_text on
    go
    -- Hash Match(Inner Join, HASH:([ghj_Demo].[dbo].[t2].[i])=([ghj_Demo].[dbo].[t1].[i]))
    select * from t1 join t2 on t1.i = t2.i
    go

    set showplan_text off
    go
    alter table t1 add primary key ( i )
    alter table t2 add primary key ( i )
    go

    set showplan_text on
    go
    -- |--Nested Loops(Inner Join, OUTER REFERENCES:([ghj_Demo].[dbo].[t1].[i]))
    select * from t1 join t2 on t1.i = t2.i
    go
    set showplan_text off
    go

    set showplan_text on
    go
    -- Merge Join(Inner Join, MERGE:([ghj_Demo].[dbo].[t1].[i])=([ghj_Demo].[dbo].[t2].[i]),

    -- RESIDUAL:([ghj_Demo].[dbo].[t2].[i]=[ghj_Demo].[dbo].[t1].[i]))
    select * from t1 join t2 on t1.i = t2.i option(merge join)
    go
    set showplan_text off
    go


    drop table t1, t2

     

    参考资料:

    浅谈查询优化器中的JOIN算法
    http://blog.csdn.net/hdy007/archive/2007/02/28/1516467.aspx

     

    Example of merge ,hash and nested join
    http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=498748&SiteId=1

     

    Inside SQL Server Joins
    http://blog.csdn.net/happydreamer/archive/2007/05/16/1611523.aspx

    January 18

    SQL Server 索引基础知识(9)----Indexing for OR

    我们仍然是通过例子来理解OR运算符的特征

    我们仍然使用 http://blog.joycode.com/ghj/archive/2008/01/18/113870.aspx 中的 member 表,这时候,这个表的索引如下:

    名字 描述
    member_corporation_link nonclustered located on PRIMARY corp_no
    member_ident clustered, unique, primary key located on PRIMARY member_no
    member_region_link nonclustered located on PRIMARY region_no
    MemberFirstName nonclustered located on PRIMARY firstname
    MemberLastName nonclustered located on PRIMARY lastname

    我们执行下面的查询

    SELECT m.LastName, m.FirstName, m.Region_No
    FROM dbo.Member AS m
    WHERE m.FirstName = 'Kimberly'
    OR m.LastName = 'Tripp'
    go

    我们用另外一个SQL来模拟 SQL 的执行计划,就是下面的语句:
    我们会看到上面语句跟下面的语句基本一样,只是一个是 Key Lookup ,一个是 Clustered Index Seek。 这里由于是模拟情况,可以认为是一样的。

    select m.LastName, m.FirstName, m.Region_No
    FROM dbo.Member AS m with(index(member_ident)),
    (
    select ww.Member_No from (
    select Member_No from dbo.Member where FirstName = 'Kimberly'
    union all
    select Member_No from dbo.Member where LastName = 'Tripp'
    ) ww
    group by ww.Member_No
    )
    n
    where m.Member_No = n.Member_No
    go
    这两个查询的扫描计数均是 2,逻辑读取均是 10 次。

    他们俩个的执行计划如下图,点击看大图:

    测试 OR 操作符对查询的影响

     

    知识点小结:

    OR 会做什么?

    • 将多个结果集集合起来,上图中,使用 Merge Join(Concatenation)把数据汇集起来。
    • 保证每一个row只出现一回,上图中,使用Stream Aggregate(Aggregate)进行排重。

    上面的例子中,我们用 union 来演示 or的情况。 OR和UNION相似。不同之处如下:

    • OR 根据 row’s unique identifier (RID or Clustering Key) 去掉副本
    • UNION 根据 SELECT list 去掉副本
    • UNION ALL 不去除副本

    OR 的一个简单应用就是 In 关键字。

    • 如果有In搜索关键字的对应索引。则系统会使用这个索引。
    • 如果没有,则遍历(表遍历或者索引遍历)是高性能的选择。

    SQL Server 索引基础知识(8)--- 数据基本格式补充

    我在SQL Server 索引基础知识系列中,第一篇就讲了记录数据的基本格式。那里主要讲解的是,数据库的最小读存单元:数据页。一个数据页是8K大小。

    对于数据库来说,它不会每次有一个数据页变化后,就存到硬盘。而是变化达到一定数量级后才会作这个操作。 这时候,数据库并不是以数据页来作为操作单元,而是以64k的数据(8个数据页,一个区)作为操作单元。

    区是管理空间的基本单位。一个区是八个物理上连续的页(即 64 KB)。这意味着 SQL Server 数据库中每 MB 有 16 个区。

    为了使空间分配更有效,SQL Server 不会将所有区分配给包含少量数据的表。SQL Server 有两种类型的区:

    • 统一区,由单个对象所有。区中的所有 8 页只能由所属对象使用。
    • 混合区,最多可由八个对象共享。区中八页的每页可由不同的对象所有。

    通常从混合区向新表或索引分配页。当表或索引增长到 8 页时,将变成使用统一区进行后续分配。如果对现有表创建索引,并且该表包含的行足以在索引中生成 8 页,则对该索引的所有分配都使用统一区进行。

    统一区、混合区

    为何会这样呢?

    其实很简单:

    读或写 8KB 的时间与读或写 64 KB的时间几乎相同。
    在 8 KB 到 64 KB 范围之内,单个磁盘 I/O 传输操作所花的时间主要是磁盘取数臂和读/写磁头运动的时间。
    因此,从数学上来讲,当需要传输 64 KB 以上的 SQL 数据时,
    尽可能地执行 64 KB 磁盘传输是有益的,即分成数个64K的操作。
    因为 64 KB 传输基本上与 8 KB 传输一样快,而每次传输的 SQL Server 数据是 8 KB 传输的 8 倍。

    参看:


    磁盘 I/O 性能
    http://windows.chinaitlab.com/skill/9872.html

     

    参考资料:

    MSDN 中关于“页和区”的描述
    http://technet.microsoft.com/zh-cn/library/ms190969.aspx

    SQL Server 索引基础知识(7)----Indexing for AND

    我们通过一个实例来看 有And 操作符时候的最常见的一种情况。我们有下面一个表,

    CREATE TABLE [dbo].[member](
    [member_no] [dbo].[numeric_id] IDENTITY(1,1) NOT NULL,
    [lastname] [dbo].[shortstring] NOT NULL,
    [firstname] [dbo].[shortstring] NOT NULL,
    [middleinitial] [dbo].[letter] NULL,
    [street] [dbo].[shortstring] NOT NULL,
    [city] [dbo].[shortstring] NOT NULL,
    [state_prov] [dbo].[statecode] NOT NULL,
    [country] [dbo].[countrycode] NOT NULL,
    [mail_code] [dbo].[mailcode] NOT NULL,
    [phone_no] [dbo].[phonenumber] NULL,
    [photograph] [image] NULL,
    [issue_dt] [datetime] NOT NULL DEFAULT (getdate()),
    [expr_dt] [datetime] NOT NULL DEFAULT (dateadd(year,1,getdate())),
    [region_no] [dbo].[numeric_id] NOT NULL,
    [corp_no] [dbo].[numeric_id] NULL,
    [prev_balance] [money] NULL DEFAULT (0),
    [curr_balance] [money] NULL DEFAULT (0),
    [member_code] [dbo].[status_code] NOT NULL DEFAULT (' ')
    )

    这个表具备下面的四个索引:

    索引名 细节 索引的列
    member_corporation_link nonclustered located on PRIMARY corp_no
    member_ident clustered, unique, primary key located on PRIMARY member_no
    member_region_link nonclustered located on PRIMARY region_no
    MemberFirstName nonclustered located on PRIMARY firstname

    当我们执行下面的SQL查询时候,

    SELECT m.Member_No, m.FirstName, m.Region_No
    FROM dbo.Member AS m
    WHERE m.FirstName LIKE 'K%'
    AND m.Region_No > 6
    AND m.Member_No < 5000
    go

    SQL Server 会根据索引方式,优化成下面方式来执行。

    select a.Member_No,a.FirstName,b.Region_No
    from
    (select m.Member_No, m.FirstName from dbo.Member AS m
    where m.FirstName LIKE 'K%' and m.Member_No < 5000) a ,
    -- 这个查询可以直接使用 MemberFirstName 非聚集索引,而且这个非聚集索引覆盖了所有查询列
    -- 实际执行时,只需要 逻辑读取 3 次

    (SELECT m.Member_No, m.Region_No from dbo.Member AS m
    where m.Region_No > 6) b

    -- 这个查询可以直接使用 member_region_link 非聚集索引,而且这个非聚集索引覆盖了所有查询列
    -- 实际执行时,只需要 逻辑读取 10 次

    where a.Member_No = b.Member_No
    不信,你可以看这两个SQL 的执行计划,以及逻辑读信息,都是一样的。

    其实上面的SQL,如果优化成下面的方式,实际的逻辑读消耗也是一样的。为何SQL Server 不会优化成下面的方式。是因为 and 操作符优化的另外一个原则。

    1/26 的数据和 1/6 的数据找交集的速度要比  1/52 的数据和 1/3 的数据找交集速度要慢。


    select a.Member_No,a.FirstName,b.Region_No
    from
    (select m.Member_No, m.FirstName from dbo.Member AS m
    where m.FirstName LIKE 'K%'
    -- 1/26 数据
    ) a,

    (SELECT m.Member_No, m.Region_No from dbo.Member AS m
    where m.Region_No > 6 and m.Member_No < 5000
    -- 1/3 * 1/ 2 数据
    ) b
    where a.Member_No = b.Member_No

    当然,我们要学习SQL 如何优化的话,就会用到查询语句中的一个功能,指定查询使用哪个索引来进行。

    比如下面的查询语句

    SELECT m.Member_No, m.FirstName, m.Region_No
    FROM dbo.Member AS m WITH (INDEX (0))
    WHERE m.FirstName LIKE 'K%'
    AND m.Region_No > 6
    AND m.Member_No < 5000
    go

    SELECT m.Member_No, m.FirstName, m.Region_No
    FROM dbo.Member AS m WITH (INDEX (1))
    WHERE m.FirstName LIKE 'K%'
    AND m.Region_No > 6
    AND m.Member_No < 5000
    go
    SELECT m.Member_No, m.FirstName, m.Region_No
    FROM dbo.Member AS m WITH (INDEX (MemberCovering3))
    WHERE m.FirstName LIKE 'K%'
    AND m.Region_No > 6
    AND m.Member_No < 5000
    go
    SELECT m.Member_No, m.FirstName, m.Region_No
    FROM dbo.Member AS m WITH (INDEX (MemberFirstName, member_region_link))
    WHERE m.FirstName LIKE 'K%'
    AND m.Region_No > 6
    AND m.Member_No < 5000
    go

    这里 Index 计算符可以是 0 ,1, 指定的一个或者多个索引名字。对于 0 ,1 的意义如下:

    如果存在聚集索引,则 INDEX(0) 强制执行聚集索引扫描,INDEX(1) 强制执行聚集索引扫描或查找(使用性能最高的一种)。
    如果不存在聚集索引,则 INDEX(0) 强制执行表扫描,INDEX(1) 被解释为错误。

    总结知识点:

    • 简单来说,我们可以这么理解:SQL Server 对于每一条查询语句。会根据实际索引情况(sysindexes 系统表中存储这些信息),分析每种组合可能的成本。然后选择它认为成本最小的一种。作为它实际执行的计划。
    • 成本代价计算的一个主要组成部分是逻辑I/O的数量,特别是对于单表的查询。
    • AND 操作要满足所有条件,这样,经常会要求对几个数据集作交集。数据集越小,数据集的交集计算越节省成本。

     

    参考资料

    本文演示代码下载地址:
    http://www.sqlskills.com/pastConferences.asp

    January 16

    SQL Server 索引基础知识(6)----索引的代价,使用场景

           前几天给同事培训了聚集索引,非聚集索引的知识后,在一个同事新作的项目中,竟然出现了滥用聚集索引的问题。看来没有培训最最基础的索引的意义,代价,使用场景,是一个非常大的失误。这篇博客就是从这个角度来罗列索引的基础知识。

     

    使用索引的意义

    • 索引在数据库中的作用类似于目录在书籍中的作用,用来提高查找信息的速度。
    • 使用索引查找数据,无需对整表进行扫描,可以快速找到所需数据。

    使用索引的代价

    • 索引需要占用数据表以外的物理存储空间。
    • 创建索引和维护索引要花费一定的时间。
    • 当对表进行更新操作时,索引需要被重建,这样降低了数据的维护速度。

    创建索引的列

    • 主键
    • 外键或在表联接操作中经常用到的列
    • 在经常查询的字段上最好建立索引

    不创建索引的列

    • 很少在查询中被引用
    • 包含较少的惟一值
    • 定义为 text、ntext 或者 image 数据类型的列

     

    Heaps是staging data的很好选择,当它没有任何Index时

    • Excellent for high performance data loading (parallel bulk load and parallel index creation after load)
    • Excellent as a partition to a partitioned view or a partitioned table

     

    聚集索引提高性能的方法,在前面几篇博客中分别提到过,下面只是一个简单的大纲,细节请参看前面几篇博客。

    何时创建聚集索引?

    Clustered Index会提高大多数table的性能,尤其是当它满足以下条件时:

    • 独特, 狭窄, 静止: 最重要的条件
    • 持续增长的,最好是只向上增加。例如:
      • Identity
      • Date, identity
      • GUID (only when using newsequentialid() function)

    聚集索引唯一性(独特型的问题)

    由于聚集索引的B+树结构的叶子节点必须指向具体数据。如果你要建立聚集索引的列不唯一,并且你指定的创建的聚集索引是非唯一的聚集索引,则会有以下情况:
    如果未使用 UNIQUE 属性创建聚集索引,数据库引擎 将向表自动添加一个四字节 uniqueifier 列。必要时,数据库引擎 将向行自动添加一个 uniqueifier 值,使每个键唯一。此列和列值供内部使用,用户不能查看或访问。

    参看我的这篇博客:

    SQL Server 索引基础知识(4)----主键与聚集索引
    http://blog.joycode.com/ghj/archive/2008/01/04/113373.aspx

    聚集索引持续向上增长的需求

    具体来说下面两个问题要求建立聚集索引的列最好是持续向上增长的

    1、缓存的命中率问题。(需要从B+树的结构分析)
    2、连续和不连续的磁盘 I/O 操作对性能的影响 。

    细节参看我的这篇博客:

    SQL Server 索引基础知识(5)----理解newid()和newsequentialid()
    http://blog.joycode.com/ghj/archive/2008/01/08/113521.aspx

    至于,如果你的数据已经存在重复了,而且是不应该出现的,则可以参看下面这篇KB :

    如何删除 SQL Server 表中的重复行
    http://support.microsoft.com/kb/139444/zh-cn

     

    非聚集索引提高性能的方法

    非聚集索引由于B+树的节点不是具体数据页,有时候由于这个原因,会导致非聚集索引甚至不如表遍历来的快,参看我在下面这篇博客中给的例子
    http://blog.joycode.com/ghj/archive/2008/01/02/113291.aspx

    但是,非聚集索引有个特性,如果你要查询的内容,在非聚集索引中以及被覆盖到了,则不需要继续到聚集索引,或者RID中去寻找数据了,这时候就可以很大的提高性能,这就是 覆盖面(Covering) 的问题。

     

    由于聚集索引叶子节点就是具体数据,所以 聚集索引的覆盖率是 100%,

    通过提高覆盖面来提高性能的问题也就只有非聚集索引(Nonclustered Indexes)才存在。

    当查询中所有的columns 都包括在index上时,我们说这个 index covers the query. Columns的顺序在此不重要

    (Select 时候的顺序不重要,但是Index 建立的顺序可得小心了)。

     

    在 SQL Server 2005 中,为了提高这种 Covering 带来的好处,甚至 可以通过将非键列添加到非聚集索引的叶级别来扩展非聚集索引的功能。

    比如下面的脚本, 虽然我们是对 Title, Revision 建立的非聚集索引,但是这个非聚集索引的叶子节点上还包含 FileName 字段的信息。

    USE AdventureWorks;
    GO
    CREATE INDEX IX_Document_Title      
    ON Production.Document (Title, Revision)      
    INCLUDE (FileName); 

    下面的代码就是测试 Covering 的.
    我已经在每个查询使用的方式,逻辑读的个数都标在每个查询前面了。 
     

    SET STATISTICS IO ON
    -- Turn Graphical Showplan ON (Ctrl+K)

    USE CREDIT
    go
    -- 逻辑读取144 次Clustered Index Scan
    SELECT m.LastName, m.FirstName, m.Phone_No
    FROM dbo.Member AS m WITH (INDEX (0))
    WHERE m.LastName LIKE '[S-Z]%'
    go

    --CREATE INDEX MemberLastName ON Member(LastName)
    go
    -- 逻辑读取6354 次 BookMark Lookup
    SELECT m.LastName, m.FirstName, m.Phone_No
    FROM dbo.Member AS m WITH (INDEX (MemberLastName))
    WHERE m.LastName LIKE '[S-Z]%'
    go

    --CREATE INDEX NCLastNameCombo ON Member(LastName, FirstName, Phone_No)
    go

    SELECT m.LastName, m.FirstName, m.Phone_No
    FROM dbo.Member AS m
    WHERE m.LastName LIKE '[S-Z]%'
    go

    --CREATE INDEX NCLastNameCombo2 ON Member(FirstName, LastName, Phone_No)
    go

    SELECT m.LastName, m.FirstName, m.Phone_No
    FROM dbo.Member AS m WITH (INDEX (NCLastNameCombo2))
    WHERE m.LastName LIKE '[S-Z]%'
    go

    -- If you want to clean up the indexes:
    --DROP INDEX Member.MemberLastName
    --DROP INDEX Member.NCLastNameCombo
    --DROP INDEX Member.NCLastNameCombo2
     
    参考资料

    Teched 2007 上 吴家震 主讲的"微软SQL服务器Always-On Tech-nologies: 高级索引策略"  录像下载地址:
    http://msevents.microsoft.com/CUI/EventDetail.aspx?EventID=1032364059&Culture=zh-CN
    注意, 这个页面标示的是 "SharePoint 2007 网站性能调优" ,但是其实是高级索引策略,微软弄错文件了,害得我一个个下下来看,哪个是需要的录像.

     

    January 14

    ASP.net 2.0 中 WebResource.axd 管理资源的一些知识点

    在 ASP.net 2.0 构建的Web页面中,查看源文件,我们经常会看到下面的Html文本

    <script src="/WebResource.axd?d=QfRKDnWw93T08KaF3ioSKQ2&amp;t=633313193233609691" type="text/javascript"></script>

    <script src="/WebResource.axd?d=9iVKU5SS0wd5al1SYg8zjL8XXbP97LbENHerY4aLtJk1&amp;t=633313193233609691" type="text/javascript"></script>

    这是 ASP.net 2.0 提供的新的资源管理方式产生的脚本。

    新的资源管理方式如何使用,你可以参看以下几篇博客:

    使用ASP.NET 2.0提供的WebResource管理资源
    http://birdshome.cnblogs.com/archive/2004/12/19/79309.html

    在自定义Server Control中捆绑JS文件 Step by Step
    http://www.cnblogs.com/jackielin/archive/2005/11/29/286570.html

    使用 ASP.NET 2.0 中 Web 资源
    http://support.microsoft.com/kb/910442

    在.NET 1.1下实现WebResource.axd
    http://www.cnblogs.com/yeahooh/archive/2007/07/27/833846.html

     

    使用 WebResource管理资源时, 我们会经常收到类似下面的异常

    System.Web.HttpException: 无效的视图状态。
    System.Security.Cryptography.CryptographicException: 填充无效,无法被移除。

    比如下面几个文章就提到了这个问题:

    Annoying CryptographicException on WebResource.axd
    http://forums.asp.net/t/934913.aspx

    ASP.Net’s WebResource.axd and machineKey badness
    http://blog.aproductofsociety.org/?p=11

     

    这是因为 WebResource.axd  URL 的参数具有时效性,但是对于搜索引擎的爬虫来说,他们会经常访问这些参数过期的地址,所以就会出现上面的异常。

    这个问题的解决方案,目前没有更好的方案,微软论坛中只是建议在robots.txt 文件中增加下面的信息:

    User-agent: *
    Disallow: /*.axd$

    但是这要求遵循 robots.txt 规范的爬虫们下次获得最新的 robots.txt 才会起作用。而对于那些不遵循 robots.txt 规范的爬虫,可一点办法都没有。

     

    我现在的想法是,能不用 WebResource.axd  就不要用,因为国内不遵循robots.txt 规范的爬虫们太多了。这就需要我们来分析那些场景使用了  WebResource.axd  中的资源,也就是 需要对 WebResource.axd  的格式进行分析。

     

    WebResource.axd 的 URL 的格式是:

             WebResource.axd?d=encrypted 标识符 & T = 时间戳值。
    其中:

            " d " 代表请求 Web 资源。  (encrypted identifier)
           " t " 是 timestamp 对程序集, 这有助于在确定如果已经对资源的更改请求。

     

    t 参数对于我们分析谁使用它,没有意义,我们下面就来分析 d 参数。

     

    d 参数的解析代码如下:

    <%@ Page Language="C#" AutoEventWireup="true" %>
    <script runat="server">
        public static string PageDecryptString(string input)
        {
            Type type = typeof(System.Web.UI.Page);
            object o = Activator.CreateInstance(type);
            System.Reflection.MethodInfo mi = type.GetMethod("DecryptString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static, null, new Type[] { typeof(string) }, null);
            object result = mi.Invoke(o, new object[] { input });
            return result.ToString();
        }
    
        protected void btn_Post_Click(object sender, EventArgs e)
        {
            this.l_Info.Text = PageDecryptString(HttpUtility.UrlDecode(tb_WebResourceDValue.Text));
        }
    </script>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <body>
        <form id="form1" runat="server">
        <asp:TextBox ID="tb_WebResourceDValue" runat="server" /><br />
        <asp:Label ID="l_Info" runat="server" /><br />
        <asp:Button ID="btn_Post" runat="server" Text="计算" OnClick="btn_Post_Click" />
        </form>
    </body>
    </html>
    

    我们在 C:\Windows\Microsoft.NET\Framework64\v2.0.50727\CONFIG\web.config 文件中,可以看到 WebResource.axd 文件是配置的通过下面 HttpHandle 来解析的:

    <add path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" validate="True"/>

    在  System.Web.Handlers.AssemblyResourceLoader 类中,使用 Reflector 工具看,又可以看到下面代码:

     

    void IHttpHandler.ProcessRequest(HttpContext context)
    {
    	// ...
    	string str = context.Request.QueryString["d"];
    	// ....
    	string str2 = Page.DecryptString(str);
    	// ...
    }

    显然, d 的参数, 是应该通过 Page.DecryptString 函数来解析的。

    而 Page.DecryptString 函数 中,涉及到调用 web 配置中配置的默认加密key。 简单起见,我们这里的解析方法就用 ASP.net 页面来实现了。由于 Page.DecryptString 函数是 internal static 的, 我们上面代码就用反射来调用这个函数,就会获得我们期望的值。

    (为了保证解密算法的解密key一致,最简单的做法就是我们把上面这个解密ASPX页面跟需要解析的放在同一个服务器上)

    上面 的  QfRKDnWw93T08KaF3ioSKQ2  解密的结果是: s|WebForms.js   竖线只是用于分隔字符串中不同的值。“s”表示该数据为脚本,“WebForms.js”是要检索的资源名称。WebForms.js 资源可从 System.Web.dll 检索。

    同理上面 的 9iVKU5SS0wd5al1SYg8zjL8XXbP97LbENHerY4aLtJk1 解密的结果是 : s|WebUIValidation.js

    显然如果出现这样的 WebResource.axd 调用,应该是验证控件在调用.

    我们要想上面的

    <script src="/WebResource.axd?d=QfRKDnWw93T08KaF3ioSKQ2&amp;t=633313193233609691" type="text/javascript"></script>
    <script src="/WebResource.axd?d=9iVKU5SS0wd5al1SYg8zjL8XXbP97LbENHerY4aLtJk1&amp;t=633313193233609691" type="text/javascript"></script>

    不出现,就需要让调用 WebForms.js , WebUIValidation.js  脚本的验证控件不使用。 

     

    参考资料:

    全球化就绪: 和 ASP.NET AJAX 应用程序环游地球 -- MSDN Magazine, January 2008
    http://msdn.microsoft.com/msdnmag/issues/08/01/InternationalizingASPNETAJAX/default.aspx?loc=zh

    对于Asp.Net 2.0中脚本资源的研究(1)
    http://www.cnblogs.com/Truly/archive/2007/07/07/809576.html

    对于Asp.Net 2.0中脚本资源的研究(2)
    http://www.cnblogs.com/Truly/archive/2007/07/10/812707.html

    January 08

    SQL Server 索引基础知识(5)----理解newid()和newsequentialid()

    在SQL Server 2005 中新增了一个函数:newsequentialid(),MSDN 中对这个函数的描述如下:

    在指定计算机上创建大于先前通过该函数生成的任何 GUID 的 GUID。
    NEWSEQUENTIALID() 不能在查询中引用。
    NEWSEQUENTIALID() 只能与 uniqueidentifier 类型表列上的 DEFAULT 约束一起使用。

    这个函数的具体用法在下面这篇博客中已经有详细的描述了。


    使用NEWSEQUENTIALID解决GUID聚集索引问题
    http://www.cnblogs.com/Mirricle/archive/2007/08/15/856726.html

    简单来说,newsequentialid 函数比起 newid 函数最大的好处是:


    如果你在一个 UNIQUEIDENTIFIER 字段上建立索引,使用 newid 产生的新的值是不固定的,所以新的值导致索引B+树的变化是随机的。
    而 newsequentialid 产生的新的值是有规律的,则索引B+树的变化是有规律的。有规律和无规律就会带来性能的改进。

     

    上面是一个粗略的描述,下面是比较详细点的解释:

    (我们这里解释的更详细一些,是为了让大家对索引的基础知识了解得更深入些。)

     

    B+ 树不考虑层级变化,增加数据的情况分以下几种情况:

    The insert algorithm for B+ Trees 

    Leaf Page Full

    Index Page FULL


                                                      Action
    NO NO Place the record in sorted position in the appropriate leaf page
    YES NO

    1. Split the leaf page
    2. Place Middle Key in the index page in sorted order.
    3. Left leaf page contains records with keys below the middle key.
    4. Right leaf page contains records with keys equal to or greater than the middle key.

    YES YES

    1. Split the leaf page.
    2. Records with keys < middle key go to the left leaf page.
    3. Records with keys >= middle key go to the right leaf page.
    4. Split the index page.
    5. Keys < middle key go to the left index page.
    6. Keys > middle key go to the right index page.
    7. The middle key goes to the next (higher level) index.

    IF the next level index page is full, continue splitting the index pages.

    更多 B+ 树的算法请参看后面链接:  http://www.sci.unich.it/~acciaro/bpiutrees.pdf

    对于数据库的索引来说,上面情况中,第三种情况发生的概率很低,更多的是 1,2 这两种情况。


    数据库中增加记录时,对索引的B+树的操作,其实就是对 左右叶子节点,上级节点的操作。

    而找到这几个节点后的操作,在实际上,都不是性能消耗最大的地方。性能消耗最大的地方在于搜索找到需要操作的叶子节点。

     

    对于 B+ 树来说, 几层的B+ 树,找到叶子节点就需要找几个数据页。那为何说 Guid 有规律时速度要比无规律时候快呢?

    原因很简单:

    1、缓存的命中率问题
    ( 你可以参看我之前写的这篇博客:理解缓存 http://blog.joycode.com/ghj/archive/2007/09/01/107863.aspx )

    当每次产生的Guid是有规律时,找到需要操作的叶子节点的几个中间节点,可能已经在之前的访问中被缓存了。

    这样,系统不需要大量的读入缓存命中率很低的索引数据页,这样可以节省内存,同时提高搜索速度。

     

    2、连续和不连续的磁盘 I/O 操作对性能的影响

             我们都知道,现在很多业务逻辑的瓶颈是硬盘的速度。而硬盘速度提升的空间仍然不大。下面对硬盘读写操作的一些法则对我们优化跟硬盘I/O有关的方面很有帮助。

       请记住下面的经验法则:标准的 Wide Ultra SCSI-3 硬盘每秒钟可为 Windows 和 SQL Server 提供 75 个不连续(随机)的 I/O 操作和 150 个连续的 I/O 操作。这种硬盘的标称传输率在 40 MB/秒左右。请记住更有可能限制数据库服务器的传输率是每秒钟 75/150 I/O,而不是 40 MB/秒。

             读/写磁头和相关的磁盘取数臂需要移动才能在 SQL Server 和 Windows 所要求的硬盘盘片的位置上进行查找和操作。如果数据所在的硬盘盘片的位置不连续,硬盘驱动器要花多得多的时间才能将磁盘取数臂和读/写磁头移动到所有需要的硬盘盘片位置。如果所需要的数据全部位于硬盘盘片上的连续物理扇区,情况则相反,磁盘取数臂和读/写磁头只需进行很小的移动就能完成所需磁盘 I/O 操作。连续和不连续的情况下所花的时间有很大的差异,每个不连续的数据查找大约要花 50 毫秒,而连续的数据查找则只需大约 2-3 毫秒。请注意这些值是粗略估计出来的,具体值将取决于不连续的数据在磁盘上分布的疏密、硬盘盘片的旋转速度 (RPM) 以及硬盘的其它物理属性。主要要记住的一点是连续 I/O 有益于 SQL Server 性能。

       之前已提到标准的硬盘支持每秒 75 个不连续的 I/O 和每秒 150 个连续的 I/O。还要记住的重要一点是读或写 8KB 的时间与读或写 64 KB的时间几乎相同。在 8 KB 到 64 KB 范围之内,单个磁盘 I/O 传输操作所花的时间主要是磁盘取数臂和读/写磁头运动的时间。因此,从数学上来讲,当需要传输 64 KB 以上的 SQL 数据时,尽可能地执行 64 KB 磁盘传输是有益的,因为 64 KB 传输基本上与 8 KB 传输一样快,而每次传输的 SQL Server 数据是 8 KB 传输的 8 倍。请记住 Read-Ahead Manager 以 64 KB 字节片(也称为 SQL Server 扩展盘区)执行磁盘操作。Log Manager 也以较大的 I/O 传输量来执行连续写操作。要记住的主要事项是充分利用 Read-Ahead Manager,并将 SQL Server 日志文件与其它非连续存取的文件分开,以有效提高 SQL Server 的性能。

     

    参考资料:

    Using NewSequentialID Instead of NewID
    http://www.sqlmag.com/Article/ArticleID/49960/sql_server_49960.html

    NEWSEQUENTIALID()
    http://technet.microsoft.com/en-us/library/ms189786.aspx

    重新组织和重新生成索引
    http://technet.microsoft.com/zh-cn/library/ms189858.aspx 

    磁盘 I/O 性能
    http://windows.chinaitlab.com/skill/9872.html

    序列化,反序列化时低序位非打印 ASCII 字符的问题

    最近碰到一个问题,我的一个把数据库中记录的信息暴露出来的Web Service调用时候出问题了。报下面的错误信息:

    System.InvalidOperationException was unhandled
      Message="XML 文档(1, 823)中有错误。"
      Source="System.Xml"
        Message="“”(十六进制值 0x0E)是无效的字符。 行 1,位置 823。"
        Source="System.Xml"

    当这个错误发生时,Web Service 服务器端不会有任何错误,而调用这个 Web Service 的客户端则会报上述错误。
    是何原因导致的这个问题呢?
    答案很简单,是WEB Service 暴露的XML文档中存在低序位非打印 ASCII 字符所致。
    我们查看 Web Service 返回的XML 文档文档中,会有下面的XML文档节:其中的 &#xE; 就是低序位 ASCII 字符。&#xE;对应的字符如后:

    <Value>&#xE;在神奇天地裏誰叱咤風雨</Value>

    会导致这些问题的 低序位非打印 ASCII 字符包含以下字符:
    #x0 - #x8 (ASCII 0 - 8)
    #xB - #xC (ASCII 11 - 12)
    #xE - #x1F (ASCII 14 - 31)

    下面就是一个简单演示这个问题的控制台程序,
    为了简单起见,这里没有建立 WebService, 而是把一个类XML序列化存储到文件,然后再把这个文件反序列化读取出来:
    其中的这个类的Value值中,放了一个低序位非打印 ASCII 字符。
    执行这个控制台程序,就会报异常。“XML 文档(3, 12)中有错误。”

    using System;
    using System.Xml.Serialization;
    using System.IO;
    using System.Text;
    using System.Globalization;
    
    namespace TextSerialize
    {
        [Serializable]
        public class MyClass
        {
            public string Value { get; set; }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                string fileName = "d:\\1.txt";
    
                MyClass c = new MyClass();
                c.Value = string.Format("在神奇{0}天地裏誰叱咤風雨", Convert.ToChar(14));
    
                SaveAsXML(c, fileName, Encoding.UTF8);
    
                object o = ConvertFileToObject(fileName, typeof(MyClass), Encoding.UTF8);
                MyClass d = o as MyClass;
                if (d != null) Console.WriteLine(d.Value);
                else Console.WriteLine("null");
    
                Console.ReadLine();
            }
    
    
            /// <summary>
            /// 序列化
            /// </summary>
            /// <param name="objectToConvert"></param>
            /// <param name="path"></param>
            /// <param name="encoding"></param>
            public static void SaveAsXML(object objectToConvert, string path, Encoding encoding)
            {
                if (objectToConvert != null)
                {
                    Type t = objectToConvert.GetType();
                    XmlSerializer ser = new XmlSerializer(t);
                    using (StreamWriter writer = new StreamWriter(path, false, encoding))
                    {
                        ser.Serialize(writer, objectToConvert);
                        writer.Close();
                    }
                }
            }
    
    
            /// <summary>
            /// 反序列化
            /// </summary>
            /// <param name="path"></param>
            /// <param name="objectType"></param>
            /// <param name="encoding"></param>
            /// <returns></returns>
            public static object ConvertFileToObject(string path, Type objectType, Encoding encoding)
            {
                object convertedObject = null;
                if (!string.IsNullOrEmpty(path))
                {
                    XmlSerializer ser = new XmlSerializer(objectType);
                    using (StreamReader reader = new StreamReader(path, encoding))
                    {
                        convertedObject = ser.Deserialize(reader);
                        reader.Close();
                    }
                }
                return convertedObject;
            }
        }
    }
    

    上面提到的Web Service 的那个问题,跟这个演示程序是一样的。

    我们需要被序列化的内容中,存在 低序位非打印 ASCII 字符 时, .net 会给我们正常序列化, 会自动把 低序位非打印 ASCII 字符 转换成 &#x 编码的字符(这个XML规范中要求这么做的)。

    但是,反序列化时候,如果需要反序列化的内容如果存在 &#x 编码的字符(映射到低序位非打印 ASCII 字符),则反序列化就会出错。

     

    如果解决这个问题呢?

    当然,最彻底的解决方法是修改反序列化的代码,让这些字符不会出错。但这个东西很多时候不归我们控制。这个方案不可行。

    下一个方案就是剔除这些捣乱的字符。

    我这里要给出的方案,是对这些字符序列化时作一次预处理,反序列化时,作一次反向处理。
    这里为了演示的更有意义,我这里处理逻辑就是把 低序位非打印 ASCII 字符 转换成 &#x 编码的字符 ,和把&#x 编码的字符 转换成 低序位非打印 ASCII 字符。
    这样就可以使用我这里提供的函数,实现更多的处理逻辑。这两个函数的代码如下:

     

            /// <summary>
            /// 把一个字符串中的 低序位 ASCII 字符 替换成 &#x  字符
             /// 转换  ASCII  0 - 8  -> &#x0 - &#x8
            /// 转换  ASCII 11 - 12 -> &#xB - &#xC
            /// 转换  ASCII 14 - 31 -> &#xE - &#x1F
            /// </summary>
            /// <param name="tmp"></param>
            /// <returns></returns>
            public static string ReplaceLowOrderASCIICharacters(string tmp)
            {
                StringBuilder info = new StringBuilder();
                foreach (char cc in tmp)
                {
                    int ss = (int)cc;
                    if (((ss >= 0) && (ss <= 8)) || ((ss >= 11) && (ss <= 12)) || ((ss >= 14) && (ss <= 32)))
                        info.AppendFormat("&#x{0:X};", ss);
                    else info.Append(cc);
                }
                return info.ToString();
            }
    
            /// <summary>
            /// 把一个字符串中的下列字符替换成 低序位 ASCII 字符
             /// 转换  &#x0 - &#x8  -> ASCII  0 - 8
            /// 转换  &#xB - &#xC  -> ASCII 11 - 12
            /// 转换  &#xE - &#x1F -> ASCII 14 - 31
            /// </summary>
            /// <param name="input"></param>
            /// <returns></returns>
            public static string GetLowOrderASCIICharacters(string input)
            {
                if (string.IsNullOrEmpty(input)) return string.Empty;
                int pos, startIndex = 0, len = input.Length;
                if (len <= 4) return input;
    
                StringBuilder result = new StringBuilder();
                while ((pos = input.IndexOf("&#x", startIndex)) >= 0)
                {
                    bool needReplace = false;
                    string rOldV = string.Empty, rNewV = string.Empty;
    
                    int le = (len - pos < 6) ? len - pos : 6;
                    int p = input.IndexOf(";", pos, le);
    
                    if (p >= 0)
                    {
                        rOldV = input.Substring(pos, p - pos + 1);
    
                        // 计算 对应的低位字符
                        short ss;
                        if (short.TryParse(rOldV.Substring(3, p - pos - 3), NumberStyles.AllowHexSpecifier, null, out ss))
                        {
                            if (((ss >= 0) && (ss <= 8)) || ((ss >= 11) && (ss <= 12)) || ((ss >= 14) && (ss <= 32)))
                            {
                                needReplace = true;
                                rNewV = Convert.ToChar(ss).ToString();
                            }
                        }
                        pos = p + 1;
                    }
                    else pos += le;
    
                    string part = input.Substring(startIndex, pos - startIndex);
                    if (needReplace) result.Append(part.Replace(rOldV, rNewV));
                    else result.Append(part);
    
                    startIndex = pos;
                }
                result.Append(input.Substring(startIndex));
                return result.ToString();
            }
    

     

    这样,我们这个演示程序的 Main 函数修改为下面的代码,也不会有任何错误发生。

     

            static void Main(string[] args)
            {
                Console.WriteLine(GetLowOrderASCIICharacters("123456&#x50000"));
                Console.WriteLine(GetLowOrderASCIICharacters("123456&#x5"));
                Console.WriteLine(GetLowOrderASCIICharacters("&#x5"));
                Console.WriteLine(GetLowOrderASCIICharacters("0123&#x1F;456789"));
                Console.WriteLine(GetLowOrderASCIICharacters("\f"));
    Console.WriteLine(GetLowOrderASCIICharacters("&#xE;=-1")); Console.WriteLine(GetLowOrderASCIICharacters("&#xF;")); Console.WriteLine(GetLowOrderASCIICharacters("&#x1F;")); string fileName = "d:\\1.txt"; MyClass c = new MyClass(); c.Value = string.Format("在神奇{0}天地裏誰叱咤風雨", Convert.ToChar(14)); c.Value = ReplaceLowOrderASCIICharacters(c.Value); SaveAsXML(c, fileName, Encoding.UTF8); object o = ConvertFileToObject(fileName, typeof(MyClass), Encoding.UTF8); MyClass d = o as MyClass; if (d != null) { d.Value = GetLowOrderASCIICharacters(d.Value); Console.WriteLine(d.Value); } else Console.WriteLine("null"); Console.ReadLine(); }

     

     

     

    小结

    低序位非打印 ASCII 字符 在很多时候会给我们的系统带来问题,这部分字符必须作特殊处理。

     

    参考资料:

    PRB: ErrorMessage 当 XML 文档包含低序位 ASCII 字符
    http://support.microsoft.com/kb/315580

    如何解决用XmlSerializer序列化和反序列化类的过程中换行 \r 丢失?
    http://topic.csdn.net/t/20051116/15/4397556.html

    January 04

    SQL Server 索引基础知识(4)----主键与聚集索引

    有些人可能对主键和聚集索引有所混淆,其实这两个是不同的概念,下面是一个简单的描述。不想看绕口文字者,直接看两者的对比表。尤其是最后一项的比较。

    主键(PRIMARY KEY )

    来自MSDN的描述:

    表通常具有包含唯一标识表中每一行的值的一列或一组列。这样的一列或多列称为表的主键 (PK),用于强制表的实体完整性。在创建或修改表时,您可以通过定义 PRIMARY KEY 约束来创建主键。

    一个表只能有一个 PRIMARY KEY 约束,并且 PRIMARY KEY 约束中的列不能接受空值。由于 PRIMARY KEY 约束可保证数据的唯一性,因此经常对标识列定义这种约束。

    如果为表指定了 PRIMARY KEY 约束,则 SQL Server 2005 数据库引擎 将通过为主键列创建唯一索引来强制数据的唯一性。当在查询中使用主键时,此索引还可用来对数据进行快速访问。因此,所选的主键必须遵守创建唯一索引的规则。

    创建主键时,数据库引擎 会自动创建唯一的索引来强制实施 PRIMARY KEY 约束的唯一性要求。如果表中不存在聚集索引或未显式指定非聚集索引,则将创建唯一的聚集索引以强制实施 PRIMARY KEY 约束。

     

    聚集索引

    聚集索引基于数据行的键值在表内排序和存储这些数据行。每个表只能有一个聚集索引,因为数据行本身只能按一个顺序存储。

    每个表几乎都对列定义聚集索引来实现下列功能:

    • 可用于经常使用的查询。
    • 提供高度唯一性。

    两者的比较

    下面是一个简单的比较表

      主键 聚集索引
    用途 强制表的实体完整性 对数据行的排序,方便查询用
    一个表多少个 一个表最多一个主键 一个表最多一个聚集索引
    是否允许多个字段来定义 一个主键可以多个字段来定义 一个索引可以多个字段来定义
         
    是否允许 null 数据行出现 如果要创建的数据列中数据存在null,无法建立主键。
    创建表时指定的 PRIMARY KEY 约束列隐式转换为 NOT NULL。
    没有限制建立聚集索引的列一定必须 not null .
    也就是可以列的数据是 null
    参看最后一项比较
    是否要求数据必须唯一 要求数据必须唯一 数据即可以唯一,也可以不唯一。看你定义这个索引的 UNIQUE 设置。
    (这一点需要看后面的一个比较,虽然你的数据列可能不唯一,但是系统会替你产生一个你看不到的唯一列)
         
    创建的逻辑 数据库在创建主键同时,会自动建立一个唯一索引。
    如果这个表之前没有聚集索引,同时建立主键时候没有强制指定使用非聚集索引,则建立主键时候,同时建立一个唯一的聚集索引
    如果未使用 UNIQUE 属性创建聚集索引,数据库引擎 将向表自动添加一个四字节 uniqueifier 列。
    必要时,数据库引擎 将向行自动添加一个 uniqueifier 值,使每个键唯一。此列和列值供内部使用,用户不能查看或访问。

    参考:

    下面这个帖子中大力的回复:
    http://topic.csdn.net/t/20021212/16/1255429.html

    SQL Server 索引基础知识(3)----测试中一些常看的指标和清除缓存的方法

    之前的两篇博客中有2个例子,来演示要讲述的内容。其中提到了部分查看数据库状态的方法,那里并不是很全面,这篇博客罗列几个我们在后面系列博客中会用到查看这些状态,数据的地方。以及测试中清除缓存的方法。
    前面两篇博客的链接地址如下:

    SQL Server 索引基础知识(1)--- 记录数据的基本格式
    http://blog.joycode.com/ghj/archive/2008/01/02/113290.aspx

    SQL Server 索引基础知识(2)----聚集索引,非聚集索引
    http://blog.joycode.com/ghj/archive/2008/01/02/113291.aspx

    如何获得索引的一些信息

    比如:查看索引的深度SQL 脚本如下:

    select INDEXPROPERTY (OBJECT_ID('ChargeHeap'),'ChargeHeap_NCInd','IndexDepth')
    其中的 'ChargeHeap' 为我们要查看索引所在的表名,'ChargeHeap_NCInd' 为所要查看的索引名,'IndexDepth' 为所要查看的索引属性。
    更多属性请参看下面页面的参数说明:
    http://technet.microsoft.com/zh-cn/library/ms187729.aspx
    或者我们在 SQL Server Management Studio 中选中我们要查看的索引,然后在右键菜单中查看索引的属性。其中 Fragmentation 标签页会有很多我们对
    这个索引感兴趣的内容,比如下图:

    我们可以在这里看到索引的深度,子节点数,数据页数等等信息。这些信息对我们分析查询语句的性能非常有帮助。

     

    如何查看磁盘I/O操作信息

    SET STATISTICS IO ON 命令是一个 使 SQL Server 显示有关由 Transact-SQL 语句生成的磁盘活动量的信息。

    我们在分析索引性能的时候,会非常有用。

    启用了这个属性后,我们在执行 SQL 语句后,会收到类似如下的信息,这有利于我们分析SQL的性能:

    (3999 row(s) affected)
    表 'ChargeCL'。扫描计数 1,逻辑读取 9547 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。

    其中的 lob 逻辑读取、lob 物理读取、lob 预读 这三个指标是 读取 text、ntext、image 或大值类型 (varchar(max)、nvarchar(max)、varbinary(max)) 时的指标。
    而 逻辑读取、物理读取、预读  是对普通数据页的读取。

    使用 SQL Server Management Studio Standard Reports

    我们在 SQL Server Management Studio  中,选择数据库服务器,或者具体数据库,或者Security -- Logins 时,或者Management 时,Notification Services 或者  SQL Server Agent 对象时候,都会看到SQL Server 替我们提供的一些现成报表,这些报表的数据,有利于我们分析数据库的状态。
    比如在  SQL Server 索引基础知识(1)--- 记录数据的基本格式
    http://blog.joycode.com/ghj/archive/2008/01/02/113290.aspx
    中,我们就使用数据表占用空间的报表

    具体报表可以参考以下链接:
    SQL Server Management Studio Standard Reports - Overview
    http://blogs.msdn.com/buckwoody/archive/2007/10/09/sql-server-management-studio-standard-reports-overview.aspx

    测试中,释放缓存的一些方法

    尤其查询语句性能测试时,数据是否被缓存,这是测试中一个重要点。下面几个命令帮助我们清除缓存。方便测试。

    清除缓存有关的命令:
    SQL 2000里面除了dbcc unpintable好像就没有了   而且这个操作也不会立即释放表内存Buffer
    (DBCC   UNPINTABLE   does   not   cause   the   table   to   be   immediately   flushed   from   the   data   cache.   It   specifies   that   all   of   the   pages   for   the   table   in   the   buffer   cache   can   be   flushed   if   space   is   needed   to   read   in   a   new   page   from   disk.)

    SQL 2005/2008让DBA能够更自由的对SQL所占用的内存空间做处理   如:
    CHECKPOINT
    将当前数据库的全部脏页写入磁盘。“脏页”是已输入缓存区高速缓存且已修改但尚未写入磁盘的数据页。CHECKPOINT 可创建一个检查点,在该点保证全部脏页都已写入磁盘,从而在以后的恢复过程中节省时间。

    DBCC   DROPCLEANBUFFERS  
    从缓冲池中删除所有清除缓冲区。

    DBCC   FREEPROCCACHE  
    从过程缓存中删除所有元素。

    DBCC FREESYSTEMCACHE
    从所有缓存中释放所有未使用的缓存条目。SQL Server 2005 数据库引擎会事先在后台清理未使用的缓存条目,以使内存可用于当前条目。但是,可以使用此命令从所有缓存中手动删除未使用的条目。

    另外还可以 sp_cursor_list 查看全部游标  
    DBCC   OPENTRAN查看数据库打开事务状态等  

    参考资料:
    CSDN 这个帖子的 rouqu(石林#黄果树)的回复
    http://topic.csdn.net/u/20070404/14/6ee765a4-fc32-4d2c-a62c-6d51291e6007.html

    January 02

    SQL Server 索引基础知识(2)----聚集索引,非聚集索引

    由于需要给同事培训数据库的索引知识,就收集整理了这个系列的博客。发表在这里,也是对索引知识的一个总结回顾吧。通过总结,我发现自己以前很多很模糊的概念都清晰了很多。

    不论是 聚集索引,还是非聚集索引,都是用B+树来实现的。我们在了解这两种索引之前,需要先了解B+树。如果你对B树不了解的话,建议参看以下几篇文章:

    BTree,B-Tree,B+Tree,B*Tree都是什么
    http://blog.csdn.net/manesking/archive/2007/02/09/1505979.aspx

    B+ 树的结构图:

    B+ 树的特点:

    • 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
    • 不可能在非叶子结点命中;
    • 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;

    B+ 树中增加一个数据,或者删除一个数据,需要分多种情况处理,比较复杂,这里就不详述这个内容了。 

    聚集索引(Clustered Index)

    • 聚集索引的叶节点就是实际的数据页
    • 在数据页中数据按照索引顺序存储
    • 行的物理位置和行在索引中的位置是相同的
    • 每个表只能有一个聚集索引
    • 聚集索引的平均大小大约为表大小的5%左右

    下面是两副简单描述聚集索引的示意图: 

    在聚集索引中执行下面语句的的过程:

    select * from table where firstName = 'Ota'

     在聚集索引中搜索

    一个比较抽象点的聚集索引图示:

    聚集索引单个分区中的结构

     

    非聚集索引 (Unclustered Index)  

    • 非聚集索引的页,不是数据,而是指向数据页的页。
    • 若未指定索引类型,则默认为非聚集索引
    • 叶节点页的次序和表的物理存储次序不同
    • 每个表最多可以有249个非聚集索引
    • 在非聚集索引创建之前创建聚集索引(否则会引发索引重建)

    在非聚集索引中执行下面语句的的过程:

    select * from employee where lname = 'Green'

    Selecting rows using a nonclustered index

    一个比较抽象点的非聚集索引图示:

    非聚集索引的级别

     

    什么是 Bookmark Lookup

    虽然SQL 2005 中已经不在提  Bookmark Lookup 了(换汤不换药),但是我们的很多搜索都是用的这样的搜索过程,如下:
    先在非聚集中找,然后再在聚集索引中找。

    Bookmark Lookup 

    http://www.sqlskills.com/ 提供的一个例子中,就给我们演示了 Bookmark Lookup  比 Table Scan 慢的情况,例子的脚本如下:

    USE CREDIT
    go
    
    -- These samples use the Credit database. You can download and restore the
    -- credit database from here:
    -- http://www.sqlskills.com/resources/conferences/CreditBackup80.zip
    
    -- NOTE: This is a SQL Server 2000 backup and MANY examples will work on 
    -- SQL Server 2000 in addition to SQL Server 2005.
    -------------------------------------------------------------------------------
    -- (1) Create two tables which are copies of charge:
    -------------------------------------------------------------------------------
    
    -- Create the HEAP
    SELECT * INTO ChargeHeap FROM Charge
    go
    
    -- Create the CL Table
    SELECT * INTO ChargeCL FROM Charge
    go
    
    CREATE CLUSTERED INDEX ChargeCL_CLInd ON ChargeCL (member_no, charge_no)
    go
    
    -------------------------------------------------------------------------------
    -- (2) Add the same non-clustered indexes to BOTH of these tables:
    -------------------------------------------------------------------------------
    
    -- Create the NC index on the HEAP
    CREATE INDEX ChargeHeap_NCInd ON ChargeHeap (Charge_no)
    go
    
    -- Create the NC index on the CL Table
    CREATE INDEX ChargeCL_NCInd ON ChargeCL (Charge_no)
    go
    
    -------------------------------------------------------------------------------
    -- (3) Begin to query these tables and see what kind of access and I/O returns
    -------------------------------------------------------------------------------
    
    -- Get ready for a bit of analysis:
    SET STATISTICS IO ON
    -- Turn Graphical Showplan ON (Ctrl+K)
    
    -- First, a point query (also, see how a bookmark lookup looks in 2005)
    SELECT * FROM ChargeHeap WHERE Charge_no = 12345
    go
    
    SELECT * FROM ChargeCL WHERE Charge_no = 12345
    go
    
    -- What if our query is less selective?
    -- 1000 is .0625% of our data... (1,600,000 million rows)
    SELECT * FROM ChargeHeap WHERE Charge_no < 1000
    go
    
    SELECT * FROM ChargeCL WHERE Charge_no < 1000
    go
    
    -- What if our query is less selective?
    -- 16000 is 1% of our data... (1,600,000 million rows)
    SELECT * FROM ChargeHeap WHERE Charge_no < 16000
    go
    
    SELECT * FROM ChargeCL WHERE Charge_no < 16000
    go
    
    -------------------------------------------------------------------------------
    -- (4) What's the EXACT percentage where the bookmark lookup isn't worth it?
    -------------------------------------------------------------------------------
    
    -- What happens here: Table Scan or Bookmark lookup?
    SELECT * FROM ChargeHeap WHERE Charge_no < 4000
    go
    
    SELECT * FROM ChargeCL WHERE Charge_no < 4000
    go
    
    -- What happens here: Table Scan or Bookmark lookup?
    SELECT * FROM ChargeHeap WHERE Charge_no < 3000
    go
    
    SELECT * FROM ChargeCL WHERE Charge_no < 3000
    go
    
    -- And - you can narrow it down by trying the middle ground:
    -- What happens here: Table Scan or Bookmark lookup?
    SELECT * FROM ChargeHeap WHERE Charge_no < 3500
    go
    
    SELECT * FROM ChargeCL WHERE Charge_no < 3500
    go
    
    -- And again:
    SELECT * FROM ChargeHeap WHERE Charge_no < 3250
    go
    
    SELECT * FROM ChargeCL WHERE Charge_no < 3250
    go
    
    -- And again:
    SELECT * FROM ChargeHeap WHERE Charge_no < 3375
    go
    
    SELECT * FROM ChargeCL WHERE Charge_no < 3375
    go
    
    -- Don't worry, I won't make you go through it all :)
    
    
    
    -- For the Heap Table (in THIS case), the cutoff is: 0.21%
    SELECT * FROM ChargeHeap  WHERE Charge_no < 3383
    go
    SELECT * FROM ChargeHeap WHERE Charge_no < 3384
    go
    
    
    -- For the Clustered Table (in THIS case), the cut-off is: 0.21%
    SELECT * FROM ChargeCL WHERE Charge_no < 3438
    
    SELECT * FROM ChargeCL WHERE Charge_no < 3439
    go

    这个例子也就是 吴家震 在Teched 2007 上的那个演示例子。

    小结:

    这篇博客只是简单的用几个图表来介绍索引的实现方法:B+数, 聚集索引,非聚集索引,Bookmark Lookup 的信息而已。

    参考资料:

    表组织和索引组织
    http://technet.microsoft.com/zh-cn/library/ms189051.aspx
    http://technet.microsoft.com/en-us/library/ms189051.aspx

    How Indexes Work
    http://manuals.sybase.com/onlinebooks/group-asarc/asg1200e/aseperf/@Generic__BookTextView/3358

    Bookmark Lookup
    http://blogs.msdn.com/craigfr/archive/2006/06/30/652639.aspx 

    Logical and Physical Operators Reference
    http://msdn2.microsoft.com/en-us/library/ms191158.aspx