效能 五種提高 SQL 效能的方法發布日期: 4/1/2004 | 更新日期: 4/1/2004
Johnny Papa
Data Points Archive
有時, 為了讓應用程式運行得更快,所做的全部工作就是在這裡或那裡做一些很小調整。啊,但關鍵在於確定如何進行調整!遲早您會遇到這種情況:應用程式中的 SQL 查詢不能按照您想要的方式進行響應。它要麼不返回資料,要麼耗費的時間長得出奇。如果它降低了報告或您的公司專屬應用程式程式的速度,使用者必須等待的時間過長,他們就會很不滿意。就像您的父母不想聽您解釋為什麼在深更半夜才回來一樣,使用者也不會聽你解釋為什麼查詢耗費這麼長時間。(“對不起,媽媽,我使用了太多的 LEFT JOIN。”)使用者希望應用程式響應迅速,他們的報告能夠在瞬間之內返回分析資料。就我自己而言,如果在 Web 上衝浪時某個頁面要耗費十多秒才能載入(好吧,五秒更實際一些),我也會很不耐煩。
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.OrderDateFROM Orders o ORDER BY o.OrderDate DESCSELECT p.ProductName, SUM(od.Quantity) AS ProductQuantityFROM #Temp1 t INNER JOIN [Order Details] od ON t.OrderID = od.OrderID INNER JOIN Products p ON od.ProductID = p.ProductID GROUP BY p.ProductNameORDER BY p.ProductNameDROP TABLE #Temp1
SELECT p.ProductName, SUM(od.Quantity) AS ProductQuantityFROM ( 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.ProductNameORDER BY p.ProductName
此查詢不僅比前面的查詢效率更高,而且長度更短。暫存資料表會消耗大量資源。如果只需要將資料聯結到其他查詢,則可以試試使用內嵌視圖,以節省資源。
返回頁首避免 LEFT JOIN 和 NULL
當然,有很多時候您需要執行 LEFT JOIN 和使用 NULL 值。但是,它們並不適用於所有情況。改變 SQL 查詢的構建方式可能會產生將一個花幾分鐘啟動並執行報告縮短到只花幾秒鐘這樣的天壤之別的效果。有時,必須在查詢中調整資料的形態,使之適應應用程式所要求的顯示方式。雖然 TABLE 資料類型會減少大量佔用資源的情況,但在查詢中還有許多地區可以進行最佳化。SQL 的一個有價值的常用功能是 LEFT JOIN。它可以用於檢索第一個表中的所有行、第二個表中所有匹配的行、以及第二個表中與第一個表不匹配的所有行。例如,如果希望返回每個客戶及其定單,使用 LEFT JOIN 則可以顯示有定單和沒有定單的客戶。
加快使用 LEFT JOIN 的查詢速度的一項技術涉及建立一個 TABLE 資料類型,插入第一個表(LEFT JOIN 左側的表)中的所有行,然後使用第二個表中的值更新 TABLE 資料類型。此技術是一個兩步的過程,但與標準的 LEFT JOIN 相比,可以節省大量時間。一個很好的規則是嘗試各種不同的技術並記錄每種技術所需的時間,直到獲得用於您的應用程式的執行效能最佳的查詢。
測試查詢的速度時,有必要多次運行此查詢,然後取一個平均值。因為查詢(或預存程序)可能會儲存在 SQL Server 記憶體中的過程緩衝中,因此第一次嘗試耗費的時間好像稍長一些,而所有後續嘗試耗費的時間都較短。另外,運行您的查詢時,可能正在針對相同的表運行其他查詢。當其他查詢鎖定和解鎖這些表時,可能會導致您的查詢要排隊等待。例如,如果您進行查詢時某人正在更新此表中的資料,則在更新提交時您的查詢可能需要耗費更長時間來執行。
如果由於笛卡爾乘積佔用的資源可能會很多,而不需要真正的笛卡爾乘積,則可以謹慎地使用 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