Werk met binaire data

De mssql-python-driver ondersteunt binary, varbinary, en image datatypes voor Microsoft SQL.

Microsoft SQL slaat binaire gegevens op in deze kolomtypen:

Type Beschrijving Maximale grootte
binary(n) Binaire data met vaste lengte. 8.000 bytes
varbinary(n) Binaire gegevens met variabele lengte. 8.000 bytes
varbinary(max) Grote binaire gegevens. 2 GB
image Grote verouderde binaire gegevens (verouderd). 2 GB

De mssql-python-driver geeft binaire gegevens terug als Python-objectenbytes.

Wanneer moet binair opgeslagen worden in de database versus in het bestandssysteem:

  • Sla in de database wanneer bestanden klein zijn (onder 1 MB), transactionele consistentie met andere data belangrijk is, of je data en bestanden samen moet back-uppen.
  • Sla op in het bestandssysteem (of Azure Blob Storage) wanneer bestanden groot zijn (meer dan 1 MB), je CDN-levering nodig hebt, of bestanden direct aan clients moet leveren zonder database heen en terug te gaan.
  • Als middenweg slaat de FILESTREAM-functie van Microsoft SQL gegevens op in het bestandssysteem met transactionele consistentie.

Binaire gegevens invoegen

Geef Python-objecten bytes als parameters door; de driver koppelt ze aan varbinaire kolommen.

Bytes rechtstreeks invoegen

Maak een Python-object bytes aan en geef het door als queryparameter:

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()

Invoegen uit bestand

Lees een bestand in binaire modus en voeg de inhoud ervan in:

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()

Afbeeldingsbestand invoegen

Sla afbeeldingsbestanden met metadata op door MIME-types te detecteren uit bestandsextensies:

# 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")

Binaire gegevens ophalen

De driver geeft varbinaire en binaire kolomwaarden terug als Python-objectenbytes.

Kolom met binaire gegevens ophalen

Zoek binaire kolommen op en inspecteer het geretourneerde bytes object:

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

Opslaan in bestand

Haal binaire gegevens op en schrijf die naar de schijf:

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")

Exporteer binaire rijen naar bestanden

Exporteer meerdere binaire rijen naar lokale bestanden:

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")

Voeg NULL-binaire waarden in

Geef None door om een SQL NULL-waarde in een binaire kolom in te voegen. Omdat #Documents een tijdelijke tabel is, declareer je de parametertypes door eerst te gebruiken setinputsizes() zodat de driver bindt Content als varbinary:

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}
)

Opmerking

De driver leidt normaal gesproken parametertypes af via SQLDescribeParam, maar kan geen typemetadata oplossen voor een tijdelijke tabel of tabelvariabele. Invoegen None in een binary of varbinary kolom van een tijdelijke object zonder setinputsizes() verhoogt ProgrammingError: Implicit conversion from data type varchar to varbinary(max) is not allowed. Geef één invoer per parameter in volgorde door en gebruik een SQL-typeconstante zoals mssql_python.SQL_VARBINARY voor de binaire kolom. Voor een gewone (permanente) tabel lost de driver het type automatisch op en kun je direct doorgeven None .

Grote binaire gegevens

Voor bestanden van een paar megabyte voeg je de volledige inhoud in één varbinaire (max) schrijf in plaats van het bestand in kleine stukjes te lezen.

Grote bestanden streamen

Voor grote bestanden, verwerk in chunken:

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

Gebruik bulk copy voor binaire data

Gebruik de bulkcopy() methode om efficiënt meerdere binaire bestanden in één bewerking in te voegen.

Important

bulkcopy() laadt data via een aparte verbinding naar de server, dus de bestemmingstabel moet al bestaan en gecommit en zichtbaar zijn voor andere sessies. Als je de tabel in hetzelfde script aanmaakt met autocommit uitgeschakeld, roep conn.commit() dan vóór bulkcopy(). Lokale tijdelijke tabellen (#name) worden niet ondersteund omdat ze privé zijn voor de sessie die ze heeft aangemaakt. Een niet-bevestigde of onbereikbare bestemming zorgt ervoor dat bulkcopy() mislukt door een Failed to retrieve destination metadata-time-out.

Standaard wijst bulkcopy() elke waarde in een rij op basis van de positionele volgorde toe aan een tabelkolom, dus elke kolom moet in de juiste volgorde een waarde hebben. Wanneer de tabel een identiteitskolom heeft of je slechts enkele kolommen vult, ga dan door column_mappings om de doelkolommen expliciet te benoemen. Anders verschuiven de waarden naar de verkeerde kolommen en bulkcopy() faalt.

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"]

Binaire databewerkingen

Gebruik de HASHBYTES functie van Microsoft SQL om binaire inhoud te vergelijken zonder volledige waarden op te halen.

Vergelijk binaire gegevens

Vind binaire rijen die identieke inhoud delen door contenthashes te berekenen en te vergelijken in 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

Hash berekenen in Python

Bereken SHA-256 hashes in Python en sla ze op naast binaire data:

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})

Codeer en decodeer binaire gegevens

Converteer binaire gegevens naar en van base64-gecodeerde strings:

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")

Werk met specifieke binaire formaten

Deze voorbeelden laten zien hoe je veelvoorkomende bestandsformaten kunt valideren en invoegen voordat je ze opslaat.

PDF-documenten

Controleer PDF-handtekeningen voordat je ze invoegt:

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}
    )

Afbeeldingen met PIL/Pillow

Verander afbeeldingen met Pillow (PIL) voordat je ze opslaat om ruimte te besparen:

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))

Gecomprimeerde data

Verminder opslag door binaire gegevens te comprimeren met gzip vóór invoeging:

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)

Beste praktijken

Pas deze richtlijnen toe om het juiste kolomtype en opslagstrategie voor binaire data te kiezen.

Gebruik geschikte kolomtypen

Kies het kolomtype op basis van de kenmerken van je data:

Voor verschillende binaire gebruikssituaties selecteer je het juiste Microsoft SQL-datatype:

-- 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

Overweeg FILESTREAM voor grote bestanden

Voor grote bestanden (meer dan 1 MB) kunt u de Microsoft SQL FILESTREAM-functie overwegen, die gegevens opslaat in het bestandssysteem. FILESTREAM vereist server-side configuratie vóór gebruik:

# 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")

Valideer binaire gegevens

Valideer bestandstypen door magic bytes (bestandshandtekeningen) te controleren voordat binaire gegevens worden opgeslagen:

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})