Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Le pilote mssql-python associe les données décimales, numériques et monétaires de SQL Server à Python decimal.Decimal pour des calculs financiers et scientifiques précis. SQL Server fournit les types numériques précis suivants :
| Type SQL | type de Python | Précision | Cas d’usage |
|---|---|---|---|
| decimal(p,s) | decimal.Decimal |
1 à 38 chiffres | Calculs exacts |
| numeric(p,s) | decimal.Decimal |
1 à 38 chiffres | Même que la décimale |
| argent | decimal.Decimal |
19 chiffres, 4 décimales | Monnaie |
| smallmoney | decimal.Decimal |
10 chiffres, 4 décimales | Petite monnaie |
| float | float |
~15 chiffres | Approximatif |
| real | float |
~7 chiffres | Approximatif |
Type décimal Python
Recevoir des valeurs décimales
Les colonnes décimales retournent les objets 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')
Envoyer des valeurs décimales
Transmettez des objets Decimal pour une insertion précise :
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()
Éviter le flottement pour les données financières
Les types de flotteurs présentent des problèmes de précision qui les rendent inadaptés aux calculs financiers. Utilisez Decimal toujours pour obtenir des résultats précis :
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
Type d’argent
Utiliser des colonnes monétaires
Récupérer et mettre à jour les colonnes d’argent de SQL Server en utilisant des objets décimaux Python :
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()
Formatage de l’argent
Formatez les valeurs décimales comme des chaînes de monnaie avec des symboles et des séparateurs de virgules :
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
Précision décimale et échelle
Comprendre la précision et l’échelle
- Précision (p) : nombre total de chiffres
- Échelle(s) : Nombre de chiffres après la virgule décimale
Par exemple, decimal(10, 2) peut stocker des valeurs de -99999999,99 à 99999999,99, tandis qu’il decimal(5, 4) peut stocker des valeurs de -9,9999 à 9,9999.
Spécifier la précision dans les paramètres
Le pilote mssql-python déduit automatiquement la précision et l’échelle à partir des valeurs décimales :
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()
Arrondi contrôlé
Utilisez la quantize() méthode pour arrondir les valeurs décimales à une échelle spécifique :
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')
Calculs financiers
Arithmétique de sécurité
Effectuez des calculs financiers en utilisant décimal avec un arrondi approprié :
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")})
Calculs de pourcentages
Calculez les remises et ajustements basés sur le pourcentage :
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}")
Montants partagés
Divisez un montant total équitablement entre plusieurs parties, en gérant les restes :
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
Opérations d’agrégation
SUM et AVG avec décimales
Les fonctions d’agrégation de SQL Server retournent les types décimaux pour la précision :
# 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"))
Gérer NULL dans les agrégations
Les fonctions d’agrégation de SQL Server peuvent renvoyer None lorsqu’aucune ligne ne correspond à la requête :
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")
Convertisseurs de sortie pour les nombres décimaux
Les convertisseurs de sortie utilisent le type Python indiqué dans cursor.description comme clé, et non l’entier du type SQL ODBC. Enregistrez le convertisseur avec decimal.Decimal pour qu’il fonctionne pour decimal, numeric, money, et smallmoney les colonnes, qui correspondent tous à decimal.Decimal.
Convertir en flotteur (quand la précision n’est pas critique)
Pour la compatibilité avec les bibliothèques qui attendent des valeurs de flottement, définissez un convertisseur de sortie :
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'>
Convertisseur de format monétaire personnalisé
Définissez un convertisseur de sortie personnalisé pour formater automatiquement les valeurs décimales en chaînes de monnaie :
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"
Modèles courants
Conversion de devise
Convertir les montants monétaires entre devises en utilisant les taux de change :
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}")
Mise à jour du solde à l’aide des transactions
Utilisez des transactions avec des indices de verrouillage appropriés pour garantir les transferts de fonds atomiques et éviter les mises à jour perdues :
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"))
Le transfert verrouille d’emblée les deux lignes, en commençant par l’ID le plus bas, en émettant un SELECT ... WITH (UPDLOCK) distinct pour chaque compte dans l’ordre de tri.
UPDLOCK Empêche le schéma de mises à jour perdues où deux transactions lisent le même solde et appliquent des mises à jour basées sur des données obsolètes. Émettre les verrous sous forme d’instructions distinctes dans un ordre trié est ce qui garantit l’ordre d’acquisition, empêchant ainsi des transferts opposés entre les deux mêmes comptes d’acquérir les verrous dans l’ordre inverse et de provoquer un interblocage.
Insertion en masse avec des nombres décimaux
Insérer plusieurs lignes avec des valeurs décimales en utilisant 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()
Bonnes pratiques
Utilisez toujours Decimal pour les calculs financiers
N’utilisez jamais le float pour les calculs financiers ; Utilisez toujours le type décimal pour la précision :
# 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')
Utilisez des constructeurs de chaînes
Construisez des valeurs décimales à partir de chaînes pour éviter l’imprécision de flottement :
# Good: exact value
amount = Decimal("123.45")
# Risky: might inherit float imprecision
amount = Decimal(123.45) # Could be 123.4500000000000028...
Toujours arrondir avant d’afficher ou de stocker
Arrondir les valeurs décimales à l’échelle appropriée avant d’afficher ou de les faire persister dans la base de données :
from decimal import Decimal, ROUND_HALF_UP
calculated = Decimal("123.456789")
display = calculated.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
Définir le contexte décimal si nécessaire
La précision décimale du contexte affecte les résultats des calculs, pas les valeurs analysées à partir de chaînes ou lues dans la base de données. Une requête retourne toujours une money valeur ou decimal avec son échelle complète stockée, indépendamment de getcontext().prec. Le contexte ne prend effet que lorsque vous calculez avec. Pour voir la précision prendre effet, effectuez une opération telle que la division.
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)