Sql-Server

為使用完全相同的查詢重複訪問數據庫的應用程序優化 SQL Server?

  • October 25, 2016

我正在處理在後端使用 SQL Server Express (2014) 的 Windows 應用程序的性能問題。

主要是通過查看索引 SQL Server 端,我設法使這個執行得更好,但是有一個特定的報告仍然執行得很慢。

查看它在做什麼,它似乎在應用程序中循環並SELECT *針對一張表查詢出數千個非常簡單的查詢WHERE = Primary Key,因此在每種情況下只檢索一條記錄。當我說相同時,我的意思是相同的,它甚至沒有改變主鍵來獲取不同的東西,它顯然每次需要時都從數據庫中要求完全相同的記錄,僅在幾次中就多達一百次秒。

這是一個範例報告,當伺服器安靜時執行大約需要 10-15 秒 - 我已將查詢執行的次數添加為評論:

SELECT * FROM "Patient" WHERE "_Recno" = 35051  -- (runs 106 times)
SELECT * FROM "Client" WHERE "_Recno" = 15607   -- (99 times)
SELECT * FROM "SpeciesEntry" WHERE "_Recno" = 180   -- (97)
SELECT * FROM "Table" WHERE "_Recno" = 9    -- (97)
SELECT * FROM "DefaultEntry" WHERE "_Recno" = 2615  -- (96)
SELECT * FROM "Table" WHERE "_Recno" = 34   -- (96)
SELECT * FROM "DefaultEntry" WHERE "_Recno" = 2562  -- (84)
SELECT * FROM "Table" WHERE "_Recno" = 33   -- (84)
SELECT * FROM "Treatment" WHERE "_Recno" = 1682 -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 1819 -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 927  -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 934  -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 935  -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 940  -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 942  -- (33)
SELECT * FROM "Treatment" WHERE "_Recno" = 944  -- (33)
SELECT * FROM "OptionWP" WHERE "_Recno" = 103   -- (3)
SELECT * FROM "OptionWP" WHERE "_Recno" = 54    -- (1)
SELECT * FROM "PatientEstimate" WHERE "_Recno" = 8928   -- (1)
SELECT * FROM "Phrase" WHERE "_Recno" = 9718    -- (1)
SELECT * FROM "Table" WHERE "_Recno" = 4    -- (1)
SELECT * FROM "BreedEntry" WHERE "_Recno" = 3283    -- (1)

查詢後的數字是執行精確查詢的次數,例如查詢SELECT * FROM "Patient" WHERE "_Recno" = 35051被執行了 106 次,帶有 _Recno。實際上有 1,031 個查詢被執行來建構這個報告(在這種情況下,它會有所不同)——上面的 23 個左右是不同的查詢。

現在上面的每個查詢都執行得非常快,我們在每種情況下都在談論幾十*微秒。*事實上,如果您將用於生成此報告的所有 1,031 個查詢加起來,那麼所有這些查詢所用的總時間僅為 59,193 微秒,或僅 59 毫秒。

因此,問題和延遲似乎是成本——儘管此報告中只有大約 59 毫秒的實際數據庫時間,但報告需要大約 10-15 秒才能為客戶端執行,因為它來回處理超過 1,000 個查詢。

請注意,在大多數情況下,客戶端應用程序和 SQL Server 位於同一台電腦上,並且通過 RDP 訪問客戶端的多個實例。在少數情況下,客戶端位於 LAN 上的另一台機器上,我想那裡的性能會更差。但是在大多數情況下,您可以認為不應該存在網路問題,因為客戶端應用程序和 SQL Server 都在同一個物理機上。

十秒鐘甚至是可以接受的,問題是在繁忙的時間可以增加到一分鐘或更多。

關於如何進行優化的任何想法?如果它是一個應用程序,我可以訪問源顯然我會用一個或幾個使用連接的查詢替換所有這些,但這不是一個選項,應用程序是一個黑盒子——我能做的就是從 SQL Server 端進行優化。

與客戶交談時,雖然無論他們是通過 RDP 還是遠端客戶端應用程序安裝使用它,性能都很差,但遠端客戶端應用程序的性能要差得多,這對他們來說更是一個問題。因此,任何關於我可以考慮以改善那裡的性能的事情的建議,關於網路或其他的,將不勝感激。需要注意的一點是,這個 SQL 2014 機器現在是虛擬化的,以前他們使用我認為是 2008 或 2012 但它沒有虛擬化 - 他們說這個報告當時更快。他們希望將其虛擬化還有其他原因,將其從虛擬化中移除不是一種選擇。

它使用 Windows 身份驗證和(我很確定)TCP/IP 進行連接。我認為我無法改變這一點。據我所知,它並沒有刪除和重新建立連接,它似乎至少在使用連接池。

我在日常工作中使用 Hibernate,之前遇到過這種情況,ORM 會生成數千個查詢,我通常的解決方案是查看程式碼中的獲取策略(延遲載入與急切載入),或者實際上在報告的情況下,通常用 SQL 重寫整個事情。在這種情況下,儘管軟體就是它,一個 Windows 執行檔,對此我無能為力,我只能解決 SQL 方面的問題。

我的理解是,供應商不再支持這個特定版本,並且已經回到使用平面文件而不是 SQL 的版本。這對客戶不起作用-他們將此數據庫與其他各種東西集成在一起。它是小眾軟體,與大多數此類軟體一樣,它在後端技術上很糟糕,但具有小眾使用者需要的功能。無論如何,我無法更改軟體,只能更改 SQL Server 上的內容。它無法使用,而我已經可以使用它,因此在這些限制條件下取得了進展。

沒有任何阻塞或鎖定,我已經檢查過了。這實際上是我修復的主要性能問題,但在過去一個月左右的時間裡,根本沒有任何阻塞足夠長的時間來記錄。記憶體並不是真正的問題,因為它是 SQL Server Express,所以無論如何都限制為 1GB。從外觀上看,雖然我認為它沒有記憶體問題,但磁碟(如果有的話)似乎是最大的硬體瓶頸。

相同的查詢不是問題,並且在某些方面有所幫助,因為執行計劃被記憶體並且查詢所需的數據頁仍應被記憶體。然後,問題往往是身份驗證和初始化會話的每個連接成本。

首先要研究的是:是否使用了“連接池”?您可以使用 SQL Server Profiler 對此進行測試,在“儲存過程”類別中選擇“RPC:Completed”事件,確保針對該事件檢查“TextData”、“ClientProcessID”和“SPID”(在至少,您可以根據需要選擇其他列)。然後,轉到“列過濾器”,選擇“TextData”,並在“Like”條件中添加以下條件:exec sp[_]reset[_]connection. 現在執行該跟踪。如果您看到exec sp_reset_connection實例通過,那是由於使用了連接池。但這並不一定意味著使用它的是這個應用程序。所以看一個“SPID”

SELECT sssn.login_time,
      DATEDIFF(MILLISECOND, conn.connect_time, sssn.login_time)
                 AS [MillisecondsBetweenConnectionAndSessionStart],
      conn.*,
      sssn.[program_name],
      sssn.host_process_id,
      sssn.client_interface_name,
      sssn.login_name,
      qry.[text]
FROM sys.dm_exec_connections conn
INNER JOIN sys.dm_exec_sessions sssn
       ON sssn.session_id = conn.session_id
OUTER APPLY sys.dm_exec_sql_text(conn.most_recent_sql_handle) qry
WHERE conn.session_id <> conn.most_recent_session_id
OR    DATEDIFF(MILLISECOND, conn.connect_time, sssn.login_time) > 50
ORDER BY conn.connect_time;

最右邊/末端的欄位執行了最近的查詢。這應該確認會話是有問題的應用程序(通過它正在做什麼)。

如果您找不到任何一般使用或至少用於此應用程序的連接池的證據,則需要更新應用程序使用的連接字元串以啟用連接池。

…在大多數情況下,客戶端應用程序和 SQL Server 位於同一台電腦上,並且通過 RDP 訪問客戶端的多個實例。在少數情況下,客戶端位於 LAN 上的另一台機器上……

根據問題的上述資訊,似乎有多個客戶端正在連接,對嗎?如果是這樣,那麼連接池雖然可能仍然是一個好主意並且很有幫助,但效果會降低,因為每個客戶端都維護自己的連接池。意思是,5 個實例(或任何數量的實例)仍然會為連接創建 5 個單獨的池,每個實例都會減少其各自應用程序的連接啟動成本,但不能將成本減少到 / 減少到單個共享連接)。還要記住,即使使用連接池,如果應用程序在嘗試打開新連接之前沒有正確關閉其連接,您仍然會有來自給定應用程序實例的多個連接/會話。

在這種情況下,如果單個應用程序沒有使用連接池並且無法更新其連接字元串以使用連接池,那麼,如果可能完全更新應用程序,那麼實現將非常有幫助記憶體層,例如 Redis 或 memcache(我相信 AWS 和 Azure 都提供基於雲的記憶體解決方案)。這些重複的命中通常可以被記憶體並完全繞過 RDBMS(當然,在指定的時間內),這就是這些東西存在的很大一部分原因。


現在我剛剛趕上了關於這個問題的最新評論,似乎連接字元串和應用程序的任何部分都不能被修改。在這種情況下,除了可能檢查與 SQL Server 在同一伺服器上執行的應用程序建立的連接是否使用共享記憶體或 TCP/IP 進行連接,以及是否是 TCP 連接之外,我們無能為力/IP 用於同一伺服器連接,然後檢查是否為 SQL Server 啟用了共享記憶體協議,如果沒有,則啟用它。這不是保證的改進,因為連接字元串可能會強制協議為 TCP/IP(例如,使用語法:)server=tcp:{something},但仍然值得嘗試,因為可能不使用此語法。



更新

從對此答案的評論:

$$ the query above $$什麼都沒有給我,但是尋找sys.dm_exec_connections.connect_time有問題的應用程序是幾個小時到幾天前。…在每種情況下…連接和會話開始之間的差異始終低於 50 毫秒(在 3 到 16 之間)

這可以很好地表明問題,或者至少是其中的很大一部分。如果在幾小時到幾天前建立了連接,並且會話隨後立即開始,則該應用程序是一個桌面應用程序,它建立一個單獨的連接會話,它執行所有查詢(類似於 SSMS 查詢選項卡的工作方式),或者應用程式碼每次都錯誤地沒有關閉連接對象,在這種情況下,您可能會看到很多並發連接,其中許多實際上已被放棄但仍佔用記憶體(在 SQL Server Express 上,可能存在連接無論如何限制)。

嘗試以下查詢,改編自上面的原始查詢:

SELECT conn.connect_time,
      sssn.login_time,
      CASE WHEN conn.last_read > conn.last_write THEN conn.last_read
           ELSE conn.last_write END AS [LastActivity],
      GETDATE() AS [Now],
      DATEDIFF(MILLISECOND, conn.connect_time, sssn.login_time)
                 AS [MillisecondsBetweenConnectionAndSessionStart],
      DATEDIFF(MILLISECOND, sssn.login_time, CASE WHEN conn.last_read > conn.last_write
                                              THEN conn.last_read ELSE conn.last_write END)
                 AS [MillisecondsBetweenSessionStartAndLastActivity],
      DATEDIFF(MILLISECOND, CASE WHEN conn.last_read > conn.last_write
                        THEN conn.last_read ELSE conn.last_write END, GETDATE())
                 AS [MillisecondsBetweenLastActivityAndNow],
      sssn.[program_name],
      sssn.host_process_id,
      sssn.client_interface_name,
      sssn.login_name,
      qry.[text],
      conn.*
FROM sys.dm_exec_connections conn
INNER JOIN sys.dm_exec_sessions sssn
       ON sssn.session_id = conn.session_id
OUTER APPLY sys.dm_exec_sql_text(conn.most_recent_sql_handle) qry
WHERE  sssn.is_user_process = 1
ORDER BY [MillisecondsBetweenLastActivityAndNow] DESC;

如果正在使用連接池,那麼您將看到MillisecondsBetweenConnectionAndSessionStart欄位值較高但欄位值較低的行MillisecondsBetweenSessionStartAndLastActivity。原因是連接已建立並被重新使用。每次重新使用連接時,login_time都會重置為最近的SqlConnection.Open事件,然後立即執行查詢。

如果您有一個具有穩定連接的桌面應用程序(如 SSMS),您將看到與連接池相反的行為:您將看到MillisecondsBetweenConnectionAndSessionStart欄位值較低但欄位值較高的行MillisecondsBetweenSessionStartAndLastActivity。原因是連接一旦建立,就永遠不會關閉,只是讓查詢繼續對其執行。

如果您的應用程序沒有關閉其連接,但仍在打開新的連接(由於錯誤或誤解連接池的工作原理——無論是否啟用池),那麼您將不僅有很多行,但它們的MillisecondsBetweenConnectionAndSessionStart欄位值較低,但欄位值要高得多MillisecondsBetweenLastActivityAndNow。如果建立連接,使用一次,然後SqlConnection.Open()在呼叫之前呼叫,就會發生這種情況,SqlConnection.Close()或者SqlConnection.Dispose()Dispose()將呼叫,如果對像是在構造中創建的Close(),則會自動呼叫)。SqlConnection``using()

引用自:https://dba.stackexchange.com/questions/153159