Trabalho com dados binários

O driver mssql-python suporta binary, varbinary, e image tipos de dados para Microsoft SQL.

O Microsoft SQL armazena dados binários nestes tipos de coluna:

Tipo Descrição Tamanho máximo
binary(n) Dados binários de comprimento fixo. 8.000 bytes
varbinary(n) Dados binários de comprimento variável. 8.000 bytes
varbinary(max) Grandes dados binários. 2 GB
image Dados binários grandes herdados (descontinuados). 2 GB

O driver mssql-python retorna dados binários como objetos Pythonbytes.

Quando armazenar o binário no banco de dados versus o sistema de arquivos:

  • Armazene no banco de dados quando os arquivos são pequenos (menos de 1 MB), a consistência transacional com outros dados é importante, ou você precisa fazer backup de dados e arquivos juntos.
  • Armazene no sistema de arquivos (ou Armazenamento de Blobs do Azure) quando os arquivos são grandes (mais de 1 MB), você precisa entregar CDN, ou precisa servir arquivos diretamente para clientes sem idas e voltas ao banco de dados.
  • Como solução intermediária, o recurso FILESTREAM do SQL Server da Microsoft armazena dados no sistema de arquivos com consistência transacional.

Inserir dados binários

Passe objetos Python bytes como parâmetros; o driver os mapeia para colunas varbinary.

Insira bytes diretamente

Crie um objeto Python bytes e passe como parâmetro de consulta:

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

Inserir a partir do arquivo

Leia um arquivo em modo binário e insira seu conteúdo:

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

Inserir arquivo de imagem

Armazene arquivos de imagem com metadados detectando tipos MIME a partir de extensões de arquivo:

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

Recuperar dados binários

O driver retorna valores de colunas varbinary e binary como objetos Python bytes.

Buscar coluna binária

Consulte colunas binárias e inspecione o objeto retornado 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

Salvar em arquivo

Recupere dados binários e escreva-os no disco:

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

Exportar linhas binárias para arquivos

Exporte múltiplas linhas binárias para arquivos locais:

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

Insira valores binários NULL

Passe None para inserir um SQL NULL em uma coluna binária. Como #Documents é uma tabela temporária, declare primeiro os tipos dos parâmetros usando setinputsizes() para que o driver associe Content como 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}
)

Note

O driver normalmente infere os tipos de parâmetros através de SQLDescribeParam, mas não consegue resolver metadados de tipo para uma tabela ou variável temporária. Inserir None em uma coluna binary ou varbinary de um objeto temporário sem setinputsizes() gera ProgrammingError: Implicit conversion from data type varchar to varbinary(max) is not allowed. Passe uma entrada por parâmetro, em ordem, e use uma constante do tipo SQL, como mssql_python.SQL_VARBINARY para a coluna binária. Para uma tabela regular (permanente), o driver resolve o tipo automaticamente e você pode passar None diretamente.

Grandes dados binários

Para arquivos com mais de alguns megabytes de tamanho, insira todo o conteúdo em uma única gravação varbinary(max), em vez de ler o arquivo em pequenos blocos.

Transmitir arquivos grandes

Para arquivos grandes, processe em blocos:

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

Use cópia em massa para dados binários

Use o bulkcopy() método para inserir eficientemente múltiplos arquivos binários em uma única operação.

Importante

bulkcopy() carrega os dados por meio de uma conexão separada com o servidor, então a tabela de destino já deve existir e estar confirmada e visível para outras sessões. Se você criar a tabela no mesmo script com o autocommit desativado, chame conn.commit() antes de bulkcopy(). Tabelas temporárias locais (#name) não são suportadas porque são privadas da sessão que as criou. Um destino não confirmado ou inacessível faz com que bulkcopy() falhe devido a um timeout de Failed to retrieve destination metadata.

Por padrão, bulkcopy() mapeia cada valor em uma linha para uma coluna de tabela pela posição ordinal, então cada coluna deve ter um valor em ordem. Quando a tabela tem uma coluna identidade ou você preenche apenas algumas colunas, passe column_mappings para nomear explicitamente as colunas de destino. Caso contrário, os valores são deslocados para as colunas erradas e bulkcopy() falha.

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

Operações de dados binários

Use a função do HASHBYTES Microsoft SQL para comparar conteúdo binário sem buscar valores completos.

Comparar dados binários

Encontre linhas binárias que compartilham conteúdo idêntico calculando e comparando hashes de conteúdo no 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

Computar hash em Python

Calcule hashes SHA-256 em Python e armazene-os junto com dados binários:

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

Codificar e decodificar dados binários

Converter dados binários para e de cadeias codificadas em 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")

Trabalho com formatos binários específicos

Esses exemplos mostram como validar e inserir formatos de arquivo comuns antes de armazená-los.

Documentos PDF

Verifique as assinaturas dos arquivos PDF antes de inseri-las:

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

Imagens com PIL/Pillow

Redimensione as imagens usando o Pillow (PIL) antes de armazená-las para economizar espaço:

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

Dados comprimidos

Reduza o armazenamento comprimindo dados binários com gzip antes da inserção:

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)

Práticas recomendadas

Aplique essas diretrizes para escolher o tipo de coluna correto e a estratégia de armazenamento para dados binários.

Use tipos de colunas apropriados

Escolha o tipo de coluna com base nas características dos seus dados:

Para diferentes casos de uso binários, selecione o tipo de dado Microsoft SQL apropriado:

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

Considere o FILESTREAM para arquivos grandes

Para arquivos grandes (mais de 1 MB), considere o recurso FILESTREAM do Microsoft SQL, que armazena dados no sistema de arquivos. O FILESTREAM requer configuração do lado do servidor antes do uso:

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

Validar dados binários

Valide os tipos de arquivo verificando bytes mágicos (assinaturas de arquivo) antes de armazenar dados binários:

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