mssql-python 드라이버는 SQL Server의 십진, 숫자, 돈 데이터 타입을 Python decimal.Decimal 에 매핑하여 정확한 재무 및 과학 계산을 수행합니다. SQL Server는 다음과 같은 정확한 수치 유형을 제공합니다:
| SQL 형식 | Python 형식 | 정밀성 | 사용 사례 |
|---|---|---|---|
| decimal(p,s) | decimal.Decimal |
1-38자리 | 정확한 계산 |
| numeric(p,s) | decimal.Decimal |
1-38자리 | 십진수와 동일합니다 |
| money | decimal.Decimal |
19자리, 소수점 4자리 | 통화 |
| smallmoney | decimal.Decimal |
10자리, 소수점 4자리 | 소액 화폐 |
| float | float |
~15자리 | 대략적 |
| real | float |
~7자리 | 대략적 |
Python 십진분류 타입
십진수 값을 받기
십진수 열은 Python decimal.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')
십진수 값을 전송하기
정밀한 삽입을 위해 Decimal 객체를 전달:
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()
금융 데이터에는 부동소수점 형식을 사용하지 마세요
플로트 타입은 정밀도 문제로 인해 재무 계산에 적합하지 않습니다. 정확한 결과를 위해 항상 다음을 사용 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
돈 종류
머니 칼럼 작업
Python 십진수 객체를 사용하여 SQL Server 머니 컬럼을 검색하고 업데이트하기:
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()
머니 포맷팅
십진법 값을 기호와 쉼표를 사용하는 통화 문자열로 형식화하세요:
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
십진법 정밀도와 척도
정밀도와 규모를 이해하세요
- 정밀도(p): 총 자리 수
- 스케일: 소수점 이후의 숫자 수
예를 들어, 는 decimal(10, 2) -9999999.99에서 9999999.99.99까지의 값을 저장할 수 있고 decimal(5, 4) , 는 -9.9999에서 9.9999까지의 값을 저장할 수 있습니다.
매개변수의 정밀도를 명시하세요
mssql-python 드라이버는 십진수 값에서 정밀도와 스케일을 자동으로 추론합니다:
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()
반올림 제어
quantize() 메서드를 사용하여 Decimal 값을 특정 소수 자릿수로 반올림합니다:
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')
재무 계산
안전한 산술
적절한 반올림을 적용하여 Decimal을 사용한 금융 계산을 구현하세요:
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")})
백분율 계산
백분율 기반 할인 및 조정 계산:
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}")
분할 금액
잔여 금액을 여러 당사자에게 균등하게 나누어 처리하세요:
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
집계 작업
소수점 포함의 SUM과 AVG
SQL Server 집계 함수는 정밀도를 위해 십진법 타입을 반환합니다:
# 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 처리
SQL Server 집계 함수는 쿼리와 일치하는 행이 없을 때 반환 None 할 수 있습니다:
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")
십진수용 출력 변환기
출력 변환기는 ODBC SQL 정수가 아닌 키로 표시된 cursor.description Python 타입을 사용합니다.
decimal.Decimal에 변환기를 등록하여 모두 decimal.Decimal에 매핑되는 decimal, numeric, money, smallmoney 열에서 작동하도록 합니다.
부동소수점으로 변환(정밀도가 중요하지 않은 경우)
float 값을 기대하는 라이브러리와의 호환성을 위해 출력 변환기를 정의하세요:
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'>
사용자 지정 통화 형식 변환기
십진수 값을 화폐 문자열로 자동으로 포맷하는 맞춤형 출력 변환기를 정의하세요:
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"
일반적인 패턴
통화 환산
환율을 이용해 통화 간 통화 금액을 변환하기:
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}")
거래 내역과 잔액 업데이트
적절한 잠금 힌트가 있는 거래를 사용하여 원자 자금 이체를 보장하고 업데이트 손실을 방지하세요:
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"))
이체는 ID가 가장 낮은 것부터 계정을 정렬한 순서대로 각 계정에 대해 별도의 SELECT ... WITH (UPDLOCK)를 실행하여 두 행을 미리 잠급니다.
UPDLOCK 두 트랜잭션이 동일한 잔액을 읽고 오래된 데이터를 기반으로 업데이트를 적용하는 손실 업데이트 패턴을 방지합니다. 락을 정렬된 순서로 별도의 명세서로 발행하는 것이 획득 순서를 보장하며, 같은 두 계정 간 반대 이체가 역순으로 자물쇠를 획득하거나 교착 상태를 방지합니다.
소수점이 포함된 벌크 인서트
소수점 값 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()
모범 사례
재무 수학에서는 항상 십진수를 사용하세요
재무 계산에 유동주식을 절대 사용하지 마세요; 정밀도를 위해 항상 십진법 타입을 사용하세요:
# 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')
문자열 구성자 사용
부동소수점 오차를 피하려면 문자열에서 Decimal 값을 생성하세요:
# Good: exact value
amount = Decimal("123.45")
# Risky: might inherit float imprecision
amount = Decimal(123.45) # Could be 123.4500000000000028...
전시나 보관 전에 항상 둥글게 두세요
데이터베이스에 표시하거나 유지하기 전에 적절한 척도로 십진수 값을 반올림하세요:
from decimal import Decimal, ROUND_HALF_UP
calculated = Decimal("123.456789")
display = calculated.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
필요하면 십진수 문맥을 설정하세요
십진법 맥락 정밀도는 문자열에서 파싱하거나 데이터베이스에서 읽은 값이 아니라 계산 결과에 영향을 미칩니다. 쿼리는 getcontext().prec와 관계없이 항상 전체 저장 배율의 money 또는 decimal 값을 반환합니다. 맥락은 계산할 때만 적용됩니다. 정밀도가 효과를 내는지 확인하려면 나눗셈과 같은 연산을 수행하세요.
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)