原文地址web
五種提升 SQL 性能的方法
發佈日期: 4/1/2004 | 更新日期: 4/1/2004
Johnny Papa
Data Points Archive
有時, 爲了讓應用程序運行得更快,所作的所有工做就是在這裏或那裏作一些很小調整。啊,但關鍵在於肯定如何進行調整!早晚您會遇到這種狀況:應用程序中的 SQL 查詢不能按照您想要的方式進行響應。它要麼不返回數據,要麼耗費的時間長得出奇。若是它下降了報告或您的企業應用程序的速度,用戶必須等待的時間過長,他們就會很不滿意。就像您的父母不想聽您解釋爲何在深更半夜纔回來同樣,用戶也不會聽你解釋爲何查詢耗費這麼長時間。(「對不起,媽媽,我使用了太多的 LEFT JOIN。」)用戶但願應用程序響應迅速,他們的報告可以在瞬間以內返回分析數據。就我本身而言,若是在 Web 上衝浪時某個頁面要耗費十多秒才能加載(好吧,五秒更實際一些),我也會很不耐煩。
爲了解決這些問題,重要的是找到問題的根源。那麼,從哪裏開始呢?根本緣由一般在於數據庫設計和訪問它的查詢。在本月的專欄中,我將講述四項技術,這些技術可用於提升基於 SQL Server? 的應用程序的性能或改善其可伸縮性。我將仔細說明 LEFT JOIN、CROSS JOIN 的使用以及 IDENTITY 值的檢索。請記住,根本沒有神奇的解決方案。調整您的數據庫及其查詢須要佔用時間、進行分析,還須要大量的測試。這些技術都已被證實行之有效,但對您的應用程序而言,可能其中一些技術比另外一些技術更適用。
*
本頁內容sql
從 INSERT 返回 IDENTITY 從 INSERT 返回 IDENTITY 內嵌視圖與臨時表 內嵌視圖與臨時表 避免 LEFT JOIN 和 NULL 避免 LEFT JOIN 和 NULL 靈活使用笛卡爾乘積 靈活使用笛卡爾乘積 拾遺補零 拾遺補零
從 INSERT 返回 IDENTITY
我決定從遇到許多問題的內容入手:如何在執行 SQL INSERT 後檢索 IDENTITY 值。一般,問題不在於如何編寫檢索值的查詢,而在於在哪裏以及什麼時候進行檢索。在 SQL Server 中,下面的語句可用於檢索由最新在活動數據庫鏈接上運行的 SQL 語句所建立的 IDENTITY 值:
SELECT @@IDENTITY
這個 SQL 語句並不複雜,但須要記住的一點是:若是這個最新的 SQL 語句不是 INSERT,或者您針對非 INSERT SQL 的其餘鏈接運行了此 SQL,則不會得到指望的值。您必須運行下列代碼才能檢索緊跟在 INSERT SQL 以後且位於同一鏈接上的 IDENTITY,以下所示:
INSERT INTO Products (ProductName) VALUES ('Chalk')數據庫
SELECT @@IDENTITY
在一個鏈接上針對 Northwind 數據庫運行這些查詢將返回一個名稱爲 Chalk 的新產品的 IDENTITY 值。因此,在使用 ADO 的 Visual Basic? 應用程序中,能夠運行如下語句:
Set oRs = oCn.Execute("SET NOCOUNT ON;INSERT INTO Products _
(ProductName) VALUES ('Chalk');SELECT @@IDENTITY")緩存
lProductID = oRs(0)
此代碼告訴 SQL Server 不要返回查詢的行計數,而後執行 INSERT 語句,並返回剛剛爲這個新行建立的 IDENTITY 值。SET NOCOUNT ON 語句表示返回的記錄集有一行和一列,其中包含了這個新的 IDENTITY 值。若是沒有此語句,則會首先返回一個空的記錄集(由於 INSERT 語句不返回任何數據),而後會返回第二個記錄集,第二個記錄集中包含 IDENTITY 值。這可能有些使人困惑,尤爲是由於您歷來就沒有但願過 INSERT 會返回記錄集。之因此會發生此狀況,是由於 SQL Server 看到了這個行計數(即一行受到影響)並將其解釋爲表示一個記錄集。所以,真正的數據被推回到了第二個記錄集。固然您可使用 ADO 中的 NextRecordset 方法獲取此第二個記錄集,但若是總可以首先返回該記錄集且只返回該記錄集,則會更方便,也更有效率。
此方法雖然有效,但須要在 SQL 語句中額外添加一些代碼。得到相同結果的另外一方法是在 INSERT 以前使用 SET NOCOUNT ON 語句,並將 SELECT @@IDENTITY 語句放在表中的 FOR INSERT 觸發器中,以下面的代碼片斷所示。這樣,任何進入該表的 INSERT 語句都將自動返回 IDENTITY 值。
CREATE TRIGGER trProducts_Insert ON Products FOR INSERT AS
SELECT @@IDENTITY
GO
觸發器只在 Products 表上發生 INSERT 時啓動,因此它老是會在成功 INSERT 以後返回一個 IDENTITY。使用此技術,您能夠始終以相同的方式在應用程序中檢索 IDENTITY 值。
返回頁首返回頁首
內嵌視圖與臨時表
某些時候,查詢須要將數據與其餘一些可能只能經過執行 GROUP BY 而後執行標準查詢才能收集的數據進行聯接。例如,若是要查詢最新五個定單的有關信息,您首先須要知道是哪些定單。這可使用返回定單 ID 的 SQL 查詢來檢索。此數據就會存儲在臨時表(這是一個經常使用技術)中,而後與 Products 表進行聯接,以返回這些定單售出的產品數量:
CREATE TABLE #Temp1 (OrderID INT NOT NULL, _
OrderDate DATETIME NOT NULL)安全
INSERT INTO #Temp1 (OrderID, OrderDate)
SELECT TOP 5 o.OrderID, o.OrderDate
FROM Orders o ORDER BY o.OrderDate DESC數據庫設計
SELECT p.ProductName, SUM(od.Quantity) AS ProductQuantity
FROM #Temp1 t
INNER JOIN [Order Details] od ON t.OrderID = od.OrderID
INNER JOIN Products p ON od.ProductID = p.ProductID
GROUP BY p.ProductName
ORDER BY p.ProductName函數
DROP TABLE #Temp1
這些 SQL 語句會建立一個臨時表,將數據插入該表中,將其餘數據與該表進行聯接,而後除去該臨時表。這會致使此查詢進行大量 I/O 操做,所以,能夠從新編寫查詢,使用內嵌視圖取代臨時表。內嵌視圖只是一個能夠聯接到 FROM 子句中的查詢。因此,您不用在 tempdb 中的臨時表上耗費大量 I/O 和磁盤訪問,而可使用內嵌視圖獲得一樣的結果:
SELECT p.ProductName,
SUM(od.Quantity) AS ProductQuantity
FROM (
SELECT TOP 5 o.OrderID, o.OrderDate
FROM Orders o
ORDER BY o.OrderDate DESC
) t
INNER JOIN [Order Details] od ON t.OrderID = od.OrderID
INNER JOIN Products p ON od.ProductID = p.ProductID
GROUP BY
p.ProductName
ORDER BY
p.ProductName
此查詢不只比前面的查詢效率更高,並且長度更短。臨時表會消耗大量資源。若是隻須要將數據聯接到其餘查詢,則能夠試試使用內嵌視圖,以節省資源。
返回頁首返回頁首
避免 LEFT JOIN 和 NULL
固然,有不少時候您須要執行 LEFT JOIN 和使用 NULL 值。可是,它們並不適用於全部狀況。改變 SQL 查詢的構建方式可能會產生將一個花幾分鐘運行的報告縮短到只花幾秒鐘這樣的天壤之別的效果。有時,必須在查詢中調整數據的形態,使之適應應用程序所要求的顯示方式。雖然 TABLE 數據類型會減小大量佔用資源的狀況,但在查詢中還有許多區域能夠進行優化。SQL 的一個有價值的經常使用功能是 LEFT JOIN。它能夠用於檢索第一個表中的全部行、第二個表中全部匹配的行、以及第二個表中與第一個表不匹配的全部行。例如,若是但願返回每一個客戶及其定單,使用 LEFT JOIN 則能夠顯示有定單和沒有定單的客戶。
此工具可能會被過分使用。LEFT JOIN 消耗的資源很是之多,由於它們包含與 NULL(不存在)數據匹配的數據。在某些狀況下,這是不可避免的,可是代價可能很是高。LEFT JOIN 比 INNER JOIN 消耗資源更多,因此若是您能夠從新編寫查詢以使得該查詢不使用任何 LEFT JOIN,則會獲得很是可觀的回報(請參閱圖 1 中的圖)。工具
圖 1:查詢
加快使用 LEFT JOIN 的查詢速度的一項技術涉及建立一個 TABLE 數據類型,插入第一個表(LEFT JOIN 左側的表)中的全部行,而後使用第二個表中的值更新 TABLE 數據類型。此技術是一個兩步的過程,但與標準的 LEFT JOIN 相比,能夠節省大量時間。一個很好的規則是嘗試各類不一樣的技術並記錄每種技術所需的時間,直到得到用於您的應用程序的執行性能最佳的查詢。
測試查詢的速度時,有必要屢次運行此查詢,而後取一個平均值。由於查詢(或存儲過程)可能會存儲在 SQL Server 內存中的過程緩存中,所以第一次嘗試耗費的時間好像稍長一些,而全部後續嘗試耗費的時間都較短。另外,運行您的查詢時,可能正在針對相同的表運行其餘查詢。當其餘查詢鎖定和解鎖這些表時,可能會致使您的查詢要排隊等待。例如,若是您進行查詢時某人正在更新此表中的數據,則在更新提交時您的查詢可能須要耗費更長時間來執行。
避免使用 LEFT JOIN 時速度下降的最簡單方法是儘量多地圍繞它們設計數據庫。例如,假設某一產品可能具備類別也可能沒有類別。若是 Products 表存儲了其類別的 ID,而沒有用於某個特定產品的類別,則您能夠在字段中存儲 NULL 值。而後您必須執行 LEFT JOIN 來獲取全部產品及其類別。您能夠建立一個值爲「No Category」的類別,從而指定外鍵關係不容許 NULL 值。經過執行上述操做,如今您就可使用 INNER JOIN 檢索全部產品及其類別了。雖然這看起來好像是一個帶有多餘數據的變通方法,但多是一個頗有價值的技術,由於它能夠消除 SQL 批處理語句中消耗資源較多的 LEFT JOIN。在數據庫中所有使用此概念能夠爲您節省大量的處理時間。請記住,對於您的用戶而言,即便幾秒鐘的時間也很是重要,由於當您有許多用戶正在訪問同一個聯機數據庫應用程序時,這幾秒鐘實際上的意義會很是重大。
返回頁首返回頁首
靈活使用笛卡爾乘積
對於此技巧,我將進行很是詳細的介紹,並提倡在某些狀況下使用笛卡爾乘積。出於某些緣由,笛卡爾乘積 (CROSS JOIN) 遭到了不少譴責,開發人員一般會被警告根本就不要使用它們。在許多狀況下,它們消耗的資源太多,從而沒法高效使用。可是像 SQL 中的任何工具同樣,若是正確使用,它們也會頗有價值。例如,若是您想運行一個返回每個月數據(即便某一特定月份客戶沒有定單也要返回)的查詢,您就能夠很方便地使用笛卡爾乘積。 圖 2 中的 SQL 就執行了上述操做。
雖然這看起來好像沒什麼神奇的,可是請考慮一下,若是您從客戶到定單(這些定單按月份進行分組並對銷售額進行小計)進行了標準的 INNER JOIN,則只會得到客戶有定單的月份。所以,對於客戶未訂購任何產品的月份,您不會得到 0 值。若是您想爲每一個客戶都繪製一個圖,以顯示每月和該月銷售額,則可能但願此圖包括月銷售額爲 0 的月份,以便直觀標識出這些月份。若是使用 圖 2 中的 SQL,數據則會跳過銷售額爲 0 美圓的月份,由於在定單表中對於零銷售額不會包含任何行(假設您只存儲發生的事件)。
圖 3 中的代碼雖然較長,可是能夠達到獲取全部銷售數據(甚至包括沒有銷售額的月份)的目標。首先,它會提取去年全部月份的列表,而後將它們放入第一個 TABLE 數據類型表 (@tblMonths) 中。下一步,此代碼會獲取在該時間段內有銷售額的全部客戶公司的名稱列表,而後將它們放入另外一個 TABLE 數據類型表 (@tblCus-tomers) 中。這兩個表存儲了建立結果集所必需的全部基本數據,但實際銷售數量除外。 第一個表中列出了全部月份(12 行),第二個表中列出了這個時間段內有銷售額的全部客戶(對於我是 81 個)。並不是每一個客戶在過去 12 個月中的每月都購買了產品,因此,執行 INNER JOIN 或 LEFT JOIN 不會返回每月的每一個客戶。這些操做只會返回購買產品的客戶和月份。
笛卡爾乘積則能夠返回全部月份的全部客戶。笛卡爾乘積基本上是將第一個表與第二個表相乘,生成一個行集合,其中包含第一個表中的行數與第二個表中的行數相乘的結果。所以,笛卡爾乘積會向表 @tblFinal 返回 972 行。最後的步驟是使用此日期範圍內每一個客戶的月銷售額總計更新 @tblFinal 表,以及選擇最終的行集。
若是因爲笛卡爾乘積佔用的資源可能會不少,而不須要真正的笛卡爾乘積,則能夠謹慎地使用 CROSS JOIN。例如,若是對產品和類別執行了 CROSS JOIN,而後使用 WHERE 子句、DISTINCT 或 GROUP BY 來篩選出大多數行,那麼使用 INNER JOIN 會得到一樣的結果,並且效率高得多。若是須要爲全部的可能性都返回數據(例如在您但願使用每個月銷售日期填充一個圖表時),則笛卡爾乘積可能會很是有幫助。可是,您不該該將它們用於其餘用途,由於在大多數方案中 INNER JOIN 的效率要高得多。
拾遺補零
這裏介紹其餘一些可幫助提升 SQL 查詢效率的經常使用技術。假設您將按區域對全部銷售人員進行分組並將他們的銷售額進行小計,可是您只想要那些數據庫中標記爲處於活動狀態的銷售人員。您能夠按區域對銷售人員分組,並使用 HAVING 子句消除那些未處於活動狀態的銷售人員,也能夠在 WHERE 子句中執行此操做。在 WHERE 子句中執行此操做會減小須要分組的行數,因此比在 HAVING 子句中執行此操做效率更高。HAVING 子句中基於行的條件的篩選會強制查詢對那些在 WHERE 子句中會被去除的數據進行分組。
另外一個提升效率的技巧是使用 DISTINCT 關鍵字查找數據行的單獨報表,來代替使用 GROUP BY 子句。在這種狀況下,使用 DISTINCT 關鍵字的 SQL 效率更高。請在須要計算聚合函數(SUM、COUNT、MAX 等)的狀況下再使用 GROUP BY。另外,若是您的查詢老是本身返回一個惟一的行,則不要使用 DISTINCT 關鍵字。在這種狀況下,DISTINCT 關鍵字只會增長系統開銷。
您已經看到了,有大量技術均可用於優化查詢和實現特定的業務規則,技巧就是進行一些嘗試,而後比較它們的性能。最重要的是要測試、測試、再測試。在此專欄的未來各期內容中,我將繼續深刻講述 SQL Server 概念,包括數據庫設計、好的索引實踐以及 SQL Server 安全範例。
若有向 Johnny 提出的問題和建議,請發送電子郵件到 mmdata@microsoft.com
Johnny Papa 是北卡羅來納州羅利市的 MJM 研究公司的信息技術副總裁,他著有?Professional ADO 25 RDS Programming with ASP 30?? (Wrox, 2000),並常常在行業會議中作演講。要與他聯繫,請發送電子郵件到 datapoints@lancelotweb.com
摘自 MSDN Magazine 2002 年 7 月 刊。能夠在您當地的報攤上購買此雜誌,但最好當即 訂閱
轉到原英文頁面sqlserver