Usa columnas y conjuntos de columnas dispersos con mssql-python

Las columnas dispersas son una optimización de almacenamiento SQL de Microsoft para valores NULL en tablas con muchas columnas anulables. Las aplicaciones cliente ven las columnas dispersas como columnas regulares. El controlador mssql-python los lee y escribe como cualquier otra columna sin un manejo especial.

Feature Descripción
Columnas dispersas Los valores NULL utilizan almacenamiento cero
Conjuntos de columnas Representación XML de todas las columnas dispersas
Tablas anchas Soporte para hasta 30.000 columnas

Note

Las columnas dispersas son una característica del servidor. El controlador mssql-python no requiere ninguna configuración especial ni API para funcionar con columnas dispersas. La única diferencia visible para el cliente: cuando usas conjuntos de columnas, devuelven una representación XML de valores de columna dispersos.

Más adecuado para:

  • Tablas con entre un 20 % y un 50 % o más de valores NULL.
  • Almacenamiento de documentos con atributos variables.
  • Patrones EAV (Entidad-Atributo-Valor).
  • Datos de sensores con muchas lecturas opcionales.

Crear columnas dispersas

Define columnas dispersas en tu esquema de tabla añadiendo el SPARSE NULL modificador a columnas que frecuentemente contendrán valores NULL.

Tabla básica de columnas dispersas

Crea una tabla con columnas dispersas para atributos opcionales.

CREATE TABLE ProductAttributes (
    ProductID INT PRIMARY KEY,
    ProductName NVARCHAR(100) NOT NULL,
    -- Sparse columns for optional attributes
    Color NVARCHAR(50) SPARSE NULL,
    Size NVARCHAR(20) SPARSE NULL,
    Weight DECIMAL(10,2) SPARSE NULL,
    Material NVARCHAR(100) SPARSE NULL,
    Warranty INT SPARSE NULL,
    Manufacturer NVARCHAR(100) SPARSE NULL
);

Con conjunto de columnas

Añadir un conjunto de columnas para proporcionar acceso XML a todas las columnas dispersas simultáneamente.

CREATE TABLE ProductAttributesWithSet (
    ProductID INT PRIMARY KEY,
    ProductName NVARCHAR(100) NOT NULL,
    -- Column set provides XML access to all sparse columns
    SparseAttributes XML COLUMN_SET FOR ALL_SPARSE_COLUMNS,
    -- Sparse columns
    Color NVARCHAR(50) SPARSE NULL,
    Size NVARCHAR(20) SPARSE NULL,
    Weight DECIMAL(10,2) SPARSE NULL,
    Material NVARCHAR(100) SPARSE NULL,
    Warranty INT SPARSE NULL,
    Manufacturer NVARCHAR(100) SPARSE NULL
);

Insertar datos de columna dispersos

Inserta datos en columnas dispersas por nombre, igual que lo harías con las columnas normales.

Insertar columnas individuales

Insertar productos con valores específicos rellenados en columnas dispersas.

import mssql_python

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

# Create the table with sparse columns
cursor.execute("DROP TABLE IF EXISTS ProductAttributes")
cursor.execute("""
    CREATE TABLE ProductAttributes (
        ProductID INT PRIMARY KEY,
        ProductName NVARCHAR(100) NOT NULL,
        Color NVARCHAR(50) SPARSE NULL,
        Size NVARCHAR(20) SPARSE NULL,
        Weight DECIMAL(10,2) SPARSE NULL,
        Material NVARCHAR(100) SPARSE NULL,
        Warranty INT SPARSE NULL,
        Manufacturer NVARCHAR(100) SPARSE NULL
    )
""")

# Insert with some sparse columns populated
cursor.execute("""
    INSERT INTO ProductAttributes (ProductID, ProductName, Color, Size)
    VALUES (%(id)s, %(name)s, %(color)s, %(size)s)
""", {"id": 1, "name": "T-Shirt", "color": "Blue", "size": "Large"})

# Insert with different sparse columns
cursor.execute("""
    INSERT INTO ProductAttributes (ProductID, ProductName, Weight, Material)
    VALUES (%(id)s, %(name)s, %(weight)s, %(material)s)
""", {"id": 2, "name": "Coffee Mug", "weight": 0.35, "material": "Ceramic"})

conn.commit()

Insertar mediante conjunto de columnas (XML)

Inserta varios valores de columna dispersos a la vez pasando XML al conjunto de columnas.

# Create the table with a column set
cursor.execute("DROP TABLE IF EXISTS ProductAttributesWithSet")
cursor.execute("""
    CREATE TABLE ProductAttributesWithSet (
        ProductID INT PRIMARY KEY,
        ProductName NVARCHAR(100) NOT NULL,
        SparseAttributes XML COLUMN_SET FOR ALL_SPARSE_COLUMNS,
        Color NVARCHAR(50) SPARSE NULL,
        Size NVARCHAR(20) SPARSE NULL,
        Weight DECIMAL(10,2) SPARSE NULL,
        Material NVARCHAR(100) SPARSE NULL,
        Warranty INT SPARSE NULL,
        Manufacturer NVARCHAR(100) SPARSE NULL
    )
""")

# Insert using column set XML
cursor.execute("""
    INSERT INTO ProductAttributesWithSet (ProductID, ProductName, SparseAttributes)
    VALUES (%(id)s, %(name)s, %(xml)s)
""", {
    "id": 3,
    "name": "Laptop Bag",
    "xml": "<Color>Black</Color><Size>Medium</Size><Material>Nylon</Material><Warranty>24</Warranty>"
})
conn.commit()

Inserción dinámica de atributos

Crea una función que acepte atributos dinámicos como diccionario y construya el XML automáticamente.

def insert_with_attributes(cursor, product_id: int, name: str, attributes: dict):
    """Insert product with dynamic sparse column attributes."""
    # Build XML for column set
    xml_parts = [f"<{key}>{value}</{key}>" for key, value in attributes.items()]
    attributes_xml = "".join(xml_parts)
    
    cursor.execute("""
        INSERT INTO ProductAttributesWithSet (ProductID, ProductName, SparseAttributes)
        VALUES (%(id)s, %(name)s, %(xml)s)
    """, {"id": product_id, "name": name, "xml": attributes_xml or None})

# Usage
insert_with_attributes(cursor, 4, "Headphones", {
    "Color": "Silver",
    "Warranty": 12,
    "Manufacturer": "AudioTech"
})
conn.commit()

Consulta columnas dispersas

Consulta las columnas dispersas por nombre, o recupera todos los valores dispersos a la vez a través del conjunto de columnas.

Consulta columnas individuales

Consulta columnas dispersas específicas usando la sintaxis estándar SELECT.

# Query specific sparse columns
cursor.execute("""
    SELECT ProductID, ProductName, Color, Size
    FROM ProductAttributes
    WHERE Color IS NOT NULL
""")

for row in cursor:
    print(f"{row.ProductName}: {row.Color}, {row.Size}")

Consulta mediante el conjunto de columnas

Recupera todos los valores dispersos de columnas como XML del conjunto de columnas.

# Get column set XML
cursor.execute("""
    SELECT ProductID, ProductName, SparseAttributes
    FROM ProductAttributesWithSet
    WHERE ProductID = %(id)s
""", {"id": 3})

row = cursor.fetchone()
print(f"Product: {row.ProductName}")
print(f"Attributes XML: {row.SparseAttributes}")

Analizar el XML del conjunto de columnas en Python

Analiza el XML del conjunto de columnas para convertirlo en un diccionario de Python y facilitar la manipulación.

from xml.etree import ElementTree as ET

def get_product_attributes(cursor, product_id: int) -> dict:
    """Get product with parsed sparse attributes."""
    cursor.execute("""
        SELECT ProductName, SparseAttributes
        FROM ProductAttributesWithSet
        WHERE ProductID = %(id)s
    """, {"id": product_id})
    
    row = cursor.fetchone()
    if row is None:
        return None
    
    result = {"ProductName": row.ProductName}
    
    # Parse XML column set
    if row.SparseAttributes:
        # Wrap in root element for parsing
        xml_str = f"<root>{row.SparseAttributes}</root>"
        root = ET.fromstring(xml_str)
        
        for elem in root:
            result[elem.tag] = elem.text
    
    return result

# Usage
product = get_product_attributes(cursor, 3)
print(product)
# {'ProductName': 'Laptop Bag', 'Color': 'Black', 'Size': 'Medium', 'Material': 'Nylon', 'Warranty': '24'}

Consulta con SELECT *

Cuando usas SELECT *, el conjunto de columnas devuelve como una sola columna XML en lugar de columnas dispersas individuales.

# SELECT * returns column set instead of individual sparse columns
cursor.execute("""
    SELECT * FROM ProductAttributesWithSet WHERE ProductID = %(id)s
""", {"id": 3})

row = cursor.fetchone()
# Returns: ProductID, ProductName, SparseAttributes (not individual columns)
print(f"Columns: {[col[0] for col in cursor.description]}")

Consulta las columnas individuales explícitamente

Para recuperar columnas dispersas individuales de una tabla con un conjunto de columnas, enumérelas explícitamente en la cláusula SELECT.

# To get individual sparse columns with column set table, list them explicitly
cursor.execute("""
    SELECT ProductID, ProductName, Color, Size, Weight, Material, Warranty, Manufacturer
    FROM ProductAttributesWithSet
    WHERE ProductID = %(id)s
""", {"id": 3})

# Now each sparse column is available as separate property
row = cursor.fetchone()
print(f"Color: {row.Color}, Material: {row.Material}")

Actualizar columnas dispersas

Actualizar las columnas dispersas individuales por nombre o reemplazar todos los valores dispersos de una vez mediante el conjunto de columnas XML.

Actualizar columnas individuales

Actualizar valores específicos de columnas dispersas usando la sintaxis estándar de SQL UPDATE .

cursor.execute("""
    UPDATE ProductAttributes
    SET Color = %(color)s, Weight = %(weight)s
    WHERE ProductID = %(id)s
""", {"id": 1, "color": "Red", "weight": 0.2})
conn.commit()

Actualización mediante conjunto de columnas

Sustituye todos los valores de columna dispersos actualizando directamente el conjunto de columnas en XML.

# Replace all sparse column values via column set
cursor.execute("""
    UPDATE ProductAttributesWithSet
    SET SparseAttributes = %(xml)s
    WHERE ProductID = %(id)s
""", {
    "id": 3,
    "xml": "<Color>Navy</Color><Size>Large</Size><Material>Leather</Material>"
})
conn.commit()
# Note: This clears any sparse columns not included in the XML

Actualización parcial mediante conjunto de columnas

Actualizar solo atributos dispersos específicos, manteniendo los valores de otros atributos que no se incluyen en la actualización.

# To update only specific attributes, merge with existing
def update_attributes(cursor, product_id: int, updates: dict):
    """Update specific sparse attributes while preserving others."""
    # Get current attributes
    cursor.execute("""
        SELECT Color, Size, Weight, Material, Warranty, Manufacturer
        FROM ProductAttributesWithSet
        WHERE ProductID = %(id)s
    """, {"id": product_id})
    
    row = cursor.fetchone()
    if row is None:
        raise ValueError(f"Product {product_id} not found")
    
    # Merge updates
    current = {
        "Color": row.Color,
        "Size": row.Size,
        "Weight": row.Weight,
        "Material": row.Material,
        "Warranty": row.Warranty,
        "Manufacturer": row.Manufacturer
    }
    
    for key, value in updates.items():
        current[key] = value
    
    # Build XML with non-null values
    xml_parts = []
    for key, value in current.items():
        if value is not None:
            xml_parts.append(f"<{key}>{value}</{key}>")
    
    cursor.execute("""
        UPDATE ProductAttributesWithSet
        SET SparseAttributes = %(xml)s
        WHERE ProductID = %(id)s
    """, {"id": product_id, "xml": "".join(xml_parts) or None})

# Usage
update_attributes(cursor, 3, {"Color": "Brown", "Warranty": 36})
conn.commit()

Patrones dinámicos de columnas

Construye consultas flexibles que busquen dinámicamente en columnas dispersas, usando listas de permisos para validar los nombres de las columnas y prevenir ataques de inyección SQL.

Consulta de datos al estilo EAV

Buscar productos por un nombre y un valor de atributo específicos mediante coincidencia de patrones de Entidad-Atributo-Valor (EAV).

def find_products_by_attribute(cursor, attribute_name: str, attribute_value: str) -> list:
    """Find products with specific attribute value."""
    # Validate column name against allowed sparse columns to prevent SQL injection
    allowed_columns = {"Color", "Size", "Weight", "Material", "Warranty", "Manufacturer"}
    if attribute_name not in allowed_columns:
        raise ValueError(f"Invalid attribute: {attribute_name}")
    
    cursor.execute(f"""
        SELECT ProductID, ProductName, {attribute_name}
        FROM ProductAttributesWithSet
        WHERE {attribute_name} = %(value)s
    """, {"value": attribute_value})
    
    return cursor.fetchall()

# Find all blue products
blue_products = find_products_by_attribute(cursor, "Color", "Blue")

Busca productos que tengan múltiples atributos opcionales a la vez.

def search_by_attributes(cursor, **attributes) -> list:
    """Search products by multiple optional attributes."""
    # Validate column names against allowed sparse columns to prevent SQL injection
    allowed_columns = {"Color", "Size", "Weight", "Material", "Warranty", "Manufacturer"}
    invalid = set(attributes.keys()) - allowed_columns
    if invalid:
        raise ValueError(f"Invalid attributes: {invalid}")
    
    conditions = ["1=1"]  # Always true base condition
    params = {}
    
    for i, (key, value) in enumerate(attributes.items()):
        if value is not None:
            conditions.append(f"{key} = %(attr_{i})s")
            params[f"attr_{i}"] = value
    
    query = f"""
        SELECT ProductID, ProductName, SparseAttributes
        FROM ProductAttributesWithSet
        WHERE {' AND '.join(conditions)}
    """
    
    cursor.execute(query, params)
    return cursor.fetchall()

# Search by multiple attributes
results = search_by_attributes(cursor, Color="Black", Material="Nylon")

Consideraciones sobre el rendimiento

Evalúa cuándo las columnas dispersas proporcionan beneficios de almacenamiento, evalúa los costes generales y utiliza la monitorización del rendimiento para optimizar el diseño de tu columna escasa.

Cuándo usar columnas dispersas

Buenos candidatos para columnas escasas incluyen:

  • Columnas con más del 60-70 % de valores NULL.
  • Tablas anchas con muchas columnas opcionales.
  • Cargas de trabajo donde la optimización del almacenamiento es prioritaria.

Evitar usar columnas dispersas cuando:

  • La mayoría de las filas tienen valores (cada valor no NULL añade 4 bytes de sobrecarga).
  • La columna se usa frecuentemente en las cláusulas WHERE.
  • La columna forma parte del índice agrupado.

Comprobar el ahorro de almacenamiento

Compara el tamaño de almacenamiento de versiones dispersas y no dispersas de la misma tabla.

-- Compare storage with and without sparse
EXEC sp_spaceused 'ProductAttributes';
EXEC sp_spaceused 'ProductAttributesWithoutSparse';

Consideraciones sobre el índice

Puedes indexar columnas dispersas. Los índices filtrados funcionan bien para datos dispersos porque se saltan filas NULL:

CREATE INDEX IX_Products_Color
ON ProductAttributes(Color)
WHERE Color IS NOT NULL;

Operaciones masivas

Optimiza las inserciones de varios productos con columnas dispersas mediante la creación en Python del XML del conjunto de columnas antes de pasar las filas a bulkcopy().

Inserción masiva con columnas dispersas

Utiliza copia masiva para insertar eficazmente múltiples productos con atributos dispersos.

def bulk_insert_with_attributes(conn, products: list[dict]):
    """Bulk insert products with sparse attributes."""
    rows = []
    for product in products:
        attrs = product.get("attributes", {})
        xml = "".join(f"<{k}>{v}</{k}>" for k, v in attrs.items()) or None
        rows.append((product["id"], product["name"], xml))
    
    cursor = conn.cursor()
    result = cursor.bulkcopy("ProductAttributesWithSet", rows)
    return result["rows_copied"]

# Usage
products = [
    {"id": 100, "name": "Widget A", "attributes": {"Color": "Red", "Size": "Small"}},
    {"id": 101, "name": "Widget B", "attributes": {"Weight": 1.5, "Material": "Steel"}},
    {"id": 102, "name": "Widget C", "attributes": {}},  # No sparse attributes
]
bulk_insert_with_attributes(conn, products)
conn.commit()

procedimientos recomendados

Sigue patrones de validación, utiliza conjuntos de columnas para mayor flexibilidad y monitoriza la escasez de columnas para asegurarte de que tu diseño de columna escasa cumpla con los objetivos de rendimiento y mantenimiento.

Validar los valores de una columna dispersa

Valida que todos los atributos dispersos coincidan con el conjunto permitido antes de insertar datos.

# Sparse columns have the same constraints as regular columns
# The SPARSE keyword only affects storage

def validate_and_insert(cursor, product_id: int, name: str, attributes: dict):
    """Insert with validation."""
    allowed_attributes = {"Color", "Size", "Weight", "Material", "Warranty", "Manufacturer"}
    
    invalid = set(attributes.keys()) - allowed_attributes
    if invalid:
        raise ValueError(f"Unknown attributes: {invalid}")
    
    xml_parts = [f"<{k}>{v}</{k}>" for k, v in attributes.items()]
    
    cursor.execute("""
        INSERT INTO ProductAttributesWithSet (ProductID, ProductName, SparseAttributes)
        VALUES (%(id)s, %(name)s, %(xml)s)
    """, {"id": product_id, "name": name, "xml": "".join(xml_parts) or None})

Utiliza conjuntos de columnas para mayor flexibilidad

Los conjuntos de columnas simplifican el trabajo con columnas dispersas:

  • Añade nuevas columnas dispersas sin cambios en el código.
  • Guardar atributos dinámicos.
  • Serializar y deserializar XML automáticamente.

Sin un conjunto de columnas, necesitas listas explícitas de columnas. Con un conjunto de columnas, la columna XML gestiona automáticamente los atributos dinámicos.

Supervisar los porcentajes de valores NULL

Analiza el porcentaje de NULL de una columna para determinar si es un buen candidato para la optimización de columnas dispersas.

def analyze_sparseness(cursor, table: str, column: str) -> float:
    """Check if column is a good sparse candidate."""
    # 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_]*$', column):
        raise ValueError(f"Invalid column name: {column}")

    cursor.execute(f"""
        SELECT 
            COUNT(*) AS TotalRows,
            SUM(CASE WHEN {column} IS NULL THEN 1 ELSE 0 END) AS NullRows
        FROM {table}
    """)
    
    row = cursor.fetchone()
    null_percentage = (row.NullRows / row.TotalRows * 100) if row.TotalRows > 0 else 0
    
    print(f"Column {column}: {null_percentage:.1f}% NULL")
    print(f"Recommendation: {'Good sparse candidate' if null_percentage > 60 else 'Keep as regular column'}")
    
    return null_percentage