為了讓應用程序運行得更快,所做的全部工作就是在這裡或那裡做一些很小調整。啊,但關鍵在於確定如何進行調整!遲早您會遇到這種情況:應用程序中的 SQL 查詢不能按照您想要的方式進行響應。它要麼不返回數據,要麼耗費的時間長得出奇。如果它降低了報告或您的企業應用程序的速度,用戶必須等待的時間過長,他們就會很不滿意。就像您的父母不想聽您解釋為什麼在深更半夜才回來一樣,用戶也不會聽你解釋為什麼查詢耗費這麼長時間。(“對不起,媽媽,我使用了太多的 LEFT JOIN。”)用戶希望應用程序響應迅速,他們的報告能夠在瞬間之內返回分析數據。就我自己而言,如果在 Web 上沖浪時某個頁面要耗費十多秒才能加載(好吧,五秒更實際一些),我也會很不耐煩。
為了解決這些問題,重要的是找到問題的根源。那麼,從哪裡開始呢?根本原因通常在於數據庫設計和訪問它的查詢。在本月的專欄中,我將講述四項技術,這些技術可用於提高基於 SQL Server? 的應用程序的性能或改善其可伸縮性。我將仔細說明 LEFT JOIN、CROSS JOIN 的使用以及 IDENTITY 值的檢索。請記住,根本沒有神奇的解決方案。調整您的數據庫及其查詢需要占用時間、進行分析,還需要大量的測試。這些技術都已被證明行之有效,但對您的應用程序而言,可能其中一些技術比另一些技術更適用。
從 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 值。
以返回這些定單售出的產品數量:
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,則會得到非常可觀的回報。
加快使用 LEFT JOIN 的查詢速度的一項技術涉及創建一
如果 Products 表存儲了其類別的 ID,而沒有用於某個特定產品的類別,則您可以在字段中存儲 NULL 值。然後您必須執行 LEFT JOIN 來獲取所有產品及其類別。您可以創建一個值為“No Category”的類別,從而指定外鍵關系不允許 NULL 值。通過執行上述操作,現在您就可以使用 INNER JOIN 檢索所有產品及其類別了。雖然這看起來好像是一個帶有多余數據的變通方法,但可能是一個很有價值的技術,因為它可以消除 SQL 批處理語句中消耗資源較多的 LEFT JOIN。在數據庫中全部使用此概念可以為您節省大量的處理時間。請記住,對於您的用戶而言,即使幾秒鐘的時間也非常重要,因為當您有許多用戶正在訪問同一個聯機數據庫應用程序時,這幾秒鐘實際上的意義會非常重大。
靈活使用笛卡爾乘積
對於此技巧,我將進行非常詳細的介紹,並提倡在某些情況下使用笛卡爾乘積。出於某些原因,笛卡爾乘積 (CROSS JOIN) 遭到了很多譴責,開發人員通常會被警告根本就不要使用它們。在許多情況下,它們消耗的資源太多,從而無法高效使用。但是像 SQL 中的任何工具一樣,如果正確使用,它們也會很有價值。例如,如果您想運行一個返回每月數據(即使某一特定月份客戶沒有定單也要返回)的查詢,您就可以很方便地使用笛卡爾乘積。
圖2. Returning All Customers and Their Sales
set nocount on DECLARE @dtStartDate DATETIME, @dtEndDate DATETIME, @dtDate DATETIME SET @dtEndDate = '5/5/1997' SET @dtEndDate = DATEADD(DD, -1, CAST(CAST((MONTH(@dtEndDate) + 1) AS VARCHAR(2)) + '/01/' + CAST(YEAR(@dtEndDate) AS VARCHAR(4)) + ' 23:59:59' AS DATETIME)) SET @dtStartDate = DATEADD(MM, -1 * 12, @dtEndDate) SELECT CAST(YEAR(o.OrderDate) AS VARCHAR(4)) + '-' + CASE WHEN MONTH(o.OrderDate) < 10 THEN '0' + CAST(MONTH(o.OrderDate) AS VARCHAR(2)) ELSE CAST(MONTH(o.OrderDate) AS VARCHAR(2)) END AS sMonth, c.CustomerID, c.CompanyName, c.ContactName, SUM(od.Quantity * od.UnitPrice) AS mSales FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID INNER JOIN [Order Details] od ON o.OrderID = od.OrderID WHERE o.OrderDate BETWEEN @dtStartDate AND @dtEndDate GROUP BY CAST(YEAR(o.OrderDate) AS VARCHAR(4)) + '-' + CASE WHEN MONTH(o.OrderDate) < 10 THEN '0' + CAST(MONTH(o.OrderDate) AS VARCHAR(2)) ELSE CAST(MONTH(o.OrderDate) AS VARCHAR(2)) END, c.CustomerID, c.CompanyName, c.ContactName ORDER BY c.CompanyName, sMonth圖 3 Cartesian Product at Work
DECLARE @tblMonths TABLE (sMonth VARCHAR(7)) DECLARE @tblCustomers TABLE ( CustomerID CHAR(10), CompanyName VARCHAR(50), ContactName VARCHAR(50)) DECLARE @tblFinal TABLE ( sMonth VARCHAR(7), CustomerID CHAR(10), CompanyName VARCHAR(50), ContactName VARCHAR(50), mSales MONEY) DECLARE @dtStartDate DATETIME, @dtEndDate DATETIME, @dtDate DATETIME, @i INTEGER SET @dtEndDate = '5/5/1997' SET @dtEndDate = DATEADD(DD, -1, CAST(CAST((MONTH(@dtEndDate) + 1) AS VARCHAR(2)) + '/01/' + CAST(YEAR(@dtEndDate) AS VARCHAR(4)) + ' 23:59:59' AS DATETIME)) SET @dtStartDate = DATEADD(MM, -1 * 12, @dtEndDate) — Get all months into the first table SET @i = 0 WHILE (@i < 12) BEGIN SET @dtDate = DATEADD(mm, -1 * @i, @dtEndDate) INSERT INTO @tblMonths SELECT CAST(YEAR(@dtDate) AS VARCHAR(4)) + '-' + CASE WHEN MONTH(@dtDate) < 10 THEN '0' + CAST(MONTH(@dtDate) AS VARCHAR(2)) ELSE CAST(MONTH(@dtDate) AS VARCHAR(2)) END AS sMonth SET @i = @i + 1 END — Get all clIEnts who had sales during that period into the "y" tabl
您正在看的SQLserver教程是:五種提高SQL性能的方法(1)。e INSERT INTO @tblCustomers SELECT DISTINCT c.CustomerID, c.CompanyName, c.ContactName FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID WHERE o.OrderDate BETWEEN @dtStartDate AND @dtEndDate INSERT INTO @tblFinal SELECT m.sMonth, c.CustomerID, c.CompanyName, c.ContactName, 0 FROM @tblMonths m CROSS JOIN @tblCustomers c UPDATE @tblFinal SET mSales = mydata.mSales FROM @tblFinal f INNER JOIN ( SELECT c.CustomerID, CAST(YEAR(o.OrderDate) AS VARCHAR(4)) + '-' + CASE WHEN MONTH(o.OrderDate) < 10 THEN '0' + CAST(MONTH(o.OrderDate) AS VARCHAR(2))
ELSE CAST(MONTH(o.OrderDate) AS VARCHAR(2)) END AS sMonth, SUM(od.Quantity * od.UnitPrice) AS mSales FROM Customers c INNER JOIN Orders o ON c.CustomerID = o.CustomerID INNER JOIN [Order Details] od ON o.OrderID = od.OrderID WHERE o.OrderDate BETWEEN @dtStartDate AND @dtEndDate GROUP BY c.CustomerID, CAST(YEAR(o.OrderDate) AS VARCHAR(4)) + '-' + CASE WHEN MONTH(o.OrderDate) < 10 THEN '0' + CAST(MONTH(o.OrderDate) AS VARCHAR(2)) ELSE CAST(MONTH(o.OrderDate) AS VARCHAR(2)) END ) mydata on f.CustomerID = mydata.CustomerID AND f.sMonth = mydata.sMonth SELECT f.sMonth, f.CustomerID, f.CompanyName, f.ContactName, f.mSales FROM @tblFinal f ORDER BY f.CompanyName, f.sMonth
圖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 安全范例。 (完)