本指南協助 Go 開發者從 PostgreSQL、MySQL 及其他資料庫驅動程式遷移到 go-mssqldb SQL Server 驅動程式。 它涵蓋了 SQL 語法差異、驅動程式特定的變更,以及常見操作的等效模式。
備註
本文範例與 AdventureWorks2025 範例資料庫對比。
為什麼要使用 go-mssqldb 遷移至 SQL Server
| Feature | SQL Server 搭配 go-mssqldb |
|---|---|
| 企業安全 | 始終加密、列級安全、動態資料遮蔽、透明資料加密(TDE)。 |
| Authentication | Microsoft Entra ID(前稱 Azure AD)、managed identity、Windows 驗證、Kerberos. |
| Azure integration | 原生 Azure SQL Database 支援,支援自動加密、無密碼連線及故障轉移處理。 |
| Performance | 欄式儲存索引、記憶體內部 OLTP、用於自動計畫修正的查詢存放區。 |
| 資料格式 | 內建 JSON 與 XML 支援,支援伺服器端索引與查詢。 |
| Tooling | SQL Server Management Studio (SSMS)、Azure Data Studio、Visual Studio Code SQL 擴充。 |
移民考量清單
在逐行移植查詢前,請先檢視以下項目:
| 考慮事項 | 要改變什麼 |
|---|---|
| 佔位符語法不同於許多 Go 驅動程式。 | 將 、 ?及類似的佔位符替換$1為 @name 或 @p1,然後在適當時傳遞sql.Named()參數。 |
不支援 LastInsertId()。 |
請改用 OUTPUT INSERTED.<column> 或 SELECT SCOPE_IDENTITY()。 |
Go string 的值預設對應到 Unicode 類型。 |
除非您選擇驅動程式特定的型別,例如用於非 Unicode 資料的 mssql.VarChar,否則應預期為 nvarchar 語意。 |
| 暫存資料表的作用範圍限於實體連線。 | 如果後續陳述式會依賴該資料表,請將建立並使用臨時資料表的邏輯保留在同一個連線或同一個交易中。 |
| Microsoft Entra 認證使用不同的驅動程式註冊。 | 匯入 github.com/microsoft/go-mssqldb/azuread、使用 sql.Open("azuresql", ...),並使用 encrypt=true&TrustServerCertificate=false 明確設定 Azure SQL TLS 選項。 |
欲深入了解這些領域,請參閱查詢與語句、限制、資料型別映射、Microsoft Entra ID 驗證及故障排除。
從 lib/pq(PostgreSQL)移轉
更改匯入名稱和驅動程式名稱
將匯入和驅動程式名稱替換lib/pq成:go-mssqldb
import _ "github.com/lib/pq"
db, err := sql.Open("postgres", connString)
// After (SQL Server with go-mssqldb):
import _ "github.com/microsoft/go-mssqldb"
db, err := sql.Open("sqlserver", connString)
更改連線字串格式
將 PostgreSQL 的鍵值格式轉換為 SQL Server URL:
// lib/pq connection string:
"host=<server> port=5432 user=<user> password=<password> dbname=AdventureWorks2025 sslmode=require"
// go-mssqldb connection string:
"sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=true"
變更參數佔位符
PostgreSQL 使用 $1, $2 位置佔位符。 SQL Server 使用具名參數,包含:@
// PostgreSQL (lib/pq):
db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1 AND status = $2", id, status)
// SQL Server (go-mssqldb):
db.QueryContext(ctx, "SELECT ProductID, Name FROM Production.Product WHERE ProductID = @id AND Color = @color",
sql.Named("id", id),
sql.Named("color", color))
SQL 語法變更
| 運算 | PostgreSQL | SQL Server |
|---|---|---|
| 自動遞增 |
SERIAL 或 GENERATED ALWAYS AS IDENTITY |
IDENTITY(1,1) |
| 請輸入身分證 | RETURNING id |
OUTPUT INSERTED.id 或 SELECT SCOPE_IDENTITY() |
| 布林型 | BOOLEAN |
BIT |
| 字串串連 | \|\| |
+ 或 CONCAT() |
| 目前時間戳記 |
NOW() 或 CURRENT_TIMESTAMP |
GETUTCDATE() 或 SYSDATETIMEOFFSET() |
| 限制行數 | LIMIT 10 OFFSET 20 |
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY |
| 字串類型 |
TEXT 或 VARCHAR |
NVARCHAR(MAX) 或 NVARCHAR(n) |
| JSON 擷取 | column->>'key' |
JSON_VALUE(column, '$.key') |
| Upsert | INSERT ... ON CONFLICT DO UPDATE |
MERGE 陳述 |
| 陣列型別 | INTEGER[] |
沒有原生陣列。 使用表值參數。 |
| 區分大小寫 | 預設區分大小寫 | 預設不區分大小寫(視排序而定)。 |
取得已插入項目的 ID
將 PostgreSQL 的RETURNING子句替換為 SQL Server OUTPUT 的子句:
var id int
err := db.QueryRowContext(ctx,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
name, email).Scan(&id)
// SQL Server (go-mssqldb) using OUTPUT:
var id int
err := db.QueryRowContext(ctx,
"INSERT INTO HumanResources.Department (Name, GroupName) OUTPUT INSERTED.DepartmentID VALUES (@name, @groupName)",
sql.Named("name", name),
sql.Named("groupName", groupName)).Scan(&id)
Pagination
以 OFFSET/FETCH NEXT 語法取代 LIMIT/OFFSET 語法。
// PostgreSQL:
"SELECT * FROM employees ORDER BY id LIMIT $1 OFFSET $2"
// SQL Server (requires ORDER BY):
"SELECT ProductID, Name, ListPrice FROM Production.Product ORDER BY ProductID OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY"
從 pgx(PostgreSQL)移轉
驅動程式 pgx 使用自己的連線池和 API,與 database/sql不同。 要遷移,請切換到標準 database/sql 介面。
從 pgx 池改成資料庫/SQL
用標準pgxpool介面取代 database/sql API。
// Before (pgx):
import "github.com/jackc/pgx/v5/pgxpool"
pool, err := pgxpool.New(ctx, "postgres://user:pass@<server>:5432/AdventureWorks2025")
defer pool.Close()
rows, err := pool.Query(ctx, "SELECT * FROM users WHERE id = $1", id)
// After (go-mssqldb with database/sql):
import (
"database/sql"
_ "github.com/microsoft/go-mssqldb"
)
db, err := sql.Open("sqlserver", "sqlserver://user:pass@<server>:1433?database=AdventureWorks2025")
defer db.Close()
rows, err := db.QueryContext(ctx,
"SELECT ProductID, Name FROM Production.Product WHERE ProductID = @id", sql.Named("id", id))
替換 PGX 專屬功能
| pgx 功能 | go-mssqldb 的對等項 |
|---|---|
pgx.CollectRows |
手動 rows.Next() 與 rows.Scan() 循環。 |
pgx.RowToStructByName |
手動將 rows.Scan 填入結構體欄位中。 |
批次查詢 (pgx.Batch) |
多個 ExecContext 呼叫或預存程序。 |
COPY FROM 用於大量插入 |
mssql.CopyIn 用於大量插入。 |
pgx.ConnConfig |
msdsn.Config 或以 URL 為基礎的連線字串。 |
pgxpool.Pool |
sql.DB 搭配 SetMaxOpenConns 和 SetMaxIdleConns。 |
從 go-sql-driver/mysql (MySQL) 移轉
把 MySQL 匯入和驅動程式名稱互換
將 MySQL 匯入和驅動程式名稱替換成 go-mssqldb。
// Before (MySQL):
import _ "github.com/go-sql-driver/mysql"
db, err := sql.Open("mysql", connString)
// After (SQL Server):
import _ "github.com/microsoft/go-mssqldb"
db, err := sql.Open("sqlserver", connString)
轉換 MySQL DSN 格式
將 MySQL DSN 格式轉換成 SQL Server URL。
// MySQL DSN:
"user:password@tcp(<server>:3306)/AdventureWorks2025?tls=true"
// go-mssqldb URL:
"sqlserver://user:password@<server>:1433?database=AdventureWorks2025&encrypt=true"
替換問號佔位符
MySQL 使用 ? 位置佔位符。
// MySQL:
db.QueryContext(ctx, "SELECT * FROM users WHERE id = ? AND status = ?", id, status)
// SQL Server:
db.QueryContext(ctx, "SELECT ProductID, Name FROM Production.Product WHERE ProductID = @id AND Color = @color",
sql.Named("id", id),
sql.Named("color", color))
比較 MySQL 與 SQL Server 語法
| 運算 | MySQL | SQL Server |
|---|---|---|
| 自動遞增 | AUTO_INCREMENT |
IDENTITY(1,1) |
| 請輸入身分證 | LAST_INSERT_ID() |
SCOPE_IDENTITY() 或 OUTPUT INSERTED.id |
| 限制行數 | LIMIT 10 OFFSET 20 |
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY |
| 目前時間戳記 | NOW() |
GETUTCDATE() |
| 若為零 | IFNULL(expr, default) |
ISNULL(expr, default) 或 COALESCE(expr, default) |
| 字串長度 | LENGTH(str) |
LEN(str) |
| 子字串 | SUBSTRING(str, start, len) |
SUBSTRING(str, start, len) (相同) |
| 日期格式 | DATE_FORMAT(d, '%Y-%m-%d') |
FORMAT(d, 'yyyy-MM-dd') 或 CONVERT(VARCHAR, d, 23) |
| Upsert | INSERT ... ON DUPLICATE KEY UPDATE |
MERGE 陳述 |
| 回溯引用 | `column` |
[column] |
替換 MySQL 專屬語法
| MySQL 語法 | SQL Server 等價 |
|---|---|
AUTO_INCREMENT |
IDENTITY(1,1) |
LIMIT n OFFSET m |
OFFSET m ROWS FETCH NEXT n ROWS ONLY (需要 ORDER BY) |
AUTO_INCREMENT 至 IDENTITY:
-- MySQL:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
);
-- SQL Server:
CREATE TABLE HumanResources.NewDepartment (
DepartmentID SMALLINT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(50) NOT NULL
);
偏移/取回上限:
-- MySQL:
SELECT * FROM users LIMIT 10 OFFSET 20;
-- SQL Server:
SELECT ProductID, Name, ListPrice
FROM Production.Product
ORDER BY ProductID
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
常見的移轉模式
取得最後插入的識別值
使用 OUTPUT 子句 或 SCOPE_IDENTITY() 來取得插入列的身份值:
// Pattern 1: OUTPUT clause (recommended, works with batch inserts).
var id int64
err := db.QueryRowContext(ctx,
"INSERT INTO HumanResources.Department (Name, GroupName) OUTPUT INSERTED.DepartmentID VALUES (@name, @groupName)",
sql.Named("name", "Engineering"),
sql.Named("groupName", "Research and Development")).Scan(&id)
// Pattern 2: SCOPE_IDENTITY (works with single-row inserts).
var id int64
err := db.QueryRowContext(ctx,
"INSERT INTO HumanResources.Department (Name, GroupName) VALUES (@name, @groupName); SELECT SCOPE_IDENTITY()",
sql.Named("name", "Engineering"),
sql.Named("groupName", "Research and Development")).Scan(&id)
小提示
優先使用 OUTPUT 子句,而非 SCOPE_IDENTITY()。 該 OUTPUT 子句適用於批次插入,且不依賴於語句的順序。
Upsert(插入或更新)
PostgreSQL ON CONFLICT 與 MySQL ON DUPLICATE KEY UPDATE 對應至 SQL ServerMERGE:
_, err := db.ExecContext(ctx, `
MERGE HumanResources.Department AS target
USING (SELECT @id AS DepartmentID, @name AS Name, @groupName AS GroupName) AS source
ON target.DepartmentID = source.DepartmentID
WHEN MATCHED THEN
UPDATE SET Name = source.Name, GroupName = source.GroupName, ModifiedDate = GETDATE()
WHEN NOT MATCHED THEN
INSERT (Name, GroupName, ModifiedDate) VALUES (source.Name, source.GroupName, GETDATE());`,
sql.Named("id", dept.ID),
sql.Named("name", dept.Name),
sql.Named("groupName", dept.GroupName))
批量插入
將 PostgreSQL COPY 或 MySQL LOAD DATA 替換為 mssql.CopyIn。
import mssql "github.com/microsoft/go-mssqldb"
stmt, err := db.Prepare(mssql.CopyIn("HumanResources.Department", mssql.BulkOptions{}, "Name", "GroupName", "ModifiedDate"))
if err != nil {
return err
}
for _, dept := range departments {
_, err = stmt.Exec(dept.Name, dept.GroupName, time.Now())
if err != nil {
return err
}
}
// Flush the buffer.
_, err = stmt.Exec()
if err != nil {
return err
}
stmt.Close()
處理 NULL 值
所有 Go 資料庫驅動程式對 NULL 值的處理方式都一樣,因為它們使用 database/sql 了型別。
var color sql.NullString
err := db.QueryRowContext(ctx,
"SELECT Color FROM Production.Product WHERE ProductID = @id",
sql.Named("id", 1)).Scan(&color)
if color.Valid {
fmt.Println(color.String)
} else {
fmt.Println("NULL")
}
Transactions
所有 Go 資料庫驅動程式都使用相同的 database/sql 交易 API。
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity - @qty WHERE ProductID = @pid AND LocationID = @fromLoc",
sql.Named("qty", qty),
sql.Named("pid", productID),
sql.Named("fromLoc", fromLocationID))
if err != nil {
return err
}
_, err = tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity + @qty WHERE ProductID = @pid AND LocationID = @toLoc",
sql.Named("qty", qty),
sql.Named("pid", productID),
sql.Named("toLoc", toLocationID))
if err != nil {
return err
}
return tx.Commit()
遷徙檢查清單
| Step | Action |
|---|---|
| 安裝驅動程式 | go get github.com/microsoft/go-mssqldb |
| 變更匯入項目 | 將舊的驅動導入替換成 _ "github.com/microsoft/go-mssqldb"。 |
| 更新連接字串 | 更改為 SQL Server URL 格式: sqlserver://user:pass@host?database=db. |
| 替換參數佔位符 | 使用sql.Named將$1/$2或?變更為@name。 |
| 更新 SQL 語法 | 變更LIMIT/OFFSET、RETURNING、NOW()SERIAL及其他資料庫專用的 SQL。 |
| 取代批次作業 | 將 COPY 或 LOAD DATA 變更為 mssql.CopyIn。 |
| 更新 schema DDL | 將 SERIAL/AUTO_INCREMENT 改為 IDENTITY,將 TEXT 改為 NVARCHAR,將 BOOLEAN 改為 BIT。 |
| 測試所有查詢 | 用 SQL Server 實例來測試你的測試套件,以捕捉語法差異。 |
| 設定加密 | 新增encrypt=true(或依賴 Azure 自動偵測)用於生產環境。 |
| 設定認證 | 為 Azure 設定 Microsoft Entra ID,或為本地部署設定 SQL Server 認證。 |