#
一、要完成的任务
此系统中的三个部分是气象站(获取实际气象数据的物理装置)、WeatherData对象(追踪来自气象站的数据,并更新布告板)和布告板(显示目前天气状况给用户看)。
二、Observer模式
1、定义观察者模式
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
2.设计气象站
三、代码实现
1.定义接口
(1)Subject接口
Subject.java
package com.leanhuadeng.DesignPattern.Observer;
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}
(2)Observer接口
Observer.java
package com.leanhuadeng.DesignPattern.Observer;
public interface Observer {
public void update(float temp,float humidity,float pressure);
}
(3)Displayment接口
Displayment.java
package com.leanhuadeng.DesignPattern.Observer;
public interface Displayment {
public void display();
}
2.实现接口
(1)WeatherData
WeatherData.java
package com.leanhuadeng.DesignPattern.Observer;
import java.util.ArrayList;
public class WeatherData implements Subject {
private ArrayList observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData(){
observers=new ArrayList();
}
public void notifyObservers() {
for(int i=0;i<observers.size();i++){
Observer observer=(Observer)observers.get(i);
observer.update(temperature, humidity, pressure);
}
}
public void registerObserver(Observer o) {
observers.add(o);
}
public void removeObserver(Observer o) {
int i=observers.indexOf(o);
if(i>=0){
observers.remove(i);
}
}
public void measurementsChanged(){
notifyObservers();
}
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature=temperature;
this.humidity=humidity;
this.pressure=pressure;
measurementsChanged();
}
}
(2)CurrentConditionsDisplay
CurrentConditionsDisplay.java
package com.leanhuadeng.DesignPattern.Observer;
public class CurrentConditionsDisplay implements Observer, Displayment {
private float temperature;
private float humidity;
private Subject weatherData;
public CurrentConditionsDisplay(Subject weatherData){
this.weatherData=weatherData;
weatherData.registerObserver(this);
}
public void update(float temp, float humidity, float pressure) {
this.temperature=temp;
this.humidity=humidity;
display();
}
public void display() {
System.out.println("Current conditions:"+temperature+"F degrees and "+humidity+"% humidity");
}
}
(3)StatisticsDisplay
StatisticsDisplay.java
package com.leanhuadeng.DesignPattern.Observer;
import java.util.*;
public class StatisticsDisplay implements Observer, Displayment {
private float maxTemp = 0.0f;
private float minTemp = 200;
private float tempSum= 0.0f;
private int numReadings;
private WeatherData weatherData;
public StatisticsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(float temp, float humidity, float pressure) {
tempSum += temp;
numReadings++;
if (temp > maxTemp) {
maxTemp = temp;
}
if (temp < minTemp) {
minTemp = temp;
}
display();
}
public void display() {
System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings)+ "/" + maxTemp + "/" + minTemp);
}
}
(4)ForecastDisplay
ForecastDisplay.java
package com.leanhuadeng.DesignPattern.Observer;
import java.util.*;
public class ForecastDisplay implements Observer, Displayment {
private float currentPressure = 29.92f;
private float lastPressure;
private WeatherData weatherData;
public ForecastDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void update(float temp, float humidity, float pressure) {
lastPressure = currentPressure;
currentPressure = pressure;
display();
}
public void display() {
System.out.print("Forecast: ");
if (currentPressure > lastPressure) {
System.out.println("Improving weather on the way!");
} else if (currentPressure == lastPressure) {
System.out.println("More of the same");
} else if (currentPressure < lastPressure) {
System.out.println("Watch out for cooler, rainy weather");
}
}
}
3.实现气象站
WeatherStation.java
package com.leanhuadeng.DesignPattern.Observer;
public class WeatherStation {
public static void main(String[] args){
WeatherData weatherData=new WeatherData();
CurrentConditionsDisplay currentDisplay=new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
一、要完成的任务
星巴兹(Starbuzz)是以扩张速度最快而闻名的咖啡连锁店。如果你在街角看到它的店,在对面街上肯定还会看到另一家。因为扩张速度实在太快了,他们准备更新订单系统,以合乎他们的饮料供应要求。他们原先的类设计是这样的……
购买咖啡时,也可以要求在其中加入各种调料,例如:蒸奶(Steamed Milk)、豆浆(Soy)、摩卡(Mocha,也就是巧克力风味)或覆盖奶泡。星巴兹会根据所加入的调料收取不同的费用。所以订单系统必须考虑到这些调料部分。
二、Decorator模式
1、一个原则
类应该对扩展开放,对修改关闭
2、定义装饰者模式
装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
3.分析任务
4.设计任务
三、代码实现
1.定义抽象类
(1)饮料抽象类Beverage
Beverage.java
package com.leanhuadeng.DesignPattern.Decorator;
/*
* Beverage是一个抽象类,有两个方法
*/
public abstract class Beverage {
public String description="Unknown Beverage";
/*
* getDescription()已经在此实现了,但是cost()必须在子类中实现
*/
public String getDescription() {
return description;
}
public abstract double cost();
}
三、代码实现
(2)调料抽象类CondimentDecorator
CondimentDecorator.java
package com.leanhuadeng.DesignPattern.Decorator;
/*
* 首先,必须让CondimentDecorator能够取代Beverage,所以将CondimentDecorator扩展自Beverage类
*/
public abstract class CondimentDecorator extends Beverage {
//所有的调料装饰者都必须重新实现getDescription()方法.
public abstract String getDescription();
}
2.饮料实现
(1)Espresso
Espresso.java
package com.leanhuadeng.DesignPattern.Decorator;
/**//*
* 首先,必须让CondimentDecorator能够取代Beverage,所以将CondimentDecorator扩展自Beverage类
*/
public abstract class CondimentDecorator extends Beverage {
//所有的调料装饰者都必须重新实现getDescription()方法.
public abstract String getDescription();
}
(2)HouseBlend
HouseBlend.java
package com.leanhuadeng.DesignPattern.Decorator.drink;
import com.leanhuadeng.DesignPattern.Decorator.Beverage;
public class HouseBlend extends Beverage {
public HouseBlend() {
description="House Blend Coffee";
}
@Override
public double cost() {
return 0.89;
}
}
(3)DarkRoast
DarkRoast.java
package com.leanhuadeng.DesignPattern.Decorator.drink;
import com.leanhuadeng.DesignPattern.Decorator.Beverage;
public class DarkRoast extends Beverage {
public DarkRoast() {
description="Dark Roast Coffee";
}
@Override
public double cost() {
return 0.99;
}
}
(4)Decaf
Decaf.java
package com.leanhuadeng.DesignPattern.Decorator.drink;
import com.leanhuadeng.DesignPattern.Decorator.Beverage;
public class Decaf extends Beverage {
public Decaf() {
description="Decaf Coffee";
}
@Override
public double cost() {
return 1.05;
}
}
3.调料实现
(1)Mocha
Mocha.java
package com.leanhuadeng.DesignPattern.Decorator.condiment;
import com.leanhuadeng.DesignPattern.Decorator.Beverage;
import com.leanhuadeng.DesignPattern.Decorator.CondimentDecorator;
public class Mocha extends CondimentDecorator {
/*
* 要让Mocha能够引用一个Beverage,做法如下:一是用一个实例变量记录饮料,也就是被装饰者.
* 二是想办法让装饰者(饮料)记录到实例变量中,即把饮料当作构造器的参数,再由构造器将此饮料记录在实例变量中
*/
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
/*
* 我们希望叙述不只是描述饮料,而是完整的连调料都描述出来
*/
return beverage.getDescription()+",Mocha";
}
@Override
public double cost() {
/*
* 要计算带Mocha饮料的价钱,首先把调用委托给装饰对象,以计算价钱,然后再加上Mocha的价钱,得到最后结果
*/
return 0.20+beverage.cost();
}
}
(2)Soy
Soy.java
package com.leanhuadeng.DesignPattern.Decorator.condiment;
import com.leanhuadeng.DesignPattern.Decorator.Beverage;
import com.leanhuadeng.DesignPattern.Decorator.CondimentDecorator;
public class Soy extends CondimentDecorator {
Beverage beverage;
public Soy(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Soy";
}
public double cost() {
return .15 + beverage.cost();
}
}
(3)Whip
Whip.java
package com.leanhuadeng.DesignPattern.Decorator.condiment;
import com.leanhuadeng.DesignPattern.Decorator.Beverage;
import com.leanhuadeng.DesignPattern.Decorator.CondimentDecorator;
public class Whip extends CondimentDecorator {
Beverage beverage;
public Whip(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Whip";
}
public double cost() {
return .10 + beverage.cost();
}
}
4.测试类StarbuzzCoffee
StarbuzzCoffee.java
package com.leanhuadeng.DesignPattern.Decorator;
import com.leanhuadeng.DesignPattern.Decorator.condiment.Mocha;
import com.leanhuadeng.DesignPattern.Decorator.condiment.Soy;
import com.leanhuadeng.DesignPattern.Decorator.condiment.Whip;
import com.leanhuadeng.DesignPattern.Decorator.drink.DarkRoast;
import com.leanhuadeng.DesignPattern.Decorator.drink.Espresso;
import com.leanhuadeng.DesignPattern.Decorator.drink.HouseBlend;
public class StarbuzzCoffee {
public static void main(String args[]){
/*
* 订一杯Espresso,不需要调料,打印出它的描述和价钱.
*/
Beverage beverage=new Espresso();
System.out.println(beverage.getDescription()+" $"+beverage.cost());
/*
* 制造一个DarkRoast对象,用Mocha,Whip装饰它
*/
Beverage beverage2=new DarkRoast();
beverage2=new Mocha(beverage2);
beverage2=new Mocha(beverage2);
beverage2=new Whip(beverage2);
System.out.println(beverage2.getDescription()+" $"+beverage2.cost());
/*
* 最后,再来一杯调料为豆浆,摩卡\奶泡的HouseBlend咖啡
*/
Beverage beverage3=new HouseBlend();
beverage3=new Soy(beverage3);
beverage3=new Mocha(beverage3);
beverage3=new Whip(beverage3);
System.out.println(beverage3.getDescription()+" $"+beverage3.cost());
}
}
下表对重要的性能计数器做一个简要的说明:
性能计数器: |
|
|
Performance Object |
Counter |
Description |
Processor |
%processor Time |
指处理器执行非闲置线程时间的百分比,测量处理器繁忙的时间 这个计数器设计成用来作为处理器活动的主要指示器,可以选择单个CPU实例,也可以选择Total |
Interrupts/sec |
处理器正在处理的来自应用程序或硬件的中断的数量 |
|
|
|
PhysicalDisk |
% Disk Time |
计数器监视磁盘忙于读/写活动所用时间的百分比.在系统监视器中,PhysicalDisk: % Disk Time 计数器监视磁盘忙于读/写活动所用时间的百分比。如果 PhysicalDisk: % Disk Time 计数器的值较高(大于 90%),请检查 PhysicalDisk: Current Disk Queue Length 计数器了解等待进行磁盘访问的系统请求数量。等待 I/O 请求的数量应该保持在不超过组成物理磁盘的轴数的 1.5 到 2 倍。大多数磁盘只有一个轴,但独立磁盘冗余阵列 (RAID) 设 备通常有多个轴。硬件 RAID 设备在系统监视器中显示为一个物理磁盘。通过软件创建的多个 RAID 设备在系统监视器中显示为多个实例。
可以使用 Current Disk Queue Length 和 % Disk Time 计数器的值检测磁盘子系统中的瓶颈。如果 Current Disk Queue Length 和 % Disk Time 计数器的值一直很高,则考虑下列事项:
1.使用速度更快的磁盘驱动器。
2.将某些文件移至其他磁盘或服务器。
3.如果正在使用一个 RAID 阵列,则在该阵列中添加磁盘。
计数器监视磁盘忙于读/写活动所用时间的百分比.在系统监视器中,PhysicalDisk: % Disk Time 计数器监视磁盘忙于读/写活动所用时间的百分比。
如果 PhysicalDisk: % Disk Time 计数器的值较高(大于 90%),请检查 PhysicalDisk: Current Disk Queue Length 计数器了解等待进行磁
盘访问的系统请求数量。等待 I/O 请求的数量应该保持在不超过组成物理磁盘的轴数的 1.5 到 2 倍。大多数磁盘只有一个轴,但独立磁盘冗余阵列
(RAID) 设备通常有多个轴。硬件 RAID 设备在系统监视器中显示为一个物理磁盘。通过软件创建的多个 RAID 设备在系统监视器中显示为多个实例。
可以使用 Current Disk Queue Length 和 % Disk Time 计数器的值检测磁盘子系统中的瓶颈。如果 Current Disk Queue Length 和 % Disk Time 计数器的值一直很高,则考虑下列事项:
1.使用速度更快的磁盘驱动器。
2.将某些文件移至其他磁盘或服务器。
3.如果正在使用一个 RAID 阵列,则在该阵列中添加磁盘。 |
Avg.Disk Queue Length |
指读取和写入请求(为所选磁盘在实例间隔中列队的)的平均数 |
Current Disk Queue Length |
指示被挂起的磁盘 I/O 请求的数量。如果这个值始终高于 2, 就表示产生了拥塞 |
Avg.Disk Bytes/Transfer |
写入或读取操作时向磁盘传送或从磁盘传出字节的平均数 |
Disk Bytes/sec |
在读写操作中,从磁盘传出或传送到磁盘的字节速率 |
|
|
|
Memory |
Pages/sec |
被请求页面的数量. |
Available Bytes |
可用物理内存的数量 |
Committed Bytes |
已分配给物理 RAM 用于存储或分配给页面文件的虚拟内存 |
Pool Nonpaged Bytes |
未分页池系统内存区域中的 RAM 数量 |
Page Faults/sec |
是每秒钟出错页面的平均数量 |
|
|
|
Network Interface |
Bytes Received/sec |
使用本网络适配器接收的字节数 |
Bytes Sent/sec |
使用本网络适配器发送的字节数 |
Bytes Total/sec |
使用本网络适配器发送和接收的字节数 |
Server |
Bytes Received/sec |
把此计数器与网络适配器的总带宽相比较,确定网络连接是否产生瓶颈 |
|
|
|
SQL Server Access Methods |
Page Splits/sec |
每秒由于索引页溢出而发生的页拆分数.如果发现页分裂的次数很多,考虑提高Index的填充因子.数据页将会有更多的空间保留用于做数据的填充,从而减少页拆分 |
Pages Allocated/sec |
在此 SQL Server 实例的所有数据库中每秒分配的页数。这些页包括从混合区和统一区中分配的页 |
Full Scans/sec |
每秒不受限制的完全扫描数. 这些扫描可以是基表扫描,也可以是全文索引扫描 |
|
|
|
SQL Server: SQL Statistics |
Batch Requests/Sec |
每秒收到的 Transact-SQL 命令批数。这一统计信息受所有约束(如 I/O、用户数、高速缓存大小、请求的复杂程度等)影响。
批处理请求数值高意味着吞吐量 |
SQL Compilations/Sec |
每秒的编译数。表示编译代码路径被进入的次数。包括 SQL Server 中语句级重新编译导致的编译。当 SQL Server 用户活动稳定后,
该值将达到稳定状态 |
Re-Compilations/Sec |
每秒语句重新编译的次数。计算语句重新编译被触发的次数。一般来说,这个数最好较小,存储过程在理想情况下应该只编译一次,
然后执行计划被重复使用. 如果该计数器的值较高,或许需要换个方式编写存储过程,从而减少重编译的次数 |
|
|
|
SQL Server: Databases |
Log Flushes/sec |
每秒日志刷新数目 |
Active Transactions |
数据库的活动事务数 |
Backup/Restore Throughput/sec |
每秒数据库的备份和还原操作的读取/写入吞吐量。例如,并行使用多个备份设备或使用更快的设备时,可以测量数据库备份操作性能的变化情况。
数据库的备份或还原操作的吞吐量可以确定备份和还原操作的进程和性能 |
|
|
|
SQL Server General Statistics |
User Connections |
系统中活动的SQL连接数. 该计数器的信息可以用于找出系统的最大并发用户数 |
Temp Tables Creation Rate |
每秒创建的临时表/表变量的数目 |
Temp Tables For Destruction |
等待被清除系统线程破坏的临时表/表变量数 |
|
|
|
SQL Server Locks |
Number of Deadlocks/sec |
指每秒导致死锁的锁请求数. 死锁对于应用程序的可伸缩性非常有害, 并且会导致恶劣的用户体验. 该计数器必须为0 |
Average Wait Time (ms) |
每个导致等待的锁请求的平均等待时间 |
Lock requests/sec |
锁管理器每秒请求的新锁和锁转换数. 通过优化查询来减少读取次数, 可以减少该计数器的值 |
|
|
|
SQL Server:Memory Manager |
Total Server Memory (KB) |
从缓冲池提交的内存(这不是 SQL Server 使用的总内存) |
Target Server Memory (KB) |
服务器能够使用的动态内存总量 |
SQL Cache Memory(KB) |
服务器正在用于动态 SQL 高速缓存的动态内存总数 |
Memory Grants Pending |
指每秒等待工作空间内存授权的进程数. 该计数器应该尽可能接近0,否则预示可能存在着内存瓶颈 |
|
|
|
SQL Server Buffer Manager |
Buffer Cache Hit Ratio |
缓存命中率,在缓冲区高速缓存中找到而不需要从磁盘中读取(物理I/O)的页的百分比. 如果该值较低则可能存在内存不足或不正确的索引 |
Page Reads/sec |
每秒发出的物理数据库页读取数。此统计信息显示的是所有数据库间的物理页读取总数。由于物理 I/O 的开销大,可以通过使用更大的数据缓存、智能索引、更有效的查询或更改数据库设计等方法,将开销降到最低 |
Page Writes/sec |
每秒执行的物理数据库页写入数 |
Page Life Expectancy |
页若不被引用将在缓冲池中停留的秒数 |
Lazy Writes/Sec |
每秒被缓冲区管理器的惰性编写器写入的缓冲区数 |
Checkpoint Pages/Sec |
由要求刷新所有脏页的检查点或其他操作每秒刷新到磁盘的页数 |
|
|
|
你是否在千方百计优化SQL Server 数据库的性能?如果你的数据库中含有大量的表格,把这些表格分区放入独立的文件组可能会让你受益匪浅。SQL Server 2005引入的表分区技术,让用户能够把数据分散存放到不同的物理磁盘中,提高这些磁盘的并行处理性能以优化查询性能。
SQL Server数据库表分区操作过程由三个步骤组成:
1. 创建分区函数
2. 创建分区架构
3. 对表进行分区
下面将对每个步骤进行详细介绍。
步骤一:创建一个分区函数
此分区函数用于定义你希望SQL Server如何对数据进行分区的参数值(how)。这个操作并不涉及任何表格,只是单纯的定义了一项技术来分割数据。
我们可以通过指定每个分区的边界条件来定义分区。例如,假定我们有一份Customers表,其中包含了关于所有客户的信息,以一一对应的客户编号(从1到1,000,000)来区分。我们将通过以下的分区函数把这个表分为四个大小相同的分区:
CREATE PARTITION FUNCTION customer_partfunc (int)
AS RANGE RIGHT
FOR VALUES (250000, 500000, 750000) |
这些边界值定义了四个分区。第一个分区包括所有值小于250,000的数据,第二个分区包括值在250,000到49,999之间的数据。第三个分区包括值在500,000到7499,999之间的数据。所有值大于或等于750,000的数据被归入第四个分区。
请注意,这里调用的"RANGE RIGHT"语句表明每个分区边界值是右界。类似的,如果使用"RANGE LEFT"语句,则上述第一个分区应该包括所有值小于或等于250,000的数据,第二个分区的数据值在250,001到500,000之间,以此类推。
步骤二:创建一个分区架构
一旦给出描述如何分割数据的分区函数,接着就要创建一个分区架构,用来定义分区位置(where)。创建过程非常直截了当,只要将分区连接到指定的文件组就行了。例如,如果有四个文件组,组名从"fg1"到"fg4",那么以下的分区架构就能达到想要的效果:
CREATE PARTITION SCHEME customer_partscheme
AS PARTITION customer_partfunc
TO (fg1, fg2, fg3, fg4) |
注意,这里将一个分区函数连接到了该分区架构,但并没有将分区架构连接到任何数据表。这就是可复用性起作用的地方了。无论有多少数据库表,我们都可以使用该分区架构(或仅仅是分区函数)。
步骤三:对一个表进行分区
定义好一个分区架构后,就可以着手创建一个分区表了。这是整个分区操作过程中最简单的一个步骤。只需要在表创建指令中添加一个"ON"语句,用来指定分区架构以及应用该架构的表列。因为分区架构已经识别了分区函数,所以不需要再指定分区函数了。
例如,使用以上的分区架构创建一个客户表,可以调用以下的Transact-SQL指令:
CREATE TABLE customers (FirstName nvarchar(40), LastName nvarchar(40), CustomerNumber int)
ON customer_partscheme (CustomerNumber) |
关于SQL Server的表分区功能,你知道上述的相关知识就足够了。记住!编写能够用于多个表的一般的分区函数和分区架构就能够大大提高可复用性。
数据库性能调优是每一个优秀SQL Server管理员最终的责任。虽然保证数据的安全和可用性是我们的最高的目标,但是假如数据库应用程序无法满足用户的要求,那么DBA们会因为性能低下的设计和实现而受到指责。SQL Server 2005在数据库性能方面得到了很多提高,尤其是表分区的技术。如果你还没不了解表分区的特征,那么请你花点时间读这篇文章。
表分区的概念不是一个新的概念;只要你当过一段时间的SQL Server DBA,那么你可能已经对一些频繁访问的表进行过归档,当这个表中的历史数据变的不再经常被访问的时候。比如,假设你有一个打印时间报表的应用,你的报告很少会查询1995年的数据,因为绝大部分的预算规划会基于最近几年的数据。
在SQL Server的早期版本中,你可以创建多个表。每一个表都具有相同的列结构,用来保存不同年份的数据。这样,当存在着对历史数据访问的必要的时候,你可以创建一个视图来对这些表进行查询处理。将数据保存在多个表中是很方便的,因为相对于查询时扫描整个大表,扫描小表会更快。但是这种好处只有在你预先知道哪些时间段的数据会被访问。同时,一旦数据过期,你还需要创建新表并且转移新产生的历史数据。
SQL Server 7和SQL Server 2000支持分布式分区视图(distributed partitioned views,又称为物化视图,materialized views).分布式分区视图由分布于多台服务器上的、具有相同表结构的表构成,而且你还需要为每一个服务器增加链接服务器定义(linked server definitions),最后在其中一台服务器上创建一个视图将每台服务器上返回的数据合并起来。这里的设计思想是数据库引擎可以利用多台服务器的处理能力来满足查询。
但是,分布式分区视图(DPV)受到很多限制,你可以在SQL Server的在线帮助文档中阅读到。虽然DPV在一些情况下能够提供性能上的提高,但是这种技术不能被广泛的应用。已经被证明它们不能满足逐步增长的企业级应用的要求。何况,DPV的实现是一个费力的过程,需要DBA进行很多工作。
SQL Server 2005开始支持表分区,这种技术允许所有的表分区都保存在同一台服务器上。每一个表分区都和在某个文件组(filegroup)中的单个文件关联。同样的一个文件/文件组可以容纳多个分区表。
在这种设计架构下,数据库引擎能够判定查询过程中应该访问哪个分区,而不用扫描整个表。如果查询需要的数据行分散在多个分区中,SQL Server使用多个处理器对多个分区进行并行查询。你可以为在创建表的时候就定义分区的索引。 对小索引的搜索或者扫描要比扫描整个表或者一张大表上的索引要快很多。因此,当对大表进行查询,表分区可以产生相当大的性能提升。
现在让我们通过一个简单的例子来了解表分区是如何发挥作用的。在这篇文章中,我不想深入到分区的语法细节当中,这些你可以在SQL Server的在线帮助文档中找到。下面的例子基于存储着一个时间报表系统的数据的数据仓库。除了默认的文件组,我另外创建了7个文件组,每一个文件组仅包含一个文件,这个文件将存储由分区函数定义的一部分数据。
为了测试表分区的性能提升,我向这个分区表中插入了一千五百万行,同时向另外一个具有相同表结构、但是没有进行分区的表插入了同样的数据。对分区表执行的INSERT语句运行的更快一些。甚至在我的内存不到1G的笔记本电脑上,对分区表的INSERT语句比不分区的表的INSERT语句要快上三倍。当然,查询的执行时间依据硬件资源的差异而所有变化,但是你还是能够在你的环境中感到不同程度的提升。
我将检查更深入了一步,通过分别检查同一条返回所有行的、简单SELECT语句在分区表和非分区表上的执行计划,返回的数据范围通过WHERE语句来指定。同一条语句在这两个不同的表上有不同的执行计划。对于分区表的查询显示出一个嵌套的循环和索引的扫描。从本质上来说,SQL Server将两个分区视为独立的表,因此使用一个嵌套循环将它们连接起来。对非分区的表的同一个查询则使用索引扫描来返回同样的列。当你使用同样的分区策略创建多个表,同时在查询中连接这些表,那么性能上的提升会更加明显
你可以使用下面的查询来了解每一个分区中的行的个数:
SELECT $PARTITION.TimeEntryDateRangePFN(time_entry_date) AS Partition,
COUNT(*) AS [COUNT] FROM fact_time_entry
GROUP BY $PARTITION.TimeEntryDateRangePFN(time_entry_date)
ORDER BY Partition |
表分区对交易环境和数据仓库环境来说,都是一个重要的特征。数据仓库用户最主要的抱怨是移动事实表(fact table)会花费太多时间。当装载数据到事实表的时候,用户查询(立方体处理查询)的性能会明显下降,甚至是完全无法成功。因此,装载大量的数据到事实表的时候常常需要停机。如果使用表分区,就不再出现这样的情况——确切的讲,你一眨眼的工夫就可以移动事实表。为了演示这是如何生效的,我使用上面例子中相同的分区函数和表结构来创建一个新的表,这个表叫做fact_time_entry2。表的主键从五千万开始,这样fact_time_entry2就不会包含表fact_time_entry中已经有的数据。
现在我把2007年的数据移动到这张fact_time_entry2中。同时让我们假设fact_time_entry表中包含着2007年之前的数据。在fact_time_entry2表完成数据的转移,我执行下面的语句:
ALTER TABLE fact_time_entry2
SWITCH PARTITION 8 TO fact_time_entry PARTITION 8 |
这条语句将编号为8的分区,这个分区恰好包含着2007年的数据,从fact_time_entry2移动到了fact_time_entry表中,在我的笔记本电脑上,这个过程只花费了3毫秒。在这短短的3毫秒中,我的事实表就增加了五百万条记录!的确,我需要在交换分区之前,将数据移动到中间表,但是我的用户不需要担心——事实表随时都可以查询!在这幕后,实际上没有数据移动——只是两张表的元数据发生了变化。
我可以使用类似的查询删除事实表中不在需要的数据。例如,假设我们决定我们不再关心2004年的记录。下面的语句可以将这些记录转移到我们创建的工作表中:
ALTER TABLE fact_time_entry
SWITCH PARTITION 2 TO fact_time_entry2 PARTITION 2 |
这样的语句依旧在毫秒级内完成了。现在,我可以删除fact_time_entry2或者将它移到其他的服务器上。我的事实表不会包含2004年的任何记录。这个分区还是需要在目的表中存在,而且它必须是空的。你不能将分区转移到一个包含重复数据的表中。源表和目的表的分区必须一致,同时被转移的数据必须在同一个文件组中。即使受到这么多的限制,转换分区和无需停机就可以移动数据表的功能必将让数据仓库的实现变的前所未有的轻松。
SQL Server 表分区(partitioned table/Data Partitioning)
Partitioned Table
可伸缩性性是数据库管理系统的一个很重要的方面,在SQL Server 2005中可伸缩性方面提供了表分区功能。
其实对于有关系弄数据库产品来说,对表、数据库和服务器进行数据分区的从而提供大数据量的支持并不是什么新鲜事,但 SQL Server 2005 提供了一个新的体系结构功能,用于对数据库中的文件组进行表分区。水平分区可根据分区架构,将一个表划分为几个较小的分组。表分区功能是针对超大型数据库(从数百吉字节到数千吉字节或更大)而设计的。超大型数据库 (VLDB) 查询性能通过分区得到了改善。通过对广大分区列值进行分区,可以对数据的子集进行管理,并将其快速、高效地重新分配给其他表。
设想一个大致的电子交易网站,有一个表存储了此网站的历史交易数据,这此数据量可能有上亿条,在以前的SQL Server版本中存储在一个表中不管对于查询性能还是维护都是件麻烦事,下面我们来看一下在SQL Server2005怎么提高性能和可管理性:
-- 创建要使用的测试数据库,Demo
USE [master]
IF EXISTS (SELECT name FROM master.dbo.sysdatabases WHERE name = N'DEMO')
DROP DATABASE [DEMO]
CREATE DATABASE [DEMO]
--由于表分区使用使用新的体系结构,使用文件组来进行表分区,所以我们创建将要用到的6个文件组,来存储6个时间段的交易数据[<2000],[ 2001], [2002], [2003], [2004], [>2005]
ALTER DATABASE Demo ADD FILEGROUP YEARFG1;
ALTER DATABASE Demo ADD FILEGROUP YEARFG2;
ALTER DATABASE Demo ADD FILEGROUP YEARFG3;
ALTER DATABASE Demo ADD FILEGROUP YEARFG4;
ALTER DATABASE Demo ADD FILEGROUP YEARFG5;
ALTER DATABASE Demo ADD FILEGROUP YEARFG6;
-- 下面为这些文件组添加文件来进行物理的数据存储
ALTER DATABASE Demo ADD FILE (NAME = 'YEARF1', FILENAME = 'C:"ADVWORKSF1.NDF') TO FILEGROUP YEARFG1;
ALTER DATABASE Demo ADD FILE (NAME = 'YEARF2', FILENAME = 'C:"ADVWORKSF2.NDF') TO FILEGROUP YEARFG2;
ALTER DATABASE Demo ADD FILE (NAME = 'YEARF3', FILENAME = 'C:"ADVWORKSF3.NDF') TO FILEGROUP YEARFG3;
ALTER DATABASE Demo ADD FILE (NAME = 'YEARF4', FILENAME = 'C:"ADVWORKSF4.NDF') TO FILEGROUP YEARFG4;
ALTER DATABASE Demo ADD FILE (NAME = 'YEARF5', FILENAME = 'C:"ADVWORKSF5.NDF') TO FILEGROUP YEARFG5;
ALTER DATABASE Demo ADD FILE (NAME = 'YEARF6', FILENAME = 'C:"ADVWORKSF6.NDF') TO FILEGROUP YEARFG6;
-- HERE WE ASSOCIATE THE PARTITION FUNCTION TO
-- THE CREATED FILEGROUP VIA A PARTITIONING SCHEME
USE DEMO;
GO
-------------------------------------------------------
-- 创建分区函数
-------------------------------------------------------
CREATE PARTITION FUNCTION YEARPF(datetime)
AS
RANGE LEFT FOR VALUES ('01/01/2000'
,'01/01/2001'
,'01/01/2002'
,'01/01/2003'
,'01/01/2004')
-------------------------------------------------------
-- 创建分区架构
-------------------------------------------------------
CREATE PARTITION SCHEME YEARPS
AS PARTITION YEARPF TO (YEARFG1, YEARFG2,YEARFG3,YEARFG4,YEARFG5,YEARFG6)
-- 创建使用此Schema的表
CREATE TABLE PARTITIONEDORDERS
(
ID INT NOT NULL IDENTITY(1,1),
DUEDATE DATETIME NOT NULL,
) ON YEARPS(DUEDATE)
--为此表填充数据
declare @DT datetime
SELECT @DT = '1999-01-01'
--start looping, stop at ending date
WHILE (@DT <= '2005-12-21')
BEGIN
INSERT INTO PARTITIONEDORDERS VALUES(@DT)
SET @DT=dateadd(yy,1,@DT)
END
-- 现在我们可以看一下我们刚才插入的行都分布在哪个Partition
SELECT *, $PARTITION.YEARPF(DUEDATE) FROM PARTITIONEDORDERS
--我们可以看一下我们现在PARTITIONEDORDERS表的数据存储在哪此partition中,以及在这些分区中数据量的分布
SELECT * FROM SYS.PARTITIONS WHERE OBJECT_ID = OBJECT_ID('PARTITIONEDORDERS')
--
--现在我们设想一下,如果我们随着时间的流逝,现在已经到了2005年,按照我们先前的设定,我们想再想入一个分区,这时是不是重新创建表分区架构然后重新把数据导放到新的分区架构呢,答案是完全不用。下面我们就看如果新加一个分区。
--更改分区架构定义语言,让下一个分区使用和现在已经存在的分区YEARFG6分区中,这样此分区就存储了两段partition的数据。
ALTER PARTITION SCHEME YEARPS
NEXT USED YEARFG6;
--更改分区函数
ALTER PARTITION FUNCTION YEARPF()
SPLIT RANGE ('01/01/2005')
--现在我们可以看一下我们刚才插入的行都分布在哪个Partition?
SELECT *, $PARTITION.YEARPF(DUEDATE) FROM PARTITIONEDORDERS
--我们可以看一下我们现在PARTITIONEDORDERS表的数据存储在哪此partition中,以及在这些分区中
摘要: 以前的一次技术例会内容,拿出来共享一下,大家有问题可以提出来,一起提高。
技术会议- SQL Server Partitioning
V2※高捷
本月技术会议专题为数据库分区( SQL Server Partitioning ),主要讲述为什么要分区,在什么情况下需要对数据进行分区,如何进行分区,分区表管理等内容。
一、 摘要
◆ &nb... 阅读全文
摘要: 随着“金盾工程”建设的逐步深入和公安信息化的高速发展,公安计算机应用系统被广泛应用在各警种、各部门。与此同时,应用系统体系的核心、系统数据的存放地――数据库也随着实际应用而急剧膨胀,一些大规模的系统,如人口系统的数据甚至超过了1000万条,可谓海量。那么,如何实现快速地从这些超大容量的数据库中提取数据(查询)、分析、统计以及提取数据后进行数据分页已成为各地系统管理员和数据库... 阅读全文
清除日志:
DECLARE @LogicalFileName sysname,
@MaxMinutes INT,
@NewSize INT
USE szwzcheck -- 要操作的数据库名
SELECT @LogicalFileName = 'szwzcheck_Log', -- 日志文件名
@MaxMinutes = 10, -- Limit on time allowed to wrap log.
@NewSize = 20 -- 你想设定的日志文件的大小(M)
-- Setup / initialize
DECLARE @OriginalSize int
SELECT @OriginalSize = size
FROM sysfiles
WHERE name = @LogicalFileName
SELECT 'Original Size of ' + db_name() + ' LOG is ' +
CONVERT(VARCHAR(30),@OriginalSize) + ' 8K pages or ' +
CONVERT(VARCHAR(30),(@OriginalSize*8/1024)) + 'MB'
FROM sysfiles
WHERE name = @LogicalFileName
CREATE TABLE DummyTrans
(DummyColumn char (8000) not null)
DECLARE @Counter INT,
@StartTime DATETIME,
@TruncLog VARCHAR(255)
SELECT @StartTime = GETDATE(),
@TruncLog = 'BACKUP LOG ' + db_name() + ' WITH TRUNCATE_ONLY'
DBCC SHRINKFILE (@LogicalFileName, @NewSize)
EXEC (@TruncLog)
-- Wrap the log if necessary.
WHILE @MaxMinutes > DATEDIFF (mi, @StartTime, GETDATE()) -- time
AND @OriginalSize = (SELECT size FROM sysfiles WHERE name =
@LogicalFileName)
AND (@OriginalSize * 8 /1024) > @NewSize
BEGIN -- Outer loop.
SELECT @Counter = 0
WHILE ((@Counter < @OriginalSize / 16) AND (@Counter < 50000))
BEGIN -- update
INSERT DummyTrans VALUES ('Fill Log')
DELETE DummyTrans
SELECT @Counter = @Counter + 1
END
EXEC (@TruncLog)
END
SELECT 'Final Size of ' + db_name() + ' LOG is ' +
CONVERT(VARCHAR(30),size) + ' 8K pages or ' +
CONVERT(VARCHAR(30),(size*8/1024)) + 'MB'
FROM sysfiles
WHERE name = @LogicalFileName
DROP TABLE DummyTrans
SET NOCOUNT OFF
把szwzcheck换成你数据库的名字即可,在查询分析器里面运行。
现在SQL2005提供了DTA的工具,大家在去优化一个语句时都有意无意的使用此工具所给出的一些优化建议。不过它始终是个工具,所给出的优化建议很多时候都是使用2005新的索引功能INCLUDE把查询列表统统包括在一个索引中。因此,每个开发人员所定义的索引就会存在重复或是很相似的地方。因为索引页的数据比较密集,因此在对包含有索引列的字段做修改操作时,都会去相应的修改包含此键值列的索引。理论上对一张表多加一个索引,修改数据的速度就会比原来慢1.2倍。因此,这会增加记录被锁定的时间,从而也就会影响到查询的性能。
但是,如果通过SQL2005提供的几个与索引相关的视图,我们不能很方便的观察出索引所包含的键值列和它的包含列是哪些。同时,如果表是分区表,通过sys.partitions查看总记录数时要累加各分区的行数。
下面的脚本可以组合这些视图,查询出对象名称、对象类型(表或索引视图)、索引名称、索引编号、索引类型、是否主键、是否唯一、填充度、键值字段、包含字段、表的总记录数(取各分区中行的总数)、索引描述,如下图部分显示结果所示,这样就很方便的判断出哪些索引是重复或相似的:
对取包含字段时用到了FOR XML PATH这个功能,可以方便的把包含列组织成A,B,C的形式。然后使用CROSS APPLY得出最终的结果。脚本定义如下:
USE AdventureWorks;
GO
DROP INDEX IX_SalesOrderHeader_CustomerID ON Sales.SalesOrderHeader
GO
CREATE INDEX IX_SalesOrderHeader_CustomerID ON Sales.SalesOrderHeader(CustomerID)
INCLUDE(ShipDate,Freight)
GO
--sp_helpindex不能反应出包含字段
EXEC sp_helpindex 'Sales.SalesOrderHeader'
GO
--SQL2005下用于诊断索引重复的脚本
DECLARE @Result TABLE(
objname sysname NOT NULL,
objtype char(2) NOT NULL,
indexname sysname NOT NULL,
index_id int NOT NULL,
indextype tinyint NOT NULL,
is_primary_key bit NOT NULL,
is_unique bit NOT NULL,
fill_factor tinyint NOT NULL,
IndexKeys nvarchar(2126) NOT NULL,
Included nvarchar(max) NULL,
rows bigint NOT NULL,
IndexDesc varchar(210) NULL
)
CREATE TABLE #IndexInfo
(
IndexName sysname NOT NULL,
IndexDesc varchar(210) NULL,
IndexKeys nvarchar(2126) NULL
)
DECLARE @objname sysname
DECLARE ObjectList CURSOR FAST_FORWARD FOR
SELECT SCHEMA_NAME(o.schema_id)+'.'+o.name AS objname
FROM sys.indexes i JOIN sys.objects o ON i.object_id=o.object_id
WHERE o.type IN('U','V') AND i.index_id IN(0,1)
--AND o.object_id=OBJECT_ID(N'Sales.SalesOrderHeader')
OPEN ObjectList
FETCH NEXT FROM ObjectList INTO @objname
WHILE @@FETCH_STATUS = 0
BEGIN
INSERT INTO #IndexInfo EXEC sp_helpindex @objname--使用全名称,防止直接使用表名称时无法获取其它架构表的信息
INSERT INTO @Result
SELECT SCHEMA_NAME(o.schema_id)+'.'+o.name AS objname, o.type AS objtype,
i.name AS indexname,i.index_id,i.type AS indextype,i.is_primary_key,i.is_unique,i.fill_factor,
t.IndexKeys,
c.name AS Included,
p.rows,t.IndexDesc
FROM sys.indexes i
INNER JOIN sys.objects o ON i.object_id=o.object_id
INNER JOIN #IndexInfo t ON t.IndexName=i.name
CROSS APPLY (SELECT SUM(rows) AS rows
FROM sys.partitions p
WHERE p.index_id = i.index_id AND p.object_id = i.object_id
) p
CROSS APPLY (SELECT name=STUFF((SELECT N',' + QUOTENAME(y) AS [text()]
FROM (SELECT c.name AS y
FROM sys.index_columns ic
JOIN sys.columns c ON ic.column_id=c.column_id AND ic.object_id=c.object_id
WHERE ic.object_id=i.object_id AND ic.index_id=i.index_id AND ic.is_included_column=1
) AS Y
ORDER BY y FOR XML PATH('')), 1, 1, N'')
) c
WHERE o.object_id=OBJECT_ID(@objname)
TRUNCATE TABLE #IndexInfo
FETCH NEXT FROM ObjectList INTO @objname
END
CLOSE ObjectList
DEALLOCATE ObjectList
DROP TABLE #IndexInfo
SELECT * FROM @Result ORDER BY objname,index_id
用于SQL2000的脚本:
--SQL2000下用于诊断索引重复的脚本
DECLARE @Result TABLE (
[objname] [sysname] NOT NULL ,
[indexname] [sysname] NOT NULL ,
[indid] [smallint] NOT NULL ,
[IsUnique] [int] NOT NULL ,
[IndexKeys] [nvarchar] (2126) NOT NULL ,
[rowcnt] [bigint] NOT NULL ,
[rowmodctr] [int] NOT NULL ,
[keycnt] [smallint] NOT NULL ,
[OrigFillFactor] [tinyint] NOT NULL ,
[dpages] [int] NOT NULL ,
[IndexDesc] [varchar] (210) NULL
)
CREATE TABLE #IndexInfo
(
IndexName sysname NOT NULL,
IndexDesc varchar(210) NULL,
IndexKeys nvarchar(2126) NULL
)
DECLARE @objname sysname,
@objid int
DECLARE ObjectList CURSOR FAST_FORWARD FOR
SELECT USER_NAME(o.uid)+'.'+o.name AS objname,o.id AS objid
FROM dbo.sysobjects o JOIN dbo.sysindexes i ON i.id = o.id
WHERE o.type IN( 'U','V') AND i.indid IN(0,1) AND o.name<>'dtproperties'--用于保存关系图的系统表
ORDER BY o.name,o.uid
OPEN ObjectList
FETCH NEXT FROM ObjectList INTO @objname,@objid
WHILE @@FETCH_STATUS = 0
BEGIN
INSERT INTO #IndexInfo EXEC sp_helpindex @objname--使用全名称,防止直接使用表名称时无法获取其它用户表的信息
INSERT INTO @Result
SELECT USER_NAME(o.uid)+'.'+o.name AS objname, i.name AS indexname, i.indid,
CASE WHEN t.IndexDesc LIKE '%unique%' THEN 1 ELSE 0 END AS IsUnique,
t.IndexKeys, i.rowcnt, i.rowmodctr, i.keycnt, i.OrigFillFactor, i.dpages,t.IndexDesc
FROM dbo.sysindexes i
INNER JOIN dbo.sysobjects o ON i.id = o.id
INNER JOIN #IndexInfo t ON t.IndexName=i.name
WHERE o.id=@objid
TRUNCATE TABLE #IndexInfo
FETCH NEXT FROM ObjectList INTO @objname,@objid
END
CLOSE ObjectList
DEALLOCATE ObjectList
DROP TABLE #IndexInfo
SELECT * FROM @Result ORDER BY objname,indid
数据库表A有十万条记录,查询速度本来还可以,但导入一千条数据后,问题出现了。当选择的数据在原十万条记录之间时,速度还是挺快的;但当选择的数据在这一千条数据之间时,速度变得奇慢。
凭经验,这是索引碎片问题。检查索引碎片DBCC SHOWCONTIG(表),得到如下结果:
DBCC SHOWCONTIG 正在扫描 'A' 表...
表: 'A'(884198200);索引 ID: 1,数据库 ID: 13
已执行 TABLE 级别的扫描。
- 扫描页数.....................................: 3127
- 扫描扩展盘区数...............................: 403
- 扩展盘区开关数...............................: 1615
- 每个扩展盘区上的平均页数.....................: 7.8
- 扫描密度[最佳值:实际值]....................: 24.20%[391:1616]
- 逻辑扫描碎片.................................: 68.02%
- 扩展盘区扫描碎片.............................: 38.46%
- 每页上的平均可用字节数.......................: 2073.2
- 平均页密度(完整)...........................: 74.39%
DBCC 执行完毕。如果 DBCC 输出了错误信息,请与系统管理员联系。
由上我们看出,逻辑扫描碎片和扩展盘区扫描碎片都非常大,果真需要对索引碎片进行处理了。
一般有两种方法解决,一是利用DBCC INDEXDEFRAG整理索引碎片,二是利用DBCC DBREINDEX重建索引。二者各有优缺点。调用微软的原话如下:
DBCC INDEXDEFRAG 命令是联机操作,所以索引只有在该命令正在运行时才可用。而且可以在不丢失已完成工作的情况下中断该操作。这种方法的缺点是在重新组织数据方面没有聚集索引的除去/重新创建操作有效。
重新创建聚集索引将对数据进行重新组织,其结果是使数据页填满。填满程度可以使用 FILLFACTOR 选项进行配置。这种方法的缺点是索引在除去/重新创建周期内为脱机状态,并且操作属原子级。如果中断索引创建,则不会重新创建该索引。
也就是说,要想获得好的效果,还是得用重建索引,所以决定重建索引。
DBCC DBREINDEX(表,索引名,填充因子)
第一个参数,可以是表名,也可以是表ID。
第二个参数,如果是'',表示影响该表的所有索引。
第三个参数,填充因子,即索引页的数据填充程度。如果是100,表示每一个索引页都全部填满,此时select效率最高,但以后要插入索引时,就得移动后面的所有页,效率很低。如果是0,表示使用先前的填充因子值。
DBCC DBREINDEX(A,'',100)
重新测试查询速度,飞快。
数据文件的碎片
影响磁盘读取性能的两个主要因素:录道时间和轮询延迟。
我们在查询数据时,有两种磁盘的读取方式:顺序读和随机读。随机读发生在对表或索引的扫描时,顺序读发生在使用索引查找数据时。当数据文件有大量碎片时,随机读不会受到太大的影响,因为SQLSERVER会根据表所占用到的数据页面,不管记录的逻辑顺序随机的读取出来,所谓的预读正是这种方式。而顺序读时,因为要按记录的逻辑顺序读取相应的记录,如果逻辑上相邻的数据页在物理分布上不连续,则会因为磁头的来回移动使性能大打折扣。这也就是为什么有时我们看到表扫描比索引查找效率更高的原因。
我们在创建数据库时,会为数据文件和日志文件分别指定一个初始大小和增量大小。如果这些文件都在独自的逻辑分区中,那么不会有磁盘碎片的产生。但是,如果每个文件所在的分区中还有其它的数据库文件。则因为这些文件的自增长就会产生磁盘碎片了,如下图所示:
为了防止这些碎片的产生,我们应该每次把文件自增长的大小设置的更大些,以防止产生这么多小的碎片。但是,如果每次文件增长的过大,特别是在系统繁忙的时候,势必会影响数据库的性能。为了能快速的完全文件增长的工作,SQLSERVER借助WINDOWS的即时文件初始化功能来快速的完成此项任务。若要使用即时文件初始化,必须在 Windows 帐户下运行 MSSQLSERVER 服务帐户并为该 Windows 帐户分配 Windows SE_MANAGE_VOLUME_NAME 特权。此权限默认情况下分配给 Windows 管理员组。如果拥有系统管理员权限,您可以通过将 Windows 帐户添加到“执行卷维护任务”安全策略来分配此权限。默认MSSQLSERVER是在LocalSystem帐号启动的,但此帐号的SE_MANAGE_VOLUME_NAME 特权是被禁用的。详见http://msdn.microsoft.com/en-us/library/ms684190(VS.85).aspx
结论:定期执行磁盘碎片整理并为数据文件分配合适的初始大小。并制定任务计划,在系统空闲时根据现在数据的实际大小调整数据文件的大小,减少对系统繁忙时因为文件增长带来的开销。
日志文件的碎片
不同于数据文件,日志文件不能使用即时文件初始化进行自增长。因此,在分配一个很大自增长量时就会很耗时。在这个操作期间,所有的inset、delete、update操作都会被阻塞。那么随后一断时间数据库的整体性能也会受到很大的影响。就像高速公路突然塞车被疏导之后一样。在系统内部,会把这些日志文件分成好多个虚拟的日志文件(VLF),你可以使用DBCC LOGINFO来查看你当前的日志文件中有多少个VLF。如果返回的结果数很多,证明你应该对日志进行维护了。这就和数据文件的磁盘碎片一样,会对性能造成严重影响。这个数量是由日志文件的整体大小和扩张日志使用的增量在内部决定的,我们无法控制。
但是,因为日志是顺序写入的,真正的磁盘碎片对性能影响其实不是很大。如果你的增量设置过小,会因为频繁的调整日志文件而影响到VLF。如果你设置的增量过大,又会占用过长的文件分配时间。因此,最好的办法就是你控制你的事务尽可能的短。同时,定期的备份你的日志,以使日志可以截断。从而防止日志文件进行自增长而带来的性能开销。一直以来有种误解就是认为完整恢复模式的数据库不会自动截断事务日志。如果你从来没有对这个数据库做过完整备份,其实它也是可以对事务日志自动截断的。
结论:VLF越少越好,建议的数值是不超过5个。定时对事务日志进行备份,以最快截断以供后续使用。
索引的内部和外部碎片
这些碎片都是逻辑上的碎片。整天都在讨论索引碎片,相信这个大家应该都很清楚了。不再多罗嗦,概括如下:内部碎片受页面填充度的影响,如果碎片过多使表所占的实际页面数比无碎片时多出很多。因此在表扫描时会发生更多的I/O操作,但是索引查找时不会受到很大影响。外部碎片是因为页面的逻辑顺序和硬盘上的物理顺序不一致或是分区的不连续所造成的。这时,如果使用索引进行范围查找的话,因为要按照记录的逻辑顺序进行记取,会引起磁头来回移动。关于索引碎片的维护,请参见联机文档。
文件的目录存储及文件名要求
在目录中新建、访问、删除文件时,都会在目录的元数据中进行相应的搜索或执行Chkdsk.exe命令完成相应的任务。因此,如果文件过多或是目录层次太多,会花费更长的时间完成。建议文件数目不超过100,000,当然我们很多时候永远达不到这个数目。同时,Windwos NT之后的版本,为了提供向后兼容性,在你对目录中的任何文件修改之后,不符合8.3文件格式的长文件名都会生成一个8.3格式文件名。如果你的目录中有上百个长文件名的文件,这会带来一定的性能损失。因此,如果机器上没有运行16位的程序,可通过注册表把NtfsDisable8dot3NameCreation设置为1,禁止生成8.3文件名。注册表位置如下:HKEY_LOCAL_MACHINE"SYSTEM"CurrentControlSet"Control"FileSystem"NtfsDisable8dot3NameCreation。那么日志文件和数据文件是在什么时候才会被修改呢?如果你不怕葬你的硬盘,运行每个脚本之前创 建一个新的Northwind数据库。你可以运行一下下面的脚本,此例也正好演示一下insert into和select into的效率问题。
USE Northwind;
GO
select * into my_customers
from dbo.Customers where 1=0
GO
insert into my_customers
select c1.*
from dbo.Customers c1,dbo.Customers c2,dbo.Customers c3
--观察运行前后的数据文件和日志文件的增长
--insert into被完整记录于日志中,我们发现
--日志文件增长了很大,我的长到了500M多
--在新建Northwind数据库后,运行下面的脚本
--select into作为一个大批量操作,只记录了部分事务
--因此日志增长不是很大,我的长到了4M
--因此从性能上来说select into效率高于insert into
select c1.*
into my_customers
from dbo.Customers c1,dbo.Customers c2,dbo.Customers c3
硬盘格式化的簇大小设置
客户给我们一台新的服务器,我们可以最大调整的就是硬盘。CPU、内存就摆在那了,客户说没有更好的机器了。同时,硬盘的I/O效率也是影响查询性能的关键因素。SQL2005对tempdb的要求越来越高,如果条件允许,一般把tempdb、数据文件、索引文件、全文目录都分别存放在独立的RAID5阵列中(有时MSFTESQL服务会因为磁盘I/O过高而暂停服务),日志文件则存放在RAID1+0或RAID1中,操作系统和SQLSERVER存放于RAID1中。硬盘的扇区大小默认是512个字节,那么我们在对新的硬盘进行格式化时,选择的簇的大小多少才是最合适的?阵列的条带容量大小应该设置为多少?
因为一个数据页面是8K,数据页面在内部由扩展分区进行管理。一个扩展分区包含了8个逻辑连续的页面。分区的管理是通过全局分配映射页面(GAM,只保存超过8个页面的表,统一分区)和共享全局分配映射页面(SGAM,保存小于8个数据页面的表,混合分区)来进行管理的,一个数据文件的第2个页面是GAM,第3个页面是SGAM。每个GAM和SGAM能管理的页面范围是4G,每4G都会增加一个GAM和SGAM。在你创建一个新的数据库是,使用DBCC PAGE命令来观察这两个页面,可以看到数据库已经分配了很多扩展分区,还保留了一些分区。在创建表时,新加记录后,如果表总共占用不到8个数据页面的话会被分配到SGAM中,超过8个页面时才会被分配到GAM分区中。前面我们提到过索引的外部碎片是因为页面的逻辑顺序和硬盘上的物理顺序不一致或是分区的不连续所造成的。因此,如果我们把簇的大小设置为64K时,正好和一个分区大小一样,那么这个分区一旦被某个表所使用后,就不能被另外的表所使用了。从而减少了数据页面的外部碎片,但是分区的不连续还是不能避免。那么把簇大小设为128K呢?因为读取数据时,磁盘是按簇的大小进行读取的。设置簇过大,会一次读取出很多无用的内容。即便你只读取一条记录,SQLSERVER还是会把记录所在的整个页读取出来。这时,实际的磁盘是读取出了64K。但是因为簇是连续的扇区,因此多读取的这一部分,对性能的影响基本是可以忽略的。因为磁盘主要受寻道和轮询延迟影响。
对于RAID中的条带容量设置,内部的工作机制我现在还不是很清楚。只是通过下面的文档得出的结论256K。但是网上很多介绍的都是说作为数据库应用时应该小于簇的大小,这和下面微软的文档描述不一致。更多内容参见:http://www.microsoft.com/whdc/archive/subsys_perf.mspx
为你的硬盘启动写入缓存
在没有专门缓存控制器时,这会提高磁盘的I/O效率,但是会增加数据丢失的风险。但是并不会造成数据的不一致。我们来看一下事务操作的过程,它采用预写事务日志(WAL)的方式来保证ACID。如图所示:
事务提交后,修改先反应到事务日志中,这时可能会还存在于磁盘缓存中。如果这时突然断电,检查点操作还没有来得急把提交的事务写入数据文件。重启服务后日志文件中的并没有真正包含所提交的事务,redo操作失败了,你提交的事务丢失了。但是如果事务日志从缓存中写入了磁盘后断电,是不会丢失数据的。如果是日志文件保存在缓存中,而数据文件已从缓存中写入了磁盘。这时数据不会丢失,只是日志中看不到你提交的事务记录了。因为写入磁盘时是以8K写入的,也就是16个扇区的操作。如果只完成了部分扇区的写入后,断电了。这时我们就会收到824错误了,因为页面的校验和发生错误致使无法读取出此页了。数据库校验和设置在page_verity选项中,有三个选项可以设置:checksum、torn_page_detection、none。开销依次减少,安全性依次减弱。每次发生校验和错误时,都会在msdb.dbo.suspect_pages中得到一条记录。如果出现这样的错误,而你没有备份,你只能冒着丢失数据的风险执行DBCC命令来忽略掉这一页了。
以上各人见解,如有异议请指正!
|