Handle NULL-Werte

SQL NULL repräsentiert fehlende oder unbekannte Daten. Der mssql-python-Treiber ordnet SQL NULL Python None zu. Die Unterscheidung ist wichtig, weil NULL nichts bedeutet, auch nicht sich selbst. In SQL wird NULL = NULL zu NULL (unbekannt) ausgewertet, nicht zu wahr. Verwenden Sie daher IS NULL in Abfragen und is None in Python.

NULL-Werte empfangen

NULL in den Abrufergebnissen

Der Treiber liefert NULL-Werte aus SQL Server als Python Nonezurück:

import mssql_python

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

cursor.execute(
    "SELECT TOP 1 FirstName, MiddleName, LastName "
    "FROM Person.Person WHERE MiddleName IS NULL"
)
row = cursor.fetchone()

print(row.FirstName)   # First name value
print(row.MiddleName)  # None (NULL in database)
print(row.LastName)    # Last name value

Prüfe auf NULL-Werte

Überprüfen Sie beim Durchlaufen der Ergebnisse mit dem is-Operator, ob ein Wert None ist:

cursor.execute(
    "SELECT FirstName, MiddleName, LastName FROM Person.Person WHERE BusinessEntityID <= 10"
)

for row in cursor:
    if row.MiddleName is None:
        print(f"{row.FirstName} {row.LastName}: No middle name")
    else:
        print(f"{row.FirstName} {row.MiddleName} {row.LastName}")

Verwenden Sie is None anstelle von == None

Immer für NULL-Prüfungen verwenden is None . Der Operator is prüft die Identität (ob der Wert genau None ist), während ==__eq__ aufruft und bei benutzerdefinierten Objekten zu unerwarteten Ergebnissen führen kann:

# Correct
if row.MiddleName is None:
    full_name = f"{row.FirstName} {row.LastName}"

# Avoid (works but not idiomatic)
if row.MiddleName == None:
    full_name = f"{row.FirstName} {row.LastName}"

Senden Sie NULL-Werte

Fügen Sie NULL mit „None“ ein

Um NULL-Werte einzufügen, geben Sie None ein:

cursor.execute(
    "CREATE TABLE #NullInsertDemo "
    "(Name NVARCHAR(50), Email NVARCHAR(100), Phone NVARCHAR(20))"
)
cursor.execute(
    "INSERT INTO #NullInsertDemo (Name, Email, Phone) "
    "VALUES (%(name)s, %(email)s, %(phone)s)",
    {"name": "Alice", "email": None, "phone": "555-1234"}
)
conn.commit()

Update auf NULL

Setzen Sie eine Spalte auf NULL, indem Sie die folgenden Parameter eingeben None :

cursor.execute(
    "CREATE TABLE #UpdateDemo (ID INT, Email NVARCHAR(100))"
)
cursor.execute("INSERT INTO #UpdateDemo VALUES (100, 'old@example.com')")
cursor.execute(
    "UPDATE #UpdateDemo SET Email = %(email)s WHERE ID = %(id)s",
    {"email": None, "id": 100}
)
conn.commit()

Bedingte NULL-Behandlung

Definieren Sie Funktionen, die optionale Parameter verarbeiten, indem Sie sie auf None setzen, wenn sie nicht bereitgestellt werden:

def update_record(cursor, record_id: int, name: str, email: str | None = None):
    """Update record, setting email to NULL if not provided."""
    cursor.execute(
        "UPDATE #Records SET Name = %(name)s, Email = %(email)s "
        "WHERE ID = %(id)s",
        {"name": name, "email": email, "id": record_id}
    )

NULL in WHERE-Klauseln

IS NULL in Abfragen

Verwendung IS NULL in SQL für NULL-Vergleiche:

# Find people without a middle name
cursor.execute("SELECT FirstName FROM Person.Person WHERE MiddleName IS NULL")

# Find people with a middle name
cursor.execute("SELECT FirstName FROM Person.Person WHERE MiddleName IS NOT NULL")

Dynamische NULL-Handhabung

Wenn ein Parameter NULL sein könnte, verwenden Sie bedingte Logik, um die entsprechende Abfrage zu konstruieren:

def find_people(cursor, middle_name: str | None = None):
    """Find people, optionally filtering by middle name."""
    if middle_name is None:
        # Find people with NULL middle name
        cursor.execute("SELECT * FROM Person.Person WHERE MiddleName IS NULL")
    else:
        # Find people with specific middle name
        cursor.execute(
            "SELECT * FROM Person.Person WHERE MiddleName = %(middle_name)s",
            {"middle_name": middle_name},
        )
    return cursor.fetchall()

COALESCE zur Ersetzung von NULL

Verwenden Sie COALESCE, um NULL-Werte auf SQL-Ebene durch Standardwerte zu ersetzen. COALESCE ist effizienter, als in Python auf None zu prüfen, weil die Substitution auf dem Server erfolgt und dadurch der Umfang bedingter Logik in Ihrer Anwendung verringert wird:

cursor.execute("""
    SELECT 
        FirstName,
        COALESCE(MiddleName, '(none)') AS MiddleName,
        COALESCE(Suffix, 'N/A') AS Suffix
    FROM Person.Person
    WHERE BusinessEntityID <= 10
""")

for row in cursor:
    # MiddleName and Suffix will never be None
    print(f"{row.FirstName}: {row.MiddleName}, {row.Suffix}")

NULL-sichere Operationen

Standardwerte in Python

cursor.execute("SELECT TOP 10 Name, Color FROM Production.Product")

for row in cursor:
    # Use or to provide default
    color = row.Color or "No color"
    print(f"{row.Name}: {color}")

NULL-Werte formatieren

def format_address(row):
    """Format address handling NULL components."""
    parts = [
        row.AddressLine1,
        row.AddressLine2,
        row.City,
        row.PostalCode,
    ]
    # Filter out None values
    return ", ".join(str(p) for p in parts if p is not None)

cursor.execute(
    "SELECT TOP 10 AddressLine1, AddressLine2, City, PostalCode "
    "FROM Person.Address"
)
for row in cursor:
    print(format_address(row))

NULL in Aggregationen

SQL-Aggregatfunktionen behandeln NULL-Werte anders, als man vielleicht erwarten würde. COUNT(column) zählt nur nicht-NULL-Werte, während COUNT(*) alle Zeilen zählt. AVG, SUM, , MINund MAX alle ignorieren NULL-Werte. Wenn jeder Wert in der Spalte NULL ist, geben diese Funktionen NULL (nicht null) zurück.

# COUNT excludes NULL values
cursor.execute("SELECT COUNT(Color) FROM Production.Product")  # Counts non-NULL colors
color_count = cursor.fetchval()

# COUNT(*) includes all rows
cursor.execute("SELECT COUNT(*) FROM Production.Product")  # Counts all products
total_count = cursor.fetchval()

# AVG ignores NULL
cursor.execute("SELECT AVG(Weight) FROM Production.Product")  # Average of non-NULL weights
average_weight = cursor.fetchval()

NULL bei Datentypen

NULL-Numerische Werte

from decimal import Decimal

cursor.execute("SELECT ListPrice FROM Production.Product WHERE ProductID = 1")
row = cursor.fetchone()

# Check before arithmetic
if row.ListPrice is not None:
    tax = row.ListPrice * Decimal("0.08")
    total = row.ListPrice + tax
else:
    total = Decimal("0")

NULL-Datumswerte

Prüfen Sie, ob eine Datumsspalte vorhanden ist None , bevor Sie sie für Vergleiche oder Berechnungen verwenden:

from datetime import date

cursor.execute("SELECT Name, SellEndDate FROM Production.Product WHERE ProductID <= 10")

for row in cursor:
    if row.SellEndDate is None:
        print(f"{row.Name}: Currently selling")
    else:
        print(f"{row.Name}: Discontinued on {row.SellEndDate}")

NULL-String-Werte

Behandeln Sie NULL-Zeichenfolgenspalten, indem Sie sie vor der Verkettung auf None prüfen:

cursor.execute(
    "SELECT TOP 10 FirstName, MiddleName, LastName FROM Person.Person"
)

for row in cursor:
    # Build full name, handling NULL middle name
    if row.MiddleName:
        full_name = f"{row.FirstName} {row.MiddleName} {row.LastName}"
    else:
        full_name = f"{row.FirstName} {row.LastName}"
    print(full_name)

Massenvorgänge mit NULL

executemany mit NULL-Werten

Bei Verwendung von executemany() übergib None in Wörterbüchern für Spalten, die auf NULL gesetzt werden sollen:

users = [
    {"name": "Alice", "title": "Ms.", "suffix": "Jr."},
    {"name": "Bob", "title": None, "suffix": "Sr."},  # NULL title
    {"name": "Carol", "title": "Dr.", "suffix": None},  # NULL suffix
]

cursor.executemany(
    "SELECT FirstName FROM Person.Person WHERE FirstName = %(name)s",
    users
)

Massenkopieren mit NULL

Massenkopieroperationen bewahren NULL-Werte aus Ihren Datenstrukturen:

cursor = conn.cursor()

cursor.execute("CREATE TABLE ##NullDemo (Name NVARCHAR(50), Email NVARCHAR(100), Phone NVARCHAR(20))")
conn.commit()

data = [
    ("Alice", "alice@example.com", "555-0001"),
    ("Bob", None, "555-0002"),      # NULL Email
    ("Carol", "carol@example.com", None),  # NULL Phone
]

result = cursor.bulkcopy("##NullDemo", data)
conn.commit()
print(f"Copied {result['rows_copied']} rows")

Allgemeine Muster

Optionale Feldverarbeitung

Verwenden Sie Typhinweise, um zu verdeutlichen, welche Felder beim Abbilden von Zeilen auf Datenklassen NULL sein können:

from dataclasses import dataclass
from typing import Optional

@dataclass
class PersonRecord:
    business_entity_id: int
    first_name: str
    middle_name: Optional[str] = None
    suffix: Optional[str] = None

def fetch_person(cursor, person_id: int) -> Optional[PersonRecord]:
    cursor.execute(
        "SELECT BusinessEntityID, FirstName, MiddleName, Suffix "
        "FROM Person.Person WHERE BusinessEntityID = %(id)s",
        {"id": person_id},
    )
    row = cursor.fetchone()
    if row is None:
        return None
    return PersonRecord(
        business_entity_id=row.BusinessEntityID,
        first_name=row.FirstName,
        middle_name=row.MiddleName,  # Will be None if NULL
        suffix=row.Suffix,           # Will be None if NULL
    )

JSON-Serialisierung mit NULL

Python-Werte None werden bei Verwendung des json-Moduls automatisch in JSON null konvertiert:

import json

cursor.execute(
    "SELECT TOP 5 BusinessEntityID, FirstName, MiddleName FROM Person.Person"
)
rows = cursor.fetchall()

# Convert to JSON-serializable list
people = []
for row in rows:
    people.append({
        "id": row.BusinessEntityID,
        "name": row.FirstName,
        "middle_name": row.MiddleName,  # None becomes null in JSON
    })

json_output = json.dumps(people, indent=2)
print(json_output)
# [
#   {"id": 1, "name": "Ken", "middle_name": "J"},
#   {"id": 3, "name": "Roberto", "middle_name": null}
# ]

Wörterbuch mit NULL-Filterung

Wählen Sie aus, NULL-Werte beim Umwandeln von Zeilen in Wörterbücher auszuschließen:

def row_to_dict(row, cursor) -> dict:
    """Convert row to dict, optionally excluding NULL values."""
    columns = [col[0] for col in cursor.description]
    return {col: val for col, val in zip(columns, row) if val is not None}

cursor.execute("SELECT * FROM Person.Person WHERE BusinessEntityID = 1")
row = cursor.fetchone()
person_dict = row_to_dict(row, cursor)
# Only includes non-NULL columns

NULL in DataFrames

Wenn du mit Pandas oder Polars DataFrames arbeitest, musst du besonders auf Nullwerte achten, da diese Bibliotheken ihre eigenen Sentinel-Werte verwenden.

Pandas NaN und NaT

pandas verwendet NaN (Keine Zahl) für fehlende Zahlen- und Zeichenkettenwerte und NaT (Keine Zeit) für fehlende Datumszeitwerte. Keiner der beiden Werte ist derselbe wie bei PythonNone:

import pandas as pd
import numpy as np

# When reading SQL results into pandas, NULL becomes NaN or NaT
cursor.execute("SELECT Name, Weight, SellEndDate FROM Production.Product")
table = cursor.arrow()
df = table.to_pandas()

# Check for missing values (covers NaN, NaT, and None)
print(df["Weight"].isna().sum())       # Count of NULL weights
print(df["SellEndDate"].isna().sum())  # Count of NULL dates

# Stage the data in a temp table to avoid mutating the source table
cursor.execute("CREATE TABLE #ProductWeights (Name NVARCHAR(100), Weight DECIMAL(8, 2) NULL)")

# Convert NaN back to None so NULL values round-trip correctly
for _, row in df.iterrows():
    weight = None if pd.isna(row["Weight"]) else float(row["Weight"])
    cursor.execute(
        "INSERT INTO #ProductWeights (Name, Weight) VALUES (%(name)s, %(weight)s)",
        {"name": row["Name"], "weight": weight}
    )

Warning

Vergleiche nicht mit == np.nan oder == pd.NaT. Diese Vergleiche ergeben immer False. Verwenden Sie stattdessen pd.isna() oder pd.notna().

Umgang mit Nullwerten in Polars

Polars verwendet seinen eigenen null Wert (nicht NaN), der direkt auf Python Noneabgebildet wird:

import polars as pl

cursor.execute("SELECT Name, Weight, Color FROM Production.Product")
table = cursor.arrow()
df = pl.from_arrow(table)

# Filter rows with non-null values
has_weight = df.filter(pl.col("Weight").is_not_null())

# Replace null with a default
df = df.with_columns(pl.col("Color").fill_null("No color"))