Usa colonne e set di colonne sparse con mssql-python

Le colonne sparse sono un'ottimizzazione dello storage Microsoft SQL per i valori NULL in tabelle con molte colonne nullabili. Le applicazioni client vedono le colonne sparse come colonne regolari. Il driver mssql-python li legge e scrive come qualsiasi altra colonna senza una gestione speciale.

Feature Descrizione
Colonne di tipo sparse I valori NULL utilizzano memoria zero
Set di colonne Rappresentazione XML di tutte le colonne sparse
Tabelle ampie Supporto per fino a 30.000 colonne

Note

Le colonne sparse sono una funzionalità del server. Il driver mssql-python non richiede alcuna configurazione o API speciale per funzionare con colonne sparse. L'unica differenza visibile al client: quando si usano i set di colonne, questi restituiscono una rappresentazione XML dei valori delle colonne sparse.

Più adatto per:

  • Tabelle con il 20-50% o più di valori NULL.
  • Archiviazione documentale con attributi variabili.
  • Modelli EAV (Entità-Attributo-Valore).
  • Dati del sensore con molte letture opzionali.

Crea colonne sparse

Definisci colonne sparse nello schema della tabella aggiungendo il SPARSE NULL modificatore alle colonne che spesso conterranno valori NULL.

Tabella di base a colonne sparse

Crea una tabella con colonne sparse per attributi opzionali.

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 set di colonne

Aggiungi un set di colonne per fornire accesso XML a tutte le colonne sparse simultaneamente.

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

Inserire dati di colonna sparsi

Inserisci in colonne sparse per nome, proprio come faresti con le colonne normali.

Inserire colonne individuali

Inserire prodotti con valori specifici popolati nelle colonne sparse.

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

Inserimento tramite set di colonne (XML)

Inserire più valori di colonne sparsi contemporaneamente passando XML al set di colonne.

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

Inserimento dinamico di attributi

Crea una funzione che accetti attributi dinamici come dizionario e costruisca automaticamente l'XML.

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

Interrogare colonne sparse

Interroga le colonne sparse per nome, oppure recupera tutti i valori sparsi in una volta attraverso l'insieme di colonne.

Interrogare singole colonne

Interroga colonne sparse specifiche usando la sintassi standard 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}")

Interrogazione tramite set di colonne

Recupera tutti i valori delle colonne sparse come XML dal set di colonne.

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

Analizzare l'XML del set di colonne in Python

Analizza l'XML dal set di colonne per convertirlo in un dizionario Python per una manipolazione più semplice.

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 *

Quando usi SELECT *, il set di colonne restituisce una singola colonna XML invece che come colonne singole e sparse.

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

Interroga esplicitamente le singole colonne

Per recuperare singole colonne sparse da una tabella con un set di colonne, elencatele esplicitamente nella clausola 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}")

Aggiorna le colonne sparse

Aggiornare le singole colonne sparse per nome o sostituire tutti i valori sparsi contemporaneamente tramite l'XML del set di colonne.

Aggiorna le singole colonne

Aggiorna i valori specifici delle colonne sparse usando la sintassi SQL UPDATE standard.

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

Aggiornamento tramite insieme di colonne

Sostituire tutti i valori delle colonne sparsi aggiornando direttamente l'XML del set di colonne.

# 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

Aggiornamento parziale mediante insieme di colonne

Aggiorna solo specifici attributi sparsi preservando i valori di altri attributi non inclusi nell'aggiornamento.

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

Modelli di colonna dinamici

Costruisci query flessibili che cerchino dinamicamente tra colonne sparse, usando allowlist per convalidare i nomi delle colonne e prevenire attacchi di iniezione SQL.

Consulta dati in stile EAV

Cerca prodotti per nome e valore di un attributo specifico utilizzando il pattern matching Entity-Attribute-Value (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")

Cerca prodotti che corrispondono a più attributi opzionali contemporaneamente.

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

Considerazioni sulle prestazioni

Valuta quando le colonne sparse offrono benefici di archiviazione, valuta i costi generali e utilizza il monitoraggio delle prestazioni per ottimizzare il design delle colonne sparsi.

Quando utilizzare colonne sparse

Buoni candidati per colonne scarse includono:

  • Colonne con più di 60-70% valori NULL.
  • Tabelle larghe con molte colonne opzionali.
  • Carichi di lavoro in cui l'ottimizzazione dello storage è una priorità.

Evita di usare colonne sparse quando:

  • La maggior parte delle righe ha valori (ogni valore non NULL aggiunge 4 byte di overhead).
  • La colonna è spesso utilizzata nelle clausole WHERE.
  • La colonna fa parte dell'indice raggruppato.

Controlla i risparmi di spazio

Confronta le dimensioni di archiviazione delle versioni sparse e non sparse della stessa tabella.

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

Considerazioni sugli indici

Puoi indicizzare colonne sparse. Gli indici filtrati funzionano bene per dati scarsi perché saltano le righe NULL:

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

Operazioni in blocco

Ottimizza gli inserimenti di più prodotti con colonne sparse costruendo un set di colonne XML in Python prima di passare le righe a bulkcopy().

Inserimento in blocco con colonne sparse

Usa copia in massa per inserire in modo efficiente più prodotti con attributi scarsi.

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

Procedure consigliate

Segui i modelli di validazione, usa set di colonne per maggiore flessibilità e monitora la scarsità delle colonne per assicurarti che il design delle colonne sparso rispetti gli obiettivi di prestazioni e manutenibilità.

Convalida i valori delle colonne sparse

Verifica che tutti gli attributi sparsi corrispondano all'insieme consentito prima di inserire i dati.

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

Usa i set di colonne per maggiore flessibilità

I set di colonne semplificano il lavoro con colonne scarse:

  • Aggiungi nuove colonne sparse senza modifiche al codice.
  • Memorizza gli attributi dinamici.
  • Consente di serializzare e deserializzare automaticamente XML.

Senza un set di colonne, servono liste di colonne esplicite. Con un set di colonne, la colonna XML gestisce automaticamente gli attributi dinamici.

Monitorare le percentuali di NULL

Analizza la percentuale di NULL per una colonna per determinare se è un buon candidato per l'ottimizzazione delle colonne sparse.

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