Kezeld a tizedes és a pénzértékeket

Az mssql-python meghajtó az SQL Server decimal, numeric és money adattípusait a Python decimal.Decimal típusához rendeli, hogy pontos pénzügyi és tudományos számításokat tegyen lehetővé. Az SQL Server a következő pontos numerikus típusokat biztosítja:

SQL-típus Python-típus Precision Használati eset
decimális(p,s) decimal.Decimal 1-38 számjegy Pontos számítások
numerikus(p,s) decimal.Decimal 1-38 számjegy Megegyezik a tizedessel
money decimal.Decimal 19 számjegy, 4 tizedes Valuta
smallmoney decimal.Decimal 10 számjegy, 4 tizedesjegy Kis valuta
float float ~15 számjegy Közelítő
real float ~7 számjegy Hozzávetőleges

Python tizedes típus

Tizedes értékek vétele

A tizedes oszlopok Python decimal.Decimal objektumokat adnak vissza:

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

Tizedes értékek küldése

Adj át Decimal objektumokat a pontos beszúráshoz:

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

Kerüld a float adattípust pénzügyi adatokhoz

Az úszótípusok pontossági problémákkal rendelkeznek, ezért nem alkalmasak pénzügyi számításokhoz. Mindig használd Decimal a pontos eredményekért:

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

Pénz típus

Munka pénzoszlopokkal

Az SQL Server pénzoszlopainak lekérése és frissítése Python tizedes objektumokkal:

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

Pénzformázás

A tizedes értékeket formázza pénznemet tartalmazó karakterláncokká szimbólumokkal és vesszős elválasztókkal:

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

Tizedes pontosság és skálázás

A precizitás és a skála megértése

  • Precizitás (p): Számjegyek összesített száma
  • Skála (s): A tizedespont után következő számjegyek száma

Például decimal(10, 2) tárolhat értékeket -99999999.99.99 és 9999999.99 között, míg decimal(5, 4) -99999 és 9.9999 közötti értékek tárolására.

Paraméterek pontosságának meghatározása

Az mssql-python illesztőprogram automatikusan következteti a precizitást és skálázást a tizedes értékekből:

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

Kontrollkerekítés

Használd a quantize() módszert a tizedes értékek kerekítésére egy adott skálára:

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

Pénzügyi számítások

Biztonságos aritmetika

Hajtsa végre pénzügyi számításokat tizedes rendszerrel, megfelelő kerekítéssel:

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

Százalékos számítások

Számolja ki százalékos alapú kedvezményeket és korrekciókat:

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

Osztott összegek

Az összösszeget egyenlően oszd meg több fél között, kezelve a maradékokat:

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

Összesítési műveletek

SUM és AVG tizedesekkel

SQL Server aggregációs függvényei pontosság érdekében tizedes típusokat adnak vissza:

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

A NULL kezelése aggregációkban

Az SQL Server aggregációs függvényei akkor is visszatérhetnekNone, ha egyetlen sor sem egyezik meg a lekérdezéssel:

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

Kimeneti konvertálók tizedes számokhoz

A kimeneti konverterek a kulcsként a benne látható cursor.description Python típust használják, nem az ODBC SQL típusú egész számot. Regisztrálja az átalakítót ezzel: decimal.Decimal, hogy működjön a(z) decimal, numeric, money és smallmoney oszlopokhoz, amelyek mind a(z) decimal.Decimal elemre vannak leképezve.

Átalakítás lebegőre (amikor a pontosság nem kritikus)

A lebegő értékeket váró könyvtárakhoz való kompatibilitás érdekében definiáljunk egy kimeneti átalakítót:

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

Egyedi pénznemformázó konverter

Definiáljunk egy egyedi kimeneti konvertert, amely automatikusan formázza a tizedes értékeket valutaszövegként:

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"

Gyakori minták

Pénznemátváltás

Pénzösszegek átváltása valuták között az árfolyam alapján:

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

Egyenlegfrissítések tranzakciókkal

Használj megfelelő zárolási tippeket tartalmazó tranzakciókat az atomi pénzátutalások biztosítására és a frissítések elvesztésének megelőzésére:

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

Az átutalás már az elején zárolja mindkét sort, a legalacsonyabb azonosítójúval kezdve, úgy, hogy rendezett sorrendben minden számlához külön SELECT ... WITH (UPDLOCK) utasítást ad ki. UPDLOCK Megakadályozza az elveszett frissítési mintázatot, amikor két tranzakció ugyanazt az egyenleget olvasza, és elavult adatok alapján alkalmazza a frissítéseket. A zárak különálló utasításokként, rendezett sorrendben történő kiadása garantálja a megszerzés sorrendjét, megakadályozva, hogy az ugyanazon két számla közötti ellentétes átutalások fordított sorrendben szerezzék meg a zárakat, és ezzel holtpontot okozzanak.

Tömeges beszúrás tizedesjegyekkel

Több, Decimal értékeket tartalmazó sor beszúrása a(z) executemany() használatával:

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

Bevált gyakorlatok

Mindig használd a tizedes rendszert pénzügyi matematikához

Pénzügyi számításokhoz soha ne használj float típust; a kellő pontosság biztosítása érdekében mindig a Decimal típust használd:

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

Használjon stringkonstruktorokat

Tizedes értékeket karakterláncokból hozzon létre a lebegőpontos pontatlanság elkerülése érdekében:

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

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

Mindig kerekítsen megjelenítés vagy tárolás előtt

Kerekítsd a tizedes értékeket a megfelelő skálára, mielőtt megjelenítenénk vagy az adatbázisban maradnának:

from decimal import Decimal, ROUND_HALF_UP

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

Szükség esetén állíts be tizedes kontextust

A tizedes kontextus pontossága befolyásolja a számítások eredményét, nem pedig a stringekből elemzött vagy az adatbázisból olvasott értékeket. Egy lekérdezés mindig egy money vagy decimal értéket ad vissza a teljes tárolt skálával, függetlenül attól, hogy .getcontext().prec A kontextus csak akkor válik hatásra, ha vele számolsz. A pontosság hatásának megtekintéséhez végezz egy olyan műveletet, mint például osztás.

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)