SQL Server는 JSON 및 XML 데이터에 대한 내장 지원을 제공합니다. 이 글에서는 Go 구조와 SQL Server 간에 JSON과 XML 데이터를 쿼리, 삽입, 변환하는 데 드라이버를 go-mssqldb 사용하는 방법을 설명합니다.
이 글의 예시들은 AdventureWorks2025 샘플 데이터베이스를 기준으로 합니다. 읽기 중심 예제에서는 Sales.vSalesPerson 및 Sales.SalesOrderHeader와 같은 기본 제공 객체를 조회합니다. 쓰기 지향 예제에서는 OPENJSON 및 XML .nodes()를 사용하여 HumanResources.Department에 행을 삽입합니다.
대부분의 스니펫은 ctxdb 이미 초기화된 가정을 합니다. 다음 설정 패턴을 사용할 수 있습니다:
import (
"context"
"database/sql"
"log"
"time"
_ "github.com/microsoft/go-mssqldb"
)
db, err := sql.Open("sqlserver", "sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&encrypt=true&TrustServerCertificate=true")
if err != nil {
log.Fatal(err)
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
JSON 데이터
FOR JSON을 사용하여 JSON으로 쿼리 결과 반환
쿼리 결과를 JSON 문자열로 반환하는 데 사용 FOR JSON PATH 하세요:
var jsonResult string
err := db.QueryRowContext(ctx,
"SELECT BusinessEntityID AS Id, FirstName + ' ' + LastName AS Name, CountryRegionName AS Department FROM Sales.vSalesPerson WHERE CountryRegionName = @dept FOR JSON PATH",
sql.Named("dept", "United States")).Scan(&jsonResult)
if err != nil {
log.Fatal(err)
}
fmt.Println(jsonResult)
비고
SQL Server는 더 큰 페이로드의 경우 여러 행에 걸쳐 출력을 반환 FOR JSON 할 수 있습니다. 더 큰 결과를 신뢰성 있게 지원해야 한다면, 반환된 모든 행을 읽고 연결하세요. ' 대형 JSON 결과 처리'를 참조하세요.
큰 JSON 결과를 처리하기
SQL Server가 여러 행으로 JSON 출력을 반환할 때, 결과를 언마샬링하거나 출력하기 전에 청크들을 연결하세요:
import "strings"
func queryJSON(ctx context.Context, db *sql.DB, query string, args ...any) (string, error) {
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return "", err
}
defer rows.Close()
var sb strings.Builder
for rows.Next() {
var chunk string
if err := rows.Scan(&chunk); err != nil {
return "", err
}
sb.WriteString(chunk)
}
if err := rows.Err(); err != nil {
return "", err
}
return sb.String(), nil
}
사용법:
jsonStr, err := queryJSON(ctx, db,
"SELECT BusinessEntityID AS Id, FirstName + ' ' + LastName AS Name, CountryRegionName AS Department FROM Sales.vSalesPerson FOR JSON PATH")
if err != nil {
log.Fatal(err)
}
언마샬 JSON 결과를 Go 구조체로 전환
FOR JSON와 encoding/json을 결합하여 SQL Server 결과를 Go 타입으로 직접 역직렬화하세요:
import "encoding/json"
type Employee struct {
ID int `json:"Id"`
Name string `json:"Name"`
Department string `json:"Department"`
}
func getEmployees(ctx context.Context, db *sql.DB, dept string) ([]Employee, error) {
var jsonStr string
err := db.QueryRowContext(ctx,
"SELECT BusinessEntityID AS Id, FirstName + ' ' + LastName AS Name, CountryRegionName AS Department FROM Sales.vSalesPerson WHERE CountryRegionName = @dept FOR JSON PATH",
sql.Named("dept", dept)).Scan(&jsonStr)
if err != nil {
return nil, err
}
var employees []Employee
if err := json.Unmarshal([]byte(jsonStr), &employees); err != nil {
return nil, err
}
return employees, nil
}
FOR JSON PATH를 사용한 중첩 JSON
열 별칭에 닷 표기법을 사용해 중첩된 JSON 구조를 생성합니다:
var jsonResult string
err := db.QueryRowContext(ctx, `
SELECT
soh.SalesOrderID AS [Id],
soh.OrderDate AS [OrderDate],
CONCAT(pp.FirstName, ' ', pp.LastName) AS [Customer.Name],
ea.EmailAddress AS [Customer.Email],
soh.TotalDue AS [Total]
FROM Sales.SalesOrderHeader AS soh
JOIN Sales.Customer AS c ON soh.CustomerID = c.CustomerID
LEFT JOIN Person.Person AS pp ON c.PersonID = pp.BusinessEntityID
LEFT JOIN Person.EmailAddress AS ea ON pp.BusinessEntityID = ea.BusinessEntityID
WHERE soh.SalesOrderID = @id
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER`,
sql.Named("id", 43659)).Scan(&jsonResult)
결과는 다음과 같습니다.
{"Id":43659,"OrderDate":"2022-05-30T00:00:00","Customer":{"Name":"James Hendergart","Email":"james9@adventure-works.com"},"Total":23153.2339}
SQL Server에서 OPENJSON으로 JSON을 분석하기
OPENJSON 서버 측에서 JSON 배열을 행으로 파쇄하는 방법:
jsonData := `[
{"Name": "Alice", "Department": "Engineering"},
{"Name": "Bob", "Department": "Marketing"}
]`
rows, err := db.QueryContext(ctx, `
SELECT Name, Department
FROM OPENJSON(@json)
WITH (
Name NVARCHAR(100) '$.Name',
Department NVARCHAR(100) '$.Department'
)`, sql.Named("json", jsonData))
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var name, dept string
if err := rows.Scan(&name, &dept); err != nil {
log.Fatal(err)
}
fmt.Printf("%s - %s\n", name, dept)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
JSON 데이터를 매개변수로 삽입하세요
Go 구조체를 JSON 문자열로 저장 프로시저에 보내거나 OPENJSON:
type OrderItem struct {
ProductID int `json:"productId"`
Quantity int `json:"quantity"`
Price float64 `json:"price"`
}
func sendJSONPayload(ctx context.Context, db *sql.DB, metadata map[string]any) error {
jsonBytes, err := json.Marshal(metadata)
if err != nil {
return err
}
// Pass JSON to a stored procedure for server-side processing.
_, err = db.ExecContext(ctx,
"EXECUTE dbo.ProcessDepartmentMetadata @payload",
sql.Named("payload", string(jsonBytes)))
return err
}
사용법:
metadata := map[string]any{
"source": "docs-sample",
"batchId": "b-20260709-01",
"departments": []map[string]any{
{"name": "Data Science", "groupName": "Research and Development"},
{"name": "Cloud Ops", "groupName": "Information Technology"},
},
}
if err := sendJSONPayload(ctx, db, metadata); err != nil {
log.Fatal(err)
}
OPENJSON을 사용한 JSON 데이터 일괄 삽입
JSON 배열에서 여러 행을 하나의 문장에 삽입합니다:
func bulkInsertFromJSON(ctx context.Context, db *sql.DB, jsonData string) (int64, error) {
result, err := db.ExecContext(ctx, `
INSERT INTO HumanResources.Department (Name, GroupName)
SELECT Name, GroupName
FROM OPENJSON(@json)
WITH (
Name NVARCHAR(50) '$.name',
GroupName NVARCHAR(50) '$.groupName'
)`, sql.Named("json", jsonData))
if err != nil {
return 0, err
}
return result.RowsAffected()
}
사용법:
jsonData := `[
{"name": "Data Science", "groupName": "Research and Development"},
{"name": "Cloud Ops", "groupName": "Information Technology"},
{"name": "Developer Relations", "groupName": "Sales and Marketing"}
]`
rowsInserted, err := bulkInsertFromJSON(ctx, db, jsonData)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Inserted %d rows\n", rowsInserted)
JSON_VALUE 그리고 JSON_QUERY
전체 JSON 문서를 전송하지 않고 JSON 열에서 값을 추출하기:
// Extract a scalar value.
var productName string
err := db.QueryRowContext(ctx, `
SELECT Name
FROM Production.Product
WHERE ProductID = @id`,
sql.Named("id", 1)).Scan(&productName)
if err != nil {
log.Fatal(err)
}
fmt.Println(productName)
// Extract a JSON array from a FOR JSON subquery.
var itemsJSON string
err = db.QueryRowContext(ctx, `
SELECT (
SELECT ProductID, Name
FROM Production.Product
WHERE ProductSubcategoryID = @subId
FOR JSON PATH
) AS Items`,
sql.Named("subId", 1)).Scan(&itemsJSON)
if err != nil {
log.Fatal(err)
}
fmt.Println(itemsJSON)
Tip
JSON_VALUE 스칼라 값(문자열, 숫자)을 반환합니다.
JSON_QUERY 객체나 배열을 반환합니다. 필요한 데이터 타입에 맞는 올바른 함수를 사용하세요.
JSON은 열과 인덱스를 계산했습니다
자주 쿼리되는 JSON 속성의 경우, 더 나은 성능을 위해 인덱스가 포함된 계산된 열을 생성하세요:
-- Demo table with JSON-only rows.
IF OBJECT_ID('dbo.ProductJsonDemo', 'U') IS NOT NULL
DROP TABLE dbo.ProductJsonDemo;
CREATE TABLE dbo.ProductJsonDemo (
ProductDescriptionID INT IDENTITY(1,1) PRIMARY KEY,
Metadata NVARCHAR(MAX) NOT NULL,
CONSTRAINT CK_ProductJsonDemo_Metadata_IsJson CHECK (ISJSON(Metadata) = 1),
-- Cast to a bounded length so the index key stays under SQL Server limits.
LocaleName AS CAST(JSON_VALUE(Metadata, '$.locale') AS NVARCHAR(10)) PERSISTED
);
INSERT INTO dbo.ProductJsonDemo (Metadata)
VALUES
(N'{"locale":"en","title":"Road helmet"}'),
(N'{"locale":"fr","title":"Casque de route"}');
CREATE INDEX IX_ProductJsonDemo_LocaleName ON dbo.ProductJsonDemo(LocaleName);
그 다음 Go에서 계산된 열을 직접 쿼리합니다:
rows, err := db.QueryContext(ctx,
"SELECT ProductDescriptionID, LocaleName FROM dbo.ProductJsonDemo WHERE LocaleName = @name",
sql.Named("name", "en"))
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var localeName string
if err := rows.Scan(&id, &localeName); err != nil {
log.Fatal(err)
}
fmt.Printf("%d %s\n", id, localeName)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
XML 데이터
FOR XML을 사용하여 쿼리 결과를 XML로 반환
XML로 쿼리 결과를 반환하는 데 사용 FOR XML PATH :
var xmlResult string
err := db.QueryRowContext(ctx, `
SELECT BusinessEntityID AS [@id], FirstName + ' ' + LastName AS Name, CountryRegionName AS Department
FROM Sales.vSalesPerson
WHERE CountryRegionName = @dept
FOR XML PATH('Employee'), ROOT('Employees')`,
sql.Named("dept", "United States")).Scan(&xmlResult)
if err != nil {
log.Fatal(err)
}
fmt.Println(xmlResult)
결과는 다음과 같습니다.
<Employees>
<Employee id="274"><Name>Stephen Jiang</Name><Department>United States</Department></Employee>
<Employee id="275"><Name>Michael Blythe</Name><Department>United States</Department></Employee>
<Employee id="276"><Name>Linda Mitchell</Name><Department>United States</Department></Employee>
<Employee id="277"><Name>Jillian Carson</Name><Department>United States</Department></Employee>
<Employee id="279"><Name>Tsvi Reiter</Name><Department>United States</Department></Employee>
<Employee id="280"><Name>Pamela Ansman-Wolfe</Name><Department>United States</Department></Employee>
<Employee id="281"><Name>Shu Ito</Name><Department>United States</Department></Employee>
<Employee id="283"><Name>David Campbell</Name><Department>United States</Department></Employee>
<Employee id="284"><Name>Tete Mensa-Annan</Name><Department>United States</Department></Employee>
<Employee id="285"><Name>Syed Abbas</Name><Department>United States</Department></Employee>
<Employee id="287"><Name>Amy Alberts</Name><Department>United States</Department></Employee>
</Employees>
큰 XML 결과 처리
JSON과 마찬가지로 큰 XML 결과는 행에 걸쳐 나뉩니다.
func queryXML(ctx context.Context, db *sql.DB, query string, args ...any) (string, error) {
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return "", err
}
defer rows.Close()
var sb strings.Builder
for rows.Next() {
var chunk string
if err := rows.Scan(&chunk); err != nil {
return "", err
}
sb.WriteString(chunk)
}
if err := rows.Err(); err != nil {
return "", err
}
return sb.String(), nil
}
Go에서 XML 결과를 파싱하세요
패키지를 encoding/xml 사용해 XML 결과를 언마샬링하세요.
import "encoding/xml"
type EmployeeList struct {
XMLName xml.Name `xml:"Employees"`
Employees []Employee `xml:"Employee"`
}
type Employee struct {
ID int `xml:"id,attr"`
Name string `xml:"Name"`
Department string `xml:"Department"`
}
func getEmployeesXML(ctx context.Context, db *sql.DB, dept string) (*EmployeeList, error) {
xmlStr, err := queryXML(ctx, db, `
SELECT BusinessEntityID AS [@id], FirstName + ' ' + LastName AS Name, CountryRegionName AS Department
FROM Sales.vSalesPerson
WHERE CountryRegionName = @dept
FOR XML PATH('Employee'), ROOT('Employees')`,
sql.Named("dept", dept))
if err != nil {
return nil, err
}
var result EmployeeList
if err := xml.Unmarshal([]byte(xmlStr), &result); err != nil {
return nil, err
}
return &result, nil
}
XML 매개변수를 SQL Server에 전달합니다
XML 문서를 저장 프로시저나 쿼리로 전송하세요:
xmlData := `<Employees>
<Employee><Name>Alice</Name><Department>Engineering</Department></Employee>
<Employee><Name>Bob</Name><Department>Marketing</Department></Employee>
</Employees>`
rows, err := db.QueryContext(ctx, `
DECLARE @xmlDoc XML = CAST(@xml AS XML);
SELECT
e.value('(Name)[1]', 'NVARCHAR(100)') AS Name,
e.value('(Department)[1]', 'NVARCHAR(100)') AS Department
FROM @xmlDoc.nodes('/Employees/Employee') AS t(e)`,
sql.Named("xml", xmlData))
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var name, dept string
if err := rows.Scan(&name, &dept); err != nil {
log.Fatal(err)
}
fmt.Printf("%s - %s\n", name, dept)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
비고
매개변수는 @xml 기본적으로 nvarchar(max) 로 전송됩니다. 이 메서드는 .nodes()xml 데이터 타입을 필요로 하므로, 매개변수를 명시적으로 CAST(@xml AS XML)캐스팅하여 .
XML에서 행 삽입
INSERT...SELECT 문과 함께 .nodes() 메서드를 사용하여 XML을 테이블 행으로 분해합니다.
func bulkInsertFromXML(ctx context.Context, db *sql.DB, xmlData string) (int64, error) {
result, err := db.ExecContext(ctx, `
DECLARE @xmlDoc XML = CAST(@xml AS XML);
INSERT INTO HumanResources.Department (Name, GroupName)
SELECT
e.value('(Name)[1]', 'NVARCHAR(50)'),
e.value('(GroupName)[1]', 'NVARCHAR(50)')
FROM @xmlDoc.nodes('/Departments/Department') AS t(e)`,
sql.Named("xml", xmlData))
if err != nil {
return 0, err
}
return result.RowsAffected()
}
JSON과 XML 중 하나를 선택하세요
| 고려 사항 | JSON | XML |
|---|---|---|
| Go 에코시스템 지원 | 표준 encoding/json. 매핑을 위한 struct 태그. |
표준 encoding/xml. 더 상세한 구조체 태그. |
| SQL Server 지원 |
OPENJSON, JSON_VALUE, , JSON_QUERY( FOR JSON SQL Server 2016 및 이후 버전) |
nodes(), , value(), query()( FOR XML 모든 버전) |
| Performance | 일반적으로 파싱 속도가 더 빠릅니다. 더 간결한 와이어 형식. | 스키마와 검증을 지원합니다. 더 장황하게 말이죠. |
| 스키마 유효성 검사 | SQL Server에는 내장된 스키마 검증이 없습니다. | 서버 측 검증을 위한 XML 스키마 컬렉션을 지원합니다. |
| 인덱싱 | JSON_VALUE + 인덱스를 가진 계산된 열. | XML 인덱스(기본 및 보조 인덱스). |
| 데이터 모델링 | 배열과 중첩된 객체들. 바둑 슬라이스와 맵에 자연스럽게 적합해요. | 속성과 네임스페이스를 가진 계층적 문서. |
Tip
신규 개발에는 보통 JSON이 더 나은 선택입니다. 파싱 오버헤드가 적고, 페이로드가 작으며, 자연스럽게 Go 구조체에 매핑됩니다. 스키마 검증이 필요하거나 XML이 필요한 시스템과 통합할 때 XML을 사용하세요.