Ondalık ve para değerlerini ele alın

mssql-python sürücüsü, SQL Server ondalık, sayısal ve para veri türlerini doğru finansal ve bilimsel hesaplamalar için Python decimal.Decimal veri türlerine eşler. SQL Server aşağıdaki kesin sayısal tipleri sağlar:

SQL Türü Python Türü Hassasiyet Kullanım Örneği
decimal(p,s) decimal.Decimal 1-38 hane Kesin hesaplamalar
sayısal(p,s) decimal.Decimal 1-38 hane Ondalık ile aynı
money decimal.Decimal 19 rakam, 4 ondalık Para birimi
smallmoney decimal.Decimal 10 rakam, 4 ondalık Küçük para birimi
float float ~15 rakam Yaklaşık
real float ~7 rakam Yaklaşık

Python Decimal veri türü

Ondalık değerleri alın

Ondalık sütunlar Python decimal.Decimal nesnelerini döndürür:

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

Ondalık değerler gönder

Hassas ekleme için Decimal nesnelerini iletin:

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

Finansal veriler için float kullanmaktan kaçının

Kayan noktalı veri türleri, hassasiyet sorunları olduğundan finansal hesaplamalar için uygun değildir. Her zaman doğru sonuçlar için kullanın 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

Para türü

Para sütunlarıyla çalışma

SQL Server para sütunlarını Python ondalık nesneler kullanarak alın ve güncelleyin:

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

Para biçimlendirme

Ondalık değerleri semboller ve virgül ayırıcılarıyla para birimi dizileri olarak biçimlendirin:

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

Ondalık hassasiyet ve ölçek

Hassasiyeti ve ölçeği anlamak

  • Hassasiyet (p): Toplam basamak sayısı
  • Ölçek (s): Ondalık ayırıcıdan sonraki basamak sayısı

Örneğin, decimal(10, 2) -99999999.99 ile 9999999.99 arasında değerler depolayabilirken decimal(5, 4) , -99999 ile 9.9999 arasında olan değerleri depolayabilir.

Parametrelerde hassasiyeti belirtin

mssql-python sürücüsü, ondalık değerlerden otomatik olarak hassasiyet ve ölçek çıkarır:

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

Denetim yuvarlaması

Ondalık değerleri belirli bir ölçekte yuvarlamak için bu quantize() yöntemi kullanın:

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

Mali hesaplamalar

Güvenli aritmetik

Decimal kullanarak finansal hesaplamaları doğru yuvarlamayla uygulayın:

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

Yüzde hesaplamaları

Yüzde bazlı indirimler ve ayarlamalar hesaplayın:

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

Bölünmüş miktarlar

Toplam tutarı birden fazla taraf arasında eşit olarak paylaştırın, kalan kısmı yönetin:

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

Toplama işlemleri

Ondalıklı SUM ve AVG

SQL Server toplama işlevleri, hassasiyeti korumak için Decimal türleri döndürür:

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

Toplulaştırmalarda NULL işleme

SQL Server toplama işlevleri, sorguyla eşleşen satır olmadığında None döndürebilir:

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

Ondalık için çıkış dönüştürücüleri

Çıktı dönüştürücüler, anahtar olarak gösterilen Python tipini cursor.description kullanır, ODBC SQL tipi tam sayıyı değil. Dönüştürücüyü decimal.Decimal ile kaydederek decimal, numeric, money ve smallmoney sütunlarında çalışmasını sağlayın; bunların tümü decimal.Decimal öğesine eşlenir.

Hassasiyet kritik olmadığında float'a dönüştürün)

Float değer bekleyen kütüphanelerle uyumluluk için, bir çıkış dönüştürücü tanımlayın:

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

Özel para formatlama dönüştürücüsü

Ondalık değerleri otomatik olarak para birimi dizileri olarak biçimlendirmek için özel bir çıkış dönüştürücü tanımlayın:

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"

Ortak desenler

Para birimi dönüştürme

Parasal tutarları döviz kurlarını kullanarak para birimleri arasında dönüştürü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}")

İşlemlerle birlikte bakiye güncellemeleri

Atomik fon transferlerini sağlamak ve güncellemelerin kaybını önlemek için uygun kilit ipuçları içeren işlemler kullanın:

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

Aktarım, sıralı düzende her hesap için ayrı bir SELECT ... WITH (UPDLOCK) göndererek, her iki satırı da en baştan ID'si en düşük olan önce olacak şekilde kilitler. UPDLOCK iki işlemin aynı bakiyeyi okuyup güncel olmayan verilere dayanarak güncellemeler uyguladığı kayıp güncelleme örüntüsünü önler. Kilitlerin sıralanmış düzende ayrı komutlar halinde uygulanması, kilit edinme sırasını garanti eder; böylece aynı iki hesap arasındaki zıt yönlü transferlerin kilitleri ters sırayla edinip deadlock’a girmesi önlenir.

Ondalıklı toplu ekleme

Ondalık değerlerle birden fazla satır ekleyin: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()

En iyi uygulamalar

Finansal matematik için her zaman ondalık kullan

Finansal hesaplamalar için asla float kullanma; Hassasiyet için her zaman ondalık tipi kullanın:

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

Dizi yapıcılar kullanın

Kayan nokta hassasiyetsizliğini önlemek için Decimal değerlerini dizelerden oluşturun:

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

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

Görüntülemeden veya depolamadan önce her zaman yuvarlayın

Ondalık değerleri görüntülemeden veya veritabanına devam ettirmeden önce uygun ölçekte yuvarlayın:

from decimal import Decimal, ROUND_HALF_UP

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

Gerekirse ondalık bağlam belirleyin

Ondalık bağlam hassasiyeti, hesaplamaların sonuçlarını etkiler, dizilerden ayrıştırılan veya veritabanından okunan değerleri değil. Bir sorgu, getcontext().prec değerinden bağımsız olarak, depolanan tam ölçeğiyle her zaman bir money veya decimal değeri döndürür. Bağlam ancak onunla hesaplama yaptığınızda etkili olur. Hassasiyetin etkisini görmek için, bölme gibi bir işlem gerçekleştirin.

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)