A minha nova realidade profissional trouxe-me o contacto diário com o bem conhecido Sistema de Gestão de Base de Dados (SGBD) da Microsofot: Microsoft SQL Server.
Não é pretensão deste post tecer qualquer consideração sobre a comprovada qualidade deste motor, mas sim expôr uma situação no mínimo curiosa que, embora possa parecer irrelevante, representou um gasto de tempo de produção considerável.
Para os interessados em simular esta situação e para o decorrer deste post, propiciem um ambiente com a seguinte tabela:
1 2 3 4 5 | CREATE TABLE teste( resultado SMALLINT, valor1 FLOAT, valor2 FLOAT ) |
e com o registo:
1 | INSERT INTO teste (resultado,valor1,valor2) VALUES (0,175.1,35.02) |
O problema gira agora em torno das queries que se seguem:
1 2 | UPDATE teste SET resultado = valor1/valor2 SELECT (valor1/valor2) AS ValorEsperado, resultado AS ValorArmazenado FROM test |
Se tudo correr como esperado, o resultado da execução da instrução select será um registo com dois campos: ValorEsperado e ValorArmazenado – cujos valores serão respectivamente 4 e 5.
O que há de estranho nesta situação?!
O valor esperado da operação valor1/valor2 é efectivamente 5 (ValorEsperado) no entanto, aquando do update foi armazenado o valor 4 (ValorArmazenado).
Levei algum tempo a perceber a origem do problema que pode até parecer óbvia.
Os atributos valor1 e valor2 são do tipo float enquanto que o atributo resultado, onde será armazenado o valor resultante da divisão, é do tipo smallint.
O problema dá-se num suposto casting que é feito internamente para armazenar esse valor como smallint.
Até aqui pacífico. Embora estejamos a falar em valor absoluto de 5 unidades na verdade o que temos é 5.0 e tenho plena consciência da ousadia do casting de integer (smallint pode ver-se como um subset do tipo int) para float.
O curioso, e que me deixou a pensar foi mesmo o resultado da instrução update quando é feito um casting “explicíto”:
1 | UPDATE teste SET resultado = CAST((valor1/valor2) AS SMALLINT) |
Numa primeira análise pareceu-me lógico que o motor, sabendo da inconsistência de tipos, fizesse internamente um cast de float para smallint, mas parece que isso não acontece.
Esta brincadeira, consumiu-me aproximadamente 10min: 2min para análise do problema e 8min para considerações de mim para comigo sobre o mistério.
Para não o deitar comigo na almofada estou atento às vossas considerações.
by Paulo A.Silva
17 Abr 2009 at 08:49
“When you use the +, -, *, /, or % arithmetic operators to perform implicit or explicit conversion of int, smallint, tinyint, or bigint constant values to the float, real, decimal or numeric data types, the rules that SQL Server applies when it calculates the data type and precision of the expression results differ depending on whether the query is autoparameterized or not.
Therefore, similar expressions in queries can sometimes produce different results. When a query is not autoparameterized, the constant value is first converted to numeric, whose precision is just large enough to hold the value of the constant, before converting to the specified data type. For example, the constant value 1 is converted to numeric (1, 0), and the constant value 250 is converted to numeric (3, 0).
When a query is autoparameterized, the constant value is always converted to numeric (10, 0) before converting to the final data type. When the / operator is involved, not only can the result type’s precision differ among similar queries, but the result value can differ also. For example, the result value of an autoparameterized query that includes the expression SELECT CAST (1.0 / 7 AS float) will differ from the result value of the same query that is not autoparameterized, because the results of the autoparameterized query will be truncated to fit into the numeric (10, 0) data type. For more information about parameterized queries, see Simple Parameterization[1].”
in http://msdn.microsoft.com/en-us/library/ms187745.aspx
[1]http://msdn.microsoft.com/en-us/library/ms186219.aspx