How to Perform Calculations Using SELECT Statement in T-SQL · DpkDB
How to Perform Calculations Using SELECT Statement in T-SQL
Do arithmetic in a T-SQL SELECT and avoid the usual traps: integer division, NULL results, divide-by-zero errors, overflow, rounding and float inaccuracy.
The fourth result is the one that catches people out. 7 / 2 is 3, not 3.5, because both operands are integers. Most problems with calculations in T-SQL come down to data types, NULL or division by zero, and this page covers each in turn.
Error 8152 never said which column. Message 2628 on SQL Server 2019+ (or trace flag 460 on 2016/2017) names it. Then fix the data or the column, and watch for silent truncation.
Multiplication, division and modulo are evaluated before addition and subtraction. Operators of equal precedence are evaluated left to right. Parentheses override both:
When both operands are integer types, / returns an integer and discards the fraction. It truncates toward zero. It does not round.
SELECT7/2AS a, -- 3-7/2AS b, -- -31/2AS c, -- 07/2.0AS d, -- 3.5000007*1.0/2AS e; -- 3.5, displayed with trailing zeros
To get a fractional result, make at least one operand a decimal before the division happens. With columns, use CAST:
SELECTCAST(Delivered ASdecimal(10,2)) / Ordered AS fill_rate
FROM dbo.Shipments;
Order of evaluation matters when you calculate percentages with integers:
SELECT45/60*100AS wrong, -- 0, because 45 / 60 is 0100*45/60AS better, -- 75100.0*45/60AS best; -- 75, as a decimal with trailing zeros
How SQL Server picks the result type
When the two operands have different types, the one with lower precedence is converted to the other. The order that matters for arithmetic is, from low to high: tinyint, smallint, int, bigint, money, decimal, real, float. So int + decimal is decimal, and decimal + float is float.
A literal with a decimal point, such as 2.0, is a decimal. A literal without one, such as 2, is an int. Character strings rank below numbers, which leads to this:
SELECT'5'+5; -- 10: the string is converted to intSELECT'Total: '+5; -- error
Msg 245: Conversion failed when converting the varchar value 'Total: ' to data type int.
+ is both addition and string concatenation, and with one number involved it always means addition. To build a string, use CONCAT('Total: ', 5), which converts its arguments for you.
For decimal results, SQL Server works out precision and scale from the operands. Multiplication adds the scales, which is why 13.7858 * 3.58 has six decimal places. Division produces at least six. You rarely want the raw result, so cast the final answer to the scale you need.
The expression is evaluated once per row. Aggregates work over expressions too: SELECT SUM(Quantity * UnitPrice) FROM dbo.OrderLines; returns 490.47.
NULL makes the whole result NULL
The Keyboard row has no discount recorded, and its NetTotal is NULL. Any arithmetic involving NULL returns NULL, because the value is unknown. Supply a default where one makes sense:
SELECT Product,
CAST(Quantity * UnitPrice * (1- ISNULL(DiscountPct, 0) /100) ASdecimal(10,2)) AS NetTotal
FROM dbo.OrderLines;
The Keyboard row now returns 59.97. COALESCE(DiscountPct, 0) is the standard SQL equivalent. Only substitute a default when "missing" really means zero. Aggregates such as SUM and AVG skip NULLs, so AVG(DiscountPct) averages three rows here, not four.
Division by zero
SELECT10/0;
Msg 8134: Divide by zero error encountered.
One row with a zero divisor fails the whole statement. The usual fix is NULLIF, which turns the zero into NULL so the result is NULL instead of an error:
SELECT Product,
UnitPrice /NULLIF(Quantity, 0) AS PricePerUnit
FROM dbo.OrderLines;
Wrap it in ISNULL(..., 0) if you want zero back. Avoid SET ARITHABORT OFF and SET ANSI_WARNINGS OFF as a workaround: they hide the error for everything in the session and break features such as indexed views.
Overflow
Integer arithmetic stays in the integer type, even when the answer does not fit:
SELECT2147483647+1;
Msg 8115: Arithmetic overflow error converting expression to data type int.
Convert one operand to a larger type first: SELECT CAST(2147483647 AS bigint) + 1; returns 2147483648. The same applies to SUM over an int column with a large total (cast the column inside the SUM), and to COUNT, which has a COUNT_BIG counterpart.
Rounding
ROUND rounds the value but keeps the data type, so the trailing digits remain. CAST to a smaller scale rounds and shortens:
SELECT ROUND(123.456, 2) AS a, -- 123.460CAST(123.456ASdecimal(10,2)) AS b, -- 123.46
ROUND(123.456, 2, 1) AS c, -- 123.450 (third argument truncates)
ROUND(123.456, -1) AS d, -- 120.000CEILING(123.456) AS e, -- 124FLOOR(123.456) AS f; -- 123
decimal values round half away from zero: ROUND(2.5, 0) is 3.0 and ROUND(-2.5, 0) is -3.0.
Round once, at the end. Rounding each row and then summing gives a different total from summing and then rounding.
Use decimal for money, not float
float and real are binary approximations. They cannot represent most decimal fractions exactly:
SELECTCASEWHENCAST(0.1ASfloat) +CAST(0.2ASfloat) =CAST(0.3ASfloat)
THEN'equal'ELSE'not equal'END; -- not equal
Use decimal (also spelled numeric) for prices, quantities and anything you compare for equality or have to reconcile. Keep float for scientific measurements. The money type is exact but has only four decimal places, and intermediate results of money division are truncated to four places as well, so many teams prefer decimal for calculations.
Reusing a calculated value
A column alias cannot be referenced elsewhere in the same SELECT list or in WHERE:
SELECT Quantity * UnitPrice AS LineTotal,
LineTotal *0.2AS Tax -- Invalid column name 'LineTotal'FROM dbo.OrderLines;
Define the value once with CROSS APPLY, then use it freely:
SELECT ol.Product, c.LineTotal,
CAST(c.LineTotal *0.2ASdecimal(10,2)) AS Tax
FROM dbo.OrderLines AS ol
CROSS APPLY (SELECT ol.Quantity * ol.UnitPrice AS LineTotal) AS c
WHERE c.LineTotal >50;
A CTE or derived table works as well. If the same expression is needed in many queries, add it to the table as a computed column:
ALTER TABLE dbo.OrderLines ADD LineTotal AS (Quantity * UnitPrice);
Performance
Arithmetic in the SELECT list costs almost nothing. Arithmetic on a column in the WHERE clause is different, because it stops SQL Server from seeking on an index on that column:
WHERE UnitPrice *1.2>100-- scans every rowWHERE UnitPrice >100/1.2-- can use an index on UnitPrice
Move the calculation to the other side of the comparison. If you must filter on a computed expression, create a computed column for it and index that.
Other math functions
ABS, POWER, SQRT, SIGN, LOG, EXP and the trigonometric functions are built in. Note that POWER returns the type of its first argument, so POWER(2, 0.5) returns 1. Write POWER(2.0, 0.5) or use SQRT.
A query-first method: find the expensive queries, read the plan for signs the missing index DMVs never report, build the index online safely, then verify and roll back.