Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This guide helps Go developers migrate from PostgreSQL, MySQL, and other database drivers to go-mssqldb for SQL Server. It covers SQL syntax differences, driver-specific changes, and equivalent patterns for common operations.
Note
Examples in this article run against the AdventureWorks2025 sample database.
Why migrate to SQL Server with go-mssqldb
| Feature | SQL Server with go-mssqldb |
|---|---|
| Enterprise security | Always Encrypted, row-level security, dynamic data masking, transparent data encryption (TDE). |
| Authentication | Microsoft Entra ID (formerly Azure AD), managed identity, Windows authentication, Kerberos. |
| Azure integration | Native Azure SQL Database support with automatic encryption, passwordless connections, and failover handling. |
| Performance | Columnstore indexes, in-memory OLTP, query store for automatic plan correction. |
| Data formats | Built-in JSON and XML support with server-side indexing and querying. |
| Tooling | SQL Server Management Studio (SSMS), Azure Data Studio, Visual Studio Code SQL extensions. |
Migration considerations checklist
Review these items before you start porting queries line by line:
| Consideration | What to change |
|---|---|
| Placeholder syntax differs from many Go drivers. | Replace ?, $1, and similar placeholders with @name or @p1, then pass sql.Named() arguments where appropriate. |
LastInsertId() isn't supported. |
Use OUTPUT INSERTED.<column> or SELECT SCOPE_IDENTITY() instead. |
Go string values map to Unicode types by default. |
Expect nvarchar semantics unless you choose a driver-specific type such as mssql.VarChar for non-Unicode data. |
| Temporary tables are scoped to the physical connection. | Keep create-and-use temp table logic on the same connection or inside the same transaction if later statements depend on that table. |
| Microsoft Entra authentication uses a different driver registration. | Import github.com/microsoft/go-mssqldb/azuread, use sql.Open("azuresql", ...), and set Azure SQL TLS options explicitly with encrypt=true&TrustServerCertificate=false. |
For deeper guidance on these areas, see Queries and statements, Limitations, Data type mappings, Microsoft Entra ID authentication, and Troubleshooting.
Migrate from lib/pq (PostgreSQL)
Change the import and driver name
Replace the lib/pq import and driver name with 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)
Change the connection string format
Convert the PostgreSQL key-value format to a 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"
Change parameter placeholders
PostgreSQL uses $1, $2 positional placeholders. SQL Server uses named parameters with @:
// 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 syntax changes
| Operation | PostgreSQL | SQL Server |
|---|---|---|
| Auto-increment | SERIAL or GENERATED ALWAYS AS IDENTITY |
IDENTITY(1,1) |
| Get inserted ID | RETURNING id |
OUTPUT INSERTED.id or SELECT SCOPE_IDENTITY() |
| Boolean type | BOOLEAN |
BIT |
| String concatenation | \|\| |
+ or CONCAT() |
| Current timestamp | NOW() or CURRENT_TIMESTAMP |
GETUTCDATE() or SYSDATETIMEOFFSET() |
| Limit rows | LIMIT 10 OFFSET 20 |
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY |
| String type | TEXT or VARCHAR |
NVARCHAR(MAX) or NVARCHAR(n) |
| JSON extraction | column->>'key' |
JSON_VALUE(column, '$.key') |
| Upsert | INSERT ... ON CONFLICT DO UPDATE |
MERGE statement |
| Array type | INTEGER[] |
No native arrays. Use table-valued parameters. |
| Case sensitivity | Case-sensitive by default | Case-insensitive by default (depends on collation). |
Get the inserted ID
Replace PostgreSQL's RETURNING clause with SQL Server's OUTPUT clause:
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
Replace LIMIT/OFFSET syntax with OFFSET/FETCH NEXT syntax.
// 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"
Migrate from pgx (PostgreSQL)
The pgx driver uses its own connection pool and API that differ from database/sql. To migrate, switch to the standard database/sql interface.
Change from pgx pool to database/sql
Replace the pgxpool API with the standard database/sql interface.
// 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))
Replace pgx-specific features
| pgx feature | go-mssqldb equivalent |
|---|---|
pgx.CollectRows |
Manual rows.Next() and rows.Scan() loop. |
pgx.RowToStructByName |
Manual rows.Scan into struct fields. |
Batch queries (pgx.Batch) |
Multiple ExecContext calls or stored procedures. |
COPY FROM for bulk insert |
mssql.CopyIn for bulk insert. |
pgx.ConnConfig |
msdsn.Config or URL-based connection strings. |
pgxpool.Pool |
sql.DB with SetMaxOpenConns and SetMaxIdleConns. |
Migrate from go-sql-driver/mysql (MySQL)
Swap the MySQL import and driver name
Replace the MySQL import and driver name with 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)
Convert the MySQL DSN format
Convert the MySQL DSN format to a 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"
Replace question mark placeholders
MySQL uses ? positional placeholders.
// 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))
Compare MySQL and SQL Server syntax
| Operation | MySQL | SQL Server |
|---|---|---|
| Auto-increment | AUTO_INCREMENT |
IDENTITY(1,1) |
| Get inserted ID | LAST_INSERT_ID() |
SCOPE_IDENTITY() or OUTPUT INSERTED.id |
| Limit rows | LIMIT 10 OFFSET 20 |
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY |
| Current timestamp | NOW() |
GETUTCDATE() |
| If null | IFNULL(expr, default) |
ISNULL(expr, default) or COALESCE(expr, default) |
| String length | LENGTH(str) |
LEN(str) |
| Substring | SUBSTRING(str, start, len) |
SUBSTRING(str, start, len) (same) |
| Date format | DATE_FORMAT(d, '%Y-%m-%d') |
FORMAT(d, 'yyyy-MM-dd') or CONVERT(VARCHAR, d, 23) |
| Upsert | INSERT ... ON DUPLICATE KEY UPDATE |
MERGE statement |
| Backtick quoting | `column` |
[column] |
Replace MySQL-specific syntax
| MySQL syntax | SQL Server equivalent |
|---|---|
AUTO_INCREMENT |
IDENTITY(1,1) |
LIMIT n OFFSET m |
OFFSET m ROWS FETCH NEXT n ROWS ONLY (requires ORDER BY) |
AUTO_INCREMENT to 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
);
LIMIT to OFFSET/FETCH:
-- 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;
Common migration patterns
Get the last inserted identity
Use the OUTPUT clause or SCOPE_IDENTITY() to retrieve the inserted row's identity value:
// 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)
Tip
Prefer the OUTPUT clause over SCOPE_IDENTITY(). The OUTPUT clause works with batch inserts and doesn't depend on statement ordering.
Upsert (insert or update)
PostgreSQL's ON CONFLICT and MySQL's ON DUPLICATE KEY UPDATE map to SQL Server's MERGE:
_, 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))
Bulk insert
Replace PostgreSQL's COPY or MySQL's LOAD DATA with 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()
Handle NULL values
All Go database drivers handle NULL values the same way because they use the database/sql types.
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
All Go database drivers use the same database/sql transaction 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()
Migration checklist
| Step | Action |
|---|---|
| Install the driver | go get github.com/microsoft/go-mssqldb |
| Change imports | Replace the old driver import with _ "github.com/microsoft/go-mssqldb". |
| Update connection strings | Change to SQL Server URL format: sqlserver://user:pass@host?database=db. |
| Replace parameter placeholders | Change $1/$2 or ? to @name with sql.Named. |
| Update SQL syntax | Change LIMIT/OFFSET, RETURNING, NOW(), SERIAL, and other database-specific SQL. |
| Replace bulk operations | Change COPY or LOAD DATA to mssql.CopyIn. |
| Update schema DDL | Change SERIAL/AUTO_INCREMENT to IDENTITY, TEXT to NVARCHAR, BOOLEAN to BIT. |
| Test all queries | Run your test suite against a SQL Server instance to catch syntax differences. |
| Configure encryption | Add encrypt=true (or rely on Azure auto-detection) for production. |
| Set up authentication | Configure Microsoft Entra ID for Azure, or SQL Server authentication for on-premises. |