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