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