運用ADO.NET對象優化數據查詢代碼

發表于:2007-05-25來源:作者:點擊數: 標簽:優化數據查詢ADO.NET對象運用
毫無疑問,ADO.NET 向人們提供了一種功能強大、仿真 數據庫 的對象模型,它可以將數據記錄保存到內存中。尤其是ADO.NET 的 DataSet 類,它不但在功能上相當于數據庫表的集中存儲器(central repository),而且支持表間的各種約束和邏輯關系。進一步說來,Data


 

毫無疑問,ADO.NET 向人們提供了一種功能強大、仿真數據庫的對象模型,它可以將數據記錄保存到內存中。尤其是ADO.NET 的 DataSet 類,它不但在功能上相當于數據庫表的集中存儲器(central repository),而且支持表間的各種約束和邏輯關系。進一步說來,DataSet 對象其實是一種離線數據容器。
乍一看,只要把 DataSet 類的所有特性聯合起來,就能消除 SQL 查詢命令中的復雜子句,比如那些泛濫成災且層層嵌套的 INNER JOIN子句或者 GROUP BY 子句等。復雜的子句可以分解成兩個或更多個相互獨立的簡單子句,而將每個簡單子句的查詢結果分別保存在不同的 DataTable 對象中;以后只要分析這些內存數據之間的約束和邏輯關系,就能重建原先表之間必要的“參照完整性”(referential integrity)。
舉個例子:你可以把客戶(Customers)表與訂單(Orders)表分別保存到兩個不同的 DataTable 對象中,然后通過 DataRelation 對象進行綁定 (bind) 。這樣, SQL Server (或其它 DBMS 系統) 就免除了 INNER JOIN 子句帶來的沉重負擔;更重要的是,網絡傳輸負荷也因此而大大減輕。象這樣簡化 SQL 查詢的方案固然行之有效,卻并不一定總是最佳選擇,尤其是當你的數據庫規模龐大而且更新頻繁時。
本文將為大家介紹另一種用于簡化 SQL 查詢的技術,它充分利用 ADO.NET 的內存數據對象減輕了用戶和 DBMS 系統的負擔。
分解 SQL 查詢命令
許多有關 ADO.NET 的書籍,比如 David Sceppa 的大作《Programming ADO.NET Core Reference》(微軟出版社),都建議把復雜的 SQL 查詢命令分解成若干簡單的子查詢,然后把各個子查詢的返回結果分別保存到同一個 DataSet 容器內部的若干個 DataTable 對象中。請看一個實例。
假設你需要獲取一些客戶訂單信息,要求訂單是提交于指定年份而且按客戶進行分組,還要求訂單中至少包含 30 件商品。同時,你還希望獲取每個訂單的提交者(employee)名字以及客戶(customer)的公司名。你可以用下列 SQL 查詢語句來實現它:
DECLARE @TheYear int
SET @TheYear = 1997

SELECT o.customerid, od.orderid, o.orderdate, o.shippeddate,
SUM(od.quantity*od.unitprice) AS price,
c.companyname, e.lastname FROM Orders AS o
INNER JOIN Customers AS c ON c.customerid=o.customerid
INNER JOIN Employees AS e ON e.employeeid=o.employeeid
INNER JOIN [Order Details] AS od ON o.orderid=od.orderid
WHERE Year(o.orderdate) = @TheYear AND od.orderid=o.orderid
GROUP BY o.customerid, c.companyname, od.orderid,
o.orderdate, o.shippeddate, e.lastname
HAVING SUM(od.quantity) >30
ORDER BY o.customerid
暫且拋開你所用的 ADO 或者 ADO.NET吧。用最原始的命令提交方式執行上述 SQL 查詢,可以看到如圖 1 所示的結果集:


圖 1. 第一個 SQL 查詢命令的輸出結果,由 SQL Server Query Analyzer 生成并顯示。
在本次查詢中,以一條子句為核心,而另外兩條 INNER JOIN 子句起輔助作用。核心子句的功能是從數據庫中查詢所有提交于指定年份、至少包含 30 件商品的訂單。核心子句如下:
SELECT o.customerid, o.orderid, o.orderdate,
o.shippeddate, SUM(od.quantity*od.unitprice) AS price, o.employeeid
FROM orders AS o
INNER JOIN [Order Details] AS od ON o.orderid=od.orderid
WHERE Year(o.orderdate) = @TheYear AND od.orderid=o.orderid
GROUP BY o.customerid, o.orderid, o.orderdate, o.shippeddate,
o.employeeid
HAVING SUM(od.quantity) >30
ORDER BY o.customerid
在返回結果集中,客戶和提交者均用 ID 來表示。然而,本例需要的是客戶的公司名(compayname)和提交者的名字(lastname)。末尾的 ORDER BY o.customerid 語句顯得特別簡單,可是其功能卻很重要:由于客戶公司名和提交者名字所含的字符較多,使用該語句就能避免它們的重復出現,從而得到更緊湊的結果集。
綜上所述,整個 SQL 查詢可以被分解成 3 條子查詢命令—— 1 條核心子查詢,用于獲取訂單記錄;2 條輔助子查詢,用于建立提交者ID - 提交者名字和客戶ID - 客戶公司名兩個對照表,即:
SELECT employeeid, lastname FROM Employees
SELECT customerid, companyname FROM Customers
以下 ADO.NET 代碼演示了如何把這 3 條子查詢的返回結果集保存到 DataSet 對象中。
Dim conn As SqlConnection = New SqlConnection(connString)
Dim adapter As SqlDataAdapter = New SqlDataAdapter()

conn.Open()
adapter.SelectCommand = New SqlCommand(cmdCore, conn)
adapter.SelectCommand.Parameters.Add("@TheYear", 1997)
adapter.SelectCommand.Parameters.Add("@TheQuantity", 30)
adapter.Fill(ds, "Orders")
adapter.SelectCommand = New SqlCommand(cmdCust, conn)
adapter.Fill(ds, "Customers")
adapter.SelectCommand = New SqlCommand(cmdEmpl, conn)
adapter.Fill(ds, "Employees")
conn.Close()
請注意:在連續執行 SQL 查詢命令時,你通常都要自行操作數據庫連接,以免出現多余的open/close 操作。本例的 adapter.Fill 方法會自動執行 open/close 操作,除非你設置 adapter.SelectCommmand 屬性把某個連接顯式關聯到 adapter 對象之上。
為了建立內存數據表之間的關系鏈,你可以創建兩個關系,把 employeeid (提交者的ID) 關聯到 lastname (提交者的名字),把 customerid (客戶的ID) 關聯到 companyname (客戶的公司名)。一般情況下,可以用 DataRelation 對象在同一 DataSet 對象內創建兩個獨立表之間的一對多關系。然而,本例卻需要建立多對一關系,這是很少見的。其實,只要把一對多關系中的父表(Orders) 變成子表,而把子表(Employees、Customers) 變成父表就行了。


圖 2. 關系中的父表與子表角色互換

ADO.NET 中的 DataRelation 對象相當靈活,足以構建多對一關系。每生成一個DataRelation 對象,ADO.NET 都會在后臺為之建立一致性約束,以免父表內的鍵值重復。當然了,一旦重復的鍵值出現,ADO.NET 就會拋出(throw)一個例外。請看下列代碼:
Dim relOrder2Employees As DataRelation
relOrder2Employees = New DataRelation("Orders2Employees", _
ds.Tables("Orders").Columns("employeeid"), _
ds.Tables("Employees").Columns("employeeid"))
ds.Relations.Add(relOrder2Employees)
此處的 DataRelation 對象構造器初始化了三個參數:第一個是關系名稱,后面兩個是 DataColumn 對象,分別代表構成關系的兩個列(column):前一個 DataColumn 對象代表父列,后一個 DataColumn 對象代表子列。一旦構造器發現父列中不存在合法記錄,便會激活(raise) 一個 ArgumentException 例外。消除此例外最簡單的解決方案是在構造器中添加一個布爾值作為第四參數:
relOrder2Employees = New DataRelation("Orders2Employees", _
ds.Tables("Orders").Columns("employeeid"), _
ds.Tables("Employees").Columns("employeeid"), _
False)
當構造器的第四參數值為 false 時,ADO.NET 就不會建立一致性約束,而后者正是引發 ArgumentException 例外的罪魁禍首。
設置了數據關系之后,你就可以用列表達式 (computed column) 給 Orders 表添加兩列以顯示其內容了。理論上,這么做完全符合邏輯:
Dim orders As DataTable = ds.Tables("Orders")
orders.Columns.Add("Employee", GetType(String), _
"Child(Orders2Employees).lastname")
orders.Columns.Add("Customer", GetType(String), _
"Child(Orders2Customers).companyname")
可惜,它根本行不通。更糟的是,當它運行到包含 Child 的代碼時,就會拋出 (throw) 一條 “句法錯誤”信息,而這條出錯信息很容易誤導程序員。(有關列表達式的更多信息,請參閱《last month's column》。)
為什么會出錯?因為只有當父列存在一致性約束時,才允許在列表達式中使用 Child 關鍵字。這一點在開發文檔中并未明確指出,然而它卻是事實,而且非常重要。令人不解的是,你不但可以順利地訪問 Orders 表任一行的子元素,還能直接訪問 Employees 表或 Customers 表的任一列。以下代碼可以證明這一點:
Dim orders As DataTable = ds.Tables("Orders")
Dim employee As DataRow = orders.Rows(0).GetChildRows(relOrder2Employees)
MsgBox employee("lastname")
因此,所謂的“句法錯誤”,并不代表你無法建立多對一關系。它只是提醒你:除非事先建立一致性約束,否則就不能在列表達式中使用 Child 關鍵字。
在初始關系中,Orders 表是父表。然而,為了從 ID 獲取提交者名字或客戶公司名,你就必須改變諸表所扮演的角色:讓 Orders 表充當子表,而讓 Employees 表、Customers 表充當父表。為了確保做到這一點,你必須改變 DataRelation 對象構造器代碼中的列名,并且象這樣使用列表達式:
Dim orders As DataTable = ds.Tables("Orders")
orders.Columns.Add("Employee", GetType(String), _
"Parent(Orders2Employees).lastname")
orders.Columns.Add("Customer", GetType(String), _
"Parent(Orders2Customers).companyname")
小結:在本例中,我們把一個復雜的 SQL 查詢分解成 3 個較為簡單的子查詢,從而消除了兩個 INNER JOIN 語句,減輕了數據庫服務器的負擔;更重要的是,大大減少了從服務器到客戶端的網絡傳輸負荷??磥?,這似乎是最好的解決方案了?

替代方案
前面的解決方案是以對照表為基礎的,而且在對照表的生成過程中沒有進行數據過濾。一旦對照表的規模過大,會有什么后果呢?難道你愿意為了得到區區數百個提交者的名字就從服務器下載 10,000 條記錄?難道你甘心下載那一大堆冗余數據?更何況,那些冗余數據對你毫無用處!
可是,請換個角度想一想。對照表在應用程序的整個生命周期中往往都是有價值的。也換言之,雖然對單獨一次查詢來說,下載許多記錄以構建完整的對照表未免過于奢侈,但是它對整個應用程序來說也未必不是公平交易。
既然這樣,我們何不嘗試用另一種技術來縮減對照表的規模呢?最容易想到的方案莫過于借助 WHERE 子句來縮小結果集了。非常不幸,此方案要么難以實現,要么效果欠佳,尤其是在對照表諸列并不包括你所要查詢的對象時。例如:為了對提交者的名字進行過濾,你就必須對其它表進行聯合查詢——比如 Order 表和 Order Details(訂單細節) 表。我認為,最佳方案是重新獲取上次 SQL 查詢的返回結果集,并從中解析出每個提交者的信息。也就是說,完成前述 SQL 查詢之后,再次發送一個幾乎相同的查詢命令,令數據庫服務器重新運行分解后的子查詢。這樣,數據庫將以最小的查詢代價返回完全相同的數據。更妙的是,SQL 服務器還特別設置了查詢優化引擎,使得此類重復查詢的代價減到最低。
SELECT DISTINCT t.customerid, t.companyname FROM
(SELECT o.customerid, o.orderid, o.orderdate, o.shippeddate,
SUM(od.quantity*od.unitprice) AS price,
c.companyname, o.employeeid
FROM orders AS o
INNER JOIN Customers AS c ON c.customerid=o.customerid
INNER JOIN [Order Details] AS od ON o.orderid=od.orderid
WHERE Year(o.orderdate) = @TheYear AND od.orderid=o.orderid
GROUP BY o.customerid, o.orderid, c.companyname,
o.orderdate, o.shippeddate, o.employeeid
HAVING SUM(od.quantity) >30) AS t
總而言之,以若干簡單查詢為基礎而設計的資料檢索代碼最大的優點是:它把數據聯結 (joining) 的重任由服務器轉移到了客戶端。另一方面,由于客戶端把數據記錄分布于幾個相互獨立、易于鏈接的表內,因而數據查詢操作異常靈活。
用一些短小、簡單的 SQL 查詢從數據庫讀取記錄并分別保存到額外的DataTable 對象中,這對客戶端應用程序來說是一個好消息??墒?,如果這些子查詢返回的部分數據不符合一致性約束,那又會如何呢?
事務的應用
通常,每個查詢命令,無論它有多復雜,都是在同一個默認的事務(transaction)中執行的。正因為如此,它才能確保在執行過程中不會因為其它代碼的干擾而破壞數據的整體一致性??墒?,假如你把作為邏輯整體的查詢命令分解成為若干子查詢,結果又會怎么樣?
不妨假設你的軟件需要處理一個富于變化的環境,而且數據庫中的記錄也在迅速更新?;蛟S你的子查詢還在執行過程中,數據就已經被某個用戶替換了。在前面的范例程序中,這種意外尚且不會造成多大的損失,因為第二個和第三個子查詢命令僅僅處理已經生成的對照表。盡管如此,一旦有人在你獲取訂單數據后把某個提交者的記錄刪除了,你所查詢到的數據還是可能違背一致性約束。因此,必須把分解得到的子查詢及相關的處理代碼全部放入同一個事務中運行。除此之外,你別無選擇。
所謂事務(Transaction),是指一組操作,它在執行時嚴格遵守下列規則:
·不可分性(原子性 Atomicity)
·一致性(Consistency)
·獨立性(Isolation)
·持續性(Durability)
人們通常提取這 4 條規則( 4 種屬性) 的首字母合稱 ACID 。對本例而言,最重要的規則(屬性)是獨立性。所謂獨立性,是指數據庫有能力確保每個正在運行中的事務不被任何其它并行事務所干擾。當你的查詢命令正在某個事務中運行的時候,如果其它用戶的數據庫操作也在別的事務中同時運行,則你最終得到的結果將與事務的獨立性等級有關。正常情況下,數據庫能根據每個事務中的操作合理分配獨立性等級。如果應用程序要求數據保持絕對的一致性,絕不容許出現“虛幻行”(phantom rows) 時,它就必須獲得“可串行”(serializable) 獨立等級。
當一個“可串行”的事務在運行時,它將鎖定一切相關表,防止任何其他用戶更新或插入字段。只有當事務運行完畢時,表才會解鎖。在此獨立性等級下,“讀污染”(dirty reads,即讀入未授權的數據) 和“虛幻行”(phantom rows,即尚無記錄的行,或者已被其它事務刪除的行) 自然不會出現;可是,數據的整體一致性仍然無法確保。
既然你的子查詢命令在運行過程中可能出現提交者記錄被刪除、訂單記錄被修改等情況,那么你當然應該把所有子查詢都包裹到一個“可串行”的事務中。
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
-- get Orders
-- get Customers
-- get Employees
COMMIT TRANSACTION
再強調一次,如果條件允許,你的事務就應該具有“可串行”獨立性等級。如果你擔心鎖定全部表會帶來不利影響的話,請不妨試試 ADO.NET 的內存數據對象。稍后我們將討論這些對象。

解除數據聯結
讓我們回過頭來看一看本文開頭那個查詢范例。它的目標是從數據庫中讀取在指定年份提交且所含商品件數符合條件的所有訂單記錄;我們也需要知道訂單的總數、客戶的公司名和訂單提交者的名字。
DECLARE @TheYear int
DECLARE @TheAmount int
SET @TheYear = 1997
SET @TheAmount = 30

SELECT o.customerid, od.orderid, o.orderdate, o.shippeddate,
SUM(od.quantity*od.unitprice) AS price,
c.companyname, e.lastname FROM Orders AS o
INNER JOIN Customers AS c ON c.customerid=o.customerid
INNER JOIN Employees AS e ON e.employeeid=o.employeeid
INNER JOIN [Order Details] AS od ON o.orderid=od.orderid
WHERE Year(o.orderdate) = @TheYear AND od.orderid=o.orderid
GROUP BY o.customerid, c.companyname, od.orderid,
o.orderdate, o.shippeddate, e.lastname
HAVING SUM(od.quantity) >@TheAmount
ORDER BY o.customerid
以上 SQL 查詢命令確實可以一次性地返回全部所需資料。只要讓它們在同一個事務中運行,就能確保返回數據的一致性和“可串行”性。然而此方案已經過時,我們也沒有選用它。為什么呢?
事實上,它存在兩個問題:第一,返回結果集各行分別來自 3 個不同的表:
·訂單 (Orders)
·客戶 (Customers)
·提交者 (Employees)
這還不包括 Order Details 表。
第二,INNER JOIN 語句造成一些不必要的數據移動。我們無法解決第二個問題,可是某些 ADO.NET 代碼卻有助于解決第一個問題。因此,我們仍然有機會提高整個解決方案的可行性和有效性。
具體思路如下:首先執行 SQL 查詢,將返回結果集保存到一個 DataTable 對象中;然后把 DataTable 中的數據分散到 3 個不同卻又相關的 DataTable 對象中。最終輸出的結果與分別查詢諸表沒什么區別,可它卻節省了定義和設置“可串行”事務的開銷,同時也避免了從數據庫下載多余記錄;美中不足是每一行都可能包含少量冗余信息。
何時可以采用本方案呢?我發現,當客戶端需要借助 group-by 函數及各種過濾器來組建一個復雜的“主從復合結構”(master/detail) 視圖時,本方案是一個不錯的選擇。順便提一句,此時采用多個不同而相關的表是非常有效的,ADO.NET 也為此提供了不少優化特性。
我們來討論具體操作。以下代碼示范了它的主要流程:
Function SplitData(ByVal ds As DataSet) As DataSet
Dim _dataset As New DataSet()

' Make a full worker copy of the DataSet
_dataset = ds.Copy()

CreateCustomers(_dataset, ds)
CreateEmployees(_dataset, ds)

' Remove columns from Orders(companyname [2] and lastname [4])
_dataset.Tables("Orders").Columns.RemoveAt(1)
_dataset.Tables("Orders").Columns.RemoveAt(3)

Return _dataset
End Function
代碼首先完整地復制了 DataSet 對象(ds),以它作為新 DataSet 對象(_dataset) 中的Orders 表。接下來,代碼又在新 DataSet 對象中動態添加了 customers 表和 employees 表。最后,它又從新 DataSet 對象的 Orders 表中刪除了其它兩個子表所包含的列。下圖顯示了新 DataSet 對象中 customers 表的內容。瞧,它只留下了 Orders 表中 (所有) 訂單的客戶 ID 和公司名兩列。由于這兩個表都有 customerid 列,故它們仍然可以建立關系。


圖 3 . 根據第一次查詢的返回結果新生成的 Customers 表

下面來簡單談談用于創建和填充 customers 表與 employees 表所必需的代碼。
一開始,你必須調用 clone 方法克隆原先的訂單表以創建一個新的 DataTable 對象。與Copy 方法不同,Clone 方法僅僅復制元數據(metadata)。由于 DataTable 接口不允許克隆單個列,所以本方法是生成對等表的最簡單途徑。然而,這樣生成的表將包含某些多余列,我們必須刪除之。
只要分析第一個 DataSet 對象的結構,你就會發現 customerid 列和 companyname 列正是返回結果集的第一列和第二列。
Dim _customers As DataTable = orig.Tables("Orders").Clone()
_customers.TableName = "Customers"

' Remove unneeded columns
Dim i As Integer
For i = 2 To _customers.Columns.Count - 1
_customers.Columns.RemoveAt(2)
Next
建立表結構之后,還得載入數據。然而,在 Orders 表中可能多次出現同一個提交者。此外,你必須對源 DataSet 對象中的數據加以過濾。好在 Orders 表已經根據 customerid 列進行排序,所以你只要循環遍歷所有行,從中選出符合條件者即可。
Dim row As DataRow
Dim customerKey As String = ""
For Each row In _dataset.Tables("Orders").Rows
' Already sorted by CustomerID
If customerKey <> row("customerid") Then
' select distinct
_customers.ImportRow(row)
customerKey = row("customerid")
End If
Next

' Add to the DataSet
_dataset.Tables.Add(_customers)
ImportRow 是從數據庫導出指定行到新表的最快途徑。通常,ImportRow 方法會根據模式 (schema) 的要求來挑選被導出的列。
原則上,employeess 表的創建和 customers 表的創建大體相同。當然了,你應該刪除的列有所不同。從 Orders 表的結構來分析,我們必須保留第 3 列與第 4 列。下列代碼首先刪除第1列和第2列,然后用一個循環解決其它列。
Dim _employees As DataTable = orig.Tables("Orders").Clone()
_employees.TableName = "Employees"

' Remove unneeded columns
_employees.Columns.RemoveAt(0)
_employees.Columns.RemoveAt(0)
Dim i As Integer
For i = 2 To _employees.Columns.Count - 1
_employees.Columns.RemoveAt(2)
Next
最后,你還必須清除 employees 表中的重復行。在本例中,對 Orders 表的排序有助于簡化該操作。你可以先創建 Orders 表的已排序視圖(sorted view),然后循環遍歷所有行。
Dim employeeKey As Integer = 0
Dim view As DataView = New DataView(_dataset.Tables("Orders"))
view.Sort = "employeeid"

Dim rowView As DataRowView
For Each rowView In view
If employeeKey <> Convert.ToInt32(rowView("employeeid")) Then
' select distinct
_employees.ImportRow(rowView.Row)
employeeKey = Convert.ToInt32(rowView("employeeid"))
End If
Next

' Add to the DataSet
_dataset.Tables.Add(_employees)
總結
本文示范了一個復雜的 SQL 查詢實例,并討論了 3 種提高其效率的方案。不得不承認,經典的 ADO 對于此類問題的解決幫助有限,而 ADO.NET 卻能讓你構造一種功能強大的離線數據對象模型以提高程序性能。本文提到了幾種解決方案,究竟哪種是最佳選擇?很難說。影響運行效率的因素非常多,比如:網絡的有效帶寬,數據的繼承一致性,程序對數據穩定性的要求,等等。為了確定最佳方案,你就必須逐一嘗試各種方案,分別測試性能。
原文見 http://msdn.microsoft.com/library/en-us/dndive/html/data06132002.asp

原文轉自:http://www.anti-gravitydesign.com

評論列表(網友評論僅供網友表達個人看法,并不表明本站同意其觀點或證實其描述)
国产97人人超碰caoprom_尤物国产在线一区手机播放_精品国产一区二区三_色天使久久综合给合久久97