5.3 Debugging Cross-Platform Mismatch
“ฝ่ายบัญชีบอก ยอดรวมเดือนนี้ ฿1,200,000 แต่ database ของเราบอก ฿1,100,000 — ต่างกัน 100,000 บาท ใครผิด?”
เวลาที่ใช้: ~12 นาที
สถานการณ์จริง
Section titled “สถานการณ์จริง”ฝ่ายบัญชีใช้ Google Sheets สรุปยอดรายเดือน ฝ่าย IT เก็บข้อมูลเดียวกันใน PostgreSQL ทุกเดือนตัวเลขเท่ากัน — จนเดือนนี้:
| แหล่ง | ยอดรวม |
|---|---|
| Google Sheets (บัญชี) | ฿1,200,000.00 |
| PostgreSQL (IT) | ฿1,100,000.00 |
| ผลต่าง | ฿100,000.00 |
ปัญหาอยู่ที่ไหน?
สืบหาต้นตอ
Section titled “สืบหาต้นตอ”-
ตรวจ row count — ทั้งสองฝ่ายมี 50,000 แถว เท่ากัน ไม่ใช่ปัญหาแถวหาย
-
ตรวจ data type ของ column
amountใน SQL:
SELECT column_name, data_typeFROM information_schema.columnsWHERE table_name = 'transactions' AND column_name = 'amount';ผลลัพธ์:
data_type = 'real'ปัญหาอยู่ตรงนี้!
REAL= floating-point 4 bytes -
ดูว่า rounding error สะสมแค่ไหน
-- เปรียบเทียบ REAL vs NUMERICSELECTSUM(amount) AS real_sum,SUM(CAST(amount AS NUMERIC(10,2))) AS numeric_sumFROM transactions;REALทำให้ทุกแถวมี error เล็กๆ — แต่ 50,000 แถวรวมกัน error สะสมเป็น แสนบาท
ทำไม REAL ถึงผิด?
Section titled “ทำไม REAL ถึงผิด?”ลองจำลองปัญหานี้ใน 4 เครื่องมือ:
// จำลอง: แต่ละแถวมี error เล็กๆ จาก float// ลาก copy ลง 50,000 แถว (หรือใช้ SEQUENCE)
// ยอดที่ควรได้:=24.99 * 50000 → 1,249,500.00
// แต่ถ้าเก็บเป็น float จริงๆ:=TEXT(24.99, "0.00000000000000000") → 24.98999999999999900// error ต่อแถว ≈ 0.00000000000000100
// 50,000 แถว × error เล็กๆ = error ใหญ่พอจับได้#include <stdio.h>
int main() { // จำลอง 50,000 transactions ที่ใช้ float float sum_float = 0.0f; double sum_double = 0.0; long sum_cents = 0; // integer cents = exact
for (int i = 0; i < 50000; i++) { sum_float += 24.99f; sum_double += 24.99; sum_cents += 2499; // เก็บเป็น cents }
printf("float: %.2f\n", sum_float); printf("double: %.2f\n", sum_double); printf("int cents: %.2f\n", sum_cents / 100.0); printf("exact: %.2f\n", 24.99 * 50000);
printf("\nfloat error: %.6f\n", sum_float - (24.99f * 50000)); printf("double error: %.6f\n", sum_double - (24.99 * 50000));
return 0;}ลองรันดู — float จะมี error มากกว่า double อย่างเห็นได้ชัด
from decimal import Decimal
# จำลอง 50,000 transactionsn = 50_000amount = 24.99
# วิธีที่ 1: float (เหมือน SQL REAL)sum_float = sum(amount for _ in range(n))
# วิธีที่ 2: Decimal (เหมือน SQL NUMERIC)sum_decimal = sum(Decimal('24.99') for _ in range(n))
# วิธีที่ 3: integer centssum_cents = 2499 * n
exact = Decimal('24.99') * n
print(f"float: {sum_float:.2f}")print(f"Decimal: {sum_decimal}")print(f"int cents: {sum_cents / 100:.2f}")print(f"exact: {exact}")
print(f"\nfloat error: {Decimal(str(sum_float)) - exact}")ผลลัพธ์: float จะมี error สะสม แต่ Decimal จะตรง 100%
-- สร้างตาราง 2 แบบ: REAL vs NUMERICCREATE TABLE txn_real (amount REAL);CREATE TABLE txn_numeric (amount NUMERIC(10,2));
-- ใส่ข้อมูลเดียวกัน 50,000 แถวINSERT INTO txn_real SELECT 24.99 FROM generate_series(1, 50000);INSERT INTO txn_numeric SELECT 24.99 FROM generate_series(1, 50000);
-- เปรียบเทียบSELECT (SELECT SUM(amount) FROM txn_real) AS real_sum, (SELECT SUM(amount) FROM txn_numeric) AS numeric_sum, (SELECT SUM(amount) FROM txn_numeric) - (SELECT SUM(amount) FROM txn_real) AS difference;
-- real_sum อาจได้ 1249500.0X (ผิด!)-- numeric_sum ได้ 1249500.00 (ถูก!)Root Cause Analysis
Section titled “Root Cause Analysis”วิธีแก้ไข
Section titled “วิธีแก้ไข”-
แก้ schema:
ALTER TABLE transactionsALTER COLUMN amount TYPE NUMERIC(10,2); -
ตรวจยอดใหม่:
SELECT SUM(amount) FROM transactions;-- ตอนนี้ตรงกับ Sheets แล้ว -
อัปเดต type contract ให้ระบุ
NUMERIC(10,2)ไม่ใช่REAL -
เพิ่ม check ใน import checklist ว่า columns ที่เก็บเงินต้องเป็น
NUMERICเสมอ
บทเรียนที่ได้
Section titled “บทเรียนที่ได้”| สิ่งที่ได้เรียนรู้ | รายละเอียด |
|---|---|
| Float ไม่ใช่สำหรับเงิน | REAL / float มี rounding error ที่สะสมได้ |
| Error เล็กๆ รวมกันใหญ่ | 0.000001 x 50,000 แถว = จับต้องได้ |
| Type contract ป้องกันปัญหา | ถ้า contract ระบุ NUMERIC ตั้งแต่แรก ปัญหานี้ไม่เกิด |
| ตรวจ schema ก่อน debug logic | ส่วนใหญ่ bug ไม่ได้อยู่ที่ logic แต่อยู่ที่ data type |
- สร้างตาราง 2 แบบใน SQL (REAL vs NUMERIC) แล้ว insert ข้อมูลเดียวกัน 10,000 แถว
- เปรียบเทียบ
SUM()ของทั้งสองตาราง - ลองเปลี่ยนจำนวนเงินต่อแถวเป็นค่าที่มี rounding error มากๆ (เช่น 19.99, 0.33) แล้วดูว่า difference เปลี่ยนไปอย่างไร
ดูเฉลย
CREATE TABLE test_real (amount REAL);CREATE TABLE test_numeric (amount NUMERIC(10,2));
-- ใช้ 0.33 ซึ่งเป็น repeating decimal ใน binaryINSERT INTO test_real SELECT 0.33 FROM generate_series(1, 10000);INSERT INTO test_numeric SELECT 0.33 FROM generate_series(1, 10000);
SELECT (SELECT SUM(amount) FROM test_real) AS real_sum, (SELECT SUM(amount) FROM test_numeric) AS numeric_sum;
-- real_sum ≈ 3299.999... (ผิด)-- numeric_sum = 3300.00 (ถูก)ยิ่งจำนวนเงินมี repeating decimal ใน binary มากเท่าไหร่ (0.1, 0.33, 0.99) error ก็ยิ่งมาก