Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Driver mssql-python memetakan tipe data desimal, numerik, dan uang SQL Server ke Python decimal.Decimal untuk perhitungan keuangan dan ilmiah yang akurat. SQL Server menyediakan jenis numerik yang tepat berikut:
| Jenis SQL | Jenis Python | Presisi | Kasus Penggunaan |
|---|---|---|---|
| desimal(p,s) | decimal.Decimal |
1-38 digit | Perhitungan yang tepat |
| numerik(p,s) | decimal.Decimal |
1-38 angka | Sama dengan desimal |
| uang | decimal.Decimal |
19 digit, 4 desimal | Mata uang |
| smallmoney | decimal.Decimal |
10 digit, 4 desimal | Mata uang kecil |
| float | float |
sekitar 15 digit | Perkiraan |
| real | float |
~7 digit | Perkiraan |
Python Tipe desimal
Menerima nilai desimal
Kolom desimal mengembalikan objek Pythondecimal.Decimal:
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')
Mengirim nilai desimal
Lewati Decimal objek untuk penyisipan yang tepat:
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()
Hindari float untuk data keuangan
Tipe float memiliki masalah presisi sehingga tidak cocok untuk perhitungan keuangan. Selalu gunakan Decimal untuk hasil yang akurat:
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
Jenis uang
Bekerja dengan kolom uang
Mengambil dan memperbarui kolom uang SQL Server menggunakan objek Python Decimal:
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()
Pemformatan uang
Format nilai desimal sebagai string mata uang dengan simbol dan pemisah koma:
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
Presisi dan skala desimal
Pahami presisi dan skala
- Presisi (p): Jumlah total digit
- Skala (s): Jumlah digit setelah tanda desimal
Misalnya, decimal(10, 2) dapat menyimpan nilai dari -999999999.99 hingga 99999999.99, sedangkan decimal(5, 4) dapat menyimpan nilai dari -9.9999 hingga 9.9999.
Tentukan presisi pada parameter
Driver mssql-python menyimpulkan presisi dan skala dari nilai Desimal secara otomatis:
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()
Kontrol pembulatan
Gunakan metode quantize() untuk membulatkan nilai desimal ke skala tertentu:
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')
Perhitungan keuangan
Aritmatika yang aman
Terapkan perhitungan keuangan menggunakan Desimal dengan pembulatan yang tepat:
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")})
Perhitungan persentase
Hitung diskon dan penyesuaian berbasis persentase:
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}")
Membagi jumlah
Bagi jumlah total secara merata di antara beberapa pihak, tangani sisanya:
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
Operasi agregasi
SUM dan AVG dengan desimal
Fungsi agregasi SQL Server mengembalikan jenis desimal untuk presisi:
# 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"))
Menangani NULL dalam agregasi
Fungsi agregasi SQL Server dapat dikembalikan None ketika tidak ada baris yang cocok dengan kueri:
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")
Konverter output untuk bilangan desimal
Konverter keluaran menggunakan jenis Python yang ditampilkan di cursor.description sebagai kunci, bukan bilangan bulat tipe SQL ODBC. Daftarkan konverter dengan decimal.Decimal sehingga berfungsi untuk kolom decimal, numeric, money, dan smallmoney, yang semuanya dipetakan ke decimal.Decimal.
Konversi ke float (saat presisi tidak penting)
Untuk kompatibilitas dengan pustaka yang mengharapkan nilai float, tentukan konverter output:
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'>
Konverter pemformatan uang khusus
Tentukan konverter output kustom untuk secara otomatis memformat nilai Desimal sebagai string mata uang:
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"
Pola umum
Konversi mata uang
Konversi jumlah moneter antar mata uang menggunakan nilai tukar:
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}")
Pembaruan saldo dengan transaksi
Gunakan transaksi dengan petunjuk kunci yang sesuai untuk memastikan transfer dana atom dan mencegah kehilangan update:
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"))
Transfer mengunci kedua baris sejak awal, dimulai dari ID terendah, dengan menjalankan SELECT ... WITH (UPDLOCK) terpisah untuk setiap akun berdasarkan urutan yang diurutkan.
UPDLOCK mencegah pola pembaruan yang hilang di mana dua transaksi membaca saldo yang sama dan menerapkan pembaruan berdasarkan data kedaluwarsa. Menerapkan penguncian melalui pernyataan-pernyataan terpisah dalam urutan terurut inilah yang menjamin urutan perolehan kunci, sehingga transfer berlawanan arah antara dua akun yang sama tidak memperoleh kunci dalam urutan terbalik dan mengalami deadlock.
Penyisipan massal dengan desimal
Sisipkan beberapa baris dengan nilai Desimal menggunakan 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()
Praktik terbaik
Selalu gunakan Desimal untuk matematika keuangan
Jangan pernah menggunakan float untuk perhitungan keuangan; selalu gunakan tipe Desimal untuk presisi:
# 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')
Gunakan konstruktor string
Buat nilai Desimal dari string untuk menghindari ketidaktepatan float:
# Good: exact value
amount = Decimal("123.45")
# Risky: might inherit float imprecision
amount = Decimal(123.45) # Could be 123.4500000000000028...
Selalu bulatkan sebelum ditampilkan atau disimpan
Bulatkan nilai desimal ke skala yang sesuai sebelum ditampilkan atau disimpan ke basis data:
from decimal import Decimal, ROUND_HALF_UP
calculated = Decimal("123.456789")
display = calculated.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
Tetapkan konteks desimal jika diperlukan
Presisi konteks desimal memengaruhi hasil perhitungan, bukan nilai yang diurai dari string atau dibaca dari database. Kueri selalu mengembalikan nilai atau money atau decimal dengan skala tersimpan penuhnya, terlepas dari getcontext().prec. Konteks hanya berlaku saat Anda melakukan komputasi dengannya. Untuk melihat efek presisi mulai berlaku, lakukan operasi seperti pembagian.
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)