Tại sao 199,96 - 0 = 200 trong SQL?


84

Tôi có một số khách hàng nhận được những hóa đơn kỳ lạ. Tôi đã có thể tách ra vấn đề cốt lõi:

SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 200 what the?
SELECT 199.96 - (0.0 * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96)) -- 199.96

SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4))))                         -- 199.96
SELECT 199.96 - (CAST(0.0 AS DECIMAL(19, 4)) * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96))                         -- 199.96

-- It gets weirder...
SELECT (0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))) -- 0
SELECT (0 * FLOOR(1.0 * CAST(199.96 AS DECIMAL(19, 4))))                         -- 0
SELECT (0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * 199.96))                         -- 0

-- so... ... 199.06 - 0 equals 200... ... right???
SELECT 199.96 - 0 -- 199.96 ...NO....

Có ai manh mối không, cái quái gì đang xảy ra ở đây? Ý tôi là, nó chắc chắn có liên quan gì đó đến kiểu dữ liệu thập phân, nhưng tôi thực sự không thể quấn lấy nó ...


Có rất nhiều sự nhầm lẫn về kiểu dữ liệu của các chữ số, vì vậy tôi quyết định hiển thị dòng thực:

PS.SharePrice - (CAST((@InstallmentCount - 1) AS DECIMAL(19, 4)) * CAST(FLOOR(@InstallmentPercent * PS.SharePrice) AS DECIMAL(19, 4))))

PS.SharePrice DECIMAL(19, 4)

@InstallmentCount INT

@InstallmentPercent DECIMAL(19, 4)

Tôi đảm bảo rằng kết quả của mỗi phép toán có toán hạng khác kiểu được ép kiểu DECIMAL(19, 4)rõ ràng trước khi áp dụng nó vào ngữ cảnh bên ngoài.

Tuy nhiên, kết quả vẫn còn 200.00.


Bây giờ tôi đã tạo một mẫu tổng hợp mà bạn có thể thực hiện trên máy tính của mình.

DECLARE @InstallmentIndex INT = 1
DECLARE @InstallmentCount INT = 1
DECLARE @InstallmentPercent DECIMAL(19, 4) = 1.0
DECLARE @PS TABLE (SharePrice DECIMAL(19, 4))
INSERT INTO @PS (SharePrice) VALUES (599.96)

-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * PS.SharePrice),
  1999.96)
FROM @PS PS

-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * CAST(599.96 AS DECIMAL(19, 4))),
  1999.96)
FROM @PS PS

-- 1996.96
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * 599.96),
  1999.96)
FROM @PS PS

-- Funny enough - with this sample explicitly converting EVERYTHING to DECIMAL(19, 4) - it still doesn't work...
-- 2000
SELECT
  IIF(@InstallmentIndex < @InstallmentCount,
  FLOOR(@InstallmentPercent * CAST(199.96 AS DECIMAL(19, 4))),
  CAST(1999.96 AS DECIMAL(19, 4)))
FROM @PS PS

Bây giờ tôi có một cái gì đó ...

-- 2000
SELECT
  IIF(1 = 2,
  FLOOR(CAST(1.0 AS decimal(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))),
  CAST(1999.96 AS DECIMAL(19, 4)))

-- 1999.9600
SELECT
  IIF(1 = 2,
  CAST(FLOOR(CAST(1.0 AS decimal(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))) AS INT),
  CAST(1999.96 AS DECIMAL(19, 4)))

Cái quái gì - tầng nào cũng phải trả về một số nguyên. Những gì đang xảy ra ở đây? :-D


Tôi nghĩ rằng bây giờ tôi đã quản lý để thực sự đun sôi nó đến tận cùng bản chất :-D

-- 1.96
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (36, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

-- 2.0
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (37, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

-- 2
SELECT IIF(1 = 2,
  CAST(1.0 AS DECIMAL (38, 0)),
  CAST(1.96 AS DECIMAL(19, 4))
)

4
@Sliverdust 199,96 -0 không bằng 200. Tất cả các phôi và tầng có chuyển đổi ngầm định sang dấu phẩy động và ngược lại đều được đảm bảo sẽ dẫn đến mất độ chính xác.
Panagiotis Kanavos,

1
@Silverdust chỉ khi nó đến từ một bảng. Là một chữ trong một biểu thức nó có thể là mộtfloat
Panagiotis Kanavos

1
Oh ... và Floor()không không quay trở lại một int. Nó trả về cùng kiểu với biểu thức ban đầu , với phần thập phân bị loại bỏ. Đối với phần còn lại, IIF()hàm dẫn đến loại được ưu tiên cao nhất ( docs.microsoft.com/en-us/sql/t-sql/functions/… ). Vì vậy, mẫu thứ 2 mà bạn ép kiểu thành int, mức độ ưu tiên cao hơn là kiểu ép kiểu đơn giản dưới dạng số (19,4).
Joel Coehoorn,

1
Câu trả lời tuyệt vời (ai biết bạn có thể kiểm tra siêu dữ liệu của một biến thể sql?) Nhưng vào năm 2012, tôi nhận được kết quả như mong đợi (199,96).
benjamin moskovits

2
Tôi không quá quen thuộc với MS SQL, nhưng tôi phải nói rằng nhìn vào tất cả những hoạt động dàn diễn viên và vân vân nhanh chóng hút sự chú ý của tôi .. vì vậy tôi phải liên kết này bởi vì không ai nên bao giờ được sử dụng floatcác loại ing-point để xử lý tiền tệ .
code_dredd

Câu trả lời:


78

Tôi cần bắt đầu bằng cách mở nó ra một chút để tôi có thể thấy những gì đang xảy ra:

SELECT 199.96 - 
    (
        0.0 * 
        FLOOR(
            CAST(1.0 AS DECIMAL(19, 4)) * 
            CAST(199.96 AS DECIMAL(19, 4))
        )
    ) 

Bây giờ chúng ta hãy xem chính xác những kiểu SQL Server đang sử dụng cho mỗi bên của hoạt động trừ:

SELECT  SQL_VARIANT_PROPERTY (199.96     ,'BaseType'),
    SQL_VARIANT_PROPERTY (199.96     ,'Precision'),
    SQL_VARIANT_PROPERTY (199.96     ,'Scale')

SELECT  SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'BaseType'),
    SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'Precision'),
    SQL_VARIANT_PROPERTY (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4)))  ,'Scale')

Các kết quả:

số 5 2
số 38 1

Như vậy 199.96numeric(5,2)và lâu hơn Floor(Cast(etc))numeric(38,1).

Các quy tắc cho độ chính xác và tỷ lệ kết quả của một phép toán trừ (ví dụ e1 - e2:) trông như sau:

Độ chính xác: max (s1, s2) + max (p1-s1, p2-s2) + 1
Thang đo: max (s1, s2)

Điều đó đánh giá như thế này:

Độ chính xác: tối đa (1,2) + tối đa (38-1, 5-2) + 1 => 2 + 37 + 1 => 40
Tỷ lệ: tối đa (1,2) => 2

Bạn cũng có thể sử dụng liên kết quy tắc để tìm ra nơi numeric(38,1)xuất phát ngay từ đầu (gợi ý: bạn đã nhân hai giá trị chính xác 19).

Nhưng:

  • Độ chính xác của kết quả và tỷ lệ có tối đa tuyệt đối là 38. Khi độ chính xác của kết quả lớn hơn 38, nó sẽ giảm xuống 38 và tỷ lệ tương ứng được giảm xuống để cố gắng ngăn phần tích phân của kết quả bị cắt bớt. Trong một số trường hợp như nhân hoặc chia, hệ số tỷ lệ sẽ không bị giảm để giữ độ chính xác của phần thập phân, mặc dù lỗi tràn có thể tăng lên.

Giáo sư. Độ chính xác là 40. Chúng ta phải giảm nó, và vì việc giảm độ chính xác nên luôn luôn cắt bỏ các chữ số có nghĩa nhỏ nhất, đồng nghĩa với việc giảm tỷ lệ. Loại kết quả cuối cùng cho biểu thức sẽ là numeric(38,0), cho các 199.96vòng 200.

Bạn có thể sửa lỗi này bằng cách di chuyển và hợp nhất các CAST()phép toán từ bên trong biểu thức lớn thành một phép toán CAST() xung quanh toàn bộ kết quả biểu thức. Vì vậy, điều này:

SELECT 199.96 - 
    (
        0.0 * 
        FLOOR(
            CAST(1.0 AS DECIMAL(19, 4)) * 
            CAST(199.96 AS DECIMAL(19, 4))
        )
    ) 

Trở thành:

SELECT CAST( 199.96 - ( 0.0 * FLOOR(1.0 * 199.96) ) AS decimial(19,4))

Tôi thậm chí có thể loại bỏ dàn diễn viên bên ngoài.

Chúng ta học ở đây, chúng ta nên chọn các loại để phù hợp với độ chính xác và tỷ lệ mà chúng ta thực sự có ngay bây giờ , hơn là kết quả mong đợi. Sẽ không hợp lý nếu chỉ đi tìm các số chính xác lớn, bởi vì SQL Server sẽ thay đổi các kiểu đó trong các hoạt động số học để cố gắng tránh tràn.


Thêm thông tin:


20

Theo dõi các kiểu dữ liệu liên quan cho câu lệnh sau:

SELECT 199.96 - (0.0 * FLOOR(CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))))
  1. NUMERIC(19, 4) * NUMERIC(19, 4)NUMERIC(38, 7)(xem bên dưới)
    • FLOOR(NUMERIC(38, 7))NUMERIC(38, 0)(xem bên dưới)
  2. 0.0NUMERIC(1, 1)
    • NUMERIC(1, 1) * NUMERIC(38, 0)NUMERIC(38, 1)
  3. 199.96NUMERIC(5, 2)
    • NUMERIC(5, 2) - NUMERIC(38, 1)NUMERIC(38, 1)(xem bên dưới)

Điều này giải thích tại sao bạn kết thúc bằng 200.0( một chữ số sau số thập phân, không phải số 0 ) thay vì 199.96.

Ghi chú:

FLOORtrả về số nguyên lớn nhất nhỏ hơn hoặc bằng biểu thức số được chỉ định và kết quả có cùng kiểu với đầu vào. Nó trả về INT cho INT, FLOAT cho FLOAT và NUMERIC (x, 0) cho NUMERIC (x, y).

Theo thuật toán :

Operation | Result precision                    | Result scale*
e1 * e2   | p1 + p2 + 1                         | s1 + s2
e1 - e2   | max(s1, s2) + max(p1-s1, p2-s2) + 1 | max(s1, s2)

* Độ chính xác của kết quả và tỷ lệ có tối đa tuyệt đối là 38. Khi độ chính xác của kết quả lớn hơn 38, tỷ lệ này được giảm xuống 38 và tỷ lệ tương ứng được giảm xuống để cố gắng ngăn phần tích phân của kết quả bị cắt bớt.

Mô tả cũng chứa chi tiết về cách chính xác tỷ lệ được giảm bớt bên trong các phép toán cộng và nhân. Dựa trên mô tả đó:

  • NUMERIC(19, 4) * NUMERIC(19, 4)NUMERIC(39, 8)và bị kẹp vàoNUMERIC(38, 7)
  • NUMERIC(1, 1) * NUMERIC(38, 0)NUMERIC(40, 1)và bị kẹp vàoNUMERIC(38, 1)
  • NUMERIC(5, 2) - NUMERIC(38, 1)NUMERIC(40, 2)và bị kẹp vàoNUMERIC(38, 1)

Đây là nỗ lực của tôi để triển khai thuật toán trong JavaScript. Tôi đã kiểm tra chéo kết quả so với SQL Server. Nó trả lời phần cốt lõi của câu hỏi của bạn.

// https://docs.microsoft.com/en-us/sql/t-sql/data-types/precision-scale-and-length-transact-sql?view=sql-server-2017

function numericTest_mul(p1, s1, p2, s2) {
  // e1 * e2
  var precision = p1 + p2 + 1;
  var scale = s1 + s2;

  // see notes in the linked article about multiplication operations
  var newscale;
  if (precision - scale < 32) {
    newscale = Math.min(scale, 38 - (precision - scale));
  } else if (scale < 6 && precision - scale > 32) {
    newscale = scale;
  } else if (scale > 6 && precision - scale > 32) {
    newscale = 6;
  }

  console.log("NUMERIC(%d, %d) * NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

function numericTest_add(p1, s1, p2, s2) {
  // e1 + e2
  var precision = Math.max(s1, s2) + Math.max(p1 - s1, p2 - s2) + 1;
  var scale = Math.max(s1, s2);

  // see notes in the linked article about addition operations
  var newscale;
  if (Math.max(p1 - s1, p2 - s2) > Math.min(38, precision) - scale) {
    newscale = Math.min(precision, 38) - Math.max(p1 - s1, p2 - s2);
  } else {
    newscale = scale;
  }

  console.log("NUMERIC(%d, %d) + NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

function numericTest_union(p1, s1, p2, s2) {
  // e1 UNION e2
  var precision = Math.max(s1, s2) + Math.max(p1 - s1, p2 - s2);
  var scale = Math.max(s1, s2);

  // my idea of how newscale should be calculated, not official
  var newscale;
  if (precision > 38) {
    newscale = scale - (precision - 38);
  } else {
    newscale = scale;
  }

  console.log("NUMERIC(%d, %d) + NUMERIC(%d, %d) yields NUMERIC(%d, %d) clamped to NUMERIC(%d, %d)", p1, s1, p2, s2, precision, scale, Math.min(precision, 38), newscale);
}

/*
 * first example in question
 */

// CAST(1.0 AS DECIMAL(19, 4)) * CAST(199.96 AS DECIMAL(19, 4))
numericTest_mul(19, 4, 19, 4);

// 0.0 * FLOOR(...)
numericTest_mul(1, 1, 38, 0);

// 199.96 * ...
numericTest_add(5, 2, 38, 1);

/*
 * IIF examples in question
 * the logic used to determine result data type of IIF / CASE statement
 * is same as the logic used inside UNION operations
 */

// FLOOR(DECIMAL(38, 7)) UNION CAST(1999.96 AS DECIMAL(19, 4)))
numericTest_union(38, 0, 19, 4);

// CAST(1.0 AS DECIMAL (36, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(36, 0, 19, 4);

// CAST(1.0 AS DECIMAL (37, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(37, 0, 19, 4);

// CAST(1.0 AS DECIMAL (38, 0)) UNION CAST(1.96 AS DECIMAL(19, 4))
numericTest_union(38, 0, 19, 4);

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.