[{"data":1,"prerenderedAt":1643},["ShallowReactive",2],{"index":3,"index-blogs":23,"index-projects":1619},{"id":4,"title":5,"blog":6,"body":9,"description":10,"extension":11,"hero":9,"meta":12,"navigation":13,"path":14,"projects":15,"seo":18,"stem":21,"__hash__":22},"index\u002Findex.yml","danielnaswan, full-stack",{"title":7,"description":8},"Latest Articles","Some of my recent thoughts",null,"love to create, explore and educate","yml",{},true,"\u002F",{"title":16,"description":17},"Projects","Things I've built, and what they're built with.",{"title":19,"description":20},"danielnaswan","I'm Daniel Naswan, a full stack developer in Selangor, Malaysia. I modernise legacy systems into maintainable, component-based web applications with Nuxt, Vue.js, Laravel and Node.js.","index","w1UnMZzvNcsTW93x_oznjF3jJ-TBYaTTDtbEMS2T4gc",[24,1600],{"id":25,"title":26,"author":27,"body":32,"date":1591,"description":1592,"extension":1593,"image":9,"meta":1594,"minRead":1595,"navigation":13,"path":1596,"seo":1597,"stem":1598,"__hash__":1599},"blog\u002Fblog\u002Fwhy-round-doesnt-fix-floating-point-in-sql.md","Why ROUND() Doesn't Fix Floating Point Errors in SQL",{"name":28,"to":29,"avatar":30},"Daniel Naswan","https:\u002F\u002Fgithub.com\u002Fdanielnaswan",{"src":31,"alt":28},"\u002Favatar.webp",{"type":33,"value":34,"toc":1568},"minimark",[35,48,60,65,80,90,109,112,178,187,233,244,249,271,282,288,294,314,323,327,337,369,372,387,390,404,407,422,429,433,441,447,470,490,496,500,518,533,540,544,550,661,693,697,707,722,742,757,761,774,872,889,903,907,914,918,930,968,981,989,999,1028,1040,1054,1058,1066,1176,1183,1333,1336,1342,1345,1360,1364,1398,1414,1418,1517,1520,1524,1564],[36,37,38,39,43,44,47],"p",{},"I was building a report that adds up commission per branch. The numbers looked fine in my SQL client, but somewhere along the way a total turned into something like ",[40,41,42],"code",{},"0.00000000003"," off from what it should be. My first fix was to wrap everything in ",[40,45,46],{},"ROUND(..., 2)",". It didn't work.",[36,49,50,51,55,56,59],{},"This post explains ",[52,53,54],"strong",{},"why rounding doesn't save you",", and what actually does. You only need to know basic SQL and a bit of JavaScript. Every example below was run on MySQL 8.0 and Node.js 22 with Knex 3.3 and ",[40,57,58],{},"mysql2"," 3.23, and the outputs are copied straight from the terminal.",[61,62,64],"h2",{"id":63},"part-1-computers-cant-store-01","Part 1: Computers can't store 0.1",[36,66,67,68,71,72,75,76,79],{},"Most of us learned that ",[40,69,70],{},"1\u002F3 = 0.3333..."," goes on forever in base 10. Computers store numbers in ",[52,73,74],{},"base 2"," (binary), and in binary, ",[40,77,78],{},"0.1"," goes on forever too:",[81,82,87],"pre",{"className":83,"code":85,"language":86},[84],"language-text","0.1 (decimal) = 0.0001100110011001100110011... (binary, repeating)\n","text",[40,88,85],{"__ignoreMap":89},"",[36,91,92,93,96,97,100,101,104,105,108],{},"A ",[40,94,95],{},"DOUBLE"," (and JavaScript's ",[40,98,99],{},"Number",") uses the ",[52,102,103],{},"IEEE 754"," standard: 64 bits, split into a sign bit, 11 bits of exponent, and 52 bits of fraction. It's basically scientific notation in binary. 52 bits is a lot, but it's not infinite, so the computer chops off the repeating part and keeps the ",[52,106,107],{},"closest number it can represent",".",[36,110,111],{},"You can see what is really stored by asking for more digits:",[81,113,117],{"className":114,"code":115,"language":116,"meta":89,"style":89},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","(0.1).toPrecision(25)    \u002F\u002F '0.1000000000000000055511151'\n(1.005).toPrecision(20)  \u002F\u002F '1.0049999999999998934'\n","js",[40,118,119,153],{"__ignoreMap":89},[120,121,124,128,131,134,137,141,143,146,149],"span",{"class":122,"line":123},"line",1,[120,125,127],{"class":126},"sTEyZ","(",[120,129,78],{"class":130},"sbssI",[120,132,133],{"class":126},")",[120,135,108],{"class":136},"sMK4o",[120,138,140],{"class":139},"s2Zo4","toPrecision",[120,142,127],{"class":126},[120,144,145],{"class":130},"25",[120,147,148],{"class":126},")    ",[120,150,152],{"class":151},"sHwdD","\u002F\u002F '0.1000000000000000055511151'\n",[120,154,156,158,161,163,165,167,169,172,175],{"class":122,"line":155},2,[120,157,127],{"class":126},[120,159,160],{"class":130},"1.005",[120,162,133],{"class":126},[120,164,108],{"class":136},[120,166,140],{"class":139},[120,168,127],{"class":126},[120,170,171],{"class":130},"20",[120,173,174],{"class":126},")  ",[120,176,177],{"class":151},"\u002F\u002F '1.0049999999999998934'\n",[36,179,180,181,183,184,186],{},"So ",[40,182,78],{}," is really \"0.1 plus a tiny bit\", and ",[40,185,160],{}," is really \"1.005 minus a tiny bit\". These tiny errors are normally hidden because the computer prints the shortest number that maps back to the same bits. They show up once you start doing maths:",[81,188,190],{"className":114,"code":189,"language":116,"meta":89,"style":89},"0.1 + 0.2   \u002F\u002F 0.30000000000000004\n1.1 * 3     \u002F\u002F 3.3000000000000003\n0.07 * 100  \u002F\u002F 7.000000000000001\n",[40,191,192,205,219],{"__ignoreMap":89},[120,193,194,196,199,202],{"class":122,"line":123},[120,195,78],{"class":130},[120,197,198],{"class":136}," +",[120,200,201],{"class":130}," 0.2",[120,203,204],{"class":151},"   \u002F\u002F 0.30000000000000004\n",[120,206,207,210,213,216],{"class":122,"line":155},[120,208,209],{"class":130},"1.1",[120,211,212],{"class":136}," *",[120,214,215],{"class":130}," 3",[120,217,218],{"class":151},"     \u002F\u002F 3.3000000000000003\n",[120,220,222,225,227,230],{"class":122,"line":221},3,[120,223,224],{"class":130},"0.07",[120,226,212],{"class":136},[120,228,229],{"class":130}," 100",[120,231,232],{"class":151},"  \u002F\u002F 7.000000000000001\n",[36,234,235,236,243],{},"This isn't a bug in JavaScript or MySQL. Every language that uses IEEE 754 doubles does the same thing. (There's a whole website about it: ",[237,238,242],"a",{"href":239,"rel":240},"https:\u002F\u002F0.30000000000000004.com\u002F",[241],"nofollow","0.30000000000000004.com",".)",[245,246,248],"h3",{"id":247},"sql-does-it-too","SQL does it too",[36,250,251,252,254,255,259,260,262,263,266,267,270],{},"In MySQL and MariaDB, a plain number like ",[40,253,78],{}," is an ",[256,257,258],"em",{},"exact"," value, but a ",[40,261,95],{}," column or a number in scientific notation (",[40,264,265],{},"0.1e0",") is an ",[256,268,269],{},"approximate"," one:",[81,272,276],{"className":273,"code":274,"language":275,"meta":89,"style":89},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","SELECT 0.1 + 0.2, 0.1e0 + 0.2e0, 0.1e0 + 0.2e0 = 0.3e0;\n","sql",[40,277,278],{"__ignoreMap":89},[120,279,280],{"class":122,"line":123},[120,281,274],{},[81,283,286],{"className":284,"code":285,"language":86},[84],"+-----------+---------------------+-----------------------+\n| 0.1 + 0.2 | 0.1e0 + 0.2e0       | 0.1e0 + 0.2e0 = 0.3e0 |\n+-----------+---------------------+-----------------------+\n|       0.3 | 0.30000000000000004 |                     0 |\n+-----------+---------------------+-----------------------+\n",[40,287,285],{"__ignoreMap":89},[36,289,290,293],{},[40,291,292],{},"FLOAT"," is even worse, because it only has 32 bits:",[81,295,297],{"className":273,"code":296,"language":275,"meta":89,"style":89},"-- price_f is a FLOAT column holding 1234.56\nSELECT price_f, price_f * 1 FROM products;\n-- 1234.56 | 1234.56005859375\n",[40,298,299,304,309],{"__ignoreMap":89},[120,300,301],{"class":122,"line":123},[120,302,303],{},"-- price_f is a FLOAT column holding 1234.56\n",[120,305,306],{"class":122,"line":155},[120,307,308],{},"SELECT price_f, price_f * 1 FROM products;\n",[120,310,311],{"class":122,"line":221},[120,312,313],{},"-- 1234.56 | 1234.56005859375\n",[36,315,316,317,322],{},"The ",[237,318,321],{"href":319,"rel":320},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Fproblems-with-float.html",[241],"MySQL manual"," says it directly: floating-point numbers \"are approximate and not stored as exact values.\"",[61,324,326],{"id":325},"part-2-so-why-doesnt-round-fix-it","Part 2: So why doesn't ROUND() fix it?",[36,328,329,330,333,334,336],{},"This is the part that confused me. Let's set up a small table. Ten orders, each with a price of ",[40,331,332],{},"0.10",", stored as ",[40,335,95],{},":",[81,338,340],{"className":273,"code":339,"language":275,"meta":89,"style":89},"CREATE TABLE orders (\n  id    INT,\n  price DOUBLE\n);\n-- insert 10 rows with price = 0.1\n",[40,341,342,347,352,357,363],{"__ignoreMap":89},[120,343,344],{"class":122,"line":123},[120,345,346],{},"CREATE TABLE orders (\n",[120,348,349],{"class":122,"line":155},[120,350,351],{},"  id    INT,\n",[120,353,354],{"class":122,"line":221},[120,355,356],{},"  price DOUBLE\n",[120,358,360],{"class":122,"line":359},4,[120,361,362],{},");\n",[120,364,366],{"class":122,"line":365},5,[120,367,368],{},"-- insert 10 rows with price = 0.1\n",[36,370,371],{},"Adding them up gives the classic error:",[81,373,375],{"className":273,"code":374,"language":275,"meta":89,"style":89},"SELECT SUM(price) FROM orders;\n-- 0.9999999999999999\n",[40,376,377,382],{"__ignoreMap":89},[120,378,379],{"class":122,"line":123},[120,380,381],{},"SELECT SUM(price) FROM orders;\n",[120,383,384],{"class":122,"line":155},[120,385,386],{},"-- 0.9999999999999999\n",[36,388,389],{},"Fine, let's round each row first:",[81,391,393],{"className":273,"code":392,"language":275,"meta":89,"style":89},"SELECT SUM(ROUND(price, 2)) FROM orders;\n-- 0.9999999999999999\n",[40,394,395,400],{"__ignoreMap":89},[120,396,397],{"class":122,"line":123},[120,398,399],{},"SELECT SUM(ROUND(price, 2)) FROM orders;\n",[120,401,402],{"class":122,"line":155},[120,403,386],{},[36,405,406],{},"Still wrong! And it's not a display problem. Subtract 1 and you can see the leftover:",[81,408,410],{"className":273,"code":409,"language":275,"meta":89,"style":89},"SELECT SUM(ROUND(price, 2)) - 1 FROM orders;\n-- -1.1102230246251565e-16\n",[40,411,412,417],{"__ignoreMap":89},[120,413,414],{"class":122,"line":123},[120,415,416],{},"SELECT SUM(ROUND(price, 2)) - 1 FROM orders;\n",[120,418,419],{"class":122,"line":155},[120,420,421],{},"-- -1.1102230246251565e-16\n",[36,423,424,425,428],{},"That's ",[40,426,427],{},"-0.00000000000000011",". So why didn't rounding help?",[245,430,432],{"id":431},"reason-1-round-on-a-double-gives-you-back-a-double","Reason 1: ROUND on a DOUBLE gives you back a DOUBLE",[36,434,435,436,336],{},"From the ",[237,437,440],{"href":438,"rel":439},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F9.7\u002Fen\u002Fmathematical-functions.html",[241],"MySQL docs for ROUND()",[442,443,444],"blockquote",{},[36,445,446],{},"The return value has the same type as the first argument (assuming that it is integer, double, or decimal).",[36,448,180,449,452,453,455,456,458,459,462,463,465,466,469],{},[40,450,451],{},"ROUND(0.1e0, 2)"," gives you a ",[40,454,95],{},", and the closest double to ",[40,457,332],{}," is... the same ",[40,460,461],{},"0.1000000000000000055511151"," we started with. Rounding can't produce an exact ",[40,464,332],{}," because ",[52,467,468],{},"that number doesn't exist as a double",". You can confirm the type by saving the result into a table:",[81,471,473],{"className":273,"code":472,"language":275,"meta":89,"style":89},"CREATE TABLE t AS SELECT ROUND(price, 2) AS r FROM orders;\nSHOW CREATE TABLE t;\n-- `r` double DEFAULT NULL\n",[40,474,475,480,485],{"__ignoreMap":89},[120,476,477],{"class":122,"line":123},[120,478,479],{},"CREATE TABLE t AS SELECT ROUND(price, 2) AS r FROM orders;\n",[120,481,482],{"class":122,"line":155},[120,483,484],{},"SHOW CREATE TABLE t;\n",[120,486,487],{"class":122,"line":221},[120,488,489],{},"-- `r` double DEFAULT NULL\n",[36,491,492,493,108],{},"It's still a ",[40,494,495],{},"double",[245,497,499],{"id":498},"reason-2-sum-adds-the-errors-back-up","Reason 2: SUM adds the errors back up",[36,501,502,503,506,507,510,511,513,514,517],{},"When you write ",[40,504,505],{},"SUM(ROUND(x, 2))",", you round each row, and then ",[40,508,509],{},"SUM"," adds ten doubles together, each a little bit off. The small errors pile up again, and you're back where you started. With more rows and bigger numbers, the leftover gets bigger. I loaded 2,000 random prices between 100 and 5000 into a ",[40,512,95],{}," column and a ",[40,515,516],{},"DECIMAL"," column side by side:",[81,519,521],{"className":273,"code":520,"language":275,"meta":89,"style":89},"SELECT SUM(price), SUM(price_d), SUM(price) - SUM(price_d) FROM big;\n-- 5014534.319999986 | 5014534.32 | -0.000000013969838619232178\n",[40,522,523,528],{"__ignoreMap":89},[120,524,525],{"class":122,"line":123},[120,526,527],{},"SELECT SUM(price), SUM(price_d), SUM(price) - SUM(price_d) FROM big;\n",[120,529,530],{"class":122,"line":155},[120,531,532],{},"-- 5014534.319999986 | 5014534.32 | -0.000000013969838619232178\n",[36,534,535,536,539],{},"That's exactly the kind of ",[40,537,538],{},"0.0000000..."," leftover that started this whole investigation.",[245,541,543],{"id":542},"reason-3-your-app-gets-the-same-broken-number","Reason 3: Your app gets the same broken number",[36,545,546,547,336],{},"Knex doesn't clean this up for you. Running it from Node gives the same result whether you use the query builder or ",[40,548,549],{},"knex.raw",[81,551,553],{"className":114,"code":552,"language":116,"meta":89,"style":89},"await knex('orders').sum({ total: 'price' })\n\u002F\u002F [ { total: 0.9999999999999999 } ]\n\nconst [rows] = await knex.raw('SELECT SUM(ROUND(price, 2)) AS total FROM orders')\n\u002F\u002F rows: [ { total: 0.9999999999999999 } ]\n",[40,554,555,607,612,617,656],{"__ignoreMap":89},[120,556,557,561,564,566,569,573,575,577,579,582,584,587,591,593,596,599,601,604],{"class":122,"line":123},[120,558,560],{"class":559},"s7zQu","await",[120,562,563],{"class":139}," knex",[120,565,127],{"class":126},[120,567,568],{"class":136},"'",[120,570,572],{"class":571},"sfazB","orders",[120,574,568],{"class":136},[120,576,133],{"class":126},[120,578,108],{"class":136},[120,580,581],{"class":139},"sum",[120,583,127],{"class":126},[120,585,586],{"class":136},"{",[120,588,590],{"class":589},"swJcz"," total",[120,592,336],{"class":136},[120,594,595],{"class":136}," '",[120,597,598],{"class":571},"price",[120,600,568],{"class":136},[120,602,603],{"class":136}," }",[120,605,606],{"class":126},")\n",[120,608,609],{"class":122,"line":155},[120,610,611],{"class":151},"\u002F\u002F [ { total: 0.9999999999999999 } ]\n",[120,613,614],{"class":122,"line":221},[120,615,616],{"emptyLinePlaceholder":13},"\n",[120,618,619,623,626,629,632,635,638,640,642,645,647,649,652,654],{"class":122,"line":359},[120,620,622],{"class":621},"spNyl","const",[120,624,625],{"class":136}," [",[120,627,628],{"class":126},"rows",[120,630,631],{"class":136},"]",[120,633,634],{"class":136}," =",[120,636,637],{"class":559}," await",[120,639,563],{"class":126},[120,641,108],{"class":136},[120,643,644],{"class":139},"raw",[120,646,127],{"class":126},[120,648,568],{"class":136},[120,650,651],{"class":571},"SELECT SUM(ROUND(price, 2)) AS total FROM orders",[120,653,568],{"class":136},[120,655,606],{"class":126},[120,657,658],{"class":122,"line":365},[120,659,660],{"class":151},"\u002F\u002F rows: [ { total: 0.9999999999999999 } ]\n",[442,662,663],{},[36,664,665,668,669,672,673,676,677,680,681,684,685,688,689,692],{},[52,666,667],{},"Side note:"," I first tried this on MariaDB 10.11, and there ",[40,670,671],{},"SUM(ROUND(price, 2))"," ",[256,674,675],{},"displayed"," as ",[40,678,679],{},"1.00",", because MariaDB formats the result to 2 decimals when it sends it back as text. Subtracting 1 still gave ",[40,682,683],{},"-0.00",", and a prepared statement (",[40,686,687],{},"conn.execute()",") still returned ",[40,690,691],{},"0.9999999999999999",". So even when the error looks hidden, it's still there.",[245,694,696],{"id":695},"bonus-round-can-round-the-wrong-way","Bonus: ROUND can round the \"wrong\" way",[36,698,699,700,702,703,706],{},"Because ",[40,701,160],{}," is really ",[40,704,705],{},"1.00499999...",", rounding it can go down:",[81,708,710],{"className":273,"code":709,"language":275,"meta":89,"style":89},"SELECT ROUND(1.005e0, 2), ROUND(1.005, 2);\n-- 1               | 1.01\n",[40,711,712,717],{"__ignoreMap":89},[120,713,714],{"class":122,"line":123},[120,715,716],{},"SELECT ROUND(1.005e0, 2), ROUND(1.005, 2);\n",[120,718,719],{"class":122,"line":155},[120,720,721],{},"-- 1               | 1.01\n",[36,723,724,725,727,728,731,732,735,736,741],{},"The exact value ",[40,726,160],{}," rounds to ",[40,729,730],{},"1.01"," like you'd expect, but the double rounds down to ",[40,733,734],{},"1",". The MySQL docs also warn that for approximate values \"the result depends on the C library\", and on many systems it uses \"round to nearest even\" instead of the \"round half up\" you learned at school (",[237,737,740],{"href":738,"rel":739},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Fprecision-math-rounding.html",[241],"source","):",[81,743,745],{"className":273,"code":744,"language":275,"meta":89,"style":89},"SELECT ROUND(2.5), ROUND(25E-1);\n-- 3          | 2\n",[40,746,747,752],{"__ignoreMap":89},[120,748,749],{"class":122,"line":123},[120,750,751],{},"SELECT ROUND(2.5), ROUND(25E-1);\n",[120,753,754],{"class":122,"line":155},[120,755,756],{},"-- 3          | 2\n",[61,758,760],{"id":759},"part-3-the-error-can-also-come-from-javascript","Part 3: The error can also come from JavaScript",[36,762,763,764,766,767,769,770,773],{},"Even if your SQL is perfect, JavaScript can bring the error back. The ",[40,765,58],{}," driver (which Knex uses under the hood) returns ",[40,768,516],{}," values ",[52,771,772],{},"as strings"," on purpose:",[81,775,777],{"className":114,"code":776,"language":116,"meta":89,"style":89},"const rows = await knex('orders')\n  .select(knex.raw('CAST(SUM(price) AS DECIMAL(15,2)) AS total'))\n\ntypeof rows[0].total  \u002F\u002F 'string'\nrows[0].total         \u002F\u002F '1.00'\n",[40,778,779,803,830,834,855],{"__ignoreMap":89},[120,780,781,783,786,789,791,793,795,797,799,801],{"class":122,"line":123},[120,782,622],{"class":621},[120,784,785],{"class":126}," rows ",[120,787,788],{"class":136},"=",[120,790,637],{"class":559},[120,792,563],{"class":139},[120,794,127],{"class":126},[120,796,568],{"class":136},[120,798,572],{"class":571},[120,800,568],{"class":136},[120,802,606],{"class":126},[120,804,805,808,811,814,816,818,820,822,825,827],{"class":122,"line":155},[120,806,807],{"class":136},"  .",[120,809,810],{"class":139},"select",[120,812,813],{"class":126},"(knex",[120,815,108],{"class":136},[120,817,644],{"class":139},[120,819,127],{"class":126},[120,821,568],{"class":136},[120,823,824],{"class":571},"CAST(SUM(price) AS DECIMAL(15,2)) AS total",[120,826,568],{"class":136},[120,828,829],{"class":126},"))\n",[120,831,832],{"class":122,"line":221},[120,833,616],{"emptyLinePlaceholder":13},[120,835,836,839,842,845,847,849,852],{"class":122,"line":359},[120,837,838],{"class":136},"typeof",[120,840,841],{"class":126}," rows[",[120,843,844],{"class":130},"0",[120,846,631],{"class":126},[120,848,108],{"class":136},[120,850,851],{"class":126},"total  ",[120,853,854],{"class":151},"\u002F\u002F 'string'\n",[120,856,857,860,862,864,866,869],{"class":122,"line":365},[120,858,859],{"class":126},"rows[",[120,861,844],{"class":130},[120,863,631],{"class":126},[120,865,108],{"class":136},[120,867,868],{"class":126},"total         ",[120,870,871],{"class":151},"\u002F\u002F '1.00'\n",[36,873,874,875,880,881,884,885,888],{},"Its ",[237,876,879],{"href":877,"rel":878},"https:\u002F\u002Fgithub.com\u002Fsidorares\u002Fnode-mysql2\u002Fblob\u002Fmaster\u002Fwebsite\u002Fdocs\u002Fdocumentation\u002F00-index.mdx",[241],"docs"," explain why: you ",[256,882,883],{},"can"," turn on ",[40,886,887],{},"decimalNumbers: true",", but \"this option could lose precision on the number as Javascript Number is a Float!\"",[36,890,891,892,895,896,899,900,108],{},"So the moment you do ",[40,893,894],{},"Number(row.total)"," and start adding, subtracting, or multiplying in JavaScript, you're doing floating point maths again, and you can get things like ",[40,897,898],{},"0.1 + 0.2 = 0.30000000000000004"," or ",[40,901,902],{},"4056.45 - 4056.42 = 0.02999999999974534",[61,904,906],{"id":905},"part-4-how-to-actually-fix-it","Part 4: How to actually fix it",[36,908,909,910,913],{},"The rule is simple: ",[52,911,912],{},"don't let a floating point number touch money",", anywhere in the pipeline.",[245,915,917],{"id":916},"fix-1-store-money-as-decimal","Fix 1: Store money as DECIMAL",[36,919,920,921,924,925,927,928,336],{},"This is the real fix. ",[40,922,923],{},"DECIMAL(15,2)"," stores numbers in base 10, exactly, so ",[40,926,332],{}," really is ",[40,929,332],{},[81,931,933],{"className":273,"code":932,"language":275,"meta":89,"style":89},"CREATE TABLE orders (\n  id    INT,\n  price DECIMAL(15,2),\n  pct   DECIMAL(7,4)\n);\n\nSELECT SUM(price) FROM orders;  -- 1.00, exactly\n",[40,934,935,939,943,948,953,957,962],{"__ignoreMap":89},[120,936,937],{"class":122,"line":123},[120,938,346],{},[120,940,941],{"class":122,"line":155},[120,942,351],{},[120,944,945],{"class":122,"line":221},[120,946,947],{},"  price DECIMAL(15,2),\n",[120,949,950],{"class":122,"line":359},[120,951,952],{},"  pct   DECIMAL(7,4)\n",[120,954,955],{"class":122,"line":365},[120,956,362],{},[120,958,960],{"class":122,"line":959},6,[120,961,616],{"emptyLinePlaceholder":13},[120,963,965],{"class":122,"line":964},7,[120,966,967],{},"SELECT SUM(price) FROM orders;  -- 1.00, exactly\n",[36,969,970,971,973,974,976,977,980],{},"The MySQL manual says that for ",[40,972,516],{}," columns, \"MySQL performs operations with a precision of 65 decimal digits, which should solve most common inaccuracy problems.\" Summing the ",[40,975,516],{}," version of the column gave exactly ",[40,978,979],{},"'1.00'",", through both the MySQL client and Knex.",[245,982,984,985,988],{"id":983},"fix-2-if-you-cant-change-the-columns-cast-before-you-do-maths","Fix 2: If you can't change the columns, cast ",[256,986,987],{},"before"," you do maths",[36,990,991,992,994,995,998],{},"Sometimes the table belongs to someone else. In that case, cast to ",[40,993,516],{}," on the way ",[52,996,997],{},"in",", not just on the way out:",[81,1000,1002],{"className":273,"code":1001,"language":275,"meta":89,"style":89},"-- Bad: maths happens in DOUBLE, then you cast the damaged result\nCAST(SUM(ROUND(price * pct \u002F 100, 2)) AS DECIMAL(15,2))\n\n-- Better: everything after the cast is exact decimal maths\nSUM(ROUND(CAST(price AS DECIMAL(15,2)) * CAST(pct AS DECIMAL(7,4)) \u002F 100, 2))\n",[40,1003,1004,1009,1014,1018,1023],{"__ignoreMap":89},[120,1005,1006],{"class":122,"line":123},[120,1007,1008],{},"-- Bad: maths happens in DOUBLE, then you cast the damaged result\n",[120,1010,1011],{"class":122,"line":155},[120,1012,1013],{},"CAST(SUM(ROUND(price * pct \u002F 100, 2)) AS DECIMAL(15,2))\n",[120,1015,1016],{"class":122,"line":221},[120,1017,616],{"emptyLinePlaceholder":13},[120,1019,1020],{"class":122,"line":359},[120,1021,1022],{},"-- Better: everything after the cast is exact decimal maths\n",[120,1024,1025],{"class":122,"line":365},[120,1026,1027],{},"SUM(ROUND(CAST(price AS DECIMAL(15,2)) * CAST(pct AS DECIMAL(7,4)) \u002F 100, 2))\n",[36,1029,1030,1031,1034,1035,1037,1038,108],{},"With the cast on the way in, ",[40,1032,1033],{},"SUM(ROUND(CAST(price AS DECIMAL(15,2)), 2))"," returned exactly ",[40,1036,979],{}," through Knex, instead of ",[40,1039,691],{},[36,1041,1042,1043,1046,1047,1049,1050,1053],{},"Casting the ",[256,1044,1045],{},"final"," result to ",[40,1048,923],{}," does hide most errors, because it rounds the double to 2 decimals when converting. But it rounds a value that is already slightly wrong, so a total that should be ",[40,1051,1052],{},"x.xx5"," can land on the wrong side. Casting early avoids that.",[245,1055,1057],{"id":1056},"fix-3-in-javascript-use-a-decimal-library-or-integer-cents","Fix 3: In JavaScript, use a decimal library or integer cents",[36,1059,1060,1061,336],{},"Keep the string the driver gives you, and do the maths with a library like ",[237,1062,1065],{"href":1063,"rel":1064},"https:\u002F\u002Fmikemcl.github.io\u002Fdecimal.js\u002F",[241],"decimal.js",[81,1067,1069],{"className":114,"code":1068,"language":116,"meta":89,"style":89},"import Decimal from 'decimal.js'\n\nnew Decimal('0.1').plus('0.2').toString()          \u002F\u002F '0.3'\nnew Decimal('1.005').toDecimalPlaces(2).toString() \u002F\u002F '1.01'\n",[40,1070,1071,1089,1093,1138],{"__ignoreMap":89},[120,1072,1073,1076,1079,1082,1084,1086],{"class":122,"line":123},[120,1074,1075],{"class":559},"import",[120,1077,1078],{"class":126}," Decimal ",[120,1080,1081],{"class":559},"from",[120,1083,595],{"class":136},[120,1085,1065],{"class":571},[120,1087,1088],{"class":136},"'\n",[120,1090,1091],{"class":122,"line":155},[120,1092,616],{"emptyLinePlaceholder":13},[120,1094,1095,1098,1101,1103,1105,1107,1109,1111,1113,1116,1118,1120,1123,1125,1127,1129,1132,1135],{"class":122,"line":221},[120,1096,1097],{"class":136},"new",[120,1099,1100],{"class":139}," Decimal",[120,1102,127],{"class":126},[120,1104,568],{"class":136},[120,1106,78],{"class":571},[120,1108,568],{"class":136},[120,1110,133],{"class":126},[120,1112,108],{"class":136},[120,1114,1115],{"class":139},"plus",[120,1117,127],{"class":126},[120,1119,568],{"class":136},[120,1121,1122],{"class":571},"0.2",[120,1124,568],{"class":136},[120,1126,133],{"class":126},[120,1128,108],{"class":136},[120,1130,1131],{"class":139},"toString",[120,1133,1134],{"class":126},"()          ",[120,1136,1137],{"class":151},"\u002F\u002F '0.3'\n",[120,1139,1140,1142,1144,1146,1148,1150,1152,1154,1156,1159,1161,1164,1166,1168,1170,1173],{"class":122,"line":359},[120,1141,1097],{"class":136},[120,1143,1100],{"class":139},[120,1145,127],{"class":126},[120,1147,568],{"class":136},[120,1149,160],{"class":571},[120,1151,568],{"class":136},[120,1153,133],{"class":126},[120,1155,108],{"class":136},[120,1157,1158],{"class":139},"toDecimalPlaces",[120,1160,127],{"class":126},[120,1162,1163],{"class":130},"2",[120,1165,133],{"class":126},[120,1167,108],{"class":136},[120,1169,1131],{"class":139},[120,1171,1172],{"class":126},"() ",[120,1174,1175],{"class":151},"\u002F\u002F '1.01'\n",[36,1177,1178,1179,1182],{},"Or work in ",[52,1180,1181],{},"cents"," as whole numbers, since integers up to 2⁵³ are exact in JavaScript:",[81,1184,1186],{"className":114,"code":1185,"language":116,"meta":89,"style":89},"const cents = ['19.99', '5.01'].map(s => Math.round(Number(s) * 100))\nconst total = cents.reduce((a, b) => a + b, 0)  \u002F\u002F 2500\n;(total \u002F 100).toFixed(2)                       \u002F\u002F '25.00', only for display\n",[40,1187,1188,1254,1304],{"__ignoreMap":89},[120,1189,1190,1192,1195,1197,1199,1201,1204,1206,1209,1211,1214,1216,1218,1220,1223,1225,1229,1232,1235,1237,1240,1242,1244,1247,1250,1252],{"class":122,"line":123},[120,1191,622],{"class":621},[120,1193,1194],{"class":126}," cents ",[120,1196,788],{"class":136},[120,1198,625],{"class":126},[120,1200,568],{"class":136},[120,1202,1203],{"class":571},"19.99",[120,1205,568],{"class":136},[120,1207,1208],{"class":136},",",[120,1210,595],{"class":136},[120,1212,1213],{"class":571},"5.01",[120,1215,568],{"class":136},[120,1217,631],{"class":126},[120,1219,108],{"class":136},[120,1221,1222],{"class":139},"map",[120,1224,127],{"class":126},[120,1226,1228],{"class":1227},"sHdIc","s",[120,1230,1231],{"class":621}," =>",[120,1233,1234],{"class":126}," Math",[120,1236,108],{"class":136},[120,1238,1239],{"class":139},"round",[120,1241,127],{"class":126},[120,1243,99],{"class":139},[120,1245,1246],{"class":126},"(s) ",[120,1248,1249],{"class":136},"*",[120,1251,229],{"class":130},[120,1253,829],{"class":126},[120,1255,1256,1258,1261,1263,1266,1268,1271,1273,1275,1277,1279,1282,1284,1286,1289,1292,1294,1296,1299,1301],{"class":122,"line":155},[120,1257,622],{"class":621},[120,1259,1260],{"class":126}," total ",[120,1262,788],{"class":136},[120,1264,1265],{"class":126}," cents",[120,1267,108],{"class":136},[120,1269,1270],{"class":139},"reduce",[120,1272,127],{"class":126},[120,1274,127],{"class":136},[120,1276,237],{"class":1227},[120,1278,1208],{"class":136},[120,1280,1281],{"class":1227}," b",[120,1283,133],{"class":136},[120,1285,1231],{"class":621},[120,1287,1288],{"class":126}," a ",[120,1290,1291],{"class":136},"+",[120,1293,1281],{"class":126},[120,1295,1208],{"class":136},[120,1297,1298],{"class":130}," 0",[120,1300,174],{"class":126},[120,1302,1303],{"class":151},"\u002F\u002F 2500\n",[120,1305,1306,1309,1312,1314,1316,1318,1320,1323,1325,1327,1330],{"class":122,"line":221},[120,1307,1308],{"class":136},";",[120,1310,1311],{"class":126},"(total ",[120,1313,14],{"class":136},[120,1315,229],{"class":130},[120,1317,133],{"class":126},[120,1319,108],{"class":136},[120,1321,1322],{"class":139},"toFixed",[120,1324,127],{"class":126},[120,1326,1163],{"class":130},[120,1328,1329],{"class":126},")                       ",[120,1331,1332],{"class":151},"\u002F\u002F '25.00', only for display\n",[36,1334,1335],{},"Only convert to a normal number at the very end, when you display it.",[245,1337,1339,1340],{"id":1338},"fix-4-never-compare-floats-with","Fix 4: Never compare floats with ",[40,1341,788],{},[36,1343,1344],{},"If you're stuck with doubles, compare with a tolerance, like the MySQL manual recommends:",[81,1346,1348],{"className":273,"code":1347,"language":275,"meta":89,"style":89},"-- instead of WHERE a = b\nWHERE ABS(a - b) \u003C= 0.0001\n",[40,1349,1350,1355],{"__ignoreMap":89},[120,1351,1352],{"class":122,"line":123},[120,1353,1354],{},"-- instead of WHERE a = b\n",[120,1356,1357],{"class":122,"line":155},[120,1358,1359],{},"WHERE ABS(a - b) \u003C= 0.0001\n",[61,1361,1363],{"id":1362},"two-more-traps-i-found-along-the-way","Two more traps I found along the way",[36,1365,1366,672,1369,1371,1372,1375,1376,1378,1379,1382,1383,1386,1387,1390,1391,1394,1395,1397],{},[52,1367,1368],{},"Round once, at the end, on purpose.",[40,1370,505],{}," and ",[40,1373,1374],{},"ROUND(SUM(x), 2)"," can give different answers, even with exact ",[40,1377,516],{},"s. Three rows of ",[40,1380,1381],{},"0.005"," give ",[40,1384,1385],{},"0.03"," if you round each one first, and ",[40,1388,1389],{},"0.02"," if you add first (",[40,1392,1393],{},"0.015"," → ",[40,1396,1389],{},"). Neither is \"wrong\", but pick one on purpose. For things like commission, it's usually whatever the business rules (or your accountant) say.",[36,1399,1400,1406,1407,1409,1410,1413],{},[52,1401,1402,1405],{},[40,1403,1404],{},"UNION"," removes duplicate rows."," If you combine several tables with ",[40,1408,1404],{}," before summing, two rows that happen to be identical get merged into one, and your total quietly drops. Use ",[40,1411,1412],{},"UNION ALL"," when you're adding things up.",[61,1415,1417],{"id":1416},"summary","Summary",[1419,1420,1421,1437],"table",{},[1422,1423,1424],"thead",{},[1425,1426,1427,1431,1434],"tr",{},[1428,1429,1430],"th",{},"Problem",[1428,1432,1433],{},"Why",[1428,1435,1436],{},"Fix",[1438,1439,1440,1459,1484,1500],"tbody",{},[1425,1441,1442,1448,1454],{},[1443,1444,1445],"td",{},[40,1446,1447],{},"0.1 + 0.2 ≠ 0.3",[1443,1449,1450,1451,1453],{},"Binary can't store ",[40,1452,78],{}," exactly",[1443,1455,1456,1457],{},"Use ",[40,1458,516],{},[1425,1460,1461,1467,1478],{},[1443,1462,1463,1466],{},[40,1464,1465],{},"ROUND()"," doesn't help",[1443,1468,1469,1472,1473,1475,1476],{},[40,1470,1471],{},"ROUND"," on a ",[40,1474,95],{}," returns a ",[40,1477,95],{},[1443,1479,1480,1481,1483],{},"Cast to ",[40,1482,516],{}," before the maths",[1425,1485,1486,1492,1495],{},[1443,1487,1488,1491],{},[40,1489,1490],{},"SUM(ROUND(...))"," is off",[1443,1493,1494],{},"Summing doubles adds errors back",[1443,1496,1497,1498,1228],{},"Sum ",[40,1499,516],{},[1425,1501,1502,1505,1514],{},[1443,1503,1504],{},"Correct in SQL, wrong in app",[1443,1506,1507,1510,1511,1513],{},[40,1508,1509],{},"Number()"," turns the ",[40,1512,516],{}," string back into a float",[1443,1515,1516],{},"Keep strings, use decimal.js or cents",[36,1518,1519],{},"Floating point is great for physics and graphics, where \"very close\" is good enough. Money needs to be exact, so give it an exact type from start to finish.",[61,1521,1523],{"id":1522},"further-reading","Further reading",[1525,1526,1527,1534,1540,1546,1552,1557],"ul",{},[1528,1529,1530],"li",{},[237,1531,1533],{"href":319,"rel":1532},[241],"MySQL: Problems with Floating-Point Values",[1528,1535,1536],{},[237,1537,1539],{"href":738,"rel":1538},[241],"MySQL: Rounding Behavior",[1528,1541,1542],{},[237,1543,1545],{"href":438,"rel":1544},[241],"MySQL: Mathematical Functions (ROUND)",[1528,1547,1548],{},[237,1549,1551],{"href":877,"rel":1550},[241],"node-mysql2 documentation",[1528,1553,1554],{},[237,1555,242],{"href":239,"rel":1556},[241],[1528,1558,1559,1560,1563],{},"David Goldberg, ",[256,1561,1562],{},"What Every Computer Scientist Should Know About Floating-Point Arithmetic"," (1991)",[1565,1566,1567],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"title":89,"searchDepth":155,"depth":155,"links":1569},[1570,1573,1579,1580,1588,1589,1590],{"id":63,"depth":155,"text":64,"children":1571},[1572],{"id":247,"depth":221,"text":248},{"id":325,"depth":155,"text":326,"children":1574},[1575,1576,1577,1578],{"id":431,"depth":221,"text":432},{"id":498,"depth":221,"text":499},{"id":542,"depth":221,"text":543},{"id":695,"depth":221,"text":696},{"id":759,"depth":155,"text":760},{"id":905,"depth":155,"text":906,"children":1581},[1582,1583,1585,1586],{"id":916,"depth":221,"text":917},{"id":983,"depth":221,"text":1584},"Fix 2: If you can't change the columns, cast before you do maths",{"id":1056,"depth":221,"text":1057},{"id":1338,"depth":221,"text":1587},"Fix 4: Never compare floats with =",{"id":1362,"depth":155,"text":1363},{"id":1416,"depth":155,"text":1417},{"id":1522,"depth":155,"text":1523},"2026-10-27","I rounded every number in my query and still got 0.9999999999999999. Here is why that happens, and four ways to fix it for good.","md",{},8,"\u002Fblog\u002Fwhy-round-doesnt-fix-floating-point-in-sql",{"title":26,"description":1592},"blog\u002Fwhy-round-doesnt-fix-floating-point-in-sql","QsCrffW7QE7z6bUB7XXVT0DlkjEIVF0aqwurJcz5z04",{"id":1601,"title":1602,"author":1603,"body":1605,"date":1612,"description":1613,"extension":1593,"image":9,"meta":1614,"minRead":123,"navigation":13,"path":1615,"seo":1616,"stem":1617,"__hash__":1618},"blog\u002Fblog\u002Fhello-world.md","Hello World",{"name":28,"to":29,"avatar":1604},{"src":31,"alt":28},{"type":33,"value":1606,"toc":1610},[1607],[36,1608,1609],{},"hello world, hi :)",{"title":89,"searchDepth":155,"depth":155,"links":1611},[],"2026-10-26","My first post.",{},"\u002Fblog\u002Fhello-world",{"title":1602,"description":1613},"blog\u002Fhello-world","5qP3gb5Y2WfyUyR3TEikePFowe_zlRmSRO_mWiHTnpE",[1620,1632],{"id":1621,"title":1622,"description":1623,"extension":11,"languages":1624,"meta":1629,"order":123,"stem":1630,"url":9,"__hash__":1631},"projects\u002Fprojects\u002Fkurange-sap.yml","Kurange-sap","AI-powered vaping tracker API built around OpenAI GPT, with intake logging, analytics, AI coaching, craving prediction, streaks, health milestones and money-saved tracking.",[1625,1626,1627,1628],"JavaScript","Node.js","Express","OpenAI API",{},"projects\u002Fkurange-sap","dQOCPrE9trTehKyPpfOrlK8C2I6ydJ12QqWE7aqvoWE",{"id":1633,"title":1634,"description":1635,"extension":11,"languages":1636,"meta":1640,"order":155,"stem":1641,"url":9,"__hash__":1642},"projects\u002Fprojects\u002Fsabil-al-hikmah.yml","Sabil al-Hikmah Claim Management System","QR code based meal claim system for UTHM that cut processing time from days to minutes, with a role-based backend and RESTful APIs.",[1637,1638,1639],"PHP","CodeIgniter 3","MySQL",{},"projects\u002Fsabil-al-hikmah","efLZpCYDusIT4K7UXVzzPjgUFtdd_nS52WUunYY3VOo",1793286283459]