Dezimal- und Geldwerte verwalten

Der mssql-python-Treiber ordnet die SQL Server-Datentypen decimal, numeric und money dem Python-decimal.Decimal zu, um präzise Finanz- und wissenschaftliche Berechnungen zu ermöglichen. SQL Server bietet die folgenden präzisen numerischen Typen:

SQL-Typ Python-Typ Präzision Anwendungsfall
decimal(p,s) decimal.Decimal 1–38 Ziffern Exakte Berechnungen
numeric(p,s) decimal.Decimal 1–38 Ziffern Dasselbe wie Dezimalmal.
money decimal.Decimal 19 Ziffern, 4 Dezimalstellen Währungen
smallmoney decimal.Decimal 10 Ziffern, 4 Dezimalstellen Kleinwährung
float float ~15 Ziffern Ungefähr
real float ~7 Ziffern Ungefähr

Python-Dezimaltyp

Dezimalwerte empfangen

Dezimalspalten liefern Python-Objekte decimal.Decimal zurück:

from decimal import Decimal
import mssql_python

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

cursor.execute(
    "SELECT SubTotal, TaxAmt, TotalDue "
    "FROM Sales.SalesOrderHeader WHERE SalesOrderID = 43659"
)
row = cursor.fetchone()

print(type(row.SubTotal))  # <class 'decimal.Decimal'>
print(row.SubTotal)        # Decimal('20565.6206')
print(row.TaxAmt)          # Decimal('1971.5149')
print(row.TotalDue)        # Decimal('23153.2339')

Senden Sie Dezimalwerte

Übergeben Sie Decimal Objekte für präzises Einfügen:

from decimal import Decimal

cursor.execute("""
    CREATE TABLE #ProductDemo (
        Name NVARCHAR(50),
        Price DECIMAL(10,2),
        Cost DECIMAL(10,2)
    )
""")
cursor.execute("""
    INSERT INTO #ProductDemo (Name, Price, Cost)
    VALUES (%(name)s, %(price)s, %(cost)s)
""", {
    "name": "Widget",
    "price": Decimal("199.99"),
    "cost": Decimal("87.50")
})
conn.commit()

Vermeiden Sie Float für Finanzdaten

Float-Typen haben Präzisionsprobleme, die sie für Finanzberechnungen ungeeignet machen. Verwenden Sie für präzise Ergebnisse immer Decimal:

from decimal import Decimal

# Bad: float has precision issues
price = 0.1 + 0.2  # 0.30000000000000004

# Good: Decimal is exact
price = Decimal("0.1") + Decimal("0.2")  # Decimal('0.3')

# Use string constructor for literal values
correct = Decimal("19.99")  # Exact
avoid = Decimal(19.99)      # Might introduce float imprecision

Geldtyp

Arbeiten mit Währungsspalten

Abrufen und aktualisieren Sie die Geldspalten von SQL Server mit Python Decimal-Objekten:

from decimal import Decimal

# Create and populate a temp table with a money column
cursor.execute("""
    CREATE TABLE #Accounts (
        ID INT,
        AccountID NVARCHAR(20),
        Balance MONEY
    )
""")
cursor.execute(
    "INSERT INTO #Accounts (ID, AccountID, Balance) VALUES (1, %(acct)s, %(bal)s)",
    {"acct": "ACCT-001", "bal": Decimal("1234.5678")}
)
conn.commit()

cursor.execute("SELECT AccountID, Balance FROM #Accounts WHERE ID = 1")
row = cursor.fetchone()

print(row.Balance)         # Decimal('1234.5678')
print(type(row.Balance))   # <class 'decimal.Decimal'>

# Money arithmetic
cursor.execute("""
    UPDATE #Accounts 
    SET Balance = Balance + %(amount)s 
    WHERE ID = %(id)s
""", {"amount": Decimal("100.00"), "id": 1})
conn.commit()

Geldformatierung

Formatiere Dezimalwerte als Währungsstrings mit Symbolen und Kommatrennern:

from decimal import Decimal

def format_currency(value: Decimal, symbol: str = "$") -> str:
    """Format decimal as currency string."""
    return f"{symbol}{value:,.2f}"

cursor.execute("SELECT TotalDue FROM Sales.SalesOrderHeader")
for row in cursor:
    print(format_currency(row.TotalDue))  # $1,234.56

Dezimale Präzision und Skala

Präzision und Maßstab verstehen

  • Präzision (p): Gesamtzahl der Ziffern
  • Skala(n): Anzahl der Ziffern nach Dezimalstellen

Zum Beispiel decimal(10, 2) kann Werte von -999999999,99 bis 999999999,99 speichern, während decimal(5, 4) Werte von -9,9999 bis 9,9999 gespeichert werden können.

Spezifizieren Sie die Genauigkeit in den Parametern

Der mssql-python-Treiber leitet automatisch Präzision und Skalierung aus Dezimalwerten ab:

from decimal import Decimal

# Create a temp table for the measurement value
cursor.execute("CREATE TABLE #Measurements (Value DECIMAL(18,6))")

# The driver infers precision from the Decimal value
value = Decimal("123.456789")
cursor.execute("INSERT INTO #Measurements (Value) VALUES (%(v)s)", {"v": value})
conn.commit()

Kontrollrundung

Verwenden Sie die Methode quantize() , um Dezimalwerte auf eine bestimmte Skala abzurunden:

from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN

price = Decimal("19.999")

# Round to 2 decimal places
rounded = price.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(rounded)  # Decimal('20.00')

# Round down (truncate)
truncated = price.quantize(Decimal("0.01"), rounding=ROUND_DOWN)
print(truncated)  # Decimal('19.99')

Finanzielle Berechnungen

Sichere Arithmetik

Führen Sie finanzielle Berechnungen mit Dezimalform mit korrekter Rundung um:

from decimal import Decimal, ROUND_HALF_UP

def calculate_total(price: Decimal, quantity: int, tax_rate: Decimal) -> Decimal:
    """Calculate order total with tax."""
    subtotal = price * quantity
    tax = (subtotal * tax_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    return subtotal + tax

cursor.execute("SELECT ListPrice FROM Production.Product WHERE ProductID = 1")
price = cursor.fetchval()

total = calculate_total(
    price=price,
    quantity=3,
    tax_rate=Decimal("0.0825")  # 8.25% tax
)

cursor.execute("""
    CREATE TABLE #OrderCalc (
        ProductID INT, Quantity INT, Total DECIMAL(10,2), Tax DECIMAL(10,2)
    )
""")
cursor.execute("""
    INSERT INTO #OrderCalc (ProductID, Quantity, Total, Tax)
    VALUES (%(prod)s, %(qty)s, %(total)s, %(tax)s)
""", {"prod": 1, "qty": 3, "total": total, "tax": total * Decimal("0.0825")})

Prozentberechnungen

Berechnen Sie prozentuale Rabatte und Anpassungen:

from decimal import Decimal

def calculate_discount(price: Decimal, discount_percent: Decimal) -> Decimal:
    """Calculate discounted price."""
    discount = (price * discount_percent / 100).quantize(Decimal("0.01"))
    return price - discount

original = Decimal("99.99")
discounted = calculate_discount(original, Decimal("15"))  # 15% off
print(f"Original: ${original}, After discount: ${discounted}")

Aufteilungsbeträge

Einen Gesamtbetrag gleichmäßig auf mehrere Beteiligte verteilen und Restbeträge berücksichtigen:

from decimal import Decimal, ROUND_DOWN

def split_amount(total: Decimal, ways: int) -> list[Decimal]:
    """Split amount evenly, handling remainder."""
    per_person = (total / ways).quantize(Decimal("0.01"), rounding=ROUND_DOWN)
    remainder = total - (per_person * ways)
    
    amounts = [per_person] * ways
    # Add remainder to first person
    amounts[0] += remainder
    return amounts

total = Decimal("100.00")
split = split_amount(total, 3)
print(split)  # [Decimal('33.34'), Decimal('33.33'), Decimal('33.33')]
print(sum(split))  # Decimal('100.00') - always equals original

Aggregationsvorgänge

SUM und AVG mit Dezimalzahlen

SQL Server-Aggregationsfunktionen geben Dezimaltypen für Genauigkeit zurück:

# SUM preserves decimal type
cursor.execute("SELECT SUM(TotalDue) AS TotalSales FROM Sales.SalesOrderHeader")
total_sales = cursor.fetchval()
print(type(total_sales))  # <class 'decimal.Decimal'>

# AVG might return more decimal places
cursor.execute("SELECT AVG(ListPrice) AS AvgPrice FROM Production.Product")
avg_price = cursor.fetchval()
# Round to desired precision
avg_price = avg_price.quantize(Decimal("0.01"))

NULL bei Aggregationen behandeln

SQL Server-Aggregatfunktionen können None zurückgeben, wenn keine Zeilen der Abfrage entsprechen:

cursor.execute("SELECT SUM(TotalDue) FROM Sales.SalesOrderHeader WHERE CustomerID = 0")
total_discount = cursor.fetchval()

# SUM returns NULL if no rows match
if total_discount is None:
    total_discount = Decimal("0.00")

Ausgabekonverter für Dezimalzahlen

Ausgabekonverter verwenden den in der Definition angegebenen cursor.description Python-Typ als Schlüssel, nicht die ODBC-SQL-Ganzzahl. Registrieren Sie den Konverter mit decimal.Decimal, damit er für decimal-, numeric-, money- und smallmoney-Spalten funktioniert, die alle auf decimal.Decimal abgebildet werden.

Konvertiere in Float (wenn Präzision nicht kritisch ist)

Zur Kompatibilität mit Bibliotheken, die Float-Werte erwarten, definieren Sie einen Ausgangswandler:

import mssql_python
from decimal import Decimal

def decimal_to_float(value):
    """Convert Decimal to float."""
    if value is None:
        return None
    return float(value)  # value is already a Decimal object

conn = mssql_python.connect(connection_string)

# Convert decimals to float for compatibility with libraries that expect float
conn.add_output_converter(Decimal, decimal_to_float)

cursor = conn.cursor()
cursor.execute("SELECT ListPrice FROM Production.Product WHERE ProductID = 1")
row = cursor.fetchone()
print(type(row.ListPrice))  # <class 'float'>

Benutzerdefinierter Geldformatierungs-Konverter

Definieren Sie einen benutzerdefinierten Ausgabewandler, der Dezimalwerte automatisch als Währungsstrings formatiert:

from decimal import Decimal

def money_to_string(value):
    """Convert Decimal to formatted string."""
    if value is None:
        return None
    return f"${value:,.2f}"  # value is already a Decimal object

conn.add_output_converter(Decimal, money_to_string)

cursor = conn.cursor()
cursor.execute("CREATE TABLE #Accounts (Balance DECIMAL(19,4))")
cursor.execute(
    "INSERT INTO #Accounts (Balance) VALUES (%(bal)s)",
    {"bal": Decimal("1234.56")}
)
conn.commit()

cursor.execute("SELECT Balance FROM #Accounts")
for row in cursor:
    print(row.Balance)  # "$1,234.56"

Allgemeine Muster

Währungskonvertierung

Umrechnen Sie Geldbeträge zwischen Währungen mithilfe von Wechselkursen:

from decimal import Decimal

def convert_currency(amount: Decimal, rate: Decimal) -> Decimal:
    """Convert currency at given exchange rate."""
    return (amount * rate).quantize(Decimal("0.01"))

usd_amount = Decimal("100.00")
eur_rate = Decimal("0.92")
eur_amount = convert_currency(usd_amount, eur_rate)
print(f"${usd_amount} = €{eur_amount}")

Kontostandaktualisierungen mit Transaktionen

Verwenden Sie Transaktionen mit geeigneten Lock-Hinweisen, um Atomfonds-Überweisungen sicherzustellen und verlorene Updates zu vermeiden:

from decimal import Decimal

def transfer_funds(conn, from_account: int, to_account: int, amount: Decimal):
    """Transfer funds between accounts atomically."""
    cursor = conn.cursor()
    conn.autocommit = False
    
    try:
        balances = {}
        for account_id in sorted((from_account, to_account)):
            cursor.execute(
                "SELECT Balance FROM #Accounts WITH (UPDLOCK) WHERE ID = %(id)s",
                {"id": account_id}
            )
            row = cursor.fetchone()
            if row is None:
                raise ValueError(f"Account {account_id} not found")
            balances[account_id] = row.Balance
        
        if balances[from_account] < amount:
            raise ValueError("Insufficient funds")
        
        # Debit source
        cursor.execute("""
            UPDATE #Accounts SET Balance = Balance - %(amount)s
            WHERE ID = %(id)s
        """, {"amount": amount, "id": from_account})
        
        # Credit destination
        cursor.execute("""
            UPDATE #Accounts SET Balance = Balance + %(amount)s
            WHERE ID = %(id)s
        """, {"amount": amount, "id": to_account})
        
        conn.commit()
        
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.autocommit = True

# Set up demo accounts
cursor = conn.cursor()
cursor.execute("CREATE TABLE #Accounts (ID INT, Balance MONEY)")
cursor.executemany(
    "INSERT INTO #Accounts (ID, Balance) VALUES (%(id)s, %(bal)s)",
    [{"id": 1, "bal": Decimal("1000.00")}, {"id": 2, "bal": Decimal("500.00")}]
)
conn.commit()

# Usage
transfer_funds(conn, from_account=1, to_account=2, amount=Decimal("500.00"))

Die Überweisung sperrt beide Zeilen von vornherein, beginnend mit der niedrigsten ID, indem für jedes Konto in sortierter Reihenfolge ein separates SELECT ... WITH (UPDLOCK) ausgeführt wird. UPDLOCK verhindert das verlorene Aktualisierungsmuster, bei dem zwei Transaktionen denselben Saldo lesen und Updates basierend auf veralteten Daten anwenden. Die Ausgabe der Sperren als separate Auszüge in sortierter Reihenfolge garantiert die Erwerbsreihenfolge und verhindert, dass gegensätzliche Überweisungen zwischen denselben beiden Konten Sperren in umgekehrter Reihenfolge erwerben und Deadlocks entstehen.

Masseneinfügung mit Dezimalzahlen

Fügen Sie mehrere Zeilen mit Dezimalwerten ein:executemany()

from decimal import Decimal

cursor.execute("""
    CREATE TABLE #BulkProducts (
        Name NVARCHAR(50),
        Price DECIMAL(10,2),
        Cost DECIMAL(10,2)
    )
""")

products = [
    {"name": "Widget A", "price": Decimal("19.99"), "cost": Decimal("8.50")},
    {"name": "Widget B", "price": Decimal("29.99"), "cost": Decimal("12.75")},
    {"name": "Widget C", "price": Decimal("39.99"), "cost": Decimal("18.00")},
]

cursor.executemany("""
    INSERT INTO #BulkProducts (Name, Price, Cost)
    VALUES (%(name)s, %(price)s, %(cost)s)
""", products)
conn.commit()

Bewährte Methoden

Verwenden Sie immer Dezimalstellen für Finanzmathematik

Verwenden Sie niemals Float für Finanzberechnungen; Verwenden Sie immer die Dezimalschrift zur Präzision:

# Wrong: float precision issues
price = 0.10
quantity = 3
total = price * quantity  # 0.30000000000000004

# Correct: Decimal is exact
price = Decimal("0.10")
quantity = 3
total = price * quantity  # Decimal('0.30')

Verwenden Sie Stringkonstruktoren

Konstruiere Dezimalwerte aus Strings, um Float-Ungenauigkeit zu vermeiden:

# Good: exact value
amount = Decimal("123.45")

# Risky: might inherit float imprecision
amount = Decimal(123.45)  # Could be 123.4500000000000028...

Immer vor der Anzeige oder Speicherung runden

Runden Sie Dezimalwerte auf die entsprechende Genauigkeit, bevor sie angezeigt oder in der Datenbank gespeichert werden:

from decimal import Decimal, ROUND_HALF_UP

calculated = Decimal("123.456789")
display = calculated.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

Setze dezimalen Kontext bei Bedarf

Die dezimale Kontextgenauigkeit beeinflusst die Ergebnisse der Berechnungen, nicht Werte, die aus Strings gelesen oder aus der Datenbank gelesen werden. Eine Abfrage gibt immer einen money- oder decimal-Wert in seiner vollständig gespeicherten Skalierung zurück, unabhängig von getcontext().prec. Der Kontext wirkt erst, wenn man mit ihm rechnet. Um zu sehen, dass die Präzision wirkt, führen Sie eine Operation wie Division durch.

from decimal import Decimal, getcontext

# A money value read from SQL Server arrives at its full stored scale,
# regardless of the context precision.
cursor.execute(
    "SELECT SubTotal FROM Sales.SalesOrderHeader WHERE SalesOrderID = 43659"
)
subtotal = cursor.fetchone().SubTotal
print(subtotal)  # Decimal('20565.6206')

# The context precision governs the calculation, not the fetched value.
# Split the subtotal into three equal installments.
getcontext().prec = 10
print(subtotal / 3)  # 6855.206867 (10 significant digits)

getcontext().prec = 28  # Reset to default
print(subtotal / 3)  # 6855.206866666666666666666667 (28 significant digits)