Cơ sở dữ liệuSQL Server, phần 14/34

Tiền VND: decimal, money hay float, và làm tròn khớp giữa C# với SQL

Cột money phân bổ 19.999 đồng thay vì 20.000, cột float cộng một triệu lần lệch 34,61 đồng, và C# làm tròn 2,345 thành 2,34 trong khi SQL Server ra 2,35. Lưu và tính tiền bằng decimal(18, 2), làm tròn ở đúng bước nghiệp vụ với cùng một luật ở hai phía.

Mục lục
  1. 1. Vấn đề: ba con số tiền lệch nhau vài đồng
  2. 2. Mục đích: lệch 0 đồng ở cả ba chỗ
  3. 3. Cơ sở lý thuyết: số nhị phân, bốn chữ số lẻ và kiểu kết quả của decimal
  4. 4. Cách giải quyết: decimal(18, 2) và một luật làm tròn cho hai phía
  5. 5. Cách cài đặt: khai báo độ chính xác và một hàm làm tròn
  6. 6. Chứng minh: 20.000 đồng, lệch 0, và 2,35 ở cả hai phía
  7. 7. Kết luận
  8. Đọc tiếp
  9. Nguồn

Đọc nhanh

  • Vấn đề: Phân bổ chiết khấu 70.000 đồng của đơn 10042 bằng money cho dòng 500.000 ra 19.999 đồng, và cộng giá trước thuế bằng float một triệu lần lệch 34,61 đồng.
  • Cách giải: Lưu và tính tiền bằng decimal(18, 2), làm tròn một lần ở bước nghiệp vụ, và dùng cùng luật làm tròn nửa ra xa số 0 ở C# lẫn SQL Server.
  • Chứng minh: Phân bổ ra đúng 20.000, tổng ra đúng 1.620.370.370.000,00, và 2,345 thành 2,35 ở cả hai phía thay vì 2,34 và 2,35.
  • Trong .NET: HasPrecision(18, 2) kèm ConfigureWarnings ném lỗi khi quên khai báo, và Math.Round(x, 2, MidpointRounding.AwayFromZero).

1. Vấn đề: ba con số tiền lệch nhau vài đồng

Cuối tháng 10/2026, kế toán BanHang đối soát chiết khấu và doanh thu trước thuế, và các con số không khớp nhau từng đồng. Đơn 10042 trị giá 1.750.000 đồng, đã gồm thuế 8%, có chiết khấu 70.000 đồng chia vào các dòng theo tỷ lệ giá trị. Dòng 500.000 đồng phải nhận 500.000 / 1.750.000 × 70.000 = 20.000 đồng. Bảng phân bổ cũ lưu tiền bằng money và ghi 19.999.

Báo cáo doanh thu trước thuế cộng dồn trong biến float. Giá trước thuế của đơn 10042 là 1.750.000 / 1,08 = 1.620.370,37 đồng.

Cộng con số đó một triệu lần, tương đương khoảng 77 ngày đơn của BanHang, ra 1.620.370.370.034,61 thay vì 1.620.370.370.000,00: lệch 34,61 đồng.

Chỗ lệch thứ ba nằm giữa dịch vụ .NET và database. Cùng giá trị 2,345, Math.Round(x, 2) trong C# ra 2,34, còn ROUND(x, 2) trong thủ tục ra 2,35. Ứng dụng tính một số, database lưu một số khác, dù cả hai phía đều dùng decimal.

Ba chỗ cùng một gốc: kiểu dữ liệu và phép làm tròn đang quyết định thay cho nghiệp vụ.

2. Mục đích: lệch 0 đồng ở cả ba chỗ

  • Dòng 500.000 của đơn 10042 nhận đúng 20.000 đồng chiết khấu.
  • Cộng 1.620.370,37 một triệu lần ra đúng 1.620.370.370.000,00, lệch 0 đồng.
  • C# và SQL Server làm tròn 2,345 về hai chữ số lẻ ra cùng một số, và giá trị chưa làm tròn đi qua tham số của EF Core hay Dapper vào cột decimal(18, 2) cũng ra số đó.
  • Ngoài phạm vi: chia một khoản cho nhiều dòng sao cho tổng các dòng khớp từng đồng, ở bài Chia tiền không lệch một đồng.

3. Cơ sở lý thuyết: số nhị phân, bốn chữ số lẻ và kiểu kết quả của decimal

Kiểu Byte Bản chất
decimal(p, s) 5 (p 1–9), 9 (p 10–19), 13 (p 20–28), 17 (p 29–38) Số thập phân chính xác, p chữ số, s chữ số sau dấu phẩy
money 8 Chính xác tới 1/10.000 đơn vị tiền, khoảng ±922.337.203.685.477
float 8 Số nhị phân dấu phẩy động, 53 bit phần định trị, khoảng 15 chữ số có nghĩa

decimal(18, 2) của TongTien chứa tới 9.999.999.999.999.999,99, gần 10 triệu tỷ đồng, trong 9 byte.

float: mỗi phép cộng làm tròn thêm một lần

float lưu số theo cơ số 2. Số 0,37 bằng 37/100, mẫu số có thừa số 5, nên không có dạng nhị phân hữu hạn: 1.620.370,37 vào float đã là một số gần đúng. Kết quả mỗi phép cộng lại được làm tròn về số float gần nhất.

Khoảng cách giữa hai số float liền kề phụ thuộc độ lớn. Tổng 1,62 nghìn tỷ nằm giữa 2^40 và 2^41, nên khoảng cách là 2^(40 − 52) = 2^−12 ≈ 0,000244 đồng. Mỗi lần cộng làm tròn tối đa nửa khoảng đó:

sai số tối đa mỗi lần cộng ≈ 2^−12 / 2 ≈ 0,000122 đồng
sau 1.000.000 lần cộng       ≤ 1.000.000 × 0,000122 ≈ 122 đồng

Đo được 34,61 đồng, nằm trong giới hạn đó. Với VND, float biểu diễn đúng mọi số nguyên tới 2^53, khoảng 9 triệu tỷ, nên cộng tiền chẵn không lộ lỗi. Lỗi xuất hiện khi có phần lẻ: tách thuế, chiết khấu theo phần trăm, quy đổi ngoại tệ.

money: kết quả chia cũng chỉ có bốn chữ số lẻ

money lưu đúng tới 4 chữ số lẻ. Vấn đề nằm ở phép tính: money chia money cho ra money, nên tỷ lệ 0,285714… chỉ còn 0,2857 trước khi đi tiếp. Nhân 70.000 ra 19.999.

decimal: kiểu kết quả nới theo công thức

Phép chia hai decimal có precision và scale tính theo công thức trong tài liệu "Precision, scale, and length". Với hai vế decimal(18, 2):

chia:  scale     = max(6, s1 + p2 + 1)    = max(6, 2 + 18 + 1) = 21
       precision = p1 − s1 + s2 + scale   = 18 − 2 + 2 + 21    = 39
       39 > 38, phần nguyên 39 − 21 = 18 < 32
       → scale = min(21, 38 − 18) = 20    → decimal(38, 20)
nhân:  decimal(38, 20) × decimal(18, 2): precision = 57, scale = 22
       phần nguyên 57 − 22 = 35 > 32
       → scale đặt về 6                   → decimal(38, 6)

SQL_VARIANT_PROPERTY trên LocalDB cho đúng decimal(38, 20) và decimal(38, 6). Tỷ lệ in ra là 0,28571428571428571428: phần sau chữ số lẻ thứ 20 bị bỏ, và 6 chữ số lẻ còn lại sau phép nhân thừa đủ cho VND.

500.000 ÷ 1.750.000 × 70.000, kết quả đúng là 20.000 Kiểu của ba giá trị Bước chia: tỷ lệ Bước nhân: phần phân bổ money chia ra vẫn là money 0,2857 còn 4 chữ số lẻ 19.999,0000 thiếu 1 đồng decimal(18, 2) kiểu kết quả nới ra 0,28571428571428571428 decimal(38, 20) 20.000,000000 decimal(38, 6) Giá trị và kiểu do Tien.cs in ra trên SQL Server 2019 LocalDB.
Chỗ làm tròn nằm ở kiểu của kết quả trung gian, không ở kiểu của cột.

Làm tròn nửa ở C# và ở SQL Server

decimal của C# giữ 28 đến 29 chữ số, nên 500000m / 1750000m * 70000m ra 19.999,999999999999999999999999 chứ không ra 20.000. Kết quả vẫn phải làm tròn.

Hai phía làm tròn giá trị nằm đúng giữa theo hai luật khác nhau. ROUND và CAST sang decimal của SQL Server làm tròn nửa ra xa số 0: CAST(2.345 AS decimal(5, 2)) ra 2,35. Math.Round của .NET mặc định làm tròn nửa về số chẵn (MidpointRounding.ToEven): Math.Round(2.345m, 2) ra 2,34. Giá trị chưa làm tròn gửi qua tham số vào cột decimal(18, 2) được làm tròn như SQL Server, ra 2,35, cả khi đi qua EF Core lẫn Dapper với Microsoft.Data.SqlClient 6.1.6.

4. Cách giải quyết: decimal(18, 2) và một luật làm tròn cho hai phía

Cách lưu tiền Byte Phép cộng Phép chia Khi nào dùng
float 8 Cộng dồn sai số khi có phần lẻ Gần đúng Số đo khoa học, tọa độ, điểm số mô hình
money 8 Đúng Kết quả trung gian còn 4 chữ số lẻ Không dùng cho cột mới
bigint hoặc decimal(18, 0) đếm đồng chẵn 8 hoặc 9 Đúng bigint chia bigint cắt bỏ phần lẻ; không có chỗ cho phần lẻ của thuế hay ngoại tệ Nghiệp vụ chắc chắn chỉ có đồng chẵn
decimal(18, 2) 9 Đúng Kiểu kết quả nới tới decimal(38, s), còn ít nhất 6 chữ số lẻ Mọi cột tiền VND của BanHang

BanHang chọn decimal(18, 2) cho mọi cột tiền, như TongTien và DonGia. Tỷ lệ như thuế suất dùng decimal(5, 4), ví dụ 0.0800. Giữ (18, 2) thay vì (18, 0) cùng 9 byte để mọi cột tiền cùng một kiểu và phần lẻ của thuế hoặc ngoại tệ có chỗ.

Kiểu đúng chưa đủ, vì decimal vẫn cho ra 19.999,999… ở C#. Ba quy tắc đi kèm:

  1. Tính bằng decimal từ đầu đến cuối, không qua biến hay cột money, float.
  2. Làm tròn đúng một lần, ở bước nghiệp vụ quy định: phần phân bổ của dòng, tiền thuế của hóa đơn. Trong SQL dùng CAST(... AS decimal(18, 2)) hoặc ROUND, không để kiểu trung gian quyết định.
  3. Ở C#, luôn truyền MidpointRounding.AwayFromZero để khớp SQL Server. Bọc nó trong một hàm dùng chung thay vì gọi Math.Round rải rác.

5. Cách cài đặt: khai báo độ chính xác và một hàm làm tròn

Trong SQL, phần phân bổ tính bằng decimal rồi làm tròn một lần khi ghi:

DECLARE @DongTien decimal(18, 2) = 500000, @TongDon decimal(18, 2) = 1750000,
        @ChietKhau decimal(18, 2) = 70000;

SELECT CAST(@DongTien / @TongDon * @ChietKhau AS decimal(18, 2)) AS phan_bo;   -- 20000.00

Trong EF Core, mọi thuộc tính tiền khai báo độ chính xác, và cảnh báo DecimalTypeDefaultWarning thành lỗi để thuộc tính bị quên làm model dừng lúc khởi động:

protected override void OnConfiguring(DbContextOptionsBuilder o) => o
    .UseSqlServer(cs)
    .ConfigureWarnings(w => w.Throw(SqlServerEventId.DecimalTypeDefaultWarning));

protected override void OnModelCreating(ModelBuilder b) => b.Entity<DonHang>(e =>
    e.Property(d => d.TongTien).HasPrecision(18, 2));   // decimal(18, 2)

// Làm tròn ở ứng dụng theo cùng luật với ROUND và CAST của SQL Server
static decimal LamTronTien(decimal soTien) => Math.Round(soTien, 2, MidpointRounding.AwayFromZero);

var phanBo = LamTronTien(500_000m / 1_750_000m * 70_000m);   // 20000.00

Với database đã có, bước đầu là biết cột tiền nào còn mang money hay float. Câu dưới liệt kê mọi cột kiểu đó trong bảng của người dùng; trên database thử, nó tìm ra DonHangTienTay.TongTien money của bài chọn kiểu cột:

SELECT OBJECT_NAME(c.object_id) AS bang, c.name AS cot, t.name AS kieu
FROM sys.columns AS c
JOIN sys.tables AS tb ON tb.object_id = c.object_id
JOIN sys.types AS t ON t.user_type_id = c.user_type_id
WHERE t.name IN ('money', 'smallmoney', 'float', 'real')
ORDER BY bang, cot;

Chương trình Tien.cs chạy cả ba phép thử trên LocalDB: vòng cộng một triệu lần, phép phân bổ qua money và decimal kèm kiểu kết quả, và làm tròn ở C#, ở SQL Server và qua tham số. Môi trường: .NET 10.0.12, Microsoft.EntityFrameworkCore.SqlServer 10.0.12 (kéo theo Microsoft.Data.SqlClient 6.1.6), Dapper 2.1.89, SQL Server 2019 LocalDB 15.0.4382, Intel Core Ultra 5 125U. Tạo trước database trống Kumeo_D, rồi chạy dotnet run Tien.cs; vòng cộng mất vài giây. Đầu file có #:property PublishAot=false vì file-based app bật AOT mặc định, còn EF Core và Dapper sinh code lúc chạy.

Cộng 1.000.000 lần: float 1620370370034.61, decimal 1620370370000.00, lệch 34.61
money:   tỷ lệ 0.2857, phân bổ 19999.0000
decimal: tỷ lệ 0.28571428571428571428 [decimal(38, 20)], phân bổ 20000.000000 [decimal(38, 6)]
SQL Server: CAST(2.345 AS decimal(5, 2)) = 2.35, ROUND(2.345, 2) = 2.350
C#: Math.Round(2.345m, 2) = 2.34, AwayFromZero = 2.35
C#: 500000m / 1750000m * 70000m = 19999.999999999999999999999999, làm tròn 2 chữ số = 20000.00
Ghi 2.345m qua EF Core vào decimal(18, 2): 2.35
Ghi 2.345m qua Dapper  vào decimal(18, 2): 2.35
Tien.cs: float, money và decimal trên SQL Server, làm tròn ở C# và qua tham sốC# · 106 dòng
#:package Microsoft.EntityFrameworkCore.SqlServer@10.0.12
#:package Dapper@2.1.89
#:property PublishAot=false
// Tiền VND: float cộng dồn sai số, money làm tròn kết quả chia, decimal(18, 2) giữ đúng;
// kiểu kết quả của phép chia và nhân decimal; làm tròn nửa ở C#, ở SQL Server và ở tham số.
using System.Reflection;
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

using var conn = new SqlConnection(Db.Cs);
conn.Open();
Console.WriteLine($"Microsoft.Data.SqlClient {typeof(SqlConnection).Assembly.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>()!.InformationalVersion}");

// 1. Cộng giá trước thuế của đơn 10042 (1.750.000 / 1,08) một triệu lần bằng float và decimal(18, 2).
var cong = conn.QuerySingle<(decimal TongFloat, decimal TongDecimal, decimal ChenhLech)>("""
    SET NOCOUNT ON;
    DECLARE @f float = 0, @d decimal(18, 2) = 0, @i int = 0;
    WHILE @i < 1000000
    BEGIN
        SET @f += 1620370.37;
        SET @d += 1620370.37;
        SET @i += 1;
    END;
    SELECT CAST(@f AS decimal(18, 2)), @d, CAST(@f AS decimal(18, 2)) - @d;
    """, commandTimeout: 300);
Console.WriteLine($"Cộng 1.000.000 lần: float {cong.TongFloat}, decimal {cong.TongDecimal}, lệch {cong.ChenhLech}");

// 2. Phân bổ chiết khấu 70.000 cho dòng 500.000 của đơn 1.750.000, và kiểu của từng bước.
var pb = conn.QuerySingle<(decimal TyLeMoney, decimal PhanBoMoney, decimal TyLeDecimal, decimal PhanBoDecimal, string KieuChia, string KieuNhan, string PhanBoFloat)>("""
    DECLARE @mDong money = 500000, @mTong money = 1750000, @mCk money = 70000;
    DECLARE @dDong decimal(18, 2) = 500000, @dTong decimal(18, 2) = 1750000, @dCk decimal(18, 2) = 70000;
    DECLARE @fDong float = 500000, @fTong float = 1750000, @fCk float = 70000;
    SELECT
        @mDong / @mTong,
        @mDong / @mTong * @mCk,
        @dDong / @dTong,
        @dDong / @dTong * @dCk,
        CONCAT('decimal(', CAST(SQL_VARIANT_PROPERTY(@dDong / @dTong, 'Precision') AS int), ', ',
               CAST(SQL_VARIANT_PROPERTY(@dDong / @dTong, 'Scale') AS int), ')'),
        CONCAT('decimal(', CAST(SQL_VARIANT_PROPERTY(@dDong / @dTong * @dCk, 'Precision') AS int), ', ',
               CAST(SQL_VARIANT_PROPERTY(@dDong / @dTong * @dCk, 'Scale') AS int), ')'),
        CONVERT(varchar(40), @fDong / @fTong * @fCk, 3);
    """);
Console.WriteLine($"money:   tỷ lệ {pb.TyLeMoney}, phân bổ {pb.PhanBoMoney}");
Console.WriteLine($"decimal: tỷ lệ {pb.TyLeDecimal} [{pb.KieuChia}], phân bổ {pb.PhanBoDecimal} [{pb.KieuNhan}]");
Console.WriteLine($"float:   phân bổ {pb.PhanBoFloat}");

// 3. Làm tròn nửa: SQL Server làm tròn nửa ra xa số 0, Math.Round mặc định làm tròn về số chẵn.
var sql = conn.QuerySingle<(decimal Cast, decimal Round)>("SELECT CAST(2.345 AS decimal(5, 2)), ROUND(2.345, 2);");
Console.WriteLine($"SQL Server: CAST(2.345 AS decimal(5, 2)) = {sql.Cast}, ROUND(2.345, 2) = {sql.Round}");
Console.WriteLine($"C#: Math.Round(2.345m, 2) = {Math.Round(2.345m, 2)}, AwayFromZero = {Math.Round(2.345m, 2, MidpointRounding.AwayFromZero)}");

var phanBo = 500_000m / 1_750_000m * 70_000m;
Console.WriteLine($"C#: 500000m / 1750000m * 70000m = {phanBo}, làm tròn 2 chữ số = {Math.Round(phanBo, 2, MidpointRounding.AwayFromZero)}");

// 4. Giá trị chưa làm tròn đi qua tham số vào cột decimal(18, 2): EF Core (HasPrecision) và Dapper.
conn.Execute("DROP TABLE IF EXISTS dbo.ButToanThu; CREATE TABLE dbo.ButToanThu (Id int IDENTITY PRIMARY KEY, Nguon varchar(20) NOT NULL, SoTien decimal(18, 2) NOT NULL);");
using (var db = new TienDb())
{
    db.Add(new ButToanThu { Nguon = "EF Core", SoTien = 2.345m });
    db.SaveChanges();
}
conn.Execute("INSERT dbo.ButToanThu (Nguon, SoTien) VALUES ('Dapper', @SoTien);", new { SoTien = 2.345m });
foreach (var (nguon, soTien) in conn.Query<(string, decimal)>("SELECT Nguon, SoTien FROM dbo.ButToanThu ORDER BY Id;"))
    Console.WriteLine($"Ghi 2.345m qua {nguon,-7} vào decimal(18, 2): {soTien}");

// 5. Thuộc tính tiền quên khai báo độ chính xác: model lỗi ngay khi dựng, không đợi tới lúc ghi.
try { using var quen = new QuenKhaiBaoDb(); _ = quen.Model; }
catch (InvalidOperationException ex) { Console.WriteLine($"QuenKhaiBaoDb: {ex.GetType().Name}: {ex.Message[..ex.Message.IndexOf('\'', ex.Message.IndexOf('\'') + 1)]}'"); }
conn.Execute("DROP TABLE dbo.ButToanThu;");

class ButToanThu
{
    public int Id { get; set; }
    public string Nguon { get; set; } = "";
    public decimal SoTien { get; set; }
}

class TienDb : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder o) => o
        .UseSqlServer(Db.Cs)
        .ConfigureWarnings(w => w.Throw(SqlServerEventId.DecimalTypeDefaultWarning));
    protected override void OnModelCreating(ModelBuilder b) => b.Entity<ButToanThu>(e =>
    {
        e.ToTable("ButToanThu");
        e.Property(t => t.Nguon).IsUnicode(false).HasMaxLength(20);
        e.Property(t => t.SoTien).HasPrecision(18, 2);
    });
}

// Cùng lớp, quên HasPrecision: ConfigureWarnings biến cảnh báo thành lỗi.
class QuenKhaiBaoDb : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder o) => o
        .UseSqlServer(Db.Cs)
        .ConfigureWarnings(w => w.Throw(SqlServerEventId.DecimalTypeDefaultWarning));
    protected override void OnModelCreating(ModelBuilder b) => b.Entity<ButToanThu>().ToTable("ButToanThu");
}

static class Db
{
    public const string Cs = @"Server=(localdb)\MSSQLLocalDB;Database=Kumeo_D;Integrated Security=true;TrustServerCertificate=true";
}

6. Chứng minh: 20.000 đồng, lệch 0, và 2,35 ở cả hai phía

Cùng một máy, cùng một lần chạy Tien.cs, so từng tiêu chí của mục 2:

Tiêu chí Trước Sau
Phân bổ 70.000 cho dòng 500.000 money: 19.999,0000 decimal(18, 2): 20.000,000000
Cộng 1.620.370,37 một triệu lần float: 1.620.370.370.034,61, lệch 34,61 decimal(18, 2): 1.620.370.370.000,00, lệch 0
Làm tròn 2,345 về hai chữ số lẻ C# Math.Round mặc định 2,34; SQL Server 2,35 C# AwayFromZero 2,35; SQL Server 2,35
Giá trị 2,345 đi qua tham số vào decimal(18, 2) EF Core 2,35; Dapper 2,35

float lệch 34,61 đồng sau một triệu lần cộng, money thiếu 1 đồng ở một lần phân bổ, decimal lệch 0

float, cộng 1 triệu lần34,61 đồngdecimal(18, 2), cộng 1 triệu lần0,00 đồngmoney, phân bổ một dòng1,00 đồngdecimal(18, 2), phân bổ một dòng0,00 đồng
Độ lệch so với số đúng, theo bảng trên. Tien.cs, SQL Server 2019 LocalDB 15.0.4382, .NET 10.0.12.
Bảng số liệu
Giá trị
float, cộng 1 triệu lần34,61 đồng
decimal(18, 2), cộng 1 triệu lần0,00 đồng
money, phân bổ một dòng1,00 đồng
decimal(18, 2), phân bổ một dòng0,00 đồng

Phép phân bổ qua decimal ra 20.000,000000 vì kiểu kết quả giữ 20 chữ số lẻ ở bước chia, đúng như công thức ở mục 3 dự đoán. Vòng cộng decimal(18, 2) lệch 0 vì 1.620.370,37 có đúng 2 chữ số lẻ, biểu diễn chính xác, và tổng 1,6 nghìn tỷ vẫn nằm trong 16 chữ số phần nguyên của kiểu. Bản float lệch 34,61 đồng, trong giới hạn 122 đồng tính ở mục 3. Cùng tổng trên float, thứ tự cộng khác (ví dụ một kế hoạch song song) có thể cho chữ số cuối khác; điều này chưa đo trong bài.

Hàng làm tròn cho thấy chỗ lệch ở ranh giới là do luật, không do kiểu: cả hai phía đều là decimal, chỉ MidpointRounding khác. Truyền AwayFromZero ở C# là đủ để hai phía ra cùng 2,35. Tham số decimal(18, 2) của SqlClient 6.1.6 cũng làm tròn nửa ra xa số 0, nên giá trị chưa làm tròn gửi xuống vẫn khớp ROUND của SQL Server. Kết quả đó là của phiên bản driver đã thử; đổi driver thì chạy lại Tien.cs.

7. Kết luận

Tiền VND lưu và tính bằng decimal(18, 2); money làm tròn kết quả chia, float cộng dồn sai số. Kiểu đúng rồi vẫn phải làm tròn đúng một lần, với cùng một luật ở C# và SQL Server.

Trong dự án .NET của bạn:

  • Khai báo HasPrecision(18, 2) cho mọi thuộc tính tiền, và ConfigureWarnings(w => w.Throw(SqlServerEventId.DecimalTypeDefaultWarning)) để thuộc tính bị quên làm model lỗi ngay.
  • Đặt một hàm làm tròn tiền dùng chung với MidpointRounding.AwayFromZero, và cấm Math.Round(x, 2) không có tham số thứ ba trong code review.
  • Chạy câu liệt kê cột money và float ở mục 5, rồi lên kế hoạch đổi các cột tiền sang decimal(18, 2).

Những chỗ hay hiểu sai

  • "money là kiểu dành cho tiền nên chính xác cho tiền." Nó lưu đúng 4 chữ số lẻ, nhưng money chia money làm tròn kết quả trung gian về 4 chữ số lẻ.
  • "float có 15 chữ số có nghĩa nên đủ cho tiền." Một triệu lần cộng một số có phần lẻ đã lệch 34,61 đồng.
  • "decimal của C# không cần làm tròn." Phép chia vẫn ra 19.999,999…; làm tròn ở bước nghiệp vụ.
  • "Math.Round(2.345m, 2) ra 2,35." Mặc định nó làm tròn nửa về số chẵn, ra 2,34.

Đọc tiếp

Nguồn

Đọc tiếp

Bài tiếp theo trong series

Tìm "nguyen" ra "Nguyễn" mà vẫn Index Seek

Gõ "nguyen thi anh" vào ô tìm khách thì ra 0 người, sửa nhanh bằng COLLATE trong WHERE thì đọc cả bảng 1.144 page. Cột tính toán mang collation Latin1_General_100_CI_AI có chỉ mục tìm đủ 34 khách bằng Index Seek, 3 page, khai báo được ngay trong EF Core.

10 phút đọc

Trong SQL Server

Kiểu tham số từ .NET làm truy vấn quét cả bảng

Tra khách theo số điện thoại với tham số long đọc 1.388 page thay vì 6, và trên cột collation SQL, chuỗi mặc định của Dapper, AddWithValue hay EF Core đọc 602 page. Tham số khai báo đúng kiểu và độ dài của cột seek 6 page ở mọi thư viện, mọi collation.

10 phút đọc

Trong SQL Server

Đếm đơn theo ngày: BETWEEN, mốc .999 và giờ UTC

Báo cáo ngày 01/10 viết BETWEEN tới 23:59:59.999 qua Dapper đếm cả đơn tạo lúc nửa đêm hôm sau, còn mốc DateTimeOffset +07:00 làm khoảng lệch 7 giờ. Khoảng nửa mở với hai mốc cùng quy ước giờ với cột đếm đúng 2 đơn và vẫn seek 7 page.

10 phút đọc

Trong SQL Server

Chọn kiểu cột: mỗi byte nhân lên 10 triệu dòng

Bảng đơn hàng khai báo tiện tay bằng GUID, datetime, int và money cần thêm 27.078 page lá, khoảng 212 MiB, cho 10 triệu đơn. Chọn kiểu hẹp nhất đủ cho nghiệp vụ, khai báo ngay trong model EF Core, rồi đo lại đúng tới từng page.

12 phút đọc