享技水工坊 TechX Studio,資訊.音樂人同學會-大家都想知道的:如何將松柏 ERP Server 系統與 SQL Server 資料庫分離部署,以降低系統資源負載並提升整體效能

參考整體架構

[ODBC]
DRIVER=SQL Server
UID=frank
Trusted_Connection=Yes
Database=松測
WSID=FRANK
CONNECT=1
SERVER=192.168.1.159 *** 資料庫指向主機的
區網位置 ***
DBQ=C:\Program Files\印刷帳務管理系統(SQL)\WNSH.mdb
Site=S
*** 指主機而不是工作站 ***

項目設定值說明
ERP Server192.168.1.156安裝印刷帳務管理系統(SQL) 主程式
SQL Server192.168.1.159真正的資料庫所在地
管理工具SSMS v18.11.1安裝在 **ERP Server** 上遠端管理
資料庫名稱松測這次的測試庫
資料夾SQL存放 ERP Server 相關代碼
DSNODBC 系統資料來源ERP 連線的橋樑

ERP Server 與 SQL Server 跨伺服器分流實務指南

ERP Server 與 SQL Server 分到不同的實體/虛擬伺服器上,以提高 ERP Server 系統速度!

先前工作-在 SQL Server 設定 SQL 共用夾,方便在 ERP Server 還原資料庫時,找得到要還原的 bak 檔

一、 為什麼要「分家」?

這在資料量少、上線人數少的草創期確實行得通,但隨著公司擴大、交易單據積少成多,就會開始影響效能

  1. 記憶體爭奪戰(RAM Starvation): SQL Server 是一個對記憶體「要求」的軟體,預設情況下,只要給它實體記憶體,它就會盡可能吃滿以快取資料頁(Data Pages),此時,ERP 系統主程式要在伺服器上處理邏輯計算、產生報表時,就會發現系統根本沒有剩餘的 RAM 可用,只能一直讀寫硬碟 swap 空間,導致整體效能慘不忍睹。
  2. CPU 資源搶占(CPU Bottleneck): ERP 系統要處理商業邏輯(計算成本、應收帳款、庫存核算),SQL Server 要處理複雜的查詢(Index Scan、Join、Sort),當幾十個使用者同時點擊大型報表,伺服器的 CPU 執行緒會在應用程式與資料庫進程之間頻繁進行上下文切換(Context Switching),大幅衰減運算效率。
  3. 單點故障風險(Single Point of Failure, SPOF): 一旦這台「單獨」伺服器硬體故障(如主機板燒毀或 POWER 損壞),企業的應用程式與關鍵業務資料庫將一瞬間同時陷入癱瘓,完全沒有緩衝空間。

讓 ERP 專心處理商業邏輯與介面運算,讓 SQL Server 專心做資料查詢與交易寫入,「各司其職,分而治之」,才是打造高可用性、高效能企業 IT 環境的不二法門!讓 SQL Server 在本機 ERP Server(192.168.1.156) 完成遠端 (192.168.1.159) 還原

二、 本次實戰架構與環境設定

1. 網路與伺服器節點配置

  • ERP 應用伺服器(ERP Server):
    • IP 位址: 192.168.1.156
    • 角色: 執行 ERP 主系統、客戶端對接、SQL Server Management Studio (SSMS) 管理介面。
  • 資料庫伺服器(SQL Server):
    • IP 位址: 192.168.1.159
    • 角色: 專門運行 MS SQL Server 資料庫引擎,存放業務資料。
  • 預計管理之資料庫名稱: 松測
  • SQL Server 存放 ERP 相關代碼與工作目錄: SQL(192.168.1.159)

2. 管理工具與設定檔細節

  • 管理工具: SQL Server Management Studio (SSMS) V18.11.1(安裝於 ERP Server 192.168.1.156)。
  • ODBC 連結檔 (DSN 設定):

有了這張清晰的地圖,接下來我們就進入最核心的實作階段!

[ODBC]
DRIVER=SQL Server
UID=frank
Trusted_Connection=Yes
Database=松測
WSID=FRANK
CONNECT=1
SERVER=192.168.1.159
DBQ=C:\Program Files\印刷帳務管理系統(SQL)\WNSH.mdb
Site=S

三、 第一關:通訊管道打通——跨伺服器連線與防火牆設定

既然把 ERP 與 SQL Server 放在兩台不同的伺服器(192.168.1.156 與 192.168.1.159),第一步就是要確保兩者之間的網路通暢,不會被中間的防火牆或 SQL 預設安全機制攔截。

步驟 1:開放 SQL Server (192.168.1.159) 的 TCP/IP 協定

MS SQL Server 在預設安裝時,為了安全性考量,往往會關閉遠端 TCP/IP 連線。

  1. 登入 SQL Server 伺服器 (192.168.1.159)。
  2. 開啟 SQL Server 組態管理員(SQL Server Configuration Manager)。
  3. 展開 SQL Server 網路組態 -> 點擊 MSSQLSERVER 的協定。
  4. 找到 TCP/IP 項目,如果狀態為「已停用」,請點右鍵選擇 【啟用】。
  5. 點擊 TCP/IP 右鍵【內容】,切換到 【IP 位址】 頁籤,拉到最底部的 IPAll,確認 TCP 連接埠 設定為預設的 1433。
  6. 關鍵步驟: 重啟 SQL Server 服務(SQL Server Services -> SQL Server (MSSQLSERVER) -> 右鍵【重新啟動】),設定才會生效!

步驟 2:配置 Windows 防火牆規則

許多 IT 人員折騰半天連不上,80% 是被 Windows 防火牆硬生生檔了下來。

在 SQL Server 伺服器 (192.168.1.159) 的 Windows 防火牆中:

  1. 新增輸入規則(Inbound Rule)。
  2. 選擇 【連接埠 (Port)】,指定 TCP 1433,允許連線。
  3. 如果有使用 SQL Server Browser 服務或動態連接埠,建議另外開放 UDP 1434。
  4. 在 ERP Server (192.168.1.156) 開啟 Command Prompt (cmd),輸入:telnet 192.168.1.159 1433 如果畫面變黑或顯示連線成功,恭喜你,通訊大門已經順利開啟!

(Port 1433) 是 SQL Server 預設通訊埠

建議安全作法

PS C:\Users\Administrator>New-NetFirewallRule -DisplayName “SQL 1433 from ERP Server (192.168.1.156)” -Direction Inbound
-Protocol TCP -LocalPort 1433
-RemoteAddress 192.168.1.156 `
-Action Allow

這是一段 Windows PowerShell 的指令,用途是在系統的防火牆(Windows Defender Firewall)中新增一條「允許(Inbound)進站規則」,專門放行來自 ERP 伺服器 (IP: 192.168.1.156) 對本機 SQL Server(預設連接埠 TCP 1433)的連線存取。

以下為指令的逐行/逐個參數詳細拆解,以及所有縮寫的英文全文:

一、 指令結構與參數詳細解析

語法說明: PowerShell 中的反引號 `(Backtick)是換行續行符號,代表下一行的內容與這一行是同一條指令。

1. New-NetFirewallRule

  • 意義: PowerShell 的 Cmdlet 指令,用來建立一條新的網路防火牆規則。

2. -DisplayName “SQL 1433 from ERP Server (192.168.1.156)”

  • 意義: 設定該條規則在「進站規則(Inbound Rules)」清單中顯示的名稱,這只是給管理者看的標籤,方便日後維護與辨識用途。

3. -Direction Inbound

  • 意義: 指定封包的方向為進站(Inbound),也就是規範「外面主機嘗試連線進來這台電腦」的行為。

4. -Protocol TCP

  • 意義: 指定此規則套用的通訊協定為 TCP,SQL Server 資料庫的傳輸主要採用 TCP 協定。

5. -LocalPort 1433

  • 意義: 指定本機(目的地)開放的連接埠(Port)為 1433,1433 是 Microsoft SQL Server 的預設監聽連接埠。

6. -RemoteAddress 192.168.1.156

  • 意義: 設定來源端(遠端)的 IP 位址限制,只有來自 IP 為 192.168.1.156 的電腦發出的封包才能命中此規則;其他 IP 發起的 1433 連線會被排除(套用預設阻擋機制)。

7. -Action Allow

  • 意義: 符合上述條件(TCP、Port 1433、來自 192.168.1.156)的網路連線,發揮的動作是允許(Allow)通過。

二、 各簡稱與技術名詞英文全文

簡稱 / 名詞英文全文中文解釋
PSPowerShell微軟開發的高階自動化與管理命令列工具。
NetNetwork網路。
SQLStructured Query Language結構化查詢語言,此處通常指 Microsoft SQL Server 資料庫服務。
ERPEnterprise Resource Planning企業資源計劃系統。
TCPTransmission Control Protocol傳輸控制協定(一種可靠導向的網路連線協定)。
IPInternet Protocol網際網路協定(用於網路位址定址,如 192.168.1.156)。
PortPort (非簡稱)連接埠 / 通訊埠(作業系統用來區分不同應用程式服務的通訊管道)。

💡 總結這條命令的作用

執行這條命令後,這台 Windows 伺服器會開啟一扇專屬後門:只給 192.168.1.156 這台 ERP 伺服器存取本機的 1433 SQL 資料庫服務,既能滿足業務系統連線需求,又不會將 1433 連接埠暴露給整個區域網路的其他未授權裝置,是一種兼顧效能與資安的實務設定方式。

四、 第二關:DSN 檔案剖析與 ODBC 跨機連線設定

在我們的案例中,ERP 系統使用了一個 DSN (Data Source Name) 設定檔來尋找資料庫,讓我們來仔細剖析這段關鍵的 DSN 內容:

[ODBC]
DRIVER=SQL Server
UID=frank *** frank 只是測試用 ***
Trusted_Connection=Yes
Database=松測
WSID=FRANK *** FRANK 只是測試用 ***
CONNECT=1
SERVER=192.168.1.159
DBQ=C:\Program Files\印刷帳務管理系統(SQL)\WNSH.mdb
Site=S *** Server 的意思 ***

關鍵參數深度解讀:

  • SERVER=192.168.1.159: 這是分散式架構的靈魂!它告訴 ERP 系統,不要在本地(127.0.0.1 或 localhost)找資料,而是直接透過網路封包,前往 IP 為 192.168.1.159 的 SQL Server 資料庫主機請求資料庫服務。
  • Database=松測: 指定連接的資料庫名稱為「松測」。
  • UID=frank 與 Trusted_Connection=Yes: 這裡包含了混合認證機制。Trusted_Connection=Yes 代表使用 Windows 驗證模式(Domain/Windows 帳號),若 ERP 與 SQL 未加入同一個 Active Directory 網域,建議改用 SQL 混合驗證(設定 Trusted_Connection=No,並帶入 SQL 專屬帳號 frank 與對應密碼)。
  • DBQ=...WNSH.mdb: 這是早期許多台灣傳統 ERP(如印刷帳務管理系統)常見的雙模架構,可能作為前端暫存檔或 Access 轉接介面,但在後端交易上,核心已正式交由 192.168.1.159 的 SQL Server 處理。

五、 第三關:跨機超操控!在 ERP Server 透過 SSMS 還原資料庫

現在進入本篇最精采、也最容易讓人產生觀念混淆的重頭戲——「如何在 ERP Server (192.168.1.156) 上,利用 SSMS V18.11.1,將資料庫還原至 SQL Server (192.168.1.159) 且指定存放於 “SQL" 資料夾?」

🚨 觀念陷阱警報: 在操作跨機還原時,一定要記住:「SSMS 只是操控遙控器,真正的檔案讀取與寫入動作,全部發生在遠端的 SQL Server (192.168.1.159) 上!」

實戰步驟 1:開啟 SSMS V18.11.1 並遠端連線

打開 SSMS V18.11.1 及遠端連線後所看到的檔案總管會是 SQL Server 端的,所以要把相關要用的代碼放到 SQL Server 的 “SQL” 及設共用才能存取

  1. 在 ERP Server (192.168.1.156) 前,開啟 SQL Server Management Studio V18.11.1。
  2. 在「連線到伺服器」視窗中:
    • 伺服器類型: 資料庫引擎 (Database Engine)
    • 伺服器名稱: 輸入遠端 SQL Server IP 192.168.1.159
    • 驗證方式: 選擇 Windows 驗證或 SQL Server 驗證(帳號 frank)。
  3. 點擊【連線】。成功連入後,你在 SSMS 物件總管看到的,就是遠端 192.168.1.159 主機上的資料庫狀態。

參考建議-關鍵的「檔案」設定——把資料庫存到 DB Server(192.168.1.159) 規劃的的 “SQL” 資料夾,其他相關代碼放”SQL” 資料夾下如 “DBworkarea”

這一步是效能最佳化的核心,當你選好備份檔後,先別急著按確定,切換到左側的「檔案」頁面(或有些版本叫「選項」)。

你會看到「將資料庫檔案還原為」的列表,通常會有兩個檔案:一個是資料檔(.mdf),一個是記錄檔(.ldf)。系統預設會把它們還原到 SQL Server 的預設目錄,這往往就是造成磁碟 I/O 瓶頸。

請手動修改「還原為」欄位的路徑,把它們指向你規劃的資料夾。例如:

  • C:\SQL\松測.mdf *** 此時的 C:\SQL 是指 SQL Server 的 C:\SQL ***
  • C:\SQL\松測_log.ldf *** 此時的 C:\SQL 是指 SQL Server 的 C:\SQL ***

這樣做有兩個好處:

  1. 磁碟分離:如果 SQL資料夾 是位於 192.168.1.159 這台機器上獨立的實體磁碟或 SSD,那就等於是把資料庫的讀寫壓力跟作業系統分開,以提高效能。
  2. 路徑統一:強制把資料庫檔案放在一個你清楚知道的地方,未來要備份或搬移都方便。

設定完成後,按下「確定」,SSMS 就會把指令送到 192.168.1.159,開始還原,你會看到進度條跑動,此時 192.168.1.156 ERP Server 完全不會感到任何負擔,因為它只是在「監工」。

實戰步驟 2:資料庫備份檔 (.bak) 的擺放位置

假設你手上有一個備份檔要還原成「松測」資料庫:

  • 錯誤做法: 把 松測.bak 放在 ERP Server (192.168.1.156) 的 C:\ 磁碟機,然後在 SSMS 選擇還原。SQL Server (192.168.1.159) 會找不到檔案而報錯!
  • 正確做法: 必須將 松測.bak 複製到 SQL Server (192.168.1.159) 本地的硬碟 中(例如 C:\SQL\松測.bak),或者透過網路共享路徑(UNC Path)讓 SQL Server 服務帳戶有權限讀取。

六、 驗證與測試:如何確定還原成功?

當 SSMS 跳出「資料庫 ‘松測’ 還原成功」的提示視窗後,我們需要進行「階段驗證法」,確認系統已順利在新架構下運作:

1. 檔案實體確認

前往 SQL Server 伺服器 (192.168.1.159),打開檔案資源管理器,進入 C:\SQL\ 資料夾,你應該會看到生成的 松測.mdf 與 松測_log.ldf 檔案,且檔案建立時間為剛才還原的時間點。

2. SSMS 狀態檢查

在 ERP Server (192.168.1.156) 的 SSMS 中,重新整理【資料庫】清單,點開「松測」->【資料表】,隨機執行一次查詢:

SELECT TOP 100 * FROM [松測].[dbo].[客戶資料表];

確認資料能順利讀取且無亂碼狀況。

七、 專家調校指南:跨機分散後的效能優化建議

將 ERP 與 SQL Server 分散在兩台伺服器後,雖然解決了運算資源搶占問題,但同時也引入了一個新的變數——「網路延遲(Network Latency)」。

以前 ERP 與 SQL 在同一台伺服器時,資料交換走的是 Shared Memory(記憶體直接存取),速度極快;現在變成了跨機通訊,資料必須封包化經由網卡(NIC)傳輸。為了防止網路成為新的瓶頸

升級區域網路硬體(Gigabit / 10GbE)

確保 ERP Server (192.168.1.156) 與 SQL Server (192.168.1.159) 之間的網路交換器(Switch)支援 Gigabit (1000 Mbps) 以上規格,且網線採用 Cat.6 以上規格。如果企業條件允許,建議為這兩台伺服器配置 獨立的實體網卡,建立專用的備份/資料傳輸通道(Heartbeat / Backup Network)。

從真實 SQL 壓測數據看「多機分流」的必要性

引言: 想像一下,如果你的餐廳突然湧入 10 位顧客同時點了最繁複的手工菜,廚房裡卻只有一名主廚獨撐大局,會發生什麼事?出菜速度慢、廚師累癱、顧客抱怨連連!在 IT 系統的世界裡,單台伺服器面對高併發流量時,也面臨著一模一樣的困境。今天我們就用一份真實的 SQL 壓力測試數據,帶大家看看「單機硬撐」與「多機分流」的致命差距!

SQL Server 還原時機:比對 “松柏” 相對安裝程式的 SQL 版本(A)及客戶的松柏的系統版本(B)

若 A(如 115.10.02) 大於 B(如 114.09.28) 才要還原 SQL Server,否則只要貼上本機的主程式就可(松柏公司版本於大於客戶版本)

📊 真實壓測揭密:高併發連線的衝擊

SQL Server + ERP Server 雙機同時正式壓力測試工具(免安裝模組純 ADO.NET 版)

在 PowerShell 執行

# ============================================================
# SQL Server + ERP Server 雙機同時正式壓力測試工具(免安裝模組純 ADO.NET 版)
# ============================================================
# 功能:
# 1. 監控 ERP Server (本機 192.168.1.156) CPU / RAM
# 2. 遠端監控 SQL Server (192.168.1.159) OS CPU / RAM
# 3. 監控 SQL Server 內部 CPU (Signal Waits) / Memory / 連線數 (純 ADO.NET)
# 4. 對 SQL Server 建立高併發壓力連線 (純 ADO.NET)
# 5. 可選 ERP 端 CPU 模擬負載(模擬應用邏輯)
# 6. 完整 CSV + 統計報告
# ============================================================

# --------------------【一、測試參數】--------------------
$ErpServer          = "192.168.1.156"          # 本機(ERP Server / 腳本執行端)
$SqlServerInstance  = "192.168.1.159"          # 遠端 SQL Server
$Database           = "松測"                   # 目標資料庫
$IntegratedSecurity = $true
$Concurrency        = 20                       # 併發連線數(建議先從 10~20 開始)
$DurationSec        = 60                       # 測試時間(秒)
$MonitorIntervalSec = 2                        # 監控採樣間隔(秒)
$EnableErpCpuLoad   = $true                    # 是否同時對 ERP Server 加壓(模擬應用邏輯)
$ResultFolder       = "C:\SQL_PerfTest"

# -------------------- 建立結果資料夾 --------------------
if (-not (Test-Path $ResultFolder)) {
    New-Item -Path $ResultFolder -ItemType Directory -Force | Out-Null
}
$TimeStamp      = Get-Date -Format "yyyyMMdd_HHmmss"
$ErpHostLog     = Join-Path $ResultFolder "ERP_Host_$TimeStamp.csv"
$SqlOsLog       = Join-Path $ResultFolder "SQL_OS_$TimeStamp.csv"
$SqlInternalLog = Join-Path $ResultFolder "SQL_Internal_$TimeStamp.csv"
$StatPath       = Join-Path $ResultFolder "Summary_$TimeStamp.txt"

Write-Host ""
Write-Host "====================================================" -ForegroundColor Cyan
Write-Host "  SQL + ERP 雙機同時壓力測試工具(免模組強化版)" -ForegroundColor Cyan
Write-Host "====================================================" -ForegroundColor Cyan
Write-Host "ERP Server     : $ErpServer (本機監控 + 可選 CPU 負載)" -ForegroundColor White
Write-Host "SQL Server     : $SqlServerInstance (遠端監控 + 併發壓測)" -ForegroundColor White
Write-Host "Database       : $Database" -ForegroundColor White
Write-Host "Concurrency    : $Concurrency" -ForegroundColor White
Write-Host "Duration       : $DurationSec 秒" -ForegroundColor White
Write-Host "ERP CPU Load   : $EnableErpCpuLoad" -ForegroundColor White
Write-Host "結果資料夾     : $ResultFolder" -ForegroundColor White
Write-Host ""

# --------------------【二、ERP Host 監控 Job (192.168.1.156)】--------------------
Get-Job -Name "ErpHostMonitor" -ErrorAction SilentlyContinue | Remove-Job -Force
Write-Host ">>> [1] 啟動 ERP Server ($ErpServer) CPU / RAM 監控..." -ForegroundColor Yellow

$ErpMonitorJob = Start-Job -Name "ErpHostMonitor" -ScriptBlock {
    param($LogFile, $Interval)
    "Timestamp,CPU_Usage_Pct,Available_RAM_MB" | Out-File -FilePath $LogFile -Encoding UTF8
    while ($true) {
        $Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        try {
            $Cpu = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
            $RamMB = [math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1024, 2)
            "$Time,$([math]::Round($Cpu,2)),$RamMB" | Out-File -FilePath $LogFile -Append -Encoding UTF8
        } catch {
            "$Time,0,0" | Out-File -FilePath $LogFile -Append -Encoding UTF8
        }
        Start-Sleep -Seconds $Interval
    }
} -ArgumentList $ErpHostLog, $MonitorIntervalSec

# --------------------【三、SQL Server OS 遠端監控 Job (192.168.1.159)】--------------------
Get-Job -Name "SqlOsMonitor" -ErrorAction SilentlyContinue | Remove-Job -Force
Write-Host ">>> [2] 啟動 SQL Server OS ($SqlServerInstance) 遠端監控..." -ForegroundColor Yellow

$SqlOsJob = Start-Job -Name "SqlOsMonitor" -ScriptBlock {
    param($SqlHost, $LogFile, $Interval)
    "Timestamp,SQL_OS_CPU_Pct,SQL_OS_Available_RAM_MB" | Out-File -FilePath $LogFile -Encoding UTF8
    while ($true) {
        $Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        try {
            $Cpu = (Get-CimInstance -ComputerName $SqlHost Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
            $RamMB = [math]::Round((Get-CimInstance -ComputerName $SqlHost Win32_OperatingSystem).FreePhysicalMemory / 1024, 2)
            "$Time,$([math]::Round($Cpu,2)),$RamMB" | Out-File -FilePath $LogFile -Append -Encoding UTF8
        } catch {
            "$Time,0,0" | Out-File -FilePath $LogFile -Append -Encoding UTF8
        }
        Start-Sleep -Seconds $Interval
    }
} -ArgumentList $SqlServerInstance, $SqlOsLog, $MonitorIntervalSec

# --------------------【四、SQL 內部指標監控 Job (純 ADO.NET)】--------------------
Get-Job -Name "SqlInternalMonitor" -ErrorAction SilentlyContinue | Remove-Job -Force
Write-Host ">>> [3] 啟動 SQL Server 內部資源監控 ($SqlServerInstance)..." -ForegroundColor Yellow

$SqlInternalJob = Start-Job -Name "SqlInternalMonitor" -ScriptBlock {
    param($Server, $DB, $LogFile, $Interval)
    "Timestamp,SQL_CPU_SignalWaits_Pct,SQL_Used_Memory_MB,SQL_User_Connections" | Out-File -FilePath $LogFile -Encoding UTF8
    
    $ConnStr = "Server=$Server;Database=$DB;Integrated Security=True;TrustServerCertificate=True;Connect Timeout=15;"
    
    while ($true) {
        $Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        try {
            $conn = New-Object System.Data.SqlClient.SqlConnection($ConnStr)
            $conn.Open()
            
            # 1. Signal Waits (CPU 壓力指標)
            $cmd1 = $conn.CreateCommand()
            $cmd1.CommandText = "SELECT ISNULL(CAST(100.0 * SUM(signal_wait_time_ms) / NULLIF(SUM(wait_time_ms),0) AS DECIMAL(5,2)), 0) FROM sys.dm_os_wait_stats WHERE wait_time_ms > 0;"
            $SignalPct = $cmd1.ExecuteScalar()

            # 2. Memory 使用量
            $cmd2 = $conn.CreateCommand()
            $cmd2.CommandText = "SELECT physical_memory_in_use_kb FROM sys.dm_os_process_memory"
            $MemKB = $cmd2.ExecuteScalar()
            $MemMB = if ($MemKB) { [math]::Round($MemKB / 1024, 2) } else { 0 }

            # 3. 使用者連線數
            $cmd3 = $conn.CreateCommand()
            $cmd3.CommandText = "SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process = 1"
            $Conns = $cmd3.ExecuteScalar()

            $conn.Close()
            "$Time,$SignalPct,$MemMB,$Conns" | Out-File -FilePath $LogFile -Append -Encoding UTF8
        } catch {
            "$Time,0,0,0" | Out-File -FilePath $LogFile -Append -Encoding UTF8
        }
        Start-Sleep -Seconds $Interval
    }
} -ArgumentList $SqlServerInstance, $Database, $SqlInternalLog, $MonitorIntervalSec

Start-Sleep -Seconds 3
Write-Host ">>> 所有監控已啟動" -ForegroundColor Green

# --------------------【五、壓力查詢語句】--------------------
$SqlQuery = @"
DECLARE @Start DATETIME = GETDATE();
WHILE DATEDIFF(SECOND, @Start, GETDATE()) < $DurationSec
BEGIN
    -- 混合讀寫與 CPU 密集運算壓力
    SELECT TOP 1000 object_id, name, type_desc 
    FROM sys.all_objects 
    ORDER BY NEWID();
    
    SELECT TOP 500 * 
    FROM sys.dm_exec_query_stats 
    ORDER BY total_worker_time DESC;
    
    WAITFOR DELAY '00:00:00.010';
END
"@

# --------------------【六、啟動 SQL 併發壓力】--------------------
Write-Host ""
Write-Host ">>> [4] 啟動對 SQL Server ($SqlServerInstance) 的 $Concurrency 併發壓力測試..." -ForegroundColor Yellow

$SqlJobs = @()
for ($i = 1; $i -le $Concurrency; $i++) {
    $Job = Start-Job -Name "SqlStress_$i" -ScriptBlock {
        param($Server, $DB, $Query, $Duration, $JobNumber)
        $ConnString = "Server=$Server;Database=$DB;Integrated Security=True;TrustServerCertificate=True;Connect Timeout=15;"
        $Connection = New-Object System.Data.SqlClient.SqlConnection($ConnString)
        $Command = New-Object System.Data.SqlClient.SqlCommand($Query, $Connection)
        $Command.CommandTimeout = $Duration + 60
        try {
            $Connection.Open()
            $Start = Get-Date
            $null = $Command.ExecuteNonQuery()
            $Elapsed = [math]::Round(((Get-Date) - $Start).TotalSeconds, 2)
            [PSCustomObject]@{ Job=$JobNumber; Status="Success"; Duration=$Elapsed; Error="" }
        } catch {
            [PSCustomObject]@{ Job=$JobNumber; Status="Failed"; Duration=0; Error=$_.Exception.Message }
        } finally {
            if ($Connection.State -ne "Closed") { $Connection.Close() }
            $Connection.Dispose()
            $Command.Dispose()
        }
    } -ArgumentList $SqlServerInstance, $Database, $SqlQuery, $DurationSec, $i
    $SqlJobs += $Job
}

# --------------------【七、可選 ERP CPU 模擬負載 (192.168.1.156)】--------------------
$ErpLoadJob = $null
if ($EnableErpCpuLoad) {
    Write-Host ">>> [5] 同時啟動 ERP Server ($ErpServer) CPU 模擬負載..." -ForegroundColor Yellow
    $ErpLoadJob = Start-Job -Name "ErpCpuLoad" -ScriptBlock {
        param($Duration)
        $End = (Get-Date).AddSeconds($Duration)
        while ((Get-Date) -lt $End) {
            $x = 0
            1..50000 | ForEach-Object { $x += $_ }   # 簡單 CPU 密集運算
        }
    } -ArgumentList $DurationSec
}

# --------------------【八、等待測試結束並收集結果】--------------------
Write-Host ""
Write-Host ">>> 測試進行中,請稍候 $DurationSec 秒..." -ForegroundColor Cyan
Wait-Job -Job $SqlJobs -Timeout ($DurationSec + 90) | Out-Null

$Results = $SqlJobs | Receive-Job
$SuccessCount = ($Results | Where-Object Status -eq "Success").Count
$FailCount    = ($Results | Where-Object Status -eq "Failed").Count
$AvgDuration  = if ($SuccessCount -gt 0) { [math]::Round(($Results | Where-Object Status -eq "Success" | Measure-Object Duration -Average).Average, 2) } else { 0 }
$MaxDuration  = if ($SuccessCount -gt 0) { ($Results | Where-Object Status -eq "Success" | Measure-Object Duration -Maximum).Maximum } else { 0 }

# --------------------【九、清理背景工作】--------------------
Write-Host ">>> 清理背景工作..." -ForegroundColor Yellow
Get-Job | Stop-Job -ErrorAction SilentlyContinue
Get-Job | Remove-Job -Force -ErrorAction SilentlyContinue

# --------------------【十、產生統計報告】--------------------
$Summary = @"
====================================================
SQL + ERP 雙機壓力測試報告
測試時間:$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
====================================================
ERP Server      : $ErpServer (192.168.1.156)
SQL Server      : $SqlServerInstance (192.168.1.159)
Database        : $Database
Concurrency     : $Concurrency
Duration        : $DurationSec 秒
ERP CPU Load    : $EnableErpCpuLoad

【SQL 壓力結果】
成功連線數      : $SuccessCount
失敗連線數      : $FailCount
平均執行時間    : $AvgDuration 秒
最長執行時間    : $MaxDuration 秒

【檔案位置】
ERP Host 監控   : $ErpHostLog
SQL OS 監控     : $SqlOsLog
SQL 內部監控    : $SqlInternalLog
本報告          : $StatPath

建議後續動作:
1. 用 Excel 開啟三個 CSV,觀察 CPU / Memory / 連線數曲線
2. 比較分離前後的峰值差異
3. 若 SignalWaitPct 長期 > 20% 或連線數暴增,需檢查索引與查詢
====================================================
"@

$Summary | Out-File -FilePath $StatPath -Encoding UTF8
Write-Host $Summary -ForegroundColor Green
Write-Host ">>> 測試完成!請查看 $ResultFolder" -ForegroundColor Cyan

本報告針對 2026-09-30 15:38:28 執行的 SQL Server + ERP Server 雙機壓力測試數據(包含 Summary_20260930_153828.txt、ERP_Host_20260930_153828.csv、SQL_Internal_20260930_153828.csv 與 SQL_OS_20260930_153828.csv)進行完整交叉分析。

雙機壓力測試分析報告

1. 測試概要與總體結果

  • 測試時間:2026-09-30 15:38:29 ~ 15:39:37
  • ERP 伺服器:192.168.1.156(測試執行端 + CPU 負載模擬)
  • SQL 伺服器:192.168.1.159(目標資料庫:松測)
  • 壓力設定:併發數 $Concurrency = 20$,持續時間 $Duration = 60$ 秒

核心指標彙整表

項目 / 監控端成功 / 失敗數平均 / 最長執行時間CPU 使用率 (%)記憶體狀況備註說明
SQL 併發任務20 成功 / 0 失敗59.65s / 60.16s––併發任務 100% 順利完成
ERP 主機 (156)––平均 62.36% (最高 96%)可用 RAM 平均 1,419.38 MB成功模擬應用程式高算力負載
SQL 內部 (159)––Signal Wait 平均 4.13%使用 RAM 平均 326.74 MB連線數由 10 穩定升至 30 峰值
SQL OS (159)––數據為 0% (權限問題)數據為 0 MB (權限問題)WMI 遠端監控未開通或被阻擋

2.詳細分析與觀察

  1. SQL 壓力測試表現(優良):
    • 穩定性:20 個併發連線全數成功完成,無任何超時或連線中斷(失敗數 0)。
    • 連線變化:監控顯示使用者連線數(SQL_User_Connections)在測試啟動後 10 秒內快速由 10 提升至 30(含背景連線),並保持滿載狀態直到測試結束。
    • CPU 壓力指標:Signal Wait 保持在 4.13%(遠小於警戒值 20%),代表 SQL Server CPU 排隊延遲極低,算力十分充裕。
    • 記憶體佔用:SQL 進程記憶體穩定維持在 326 MB ~ 327 MB 之間,變化極小。
  2. ERP 主機負載表現(符合預期):
    • CPU 模擬負載:啟用 $EnableErpCpuLoad = $true 後,ERP 主機 CPU 使用率由初始 27% 衝高至 96% 峰值,測試全程平均維持在 62.36%,成功達到雙機同時加壓的模擬效果。
    • 記憶體配置:可用記憶體由 2,019 MB 降至 1,320 MB 附近,穩定供給壓測程序使用。

為何「分流負載小於單機」?(關鍵技術原因)

  1. 消除 CPU 搶佔(Context Switching 降低): 在單機架構下,若 ERP 主機 CPU 飆高至 96%,SQL Server 的 Worker Threads 會被迫等待 CPU 排程,導致 SignalWaitPct(CPU 等待時間比例)暴增至 20%~50% 以上;分流後,SQL 端的 Signal Wait 僅有 4.13%,代表 SQL Server 能以近乎 100% 的效率專心處理 SQL 查詢。
  2. 記憶體快取不失效: 單機架構下 ERP 處理大批次資料會搶奪系統 RAM,導致 OS 強制壓縮 SQL Server 的記憶體;分流後,SQL 端記憶體極度平穩(維持在 326~327 MB),確保查詢快取與索引鎖定機制運作順暢。

⚠️ 單機負載(Single Node)的致命風險

將所有業務壓力集中在單一台伺服器上,就像把所有的雞蛋放在同一個籃子裡,存在三大隱患:

  1. 運算瓶頸極低: 只要遇到促銷活動、月底結帳或突發流量,CPU 瞬間飽和,使用者只能眼睜睜看著網頁轉圈圈。
  2. 無彈性擴充空間: 單機升級(Vertical Scaling)有硬體極限且成本昂貴,無法隨時因應瞬間暴增的業務需求。
  3. 單點故障(Single Point of Failure, SPOF): 一旦這台機器因為硬體故障、藍當或記憶體耗盡而宕機,整個 ERP 系統將立刻陷入停擺,造成巨大的營運損失。

💡 多機分流的 3 大核心好處:

  1. 大幅降低單機 CPU 與記憶體壓力:分散請求後,每台伺服器都能維持在 30%~40% 的黃金健康區間,反應速度(Latency)更敏捷,使用者體驗大幅提升。
  2. 輕鬆應對突發流量(Horizontal Scaling):當未來業務量暴增時,我們只需要持續增加「伺服器節點」,就能像積木一樣無縫擴充系統容量,再也不怕流量尖峰。
  3. 實現 99.99% 高可用性(High Availability):負載平衡器具備健康檢查機制,若其中一台伺服器發生故障,流量會自動轉移至其他正常的機器,實現業務零中斷!

🎯 總結與架構優化建議

給企業 IT 與系統架構師的建議:

  • 短期優化: 針對高 CPU 消耗的查詢(如隨機排序、無索引的大表掃描)進行 SQL 語法與索引優化,降低單次查詢的算力開銷。
  • 長期規劃: 儘速導入 讀寫分離(Read-Only Replica) 或 Web/API 應用層負載平衡(Load Balancer) 架構,透過多機分流打造高韌性、高併發且不中斷的企業級系統環境!

(參考)伺服器負載平衡 (Load Balancer)」的示意圖

八、 結語:擺脫卡頓,迎向高效能企業 IT

透過本篇的詳細圖文與實戰演練,我們成功將原本擠在同一台機器上的 ERP 系統(192.168.1.156)與 SQL Server 資料庫(192.168.1.159)完美拆分。

我們不僅實現了 跨伺服器的 DSN ODBC 配置,更示範了如何透過 SSMS V18.11.1 遠端操控還原「松測」資料庫至指定的 SQL 目錄。

IT 系統架構的升級,往往不需要盲目追求高昂的新硬體,只要「架構放對位置,資源合理分配」,老舊的 ERP 系統也能煥發新生,告別卡頓、死機的噩夢!

你的公司 ERP 系統還在「單打獨鬥」嗎?快把這篇文章分享給你們的系統管理員,一起動手幫伺服器分家吧!

小編提醒:日常 SQL Server 備份可參考另一篇

享技水工坊 TechX Studio,資訊.音樂人同學會 – 如何設定 SQL SERVER 2008 R2 自動備份排程作業呢?

2 則留言

  1. ============================================================

    SQL Server + ERP Server 雙機同時正式壓力測試工具(免安裝模組純 ADO.NET 版)

    ============================================================

    功能:

    1. 監控 ERP Server (本機 192.168.1.156) CPU / RAM

    2. 遠端監控 SQL Server (192.168.1.159) OS CPU / RAM

    3. 監控 SQL Server 內部 CPU (Signal Waits) / Memory / 連線數 (純 ADO.NET)

    4. 對 SQL Server 建立高併發壓力連線 (純 ADO.NET)

    5. 可選 ERP 端 CPU 模擬負載(模擬應用邏輯)

    6. 完整 CSV + 統計報告

    ============================================================

    ——————–【一、測試參數】——————–

    $ErpServer = “192.168.1.156” # 本機(ERP Server / 腳本執行端)
    $SqlServerInstance = “192.168.1.159” # 遠端 SQL Server
    $Database = “松測” # 目標資料庫
    $IntegratedSecurity = $true
    $Concurrency = 20 # 併發連線數(建議先從 10~20 開始)
    $DurationSec = 60 # 測試時間(秒)
    $MonitorIntervalSec = 2 # 監控採樣間隔(秒)
    $EnableErpCpuLoad = $true # 是否同時對 ERP Server 加壓(模擬應用邏輯)
    $ResultFolder = “C:\SQL_PerfTest”

    ——————– 建立結果資料夾 ——————–

    if (-not (Test-Path $ResultFolder)) {
    New-Item -Path $ResultFolder -ItemType Directory -Force | Out-Null
    }
    $TimeStamp = Get-Date -Format “yyyyMMdd_HHmmss”
    $ErpHostLog = Join-Path $ResultFolder “ERP_Host_$TimeStamp.csv”
    $SqlOsLog = Join-Path $ResultFolder “SQL_OS_$TimeStamp.csv”
    $SqlInternalLog = Join-Path $ResultFolder “SQL_Internal_$TimeStamp.csv”
    $StatPath = Join-Path $ResultFolder “Summary_$TimeStamp.txt”

    Write-Host “”
    Write-Host “====================================================” -ForegroundColor Cyan
    Write-Host ” SQL + ERP 雙機同時壓力測試工具(免模組強化版)” -ForegroundColor Cyan
    Write-Host “====================================================” -ForegroundColor Cyan
    Write-Host “ERP Server : $ErpServer (本機監控 + 可選 CPU 負載)” -ForegroundColor White
    Write-Host “SQL Server : $SqlServerInstance (遠端監控 + 併發壓測)” -ForegroundColor White
    Write-Host “Database : $Database” -ForegroundColor White
    Write-Host “Concurrency : $Concurrency” -ForegroundColor White
    Write-Host “Duration : $DurationSec 秒” -ForegroundColor White
    Write-Host “ERP CPU Load : $EnableErpCpuLoad” -ForegroundColor White
    Write-Host “結果資料夾 : $ResultFolder” -ForegroundColor White
    Write-Host “”

    ——————–【二、ERP Host 監控 Job (192.168.1.156)】——————–

    Get-Job -Name “ErpHostMonitor” -ErrorAction SilentlyContinue | Remove-Job -Force
    Write-Host “>>> [1] 啟動 ERP Server ($ErpServer) CPU / RAM 監控…” -ForegroundColor Yellow

    $ErpMonitorJob = Start-Job -Name “ErpHostMonitor” -ScriptBlock {
    param($LogFile, $Interval)
    “Timestamp,CPU_Usage_Pct,Available_RAM_MB” | Out-File -FilePath $LogFile -Encoding UTF8
    while ($true) {
    $Time = Get-Date -Format “yyyy-MM-dd HH:mm:ss”
    try {
    $Cpu = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
    $RamMB = [math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1024, 2)
    “$Time,$([math]::Round($Cpu,2)),$RamMB” | Out-File -FilePath $LogFile -Append -Encoding UTF8
    } catch {
    “$Time,0,0” | Out-File -FilePath $LogFile -Append -Encoding UTF8
    }
    Start-Sleep -Seconds $Interval
    }
    } -ArgumentList $ErpHostLog, $MonitorIntervalSec

    ——————–【三、SQL Server OS 遠端監控 Job (192.168.1.159)】——————–

    Get-Job -Name “SqlOsMonitor” -ErrorAction SilentlyContinue | Remove-Job -Force
    Write-Host “>>> [2] 啟動 SQL Server OS ($SqlServerInstance) 遠端監控…” -ForegroundColor Yellow

    $SqlOsJob = Start-Job -Name “SqlOsMonitor” -ScriptBlock {
    param($SqlHost, $LogFile, $Interval)
    “Timestamp,SQL_OS_CPU_Pct,SQL_OS_Available_RAM_MB” | Out-File -FilePath $LogFile -Encoding UTF8
    while ($true) {
    $Time = Get-Date -Format “yyyy-MM-dd HH:mm:ss”
    try {
    $Cpu = (Get-CimInstance -ComputerName $SqlHost Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
    $RamMB = [math]::Round((Get-CimInstance -ComputerName $SqlHost Win32_OperatingSystem).FreePhysicalMemory / 1024, 2)
    “$Time,$([math]::Round($Cpu,2)),$RamMB” | Out-File -FilePath $LogFile -Append -Encoding UTF8
    } catch {
    “$Time,0,0” | Out-File -FilePath $LogFile -Append -Encoding UTF8
    }
    Start-Sleep -Seconds $Interval
    }
    } -ArgumentList $SqlServerInstance, $SqlOsLog, $MonitorIntervalSec

    ——————–【四、SQL 內部指標監控 Job (純 ADO.NET)】——————–

    Get-Job -Name “SqlInternalMonitor” -ErrorAction SilentlyContinue | Remove-Job -Force
    Write-Host “>>> [3] 啟動 SQL Server 內部資源監控 ($SqlServerInstance)…” -ForegroundColor Yellow

    $SqlInternalJob = Start-Job -Name “SqlInternalMonitor” -ScriptBlock {
    param($Server, $DB, $LogFile, $Interval)
    “Timestamp,SQL_CPU_SignalWaits_Pct,SQL_Used_Memory_MB,SQL_User_Connections” | Out-File -FilePath $LogFile -Encoding UTF8

    $ConnStr = "Server=$Server;Database=$DB;Integrated Security=True;TrustServerCertificate=True;Connect Timeout=15;"

    while ($true) {
    $Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    try {
    $conn = New-Object System.Data.SqlClient.SqlConnection($ConnStr)
    $conn.Open()

    # 1. Signal Waits (CPU 壓力指標)
    $cmd1 = $conn.CreateCommand()
    $cmd1.CommandText = "SELECT ISNULL(CAST(100.0 * SUM(signal_wait_time_ms) / NULLIF(SUM(wait_time_ms),0) AS DECIMAL(5,2)), 0) FROM sys.dm_os_wait_stats WHERE wait_time_ms > 0;"
    $SignalPct = $cmd1.ExecuteScalar()

    # 2. Memory 使用量
    $cmd2 = $conn.CreateCommand()
    $cmd2.CommandText = "SELECT physical_memory_in_use_kb FROM sys.dm_os_process_memory"
    $MemKB = $cmd2.ExecuteScalar()
    $MemMB = if ($MemKB) { [math]::Round($MemKB / 1024, 2) } else { 0 }

    # 3. 使用者連線數
    $cmd3 = $conn.CreateCommand()
    $cmd3.CommandText = "SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process = 1"
    $Conns = $cmd3.ExecuteScalar()

    $conn.Close()
    "$Time,$SignalPct,$MemMB,$Conns" | Out-File -FilePath $LogFile -Append -Encoding UTF8
    } catch {
    "$Time,0,0,0" | Out-File -FilePath $LogFile -Append -Encoding UTF8
    }
    Start-Sleep -Seconds $Interval
    }

    } -ArgumentList $SqlServerInstance, $Database, $SqlInternalLog, $MonitorIntervalSec

    Start-Sleep -Seconds 3
    Write-Host “>>> 所有監控已啟動” -ForegroundColor Green

    ——————–【五、壓力查詢語句】——————–

    $SqlQuery = @”
    DECLARE @Start DATETIME = GETDATE();
    WHILE DATEDIFF(SECOND, @Start, GETDATE()) < $DurationSec
    BEGIN
    — 混合讀寫與 CPU 密集運算壓力
    SELECT TOP 1000 object_id, name, type_desc
    FROM sys.all_objects
    ORDER BY NEWID();

    SELECT TOP 500 *
    FROM sys.dm_exec_query_stats
    ORDER BY total_worker_time DESC;

    WAITFOR DELAY '00:00:00.010';

    END
    “@

    ——————–【六、啟動 SQL 併發壓力】——————–

    Write-Host “”
    Write-Host “>>> [4] 啟動對 SQL Server ($SqlServerInstance) 的 $Concurrency 併發壓力測試…” -ForegroundColor Yellow

    $SqlJobs = @()
    for ($i = 1; $i -le $Concurrency; $i++) {
    $Job = Start-Job -Name “SqlStress_$i” -ScriptBlock {
    param($Server, $DB, $Query, $Duration, $JobNumber)
    $ConnString = “Server=$Server;Database=$DB;Integrated Security=True;TrustServerCertificate=True;Connect Timeout=15;”
    $Connection = New-Object System.Data.SqlClient.SqlConnection($ConnString)
    $Command = New-Object System.Data.SqlClient.SqlCommand($Query, $Connection)
    $Command.CommandTimeout = $Duration + 60
    try {
    $Connection.Open()
    $Start = Get-Date
    $null = $Command.ExecuteNonQuery()
    $Elapsed = [math]::Round(((Get-Date) – $Start).TotalSeconds, 2)
    [PSCustomObject]@{ Job=$JobNumber; Status=”Success”; Duration=$Elapsed; Error=”” }
    } catch {
    [PSCustomObject]@{ Job=$JobNumber; Status=”Failed”; Duration=0; Error=$_.Exception.Message }
    } finally {
    if ($Connection.State -ne “Closed”) { $Connection.Close() }
    $Connection.Dispose()
    $Command.Dispose()
    }
    } -ArgumentList $SqlServerInstance, $Database, $SqlQuery, $DurationSec, $i
    $SqlJobs += $Job
    }

    ——————–【七、可選 ERP CPU 模擬負載 (192.168.1.156)】——————–

    $ErpLoadJob = $null
    if ($EnableErpCpuLoad) {
    Write-Host “>>> [5] 同時啟動 ERP Server ($ErpServer) CPU 模擬負載…” -ForegroundColor Yellow
    $ErpLoadJob = Start-Job -Name “ErpCpuLoad” -ScriptBlock {
    param($Duration)
    $End = (Get-Date).AddSeconds($Duration)
    while ((Get-Date) -lt $End) {
    $x = 0
    1..50000 | ForEach-Object { $x += $_ } # 簡單 CPU 密集運算
    }
    } -ArgumentList $DurationSec
    }

    ——————–【八、等待測試結束並收集結果】——————–

    Write-Host “”
    Write-Host “>>> 測試進行中,請稍候 $DurationSec 秒…” -ForegroundColor Cyan
    Wait-Job -Job $SqlJobs -Timeout ($DurationSec + 90) | Out-Null

    $Results = $SqlJobs | Receive-Job
    $SuccessCount = ($Results | Where-Object Status -eq “Success”).Count
    $FailCount = ($Results | Where-Object Status -eq “Failed”).Count
    $AvgDuration = if ($SuccessCount -gt 0) { [math]::Round(($Results | Where-Object Status -eq “Success” | Measure-Object Duration -Average).Average, 2) } else { 0 }
    $MaxDuration = if ($SuccessCount -gt 0) { ($Results | Where-Object Status -eq “Success” | Measure-Object Duration -Maximum).Maximum } else { 0 }

    ——————–【九、清理背景工作】——————–

    Write-Host “>>> 清理背景工作…” -ForegroundColor Yellow
    Get-Job | Stop-Job -ErrorAction SilentlyContinue
    Get-Job | Remove-Job -Force -ErrorAction SilentlyContinue

    ——————–【十、產生統計報告】——————–

    $Summary = @”

    SQL + ERP 雙機壓力測試報告

    測試時間:$(Get-Date -Format “yyyy-MM-dd HH:mm:ss”)

    ERP Server : $ErpServer (192.168.1.156)
    SQL Server : $SqlServerInstance (192.168.1.159)
    Database : $Database
    Concurrency : $Concurrency
    Duration : $DurationSec 秒
    ERP CPU Load : $EnableErpCpuLoad

    【SQL 壓力結果】
    成功連線數 : $SuccessCount
    失敗連線數 : $FailCount
    平均執行時間 : $AvgDuration 秒
    最長執行時間 : $MaxDuration 秒

    【檔案位置】
    ERP Host 監控 : $ErpHostLog
    SQL OS 監控 : $SqlOsLog
    SQL 內部監控 : $SqlInternalLog
    本報告 : $StatPath

    建議後續動作:
    1. 用 Excel 開啟三個 CSV,觀察 CPU / Memory / 連線數曲線
    2. 比較分離前後的峰值差異

    3. 若 SignalWaitPct 長期 > 20% 或連線數暴增,需檢查索引與查詢

    “@

    $Summary | Out-File -FilePath $StatPath -Encoding UTF8
    Write-Host $Summary -ForegroundColor Green
    Write-Host “>>> 測試完成!請查看 $ResultFolder” -ForegroundColor Cyan

交換心得,發表迴響