3
After migrating my database from Access to Sqlite 3 in a Windows Forms application, I have been having difficulties with Decimal values. If I run a simple select:
SELECT * FROM Item WHERE id = 1;
The decimal values of the preco_item column are correctly displayed, with 2 decimal places. Example: 2.53 = 2.53. But when I do a query a little more complex, with related tables as following:
SELECT Produto.nome AS Produto,
Item.quantidade AS Quantidade,
Item.preco_item AS Preço,
Item.preco_item * Item.quantidade as Subtotal,
Item.id
FROM Item
INNER JOIN Orcamento ON Item.id_orcam = Orcamento.id
INNER JOIN Produto ON Item.id_produto = Produto.id
WHERE(Orcamento.id = 1 );
The results obtained in the Price line are presented in DataTable
thus: 2,53
= 2,00
, thus also happens with the result of multiplication preco_item * quantidade
.
Note: Monetary items are set to Decimal(7.2) in Sqlite. I have tried several other formats. In the query by Sqlite Studio, the price results present normally, with the exception of multiplication that I have not yet found a way to calculate correctly. Always returns an integer value to me.
P.S.: Values are entered into a Datagridview via Datatable.
Update to demonstrate BD schema:
private static string createQuery()
{
string createScript = "CREATE TABLE Cliente (" +
" id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" nome TEXT NOT NULL UNIQUE," +
" endereco TEXT," +
" local TEXT," +
" telefone_1 TEXT," +
" telefone_2 TEXT ); ";
createScript += "CREATE TABLE Produto (" +
" id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" nome TEXT NOT NULL UNIQUE," +
" preco DECIMAL ); ";
createScript += "CREATE TABLE Tipo (" +
" id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" nome TEXT," +
" descricao TEXT ); ";
createScript += "CREATE TABLE Orcamento (" +
" id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
" id_cliente INTEGER REFERENCES Cliente (id)," +
" data DATETIME," +
" id_tipo INTEGER REFERENCES Tipo(id)," +
" desconto DECIMAL," +
" status TEXT DEFAULT Pendente ); ";
createScript += "CREATE TABLE Item (" +
" id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" id_orcam INTEGER REFERENCES Orcamento (id)," +
" id_produto INTEGER REFERENCES Produto(id)," +
" preco_item DECIMAL," +
" quantidade INTEGER NOT NULL ); ";
return createScript;
}
Here is the code that fills the Datagridview:
string query = Queries.GetItemsByOrcId(orcId);
sql.CustomSelect(query);
dgvpProd.DataSource = sql.Datatable;
There is no "Decimal(7,2)" in Sqlite. Either it is INTEGER, or it is REAL. And REAL you should not use under any circumstances for price. The least painful path is usually to create an INTEGER of the name preco_item100, for example, and save 2.35 as 235. Or preco_item1000 if you need 3 houses, as the gas station staff does. (this number in the name is a technique to make life easier and you always know where to put the comma and/or how much is the divider, it’s just a hint). In the worst case it has to store as character, but then I understand it causes other problems.
– Bacco
Note: to tell the truth, even when the bank supports, I still find the price 100 better than decimal, unless you need some abstraction that the type offers on the client language side.
– Bacco
I get it. I’ll try removing the commas in Insert and find a way to treat the values as a currency format in c#. Thank you.
– Vanderlei
Hello friend, unfortunately I’m already refactoring everything according to Bacco’s tip. But in short I was generating a datatable with the result of select and using the Datagridview Source method to populate it. The price column type was decimal according to the BD column model, but the result always rounded when it was displayed in the DGV. Now I put the BD columns as int, dividing by 100 and parsing the decimal result for string and using the columns with type string.
– Vanderlei
Ali has the second select mentioned in the post.
– Vanderlei
@Vanderlei here has several explanations as to why one should not use floating point with money. This is a basic premise of any trusted system: http://answall.com/questions/5746/bestdatatypes-workingwith money
– Bacco
@Vanderlei notes that in principle, in the Sqlite affinity manual the Decimal is converted to "number", which is stored as "integer" if it has no decimal places and "float" when it has one. The fact that it works with "real", however, does not mean that it should be used, precisely because of the cumulative rounding problems in any mathematical operations. (often you store a 23.50, but the REAL stores as 23.499999999.... - on the screen appears right, but when doing a multiplication by quantity, starts to give error, even on the screen appearing . 50)
– Bacco
And this is not a problem unique to the language A or B, but the way the floating numbers work. One more example, from the outside: https://javeiros.wordpress.com/2008/06/25/evite-float-e-double-se-voce-quer-respostas-exact/ - if you search for "float" and "money" you will find a lot about the subject. Dbs that have "decimal" and "money" type do not use float internally, but a fixed format. As Sqlite does not have it native, you need to adapt some kind of it. Only the integers and the texts are left. The integer is more efficient and saves space, the text is simpler to view.
– Bacco
In fact wear
real
in Sqlite for monetary value does not give. To see about Sqlite: http://answall.com/q/78306/101. To see about the problem in general: http://answall.com/q/38138/101. This is something basic that every programmer who already has a minimum of experience ends up learning. It is a problem related to the processor and all languages and databases suffer from it. The detailed explanation is in these questions, but there are several other answers from various authors on and off the site. There are links to other sources for those who have not yet learned about the subject.– Maniero
I am refactoring according to your experience. I should take some time, since it is a project as a hobby. I will store as int, divide by 100 and present in DGV as string. This would be the way?
– Vanderlei
@Vanderlei the important thing is to understand where the float "betrays" our expectations (the fact that you see a 3.50, but the PC see 3.4999999982337). The whole has too much work to divide, but if you drop the rounding to the last step, it ensures that the inaccuracy will not propagate by multiple calculations.
– Bacco
Regardless of storage, another example of care when making monetary calculations, is to always maintain the chosen precision, not leaving "broken" more. If for example to sell a product fractionated whose calculation value x quantity gave 23,731 cents, and on the screen appears 23.73 only, you need to discard this 1 more at the end before adding up the total (add 23.73, not 23.731), but this "subcentavo" invisible will join with other broken subtotal, and in the end the customer has an extra penny sum without knowing where it came from, burning reliability and accounting.
– Bacco