⼤数据量数据库设计与优化⽅案(SQL优化)
⼀、数据库结构的设计
如果不能设计⼀个合理的数据库模型,不仅会增加客户端和服务器段程序的编程和维护的难度,⽽且将会影响系统实际运⾏的性能。所以,在⼀个系统开始实施之前,完备的数据库模型的设计是必须的。
在⼀个系统分析、设计阶段,因为数据量较⼩,负荷较低。我们往往只注意到功能的实现,⽽很难注意到性能的薄弱之处,等到系统投⼊实际运⾏⼀段时间后,才发现系统的性能在降低,这时再来考虑提⾼系统性能则要花费更多的⼈⼒物⼒,⽽整个系统也不可避免的形成了⼀个打补丁⼯程。
所以在考虑整个系统的流程的时候,我们必须要考虑,在⾼并发⼤数据量的访问情况下,我们的系统会不会出现极端的情况。(例:对外统计系统在7⽉16⽇出现的数据异常的情况,并发⼤数据量的的访问造成,数据库的响应时间不能跟上数据刷新的速度造成。具体情况是:在⽇期临界时(00:00:00),判断数据库中是否有当前⽇期的记录,没有则插⼊⼀条当前⽇期的记录。在低并发访问的情况下,不会发⽣问题,但是当⽇期临界时的访问量相当⼤的时候,在做这⼀判断的时候,会出现多次条件成⽴,则数据库⾥会被插⼊多条当前⽇期的记录,从⽽造成数据错误),数据库的模型确定下来之后,我们有必要做⼀个系统内数据流向图,分析可能出现的瓶颈。
为了保证数据库的⼀致性和完整性,在逻辑设计的时候往往会设计过多的表间关联,尽可能的降低数据的冗余。(例:⽤户表的地区,我们可以把地区另外存放到⼀个地区表中)如果数据冗余低,数据的完整性容易得到保证,提⾼了数据吞吐速度,保证了数据的完整性,清楚地表达数据元素之间的关系。⽽对于多表之间的关联查询(尤其是⼤数据表)时,其性能将会降低,同时也提⾼了客户端程序的编程难度,因此,物理设计需折衷考虑,根据业务规则,确定对关联表的数据量⼤⼩、数据项的访问频度,对此类数据表频繁的关联查询应适当提⾼数据冗余设计但增加了表间连接查询的操作,也使得程序的变得复杂,为了提⾼系统的响应时间,合理的数据冗余也是必要的。设计⼈员在设计阶段应根据系统操作的类型、频度加以均衡考虑。
另外,最好不要⽤⾃增属性字段作为主键与⼦表关联。不便于系统的迁移和数据恢复。对外统计系统映射关系丢失。
原来的表格必须可以通过由它分离出去的表格重新构建。使⽤这个规定的好处是,你可以确保不会在分离的表格中引⼊多余的列,所有你创建的表格结构都与它们的实际需要⼀样⼤。应⽤这条规定是⼀个好习惯,不过除⾮你要处理⼀个⾮常⼤型的数据,否则你将不需要⽤到它。(例如⼀个通⾏证系统,我可以将USERID,USERNAME,USERPASSWORD,单独出来作个表,再把USERID作为其他表的外键)。
表的设计具体注意的问题:
1、数据⾏的长度不要超过8020字节,如果超过这个长度的话在物理页中这条数据会占⽤两⾏从⽽造成存储碎⽚,降低查询效率。
2、能够⽤数字类型的字段尽量选择数字类型⽽不⽤字符串类型的(电话号码),这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接回逐个⽐较字符串中每⼀个字符,⽽对于数字型⽽⾔只需要⽐较⼀次就够了。
3、对于不可变字符类型char和可变字符类型varchar 都是8000字节,char查询快,但是耗存储空间,varchar查询相对慢⼀些但是节省存储空间。在设计字段的时候可以灵活选择,例如⽤户名、密码等长度变化不⼤的字段可以选择CHAR,对于评论等长度变化⼤的字段可以选择VARCHAR。
4、字段的长度在最⼤限度的满⾜可能的需要的前提下,应该尽可能的设得短⼀些,这样可以提⾼查询的效率,⽽且在建⽴索引的时候也可以减少资源的消耗。
⼆、查询的优化
1、保证在实现功能的基础上,尽量减少对数据库的访问次数;
2、通过搜索参数,尽量减少对表的访问⾏数,最⼩化结果集,从⽽减轻⽹络负担;
3、能够分开的操作尽量分开处理,提⾼每次的响应速度;
4、在数据窗⼝使⽤SQL时,尽量把使⽤的索引放在选择的⾸列;
5、算法的结构尽量简单;
6、在查询时,不要过多地使⽤通配符如:SELECT * FROM T1 语句,要⽤到⼏列就选择⼏列,如:SELECT COL1,COL2 FROM
T1;
7、在可能的情况下尽量限制尽量结果集⾏数如:SELECT TOP 300 COL1,COL2,COL3 FROM T1,因为某些情况下⽤户是不需要那么多的数据的。
在没有建索引的情况下,数据库查某⼀条数据,就必须进⾏全表扫描了,对所有数据进⾏⼀次遍历,查出符合条件的记录。在数据量⽐较⼩的情况下,也许看不出明显的差别,但是当数据量⼤的情况下,这种情况就是极为糟糕的了。
SQL语句在SQL SERVER中是如何执⾏的,他们担⼼⾃⼰所写的SQL语句会被SQL SERVER误解。⽐如:
SELECT * FROM TABLE1 WHERE NAME='ZHANGSAN' AND TID > 10000
和执⾏:
SELECT * FROM TABLE1 WHERE TID > 10000 AND NAME='ZHANGSAN'
⼀些⼈不知道以上两条语句的执⾏效率是否⼀样,因为如果简单的从语句先后上看,这两个语句的确是不⼀样,如果TID是⼀个聚合索引,那么后⼀句仅仅从表的10000条以后的记录中查就⾏了;⽽前⼀句则要先从全表中查看有⼏个NAME='ZHANGSAN'的,⽽后再根据限制条件条件TID>10000来提出查询结果。
事实上,这样的担⼼是不必要的。SQL SERVER中有⼀个“查询分析优化器”,它可以计算出where⼦句中的搜索条件并确定哪个索引能缩⼩表扫描的搜索空间,也就是说,它能实现⾃动优化。虽然查询优化器可以根据where⼦句⾃动的进⾏查询优化,但有时查询优化器就会不按照您的本意进⾏快速查询。
在查询分析阶段,查询优化器查看查询的每个阶段并决定限制需要扫描的数据量是否有⽤。如果⼀个阶段可以被⽤作⼀个扫描参数(SARG),那么就称之为可优化的,并且可以利⽤索引快速获得所需数据。
SARG的定义:⽤于限制搜索的⼀个操作,因为它通常是指⼀个特定的匹配,⼀个值的范围内的匹配或者两个以上条件的AND连接。形式如下:
列名操作符 <;常数或变量> 或 <;常数或变量> 操作符列名
列名可以出现在操作符的⼀边,⽽常数或变量出现在操作符的另⼀边。如:
Name='张三'
价格>5000
5000<;价格
Name='张三' AND 价格>5000
如果⼀个表达式不能满⾜SARG的形式,那它就⽆法限制搜索的范围了,也就是SQL SERVER必须对每⼀⾏都判断它是否满⾜WHERE ⼦句中的所有条件。所以⼀个索引对于不满⾜SARG形式的表达式来说是⽆⽤的。
所以,优化查询最重要的就是,尽量使语句符合查询优化器的规则避免全表扫描⽽使⽤索引查询。
具体要注意的:
1、应尽量避免在 where ⼦句中对字段进⾏ null 值判断,否则将导致引擎放弃使⽤索引⽽进⾏全表扫
描,如:
SELECT ID FROM T WHERE NUM IS NULL
可以在NUM上设置默认值0,确保表中NUM列没有NULL值,然后这样查询:
SELECT ID FROM T WHERE NUM=0
2、应尽量避免在 where ⼦句中使⽤!=或<>操作符,否则将引擎放弃使⽤索引⽽进⾏全表扫描。优化器将⽆法通过索引来确定将要命中的⾏数,因此需要搜索该表的所有⾏;
3、应尽量避免在 where ⼦句中使⽤ OR来连接条件,否则将导致引擎放弃使⽤索引⽽进⾏全表扫描,如:
SELECT ID FROM T WHERE NUM=10 OR NUM=20
可以这样查询:
SELECT ID FROM T WHERE NUM=10 UNION ALL SELECT ID FROM T WHERE NUM=20
4、IN和 NOT IN 也要慎⽤,因为IN会使系统⽆法使⽤索引,⽽只能直接搜索表中的数据。如:
SELECT ID FROM T WHERE NUM IN(1,2,3)
对于连续的数值,能⽤ BETWEEN就不要⽤ IN了,如:
SELECT ID FROM T WHERE NUM BETWEEN 1 AND 3
5、尽量避免在索引过的字符数据中,使⽤⾮打头字母搜索。这也使得引擎⽆法利⽤索引。
见如下例⼦:
SELECT * FROM T1 WHERE NAME LIKE '%L%' ----> ⽆索引
SELECT * FROM T1 WHERE SUBSTING(NAME,2,1)='L' ----> ⽆索引
SELECT * FROM T1 WHERE NAME LIKE 'L%' ----> 有索引
即使NAME字段建有索引,前两个查询依然⽆法利⽤索引完成加快操作,引擎不得不对全表所有数据逐条操作来完成任务。⽽第三个查询能够使⽤索引来加快操作。
6、必要时强制查询优化器使⽤某个索引,如在 where ⼦句中使⽤参数,也会导致全表扫描。因为SQL只有在运⾏时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运⾏时;它必须在编
译时进⾏选择。然⽽,如果在编译时建⽴访问计划,变量的值还是未知的,因⽽⽆法作为索引选择的输⼊项。如下⾯语句将进⾏全表扫描:
SELECT ID FROM T WHERE NUM=@NUM
可以改为强制查询使⽤索引:
SELECT ID FROM T WITH(INDEX(索引名)) WHERE NUM=@NUM
7、应尽量避免在 where ⼦句中对字段进⾏表达式操作,这将导致引擎放弃使⽤索引⽽进⾏全表扫描。如:
SELECT * FROM T1 WHERE F1/2=100
应改为:
SELECT * FROM T1 WHERE F1=100*2
SELECT * FROM RECORD WHERE SUBSTRING(CARD_NO,1,4)='5378'
应改为:
SELECT * FROM RECORD WHERE CARD_NO LIKE '5378%'
SELECT member_number, first_name, last_name FROM members
WHERE DATEDIFF(yy,datofbirth,GETDATE()) > 21
应改为:
SELECT member_number, first_name, last_name FROM members
WHERE dateofbirth < DATEADD(yy,-21,GETDATE())
即:任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时要尽可能将操作移⾄等号右边。
8、应尽量避免在where⼦句中对字段进⾏函数操作,这将导致引擎放弃使⽤索引⽽进⾏全表扫描。如:
select id from t where substring(name,1,3)='abc' ---->name以abc开头的id
select id from t where datediff(day,createdate,'2005-11-30')=0 ---->'2005-11-30'⽣成的id
应改为:
select id from t where name like 'abc%'
select id from t where createdate>='2005-11-30' and createdate<'2005-12-1'
9、不要在 where ⼦句中的“=”左边进⾏函数、算术运算或其他表达式运算,否则系统将可能⽆法正确使⽤索引。
10、在使⽤索引字段作为条件时,如果该索引是复合索引,那么必须使⽤到该索引中的第⼀个字段作为条件时才能保证系统使⽤该索引,否则该索引将不会被使⽤,并且应尽可能的让字段顺序与索引顺序相⼀致。
11、很多时候⽤ exists是⼀个好的选择,例:
SELECT NUM FROM A WHERE NUM IN(SELECT NUM FROM B)
⽤下⾯的语句替换:
SELECT NUM FROM A WHERE EXISTS(SELECT 1 FROM B WHERE NUM=A.NUM)
SELECT SUM(T1.C1) FROM T1 WHERE(SELECT COUNT(*)FROM T2 WHERE T2.C2=T1.C2>0) ⽤下⾯的语句替换:
SELECT SUM(T1.C1) FROM T1WHERE EXISTS(SELECT * FROM T2 WHERE T2.C2=T1.C2) 两者产⽣相同的结果,但是后者的效率显然要⾼于前者。因为后者不会产⽣⼤量锁定的表扫描或是索引扫描。
如果你想校验表⾥是否存在某条纪录,不要⽤count(*)那样效率很低,⽽且浪费服务器资源。可以⽤EXISTS代替。如:
IF (SELECT COUNT(*) FROM table_name WHERE column_name = 'xxx')
可以写成:
IF EXISTS (SELECT * FROM table_name WHERE column_name = 'xxx')
经常需要写⼀个T_SQL语句⽐较⼀个⽗结果集和⼦结果集,从⽽到是否存在在⽗结果集中有⽽在⼦结果集中没有的记录,如:
SELECT a.hdr_key FROM hdr_tbl a ---- tbl a 表⽰tbl⽤别名a代替
WHERE NOT EXISTS (SELECT * FROM dtl_tbl b WHERE a.hdr_key = b.hdr_key)
SELECT a.hdr_key FROM hdr_tbl a
LEFT JOIN dtl_tbl b ON a.hdr_key = b.hdr_key WHERE b.hdr_key IS NULL
SELECT hdr_key FROM hdr_tbl
WHERE hdr_key NOT IN (SELECT hdr_key FROM dtl_tbl)
三种写法都可以得到同样正确的结果,但是效率依次降低。
12、尽量使⽤表变量来代替临时表。如果表变量包含⼤量数据,请注意索引⾮常有限(只有主键索引)。
13、避免频繁创建和删除临时表,以减少系统表资源的消耗。
14、临时表并不是不可使⽤,适当地使⽤它们可以使某些例程更有效,例如,当需要重复引⽤⼤型表或常⽤表中的某个数据集时。但是,对于⼀次性事件,最好使⽤导出表。
15、在新建临时表时,如果⼀次性插⼊数据量很⼤,那么可以使⽤ select into 代替 create table,避免
造成⼤量 log ,以提⾼速度;如果数据量不⼤,为了缓和系统表的资源,应先create table,然后insert。
注意:SELECT INTO 语句会导致表锁定,阻⽌其他⽤户访问该表
16、如果使⽤到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
17、在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。⽆需在执⾏存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。
18、尽量避免⼤事务操作,提⾼系统并发能⼒。
19、尽量避免向客户端返回⼤数据量,若数据量过⼤,应该考虑相应需求是否合理。
20、避免使⽤不兼容的数据类型。例如float和int、char和varchar、binary和varbinary是不兼容的。数据类型的不兼容可能使优化器⽆法执⾏⼀些本来可以进⾏的优化操作。例如:
SELECT name FROM employee WHERE salary > 60000
在这条语句中,如salary字段是money型的,则优化器很难对其进⾏优化,因为60000是个整型数。我们应当在编程时将整型转化成为钱币型,⽽不要等到运⾏时转化。
21、充分利⽤连接条件,在某种情况下,两个表之间可能不只⼀个的连接条件,这时在 WHERE ⼦句中将连接条件完整的写上,有可能⼤⼤提⾼查询速度。
例:
SELECT SUM(A.AMOUNT) FROM ACCOUNT A,CARD B WHERE A.CARD_NO = B.CARD_NO
SELECT SUM(A.AMOUNT) FROM ACCOUNT A,CARD B WHERE A.CARD_NO = B.CARD_NO
AND A.ACCOUNT_NO=B.ACCOUNT_NO
第⼆句将⽐第⼀句执⾏快得多。
22、使⽤视图加速查询
把表的⼀个⼦集进⾏排序并创建视图,有时能加速查询。它有助于避免多重排序操作,⽽且在其他⽅⾯还能简化优化器的⼯作。例如:
SELECT cust.name,rcvbles.balance,……other columns
FROM cust,rcvbles
WHERE cust.customer_id = rcvlbes.customer_id
sql语句优化方式AND rcvblls.balance>0
AND cust.postcode>98000
ORDER BY cust.name
如果这个查询要被执⾏多次⽽不⽌⼀次,可以把所有未付款的客户出来放在⼀个视图中,并按客户的名字进⾏排序:
CREATE VIEW DBO.V_CUST_RCVLBES
AS
SELECT cust.name,rcvbles.balance,……other columns
FROM cust,rcvbles
WHERE cust.customer_id = rcvlbes.customer_id
AND rcvblls.balance>0
ORDER BY cust.name
然后以下⾯的⽅式在视图中查询:
SELECT * FROM V_CUST_RCVLBES WHERE postcode>98000
视图中的⾏要⽐主表中的⾏少,⽽且物理顺序就是所要求的顺序,减少了磁盘I/O,所以查询⼯作量可以得到⼤幅减少。
23、能⽤DISTINCT的就不⽤GROUP BY
SELECT OrderID FROM Details WHERE UnitPrice > 10 GROUP BY OrderID
可改为:
SELECT DISTINCT OrderID FROM Details WHERE UnitPrice > 10
24、能⽤UNION ALL就不要⽤UNION
UNION ALL不执⾏SELECT DISTINCT函数,这样就会减少很多不必要的资源
25、尽量不要⽤SELECT INTO语句
SELECT INOT 语句会导致表锁定,阻⽌其他⽤户访问该表。
上⾯我们提到的是⼀些基本的提⾼查询速度的注意事项,但是在更多的情况下,往往需要反复试验⽐较不同的语句以得到最佳⽅案。最好的⽅法当然是测试,看实现相同功能的SQL语句哪个执⾏时间最少,但是数据库中如果数据量很少,是⽐较不出来的,这时可以⽤查看执⾏计划,即:把实现相同功能的多条SQL语句考到查询分析器,按CTRL+L看查所利⽤的索引,表扫描次数(这两个对性能影响最⼤),总体上看询成本百分⽐即可。
三、算法的优化
尽量避免使⽤游标,因为游标的效率较差,如果游标操作的数据超过1万⾏,那么就应该考虑改写。使⽤基于游标的⽅法或临时表⽅法之前,应先寻基于集的解决⽅案来解决问题,基于集的⽅法通常更有效。与临时表⼀样,游标并不是不可使⽤。对⼩型数据集使⽤
FAST_FORWARD 游标通常要优于其他逐⾏处理⽅法,尤其是在必须引⽤⼏个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要⽐使⽤游标执⾏的速度快。如果开发时间允许,基于游标的⽅
法和基于集的⽅法都可以尝试⼀下,看哪⼀种⽅法的效果更好。
游标提供了对特定集合中逐⾏扫描的⼿段,⼀般使⽤游标逐⾏遍历数据,根据取出的数据不同条件进⾏不同的操作。尤其对多表和⼤表定义的游标(⼤的数据集合)循环很容易使程序进⼊⼀个漫长的等特甚⾄死机。
在有些场合,有时也⾮得使⽤游标,此时也可考虑将符合条件的数据⾏转⼊临时表中,再对临时表定义游标进⾏操作,可时性能得到明显提⾼。
(例如:对内统计第⼀版)
封装存储过程
四、建⽴⾼效的索引
创建索引⼀般有以下两个⽬的:维护被索引列的唯⼀性和提供快速访问表中数据的策略。
⼤型数据库有两种索引即簇索引和⾮簇索引:
⾮簇索引的表是按堆结构存储数据,所有的数据均添加在表的尾部;
簇索引的表,其数据在物理上会按照簇索引键的顺序存储,⼀个表只允许有⼀个簇索引。
因此,根据B树结构,可以理解添加任何⼀种索引均能提⾼按索引列查询的速度,但会降低插⼊、更新、删除操作的性能,尤其是当填充因⼦(Fill Factor)较⼤时。所以对索引较多的表进⾏频繁的插⼊、更新、删除操作,建表和索引时因设置较⼩的填充因⼦,以便在各数据页中留下较多的⾃由空间,减少页分割及重新组织的⼯作。
索引是从数据库中获取数据的最⾼效⽅式之⼀。95% 的数据库性能问题都可以采⽤索引技术得到解决。作为⼀条规则,我通常对逻辑主键使⽤唯⼀的成组索引,对系统键(作为存储过程)采⽤唯⼀的⾮成组索引,对任何外键列[字段]采⽤⾮成组索引。不过,索引就象是盐,太多了菜就咸了。你得考虑数据库的空间有多⼤,表如何进⾏访问,还有这些访问是否主要⽤作读写,(即:在实际使⽤当中,应该充分考虑到索引的开销,包括磁盘空间的开销及处理开销(如资源竞争和加锁)。如果数据频繁的更新或删加,就不宜建⽴索引)
实际上,您可以把索引理解为⼀种特殊的⽬录。微软的SQL SERVER提供了两种索引:聚集索引(clustered index,也称聚类索引、簇集索引)和⾮聚集索引(nonclustered index,也称⾮聚类索引、⾮簇集索引)。下⾯,我们举例来说明⼀下聚集索引和⾮聚集索引的区别:
其实,我们的汉语字典的正⽂本⾝就是⼀个聚集索引。⽐如,我们要查“安”字,就会很⾃然地翻开字典
的前⼏页,因为“安”的拼⾳
是“an”,⽽按照拼⾳排序汉字的字典是以英⽂字母“a”开头并以“z”结尾的,那么“安”字就⾃然地排在字典的前部。如果您翻完了所有以“a”开头的部分仍然不到这个字,那么就说明您的字典中没有这个字;同样的,如果查“张”字,那您也会将您的字典翻到最后部分,因为“张”的拼⾳是“zhang”。也就是说,字典的正⽂部分本⾝就是⼀个⽬录,您不需要再去查其他⽬录来到您需要的内容。
我们把这种正⽂内容本⾝就是⼀种按照⼀定规则排列的⽬录称为“聚集索引”。
如果您认识某个字,您可以快速地从⾃动中查到这个字。但您也可能会遇到您不认识的字,不知道它的发⾳,这时候,您就不能按照刚才的⽅法到您要查的字,⽽需要去根据“偏旁部⾸”查到您要的字,然后根据这个字后的页码直接翻到某页来到您要的字。但您结
合“部⾸⽬录”和“检字表”⽽查到的字的排序并不是真正的正⽂的排序⽅法,⽐如您查“张”字,我们可以看到在查部⾸之后的检字表中“张”的页码是672页,检字表中“张”的上⾯是“驰”字,但页码却是63页,“张”的下⾯是“弩”字,页⾯是390页。很显然,这些字并不是真正的分别位
于“张”字的上下⽅,现在您看到的连续的“驰、张、弩”三字实际上就是他们在⾮聚集索引中的排序,是字典正⽂中的字在⾮聚集索引中的映射。我们可以通过这种⽅式来到您所需要的字,但它需要两个过程,先到⽬录中的结果,然后再翻到您所需要的页码。
我们把这种⽬录纯粹是⽬录,正⽂纯粹是正⽂的排序⽅式称为“⾮聚集索引”。
进⼀步引申⼀下,我们可以很容易的理解:每个表只能有⼀个聚集索引,因为⽬录只能按照⼀种⽅法进⾏排序。
存储特点:
1. 聚集索引。表数据按照索引的顺序来存储的,也就是说索引项的顺序与表中记录的物理顺序⼀致。对于聚集索引,叶⼦结点即存储了
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论