#
一、要完成的任務
此系統中的三個部分是氣象站(獲取實際氣象數據的物理裝置)、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 |
這樣的語句依舊在毫秒級內完成了?,F在,我可以刪除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命令來忽略掉這一頁了。
以上各人見解,如有異議請指正!
|