SQL 2016以降のバージョンMicrosoft、Azure SQLはnvarchar列を操作する関数を通じてJSONサポートを提供しています。 mssql-pythonドライバーは通常のPython文字列としてJSONを送受信します。 次のようにすることができます。
- JSONを
nvarchar列の文字列として保存します。 -
JSON_VALUE、JSON_QUERY、OPENJSONを使ってパス式でJSONをクエリします。 -
FOR JSONを使用して、リレーショナルデータを JSON に変換します。 - JSONをリレーショナル形式に解析
OPENJSON。
Note
Microsoft SQLはJSONデータを専用のJSON列タイプではなく、nvarchar列に保存します。 mssql-pythonドライバーは通常の文字列としてJSONを送受信します。 Pythonの内蔵jsonモジュールを使ってクライアント側でシリアライズとデシリアライズを行います。
JSON データを保存
Python の dict を文字列json.dumps()にシリアライズしてから、nvarchar 列に挿入してください。
JSON文字列を挿入する
Python辞書をデータベーステーブルのJSONテキストとして保存します:
import json
import mssql_python
conn = mssql_python.connect(
"Server=<server>.database.windows.net;"
"Database=<database>;"
"Authentication=ActiveDirectoryDefault;"
"Encrypt=yes"
)
cursor = conn.cursor()
# Create table with JSON column
cursor.execute("""
CREATE TABLE #JsonProducts (
ProductID INT IDENTITY PRIMARY KEY,
Name NVARCHAR(100),
JsonData NVARCHAR(MAX)
)
""")
# Python dict to JSON string
product_data = {
"name": "Widget Pro",
"specs": {
"weight": 2.5,
"dimensions": {"width": 10, "height": 5, "depth": 3}
},
"tags": ["electronics", "gadgets", "bestseller"]
}
cursor.execute("""
INSERT INTO #JsonProducts (Name, JsonData)
VALUES (%(name)s, %(json)s)
""", {"name": "Widget Pro", "json": json.dumps(product_data)})
conn.commit()
挿入時にJSONを検証する
挿入前にJSON構文を検証するために ISJSON() 関数を使用してください:
data = {"name": "Widget", "specs": {"weight": 1.5}}
cursor.execute("""
CREATE TABLE #JsonValidate (
ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
)
""")
cursor.execute("""
INSERT INTO #JsonValidate (Name, JsonData)
SELECT %(name)s, %(json)s
WHERE ISJSON(%(json)s) = 1
""", {"name": "Widget", "json": json.dumps(data)})
if cursor.rowcount == 0:
raise ValueError("Invalid JSON data")
JSON のデータのクエリを実行する
MicrosoftのSQL JSONパス関数を使ってサーバー上の値を抽出し、その後クライアントに返してください。
スカラー値の抽出
JSON_VALUEを使って単一の値を抽出します:
# Create table with sample JSON data
cursor.execute("""
CREATE TABLE #JsonExtract (
ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
)
""")
cursor.execute("""
INSERT INTO #JsonExtract (Name, JsonData) VALUES (
'Widget Pro',
'{"name":"Widget Pro","specs":{"weight":2.5,"dimensions":{"width":10,"height":5,"depth":3}},"tags":["electronics","gadgets"]}'
)
""")
cursor.execute("""
SELECT
Name,
JSON_VALUE(JsonData, '$.specs.weight') AS Weight,
JSON_VALUE(JsonData, '$.specs.dimensions.width') AS Width
FROM #JsonExtract
WHERE JSON_VALUE(JsonData, '$.name') = %(name)s
""", {"name": "Widget Pro"})
row = cursor.fetchone()
print(f"Weight: {row.Weight}, Width: {row.Width}")
オブジェクトや配列の抽出
オブジェクトや配列には JSON_QUERY を使いましょう。
cursor.execute("""
CREATE TABLE #JsonQuery (
ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
)
""")
cursor.execute("""
INSERT INTO #JsonQuery (Name, JsonData) VALUES (
'Widget Pro',
'{"specs":{"weight":2.5,"color":"blue"},"tags":["electronics","gadgets"]}'
)
""")
cursor.execute("""
SELECT
Name,
JSON_QUERY(JsonData, '$.specs') AS Specs,
JSON_QUERY(JsonData, '$.tags') AS Tags
FROM #JsonQuery
""")
for row in cursor:
specs = json.loads(row.Specs) if row.Specs else {}
tags = json.loads(row.Tags) if row.Tags else []
print(f"{row.Name}: {specs}, Tags: {tags}")
JSON 配列を行に解析
OPENJSON を使用してJSON配列を行に展開する:
cursor.execute("""
CREATE TABLE #JsonArray (
ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
)
""")
cursor.execute("""
INSERT INTO #JsonArray (Name, JsonData) VALUES
('Widget Pro', '{"tags":["electronics","gadgets","bestseller"]}'),
('Gadget X', '{"tags":["tools","gadgets"]}')
""")
cursor.execute("""
SELECT p.Name, t.value AS Tag
FROM #JsonArray p
CROSS APPLY OPENJSON(p.JsonData, '$.tags') t
""")
for row in cursor:
print(f"Product: {row.Name}, Tag: {row.Tag}")
JSONオブジェクトを列に解析してください
JSON_VALUE()を使ってJSONオブジェクトから個々のフィールドを抽出し、その結果を適切なSQL型にキャストします。
cursor.execute("""
CREATE TABLE #JsonCols (
ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
)
""")
cursor.execute("""
INSERT INTO #JsonCols (Name, JsonData) VALUES (
'Widget Pro',
'{"name":"Widget Pro","specs":{"weight":2.5,"dimensions":{"width":10,"height":5}}}'
)
""")
cursor.execute("""
SELECT
p.ProductID,
j.name AS ProductName,
j.weight,
j.width,
j.height
FROM #JsonCols p
CROSS APPLY OPENJSON(p.JsonData)
WITH (
name NVARCHAR(100) '$.name',
weight DECIMAL(5,2) '$.specs.weight',
width INT '$.specs.dimensions.width',
height INT '$.specs.dimensions.height'
) j
""")
JSONデータの修正
JSON_MODIFYを使って、JSONドキュメント内の特定のパスを更新し、値全体を書き換えずに行えます。
JSON値を更新
JSON_MODIFYを使って単一のJSONプロパティを修正します:
cursor.execute("""
CREATE TABLE #JsonMod (
ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
)
""")
cursor.execute("""
INSERT INTO #JsonMod (Name, JsonData) VALUES (
'Widget Pro',
'{"specs":{"weight":2.5,"dimensions":{"width":10}},"tags":["electronics"]}'
)
""")
cursor.execute("""
UPDATE #JsonMod
SET JsonData = JSON_MODIFY(JsonData, '$.specs.weight', %(weight)s)
WHERE ProductID = %(id)s
""", {"weight": 3.0, "id": 1})
conn.commit()
JSONプロパティを追加
既存のJSONオブジェクトに新しいプロパティを挿入します:
cursor.execute("""
CREATE TABLE #JsonAdd (
ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
)
""")
cursor.execute("""
INSERT INTO #JsonAdd (Name, JsonData) VALUES (
'Widget Pro', '{"specs":{"weight":2.5}}'
)
""")
cursor.execute("""
UPDATE #JsonAdd
SET JsonData = JSON_MODIFY(JsonData, '$.specs.color', %(color)s)
WHERE ProductID = %(id)s
""", {"color": "blue", "id": 1})
JSONプロパティを削除する
JSONオブジェクトからプロパティを削除するには、 NULLに設定してください:
cursor.execute("""
CREATE TABLE #JsonRem (
ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
)
""")
cursor.execute("""
INSERT INTO #JsonRem (Name, JsonData) VALUES (
'Widget Pro', '{"specs":{"weight":2.5,"color":"blue"}}'
)
""")
cursor.execute("""
UPDATE #JsonRem
SET JsonData = JSON_MODIFY(JsonData, '$.specs.color', NULL)
WHERE ProductID = %(id)s
""", {"id": 1})
JSON配列への追加
appendのJSON_MODIFY指示を使ってJSON配列の末尾に新しい値を追加します:
cursor.execute("""
CREATE TABLE #JsonAppend (
ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
)
""")
cursor.execute("""
INSERT INTO #JsonAppend (Name, JsonData) VALUES (
'Widget Pro', '{"tags":["electronics","gadgets"]}'
)
""")
cursor.execute("""
UPDATE #JsonAppend
SET JsonData = JSON_MODIFY(
JsonData,
'append $.tags',
%(tag)s
)
WHERE ProductID = %(id)s
""", {"tag": "new-arrival", "id": 1})
リレーショナル データを JSON に変換する
FOR JSON節は、クエリ結果をサーバー側でJSON文字列に変換します。
JSON AUTO の場合
クエリ結果からJSONを生成する:
cursor.execute("""
SELECT TOP 5 o.SalesOrderID, p.LastName AS CustomerName, o.TotalDue
FROM Sales.SalesOrderHeader o
JOIN Sales.Customer c ON o.CustomerID = c.CustomerID
JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
FOR JSON AUTO
""")
# Result is a single string containing JSON
json_result = cursor.fetchval()
orders = json.loads(json_result)
print(json.dumps(orders, indent=2))
FOR JSON PATH
JSON構造をより細かくコントロールする:
cursor.execute("""
SELECT
o.SalesOrderID AS 'order.id',
o.OrderDate AS 'order.date',
p.LastName AS 'customer.name',
e.EmailAddress AS 'customer.email'
FROM Sales.SalesOrderHeader o
JOIN Sales.Customer c ON o.CustomerID = c.CustomerID
JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
JOIN Person.EmailAddress e ON p.BusinessEntityID = e.BusinessEntityID
WHERE o.SalesOrderID = %(id)s
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
""", {"id": 43659})
json_result = cursor.fetchval()
order = json.loads(json_result)
# Structure: {"order": {"id": 43659, "date": "..."}, "customer": {"name": "...", "email": "..."}}
入れ子の JSON
FOR JSONを使ったサブクエリを使って、JSON配列やオブジェクトをネストしたデータ構造をクエリし、階層的なJSON出力を構築します。
cursor.execute("""
SELECT TOP 3
c.CustomerID,
p.LastName AS CustomerName,
(SELECT TOP 3 o.SalesOrderID, o.TotalDue
FROM Sales.SalesOrderHeader o
WHERE o.CustomerID = c.CustomerID
FOR JSON PATH) AS Orders
FROM Sales.Customer c
JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
WHERE c.PersonID IS NOT NULL
FOR JSON PATH
""")
json_result = cursor.fetchval()
customers = json.loads(json_result)
# Each customer has nested Orders array
Python統合パターン
これらのパターンは、JSONでバックアップされたテーブル上でPython抽象化を構築する方法を示しています。
JSONを用いたリポジトリパターン
PythonオブジェクトをJSON列にシリアライズ・デシリアライズするデータアクセス層を実装し、データベースへの型安全なインターフェースを提供します。
from dataclasses import dataclass, asdict
from typing import Optional
import json
@dataclass
class ProductSpecs:
weight: float
color: str
dimensions: dict
@dataclass
class Product:
id: Optional[int]
name: str
specs: ProductSpecs
class ProductRepository:
def __init__(self, connection):
self.conn = connection
cursor = self.conn.cursor()
cursor.execute("""
IF OBJECT_ID('#JsonRepo') IS NULL
CREATE TABLE #JsonRepo (
ProductID INT IDENTITY PRIMARY KEY,
Name NVARCHAR(100),
JsonData NVARCHAR(MAX)
)
""")
self.conn.commit()
def save(self, product: Product) -> int:
cursor = self.conn.cursor()
specs_json = json.dumps(asdict(product.specs))
if product.id:
cursor.execute("""
UPDATE #JsonRepo SET Name = %(name)s, JsonData = %(json)s
WHERE ProductID = %(id)s
""", {"name": product.name, "json": specs_json, "id": product.id})
else:
cursor.execute("""
INSERT INTO #JsonRepo (Name, JsonData)
OUTPUT INSERTED.ProductID
VALUES (%(name)s, %(json)s)
""", {"name": product.name, "json": specs_json})
product.id = cursor.fetchval()
self.conn.commit()
return product.id
def get(self, product_id: int) -> Optional[Product]:
cursor = self.conn.cursor()
cursor.execute("""
SELECT ProductID, Name, JsonData FROM #JsonRepo WHERE ProductID = %(id)s
""", {"id": product_id})
row = cursor.fetchone()
if row is None:
return None
specs_data = json.loads(row.JsonData)
return Product(
id=row.ProductID,
name=row.Name,
specs=ProductSpecs(**specs_data)
)
データベースに接続し、リポジトリを作成し、それを使って商品を保存・取得します。
save()法はINSERTがidのときにNone分岐を取り、そうでなければUPDATE分岐を取ります。
conn = mssql_python.connect(connection_string)
repo = ProductRepository(conn)
# id is None, so save() inserts a new row and returns the generated ProductID.
product = Product(
id=None,
name="Widget Pro",
specs=ProductSpecs(weight=2.5, color="black", dimensions={"width": 10, "height": 5})
)
product_id = repo.save(product)
print(f"Saved product {product_id}")
# Read the product back into a typed Product object.
loaded = repo.get(product_id)
print(loaded)
conn.close()
リポジトリは、あなたが通す接続にスコープを持つローカルの一時テーブルとして #JsonRepo を作成します。なので、その接続を共有している save() と get() が同じ接続を共有しなければなりません。 接続が切断されるとテーブルはドロップされます。
大規模なJSON結果を効率的に処理する
JSONの結果が大きい場合は、複数の行に分けて取得します。
def fetch_json_in_parts(cursor, query: str, params: dict) -> list:
"""Handle JSON results that might span multiple rows."""
cursor.execute(query, params)
# FOR JSON might split large results across rows
json_parts = []
for row in cursor:
json_parts.append(row[0])
# Combine parts
json_string = "".join(json_parts)
return json.loads(json_string) if json_string else []
# Usage
data = fetch_json_in_parts(cursor, "SELECT TOP 100 * FROM Production.Product FOR JSON AUTO", {})
クエリ結果をPythonでJSONに変換する
リレーショナルクエリの結果をPythonでJSON形式に変換するには、各行を辞書に変換し、その後JSONにシリアライズします。
def query_to_json(cursor, query: str, params: dict = None) -> str:
"""Execute query and return results as JSON string."""
cursor.execute(query, params or {})
columns = [col[0] for col in cursor.description]
rows = []
for row in cursor:
rows.append(dict(zip(columns, row)))
return json.dumps(rows, default=str, indent=2)
# Usage
json_output = query_to_json(cursor, "SELECT TOP 5 ProductID, Name, ListPrice FROM Production.Product WHERE ProductSubcategoryID = %(cat)s", {"cat": 1})
print(json_output)
JSON データへのインデックスの追加
JSONパス式で裏付けられた計算された列を作成し、パスをインデックス可能にします。
インデックス付きの計算列
JSON値を抽出する計算カラムを定義し、頻繁にクエリされるJSONパスで効率的にフィルタリングするためにインデックスを適用します。 以下の例は、永続的なテーブルを作成し、JSON $.specs.weight パス上に永続化された計算カラムを追加し、その上にインデックスを作成します。
cursor.execute("""
IF OBJECT_ID('dbo.ProductCatalog', 'U') IS NOT NULL
DROP TABLE dbo.ProductCatalog
""")
cursor.execute("""
CREATE TABLE dbo.ProductCatalog (
ProductID INT IDENTITY PRIMARY KEY,
Name NVARCHAR(100),
JsonData NVARCHAR(MAX)
)
""")
# Insert sample rows with JSON data
rows = [
("Widget Pro", '{"specs":{"weight":2.5,"color":"blue"}}'),
("Gadget X", '{"specs":{"weight":0.8,"color":"red"}}'),
("Heavy Duty", '{"specs":{"weight":9.1,"color":"gray"}}'),
]
cursor.executemany(
"INSERT INTO dbo.ProductCatalog (Name, JsonData) VALUES (%(name)s, %(json)s)",
[{"name": n, "json": j} for n, j in rows]
)
conn.commit()
# Add a persisted computed column that extracts weight from JSON
cursor.execute("""
ALTER TABLE dbo.ProductCatalog
ADD ProductWeight AS CAST(JSON_VALUE(JsonData, '$.specs.weight') AS DECIMAL(5,2)) PERSISTED
""")
# Index the computed column for efficient range queries
cursor.execute("""
CREATE INDEX IX_ProductCatalog_Weight
ON dbo.ProductCatalog (ProductWeight)
""")
conn.commit()
インデックス付き計算列を用いたクエリ
計算された列で直接フィルタリングします。 クエリエンジンはすべてのJSONドキュメントをスキャン・解析する代わりにインデックスを使用します。
cursor.execute("""
SELECT Name, ProductWeight
FROM dbo.ProductCatalog
WHERE ProductWeight > %(min_weight)s
ORDER BY ProductWeight
""", {"min_weight": 1.0})
for row in cursor:
print(f"{row.Name}: {row.ProductWeight} kg")
# Cleanup
cursor.execute("DROP TABLE dbo.ProductCatalog")
conn.commit()
リレーショナルカラムとJSONストレージのどちらかを選びます
データが固定スキーマを持つ場合、参照整合性が必要な場合、JOINに参加する場合、またはWHERE節に頻繁に現れる場合にリレーショナルカラムを使用します。 データがスパースで、行間で変動する場合、または柔軟な構成やメタデータを表す場合はJSON列(nvarchar(max))を使用してください。
サーバーサイドとクライアントサイドのJSON処理の使い方
クライアントにすべての行を取得せずにJSONフィールド間でフィルタリング、インデックス、集計を行う必要があるときは、Microsoft SQLのJSON関数(JSON_VALUE、JSON_QUERY、OPENJSON)を活用してください。 この選択は、条件に合致する行の一部だけや、JSONパス上で計算された列インデックスを使いたい場合に適しています。
クライアントサイドPython処理(json.loads())を使って、ドキュメント全体を取得しアプリケーションロジックで処理します。 この方法は、データベース内のJSONフィールドをフィルターしなくてドキュメント全体が必要な場合にうまく機能します。
ドキュメントスタイルのワークフロー
アプリケーションがドキュメント全体を保存・取得する場合は、Python側のシリアライズを使い、JSON列を不透明なストレージとして扱いましょう。 完全なJSONブロブを取得して逆直列化することで、Pythonで文書を処理・クエリします:
import json
# Create the settings table
cursor.execute("""
CREATE TABLE #Settings (
UserID INT PRIMARY KEY,
ConfigJson NVARCHAR(MAX)
)
""")
# Store a configuration document
config = {
"theme": "dark",
"notifications": {"email": True, "sms": False},
"custom_fields": {"department": "Engineering", "cost_center": "CC-100"}
}
cursor.execute(
"INSERT INTO #Settings (UserID, ConfigJson) VALUES (%(uid)s, %(cfg)s)",
{"uid": 1, "cfg": json.dumps(config)}
)
# Retrieve and process in Python
cursor.execute("SELECT ConfigJson FROM #Settings WHERE UserID = %(uid)s", {"uid": 1})
row = cursor.fetchone()
config = json.loads(row.ConfigJson)
print(config["notifications"]["email"]) # True
サーバーサイドJSONクエリ
すべての行を取得しずにJSONフィールド間でフィルタリング、インデックス、集計が必要な場合は、Microsoft SQL JSON関数を使いましょう。 この方法は、すべての行をPythonに読み込んでメモリでフィルタリングするよりも効率的です。
-
JSON_VALUEスカラー値を抽出し、計算された列インデックスを裏付けることができます。 -
JSON_QUERYオブジェクトや配列を抽出します。 -
OPENJSONは、JOINや集約のために JSON を行形式に展開します。 -
JSON_MODIFYドキュメント全体を書き直さずに特定のパスを更新できます。
# Filter by a JSON field server-side
cursor.execute("""
SELECT UserID, ConfigJson
FROM #Settings
WHERE JSON_VALUE(ConfigJson, '$.custom_fields.department') = %(dept)s
""", {"dept": "Engineering"})
頻繁にクエリされるJSONパスについては、以下のインデックス付きの計算列を作成します。
ALTER TABLE Settings
ADD Department AS JSON_VALUE(ConfigJson, '$.custom_fields.department');
CREATE INDEX IX_Settings_Department ON Settings(Department);
ベスト プラクティス
これらのガイドラインを適用してJSON列を確実に使いましょう。
ストレージの前にJSONを検証してください
保存前にJSONやテーブル/カラム識別子を検証し、インジェクション攻撃を防ぎましょう。
def store_json_safely(cursor, table: str, json_column: str, data: dict):
"""Store JSON with validation."""
# Validate identifiers to prevent SQL injection
import re
if not re.match(r'^[A-Za-z_][A-Za-z0-9_.]*$', table):
raise ValueError(f"Invalid table name: {table}")
if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', json_column):
raise ValueError(f"Invalid column name: {json_column}")
json_str = json.dumps(data)
# Check if valid JSON in Microsoft SQL
cursor.execute("SELECT ISJSON(%(json)s)", {"json": json_str})
if cursor.fetchval() != 1:
raise ValueError("Invalid JSON")
cursor.execute(f"INSERT INTO {table} ({json_column}) VALUES (%(json)s)", {"json": json_str})
JSONを使いすぎないでください
ユーザー設定やカスタムフィールドなどの柔軟またはスパーなデータにはJSON列を使用します。 リレーショナルカラムは以下に使います:
- 頻繁に問い合わせられるデータ。
- 参照の整合性が必要なデータ。
-
WHERE節で使用される列。
None/NULLを適切に扱う
データのない列に対してNULL値を挿入することで、欠損またはオプションのJSONフィールドを処理します。
cursor.execute("""
CREATE TABLE #JsonOpt (
ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
)
""")
cursor.execute("""
INSERT INTO #JsonOpt (Name, JsonData) VALUES (
'Widget Pro', '{"required_field":"value"}'
)
""")
cursor.execute("""
SELECT
Name,
JSON_VALUE(JsonData, '$.optional_field') AS OptionalValue
FROM #JsonOpt
""")
for row in cursor:
# JSON_VALUE returns NULL if path doesn't exist
value = row.OptionalValue or "default"
print(f"{row.Name}: {value}")