Why does AVG(score) return 4 rather than 4.5 when score is an INTEGER column?
answer
- The data is fine; the type is not
- Same idea as integer division
- Truncation happens inside the function
- Where you put the CAST decides the answer
- Result type derives from the argument type
basics
~20 sAVG's result type is derived from its argument type, and the standard leaves the precision implementation-defined. Engines that keep the integer type for integer input discard the fractional part, so an average of 4 and 5 comes back as 4.
solid answer
~40 sThe result type of `AVG` is derived from the argument type, and standard SQL leaves the precision and scale of that result implementation-defined. An engine that keeps INTEGER as the result type for INTEGER input has nowhere to put `.5`, so it truncates — `AVG(score)` over 4 and 5 returns 4. SQL Server behaves this way; PostgreSQL, MySQL and SQLite all return a fractional type for the same query. The fix is to widen the **input**, not the output: `AVG(CAST(score AS DECIMAL(10,2)))` averages decimals and returns 4.50. Casting the result — `CAST(AVG(score) AS DECIMAL(10,2))` — is too late, because the truncation already happened inside the aggregate and you get 4.00. The same reasoning applies to `SUM(score) / COUNT(score)`: if both operands are integers, the division is integer division.
code
sql · 10 lines-- score is INTEGER and holds 4 and 5
-- engine-dependent: 4 where the result type stays INTEGER
SELECT AVG(score) FROM exam_results;
-- wrong fix: truncation already happened inside AVG -> 4.00
SELECT CAST(AVG(score) AS DECIMAL(10,2)) FROM exam_results;
-- right fix: widen the argument -> 4.50
SELECT AVG(CAST(score AS DECIMAL(10,2))) FROM exam_results;go deeper
Recognise the symptom: an average of whole numbers that comes back whole is a type rule, not lost rows. Know that CAST goes around the column inside the function.
Explain that AVG derives its result type from the argument and that the standard leaves the precision implementation-defined, then name which engines truncate and which do not. Show why casting the result is too late.
Treat it as a correctness risk in reporting: point out that the failure is silent, name the review rule you apply (average money and rates only over an explicit DECIMAL), and connect it to the integer-SUM overflow that shares the same rule.
Own the portability decision: if the same SQL runs on more than one engine, standardise on casting at the source or on storing the measure in DECIMAL, so results do not shift when the query moves between engines.
## The symptom A table holds two exam scores in an `INTEGER` column, 4 and 5. On some engines this query returns 4.5 and on others it returns 4: ```sql SELECT AVG(score) FROM exam_results; ``` Nothing is wrong with the data and nothing is wrong with the query. The difference is a **result-type rule**, and it catches people because averaging is the one aggregate whose natural answer usually is not the type you fed it. ## Why the type rule bites Standard SQL specifies that if the argument of `AVG` is an exact numeric type, the result is an exact numeric type — but it leaves the precision and scale of that result **implementation-defined**. Two readings are therefore both conforming: - Promote the result to a type with a fractional part (a decimal or numeric), and report 4.5. - Keep the argument's own type, INTEGER, and report the integer part of the true average: 4. The second reading is not a bug. It is the same principle as integer division in most programming languages: an operation over integers produces an integer, discarding the remainder. Because the truncation happens *inside* the aggregate, no rounding or warning surfaces — the value simply arrives with the decimals gone. ## Who does what Engines genuinely diverge here, so this is one of the few places worth memorising concrete behaviour: - **SQL Server** returns the argument's type for integer input, so `AVG` over an `int` column is an `int` and truncates. - **PostgreSQL** returns `numeric` for integer input, so you get `4.5000000000000000`. - **MySQL** returns a `DECIMAL` for exact-valued arguments, so you get `4.5000`. - **SQLite** is dynamically typed and its `avg()` always yields a floating-point value, so you get 4.5. If a query is meant to run on more than one of those, do not rely on any of it — make the input unambiguous. ## Fixing it: cast the input, not the result The correct fix widens the operand before the aggregate consumes it: ```sql SELECT AVG(CAST(score AS DECIMAL(10,2))) FROM exam_results; -- 4.50 ``` Multiplying by a decimal literal has the same effect and is shorter, though slightly less explicit: ```sql SELECT AVG(score * 1.0) FROM exam_results; ``` The tempting but wrong fix is to cast the **result**: ```sql SELECT CAST(AVG(score) AS DECIMAL(10,2)) FROM exam_results; -- 4.00, not 4.50 ``` By the time the outer `CAST` runs, `AVG` has already produced the integer 4; converting 4 to a decimal gives 4.00. Precision that was discarded inside the aggregate cannot be recovered outside it. This is the single most common wrong answer to the question, and interviewers ask it specifically to see whether you understand *where* the loss occurs. The same trap appears when people write the average manually: ```sql SELECT SUM(score) / COUNT(score) FROM exam_results; -- integer / integer SELECT SUM(score) * 1.0 / COUNT(score) FROM exam_results; -- fractional ``` If both operands are integers, the division is integer division, whatever the engine does inside `AVG`. ## Choosing the target type When you cast, pick the type that matches the meaning of the number. `DECIMAL`/`NUMERIC` with an explicit precision and scale gives exact base-10 arithmetic and is the right choice for money and for anything a human will reconcile. Casting to `DOUBLE PRECISION` avoids truncation too, but introduces binary floating-point rounding, so an average of currency amounts may print a trailing `.30000000000000004`. Choose the scale deliberately: `DECIMAL(10,2)` says "two decimal places matter", which is a statement about the business, not about SQL. ## How to answer it in an interview Say three things, in order: the result type of `AVG` is derived from the argument type and is implementation-defined in precision; engines differ, with SQL Server truncating integer input while PostgreSQL, MySQL and SQLite return a fractional type; and the portable fix is to cast the argument, because casting the result recovers nothing. If you have room for a fourth point, note that this is the *same* family of rules that makes an integer `SUM` overflow on engines that do not widen — one rule, two well-known production bugs.
- Why doesn't CAST(AVG(score) AS DECIMAL(10,2)) fix the problem?Because the conversion runs after the aggregate has already produced its value. On an engine that keeps INTEGER for integer input, `AVG` yields 4, and casting 4 to `DECIMAL(10,2)` yields 4.00. The fractional part was discarded inside the aggregate, and no outer conversion can reconstruct it. Widen the argument instead.
- Does the same truncation risk apply to SUM over an INTEGER column?Not truncation — a sum of integers is exactly an integer, so nothing is lost. The related risk is the opposite one: on an engine whose `SUM` keeps the argument type, a large total can exceed the integer range and raise an arithmetic overflow. Both bugs come from the same result-type derivation rule, and both are fixed by casting the argument.
- Should you cast to DECIMAL or to DOUBLE PRECISION?`DECIMAL`/`NUMERIC` with an explicit precision and scale for anything a human reconciles — money, rates, reported KPIs — because base-10 arithmetic is exact and the scale documents how many places matter. `DOUBLE PRECISION` avoids truncation too but introduces binary rounding artefacts, which is fine for scientific magnitudes and poor for currency.
saying these in an interview costs you the question
- Blaming NULLs or missing rows for the whole-number result
- Casting the aggregate's result instead of its argument
- Claiming AVG always returns a fractional value
- Saying it is a rounding setting rather than a type rule
- Assuming SUM(x)/COUNT(x) avoids the problem