バイナリデータの作業

mssql-pythonドライバーは、Microsoft SQLのbinaryvarbinaryimageデータ型をサポートしています。

Microsoft SQLは以下の列タイプでバイナリデータを格納します:

タイプ 説明 最大サイズ
binary(n) 固定長のバイナリデータ。 8,000バイト
varbinary(n) 可変長バイナリデータ。 8,000バイト
varbinary(max) 大規模なバイナリデータ。 2 GB
image 従来の大容量バイナリ データ(非推奨)。 2 GB

mssql-pythonドライバはバイナリデータをPython bytesオブジェクトとして返します。

データベースにバイナリを保存するタイミングとファイルシステムに保存するタイミング:

  • ファイルが小さい(1MB未満)ならデータベースに保存し、他のデータとのトランザクションの整合性が重要だったり、データとファイルを一緒にバックアップする必要がある場合に保存します。
  • ファイルが大きい場合(1MBを超える)、CDN配信が必要な場合、またはデータベース往復なしでクライアントに直接ファイルを提供したい場合、ファイルシステム(またはAzure Blob Storage)に保存してください。
  • 中間的な選択肢として、Microsoft SQLのFILESTREAM機能がトランザクションの一貫性を持ってファイルシステムにデータを保存します。

バイナリデータの挿入

パラメータとしてPython bytesオブジェクトを渡します。ドライバーはそれらをvarbinary列にマッピングします。

バイトを直接挿入する

Python bytesオブジェクトを作成し、それをクエリパラメータとして渡します:

import mssql_python

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

# Create a table to hold binary documents
cursor.execute("""
    CREATE TABLE #Documents (
        ID INT IDENTITY PRIMARY KEY,
        Name NVARCHAR(255),
        Content VARBINARY(MAX),
        Size INT NULL,
        ContentHash VARBINARY(32) NULL
    )
""")

# Binary data as bytes
binary_data = b'\x00\x01\x02\x03\x04\x05'

cursor.execute(
    "INSERT INTO #Documents (Name, Content) VALUES (%(name)s, %(content)s)",
    {"name": "sample.bin", "content": binary_data}
)
conn.commit()

ファイルから挿入

ファイルをバイナリモードで読み取り、その内容を挿入します:

def insert_file(cursor, file_path: str, name: str):
    """Insert a file as binary data."""
    with open(file_path, "rb") as f:
        content = f.read()
    
    cursor.execute(
        "INSERT INTO #Documents (Name, Content, Size) VALUES (%(name)s, %(content)s, %(size)s)",
        {"name": name, "content": content, "size": len(content)}
    )

insert_file(cursor, "image.png", "profile_picture.png")
conn.commit()

画像ファイルを挿入

ファイル拡張子からMIMEタイプを検出することでメタデータ付きの画像ファイルを保存します:

# Create a table to hold images
cursor.execute("""
    CREATE TABLE #Images (
        ID INT IDENTITY PRIMARY KEY,
        FileName NVARCHAR(255),
        FileSize INT NULL,
        ContentType NVARCHAR(100) NULL,
        Width INT NULL,
        Height INT NULL,
        ImageData VARBINARY(MAX),
        Description NVARCHAR(MAX) NULL
    )
""")

def insert_image(cursor, image_path: str, description: str):
    """Insert an image into the database."""
    import os
    
    with open(image_path, "rb") as f:
        image_data = f.read()
    
    cursor.execute("""
        INSERT INTO #Images (FileName, FileSize, ContentType, ImageData, Description)
        VALUES (%(filename)s, %(filesize)s, %(content_type)s, %(data)s, %(desc)s)
    """, {
        "filename": os.path.basename(image_path),
        "filesize": len(image_data),
        "content_type": get_content_type(image_path),
        "data": image_data,
        "desc": description
    })

def get_content_type(path: str) -> str:
    """Determine MIME type from file extension."""
    ext = path.lower().split(".")[-1]
    types = {
        "png": "image/png",
        "jpg": "image/jpeg",
        "jpeg": "image/jpeg",
        "gif": "image/gif",
        "pdf": "application/pdf",
    }
    return types.get(ext, "application/octet-stream")

バイナリ データの取得

ドライバーはvarbinaryおよびbinary列の値をPython bytesオブジェクトとして返します。

バイナリ列を取得

バイナリ列をクエリし、返された bytes オブジェクトを検査します:

cursor.execute(
    "SELECT LargePhotoFileName, LargePhoto FROM Production.ProductPhoto WHERE ProductPhotoID = %(id)s",
    {"id": 70}
)
row = cursor.fetchone()

# LargePhoto is bytes
print(type(row.LargePhoto))  # <class 'bytes'>
print(len(row.LargePhoto))   # Number of bytes

ファイルに保存する

バイナリデータを取得してディスクに書き込む:

def save_photo(cursor, photo_id: int, output_path: str):
    """Retrieve binary data and save to file."""
    cursor.execute(
        "SELECT LargePhoto FROM Production.ProductPhoto WHERE ProductPhotoID = %(id)s",
        {"id": photo_id}
    )
    row = cursor.fetchone()
    
    if row and row.LargePhoto:
        with open(output_path, "wb") as f:
            f.write(row.LargePhoto)
        print(f"Saved {len(row.LargePhoto)} bytes to {output_path}")
    else:
        print("Photo not found or empty")

save_photo(cursor, 70, "output.gif")

バイナリ行をファイルにエクスポートする

複数のバイナリ行をローカルファイルにエクスポートする:

import os

def export_photos(cursor, output_dir: str):
    """Export product photos to a directory."""
    os.makedirs(output_dir, exist_ok=True)
    
    cursor.execute("""
        SELECT ProductPhotoID, LargePhotoFileName, LargePhoto
        FROM Production.ProductPhoto
        WHERE ProductPhotoID > 1
    """)
    
    count = 0
    for row in cursor:
        output_path = os.path.join(output_dir, f"{row.ProductPhotoID}_{row.LargePhotoFileName}")
        with open(output_path, "wb") as f:
            f.write(row.LargePhoto)
        count += 1
    
    print(f"Exported {count} photos to {output_dir}")

export_photos(cursor, "./photos")

NULLバイナリ値を挿入します

Noneパスして、バイナリカラムにSQL NULLを挿入します。 #Documents一時テーブルであるため、パラメータ型を最初に setinputsizes() で宣言し、ドライバがContentvarbinaryとしてバインドします。

cursor.setinputsizes([(mssql_python.SQL_WVARCHAR, 255, 0), (mssql_python.SQL_VARBINARY, 0, 0)])
cursor.execute(
    "INSERT INTO #Documents (Name, Content) VALUES (%(name)s, %(content)s)",
    {"name": "empty", "content": None}
)

Note

ドライバは通常、パラメータ型を SQLDescribeParamで推算しますが、 一時テーブルテーブル変数の型メタデータを解決することはできません。 setinputsizes()のない一時オブジェクトのbinary列またはvarbinary列にNoneを挿入すると、ProgrammingError: Implicit conversion from data type varchar to varbinary(max) is not allowedが発生します。 パラメータごとに順番に1エントリずつパスし、バイナリ列には mssql_python.SQL_VARBINARY のようなSQL型定数を使います。 通常の(恒久的)テーブルの場合、ドライバーが自動的に型を解決し、直接 None パスできます。

大きなバイナリデータ

数メガバイトを超えるファイルの場合は、ファイルを小さなチャンクで読み取るのではなく、単一の varbinary(max) 書き込みで全内容を挿入してください。

大きなファイルのストリーミング

大きなファイルの場合、チャンク単位で処理します:

def insert_large_file(conn, table: str, file_path: str, chunk_size: int = 8192):
    """Insert large file in chunks using updatetext-style approach."""
    # Validate table name to prevent SQL injection
    import re
    if not re.match(r'^#{0,2}[A-Za-z_][A-Za-z0-9_.]*$', table):
        raise ValueError(f"Invalid table name: {table}")

    cursor = conn.cursor()
    
    # Get file size
    import os
    file_size = os.path.getsize(file_path)
    
    # Insert initial row with empty binary
    cursor.execute(f"""
        INSERT INTO {table} (Name, Content, Size)
        VALUES (%(name)s, 0x, %(size)s);
        SELECT SCOPE_IDENTITY();
    """, {"name": os.path.basename(file_path), "size": file_size})
    
    row_id = cursor.fetchval()
    
    # For modern Microsoft SQL, better to use varbinary(max) and single insert
    # This example shows streaming approach
    with open(file_path, "rb") as f:
        content = f.read()
    
    cursor.execute(f"""
        UPDATE {table} SET Content = %(content)s WHERE ID = %(id)s
    """, {"content": content, "id": row_id})
    
    conn.commit()
    return row_id

バイナリデータにはバルクコピーを使います

bulkcopy()メソッドを使って、一度の操作で複数のバイナリファイルを効率的に挿入できます。

Important

bulkcopy() データは 別の接続 を通じてサーバーにロードされるため、宛先テーブルはすでに存在し、 他のセッションにコミットされ、見える必要があります。 同じスクリプトでオートコミットをオフにしてテーブルを作成した場合は、conn.commit()する前にbulkcopy()を呼び出してください。 ローカルの一時テーブル(#name)は、作成したセッションのプライベートなものであるためサポートされていません。 宛先が未コミットであるか到達不能な場合、bulkcopy()Failed to retrieve destination metadata タイムアウトで失敗します。

デフォルトでは、 bulkcopy() 行内の各値を順序順位でテーブルの列にマッピングするため、すべての列には順序通りの値が必要です。 テーブルに識別列がある場合や、一部の列だけを埋める場合は、ターゲット列に明示的に名前を付けるために column_mappings を渡します。 そうしないと値が間違った列に移動して bulkcopy() 失敗します。

def bulk_insert_files(conn, table: str, file_paths: list[str]):
    """Bulk insert multiple binary files."""
    import os

    data = []
    for path in file_paths:
        with open(path, "rb") as f:
            content = f.read()
        data.append((os.path.basename(path), content, len(content)))

    cursor = conn.cursor()
    result = cursor.bulkcopy(table, data, column_mappings=["Name", "Content", "Size"])
    conn.commit()
    return result["rows_copied"]

バイナリデータ操作

Microsoft SQL の HASHBYTES 関数を使用すると、完全な値を取得せずにバイナリ コンテンツを比較できます。

バイナリデータの比較

Microsoft SQL Server で内容のハッシュ値を計算して比較し、同一の内容を持つバイナリ行を見つける:

# Find rows with identical binary content by hash
cursor.execute("""
    SELECT LargePhotoFileName, HASHBYTES('SHA2_256', LargePhoto) AS PhotoHash
    FROM Production.ProductPhoto
    WHERE ProductPhotoID > 1
""")

hashes = {}
for row in cursor:
    hash_value = row.PhotoHash  # bytes
    if hash_value in hashes:
        print(f"Duplicate: {row.LargePhotoFileName} matches {hashes[hash_value]}")
    else:
        hashes[hash_value] = row.LargePhotoFileName

Pythonでのハッシュ計算

PythonでSHA-256ハッシュを計算し、バイナリデータと一緒に保存します:

import hashlib

def insert_with_hash(cursor, name: str, content: bytes):
    """Insert binary data with computed hash."""
    content_hash = hashlib.sha256(content).digest()
    
    cursor.execute("""
        INSERT INTO #Documents (Name, Content, ContentHash)
        VALUES (%(name)s, %(content)s, %(hash)s)
    """, {"name": name, "content": content, "hash": content_hash})

バイナリデータのエンコードおよびデコード

base64エンコード文字列へのバイナリデータの変換:

import base64

# Store base64-encoded string
def insert_base64(cursor, name: str, base64_data: str):
    """Insert base64-encoded data as binary."""
    binary_data = base64.b64decode(base64_data)
    cursor.execute(
        "INSERT INTO #Documents (Name, Content) VALUES (%(name)s, %(content)s)",
        {"name": name, "content": binary_data}
    )

# Retrieve as base64
def get_as_base64(cursor, doc_id: int) -> str:
    """Retrieve binary data as base64 string."""
    cursor.execute("SELECT Content FROM #Documents WHERE ID = %(id)s", {"id": doc_id})
    row = cursor.fetchone()
    return base64.b64encode(row.Content).decode("utf-8")

特定のバイナリフォーマットでの作業

これらの例は、一般的なファイル形式を保存する前に検証し挿入する方法を示しています。

PDF ドキュメント

PDFファイルの署名を挿入する前に必ず確認してください:

def insert_pdf(cursor, pdf_path: str, title: str):
    """Insert a PDF document."""
    with open(pdf_path, "rb") as f:
        pdf_data = f.read()
    
    # Verify it's a PDF (magic bytes)
    if not pdf_data.startswith(b'%PDF'):
        raise ValueError("Not a valid PDF file")
    
    cursor.execute(
        "INSERT INTO #Documents (Name, Content) VALUES (%(title)s, %(data)s)",
        {"title": title, "data": pdf_data}
    )

PIL/Pillowを使った画像

画像を保存する前にPillow(PIL)を使ってサイズを調整し、スペースを節約してください:

from PIL import Image
import io
import os

def insert_resized_image(cursor, image_path: str, max_size: tuple = (800, 600)):
    """Insert a resized image."""
    # Open and resize
    img = Image.open(image_path)
    img.thumbnail(max_size, Image.LANCZOS)
    
    # Convert to bytes
    buffer = io.BytesIO()
    img.save(buffer, format=img.format or "PNG")
    image_bytes = buffer.getvalue()
    
    cursor.execute("""
        INSERT INTO #Images (FileName, Width, Height, ImageData)
        VALUES (%(name)s, %(width)s, %(height)s, %(data)s)
    """, {
        "name": os.path.basename(image_path),
        "width": img.width,
        "height": img.height,
        "data": image_bytes
    })

def get_image_as_pil(cursor, image_id: int) -> Image.Image:
    """Retrieve image as PIL Image object."""
    cursor.execute("SELECT ImageData FROM #Images WHERE ID = %(id)s", {"id": image_id})
    row = cursor.fetchone()
    return Image.open(io.BytesIO(row.ImageData))

圧縮データ

挿入前にgzipでバイナリデータを圧縮してストレージを削減します:

import gzip

def insert_compressed(cursor, name: str, data: bytes):
    """Insert data with gzip compression."""
    compressed = gzip.compress(data)
    
    cursor.execute(
        "INSERT INTO #Documents (Name, Content, Size) VALUES (%(name)s, %(content)s, %(size)s)",
        {"name": name, "content": compressed, "size": len(data)}
    )

def get_decompressed(cursor, data_id: int) -> bytes:
    """Retrieve and decompress data."""
    cursor.execute(
        "SELECT Content FROM #Documents WHERE ID = %(id)s",
        {"id": data_id}
    )
    row = cursor.fetchone()
    return gzip.decompress(row.Content)

ベスト プラクティス

これらのガイドラインを適用して、バイナリデータの適切なカラムタイプと保存戦略を選択してください。

適切なカラムタイプを使いましょう

データの特性に基づいてカラムタイプを選択してください:

異なるバイナリ用途では、適切なMicrosoft SQLデータ型を選択してください:

-- For small fixed-size binary (for example, hashes, UUIDs)
binary(32)      -- SHA-256 hash

-- For variable-size binary up to 8KB (for example, thumbnails, small icons)
varbinary(8000)

-- For large binary data (for example, documents, images)
varbinary(max)  -- Up to 2GB

大きなファイルにはFILESTREAMを考えてみてください

1MBを超える大きなファイルについては、MicrosoftのSQL FILESTREAM機能を検討してください。これはファイルシステムにデータを保存します。 FILESTREAMは使用前に サーバー側の設定 が必要です:

# FILESTREAM-enabled databases store large binaries more efficiently
# Access is still through normal queries but storage is file-based
large_binary_data = b"\x25\x50\x44\x46" + b"\x00" * 100  # sample data

cursor.execute("""
    CREATE TABLE ##FileStreamDemo (Name NVARCHAR(100), Document VARBINARY(MAX))
""")
cursor.execute("""
    INSERT INTO ##FileStreamDemo (Name, Document)
    VALUES (%(name)s, %(content)s)
""", {"name": "large_doc.pdf", "content": large_binary_data})

cursor.execute("SELECT Name, DATALENGTH(Document) AS DocSize FROM ##FileStreamDemo")
row = cursor.fetchone()
print(f"{row.Name}: {row.DocSize} bytes")

バイナリデータの検証

バイナリデータを保存する前に、マジックバイト(ファイル署名)をチェックしてファイルタイプを検証します:

def insert_safe_image(cursor, name: str, data: bytes):
    """Insert image with validation."""
    # Check file signatures (magic bytes)
    signatures = {
        b'\x89PNG': 'image/png',
        b'\xff\xd8\xff': 'image/jpeg',
        b'GIF87a': 'image/gif',
        b'GIF89a': 'image/gif',
    }
    
    content_type = None
    for sig, mime in signatures.items():
        if data.startswith(sig):
            content_type = mime
            break
    
    if content_type is None:
        raise ValueError("Unknown or unsupported image format")
    
    cursor.execute("""
        INSERT INTO #Images (FileName, ContentType, ImageData)
        VALUES (%(name)s, %(type)s, %(data)s)
    """, {"name": name, "type": content_type, "data": data})