How do ROUND, CEIL and FLOOR differ from each other, and what do they do to negative numbers?
ROUND moves a number to the nearest representable value at a given number of decimal places. CEIL rounds towards positive infinity, so 2.1 becomes 3 and -2.1 becomes -2. FLOOR rounds towards negative infinity, so 2.9 becomes 2 and -2.9 becomes -3. CEIL and FLOOR always return an integer; ROUND takes a precision argument and keeps the type of its input. Not every SQLite build compiles the CEIL and FLOOR functions in, so the sample below works them out with a cast and a case instead.
-- ROUND is built into every SQLite build and takes a precision argument.
SELECT ROUND(2.5) AS round_half_up,
ROUND(-2.5) AS round_negative,
ROUND(2.345, 2) AS round_to_two_places;
-- CEIL and FLOOR are not compiled into every SQLite build, so the sample
-- spells them out. A cast to INTEGER truncates toward zero, so the sign of
-- the remainder decides which way to go: up for a positive fraction, down
-- for a negative one.
WITH vals(v) AS (VALUES (2.1), (-2.1), (2.9), (-2.9))
SELECT v,
CAST(v AS INTEGER) + CASE WHEN v > CAST(v AS INTEGER) THEN 1 ELSE 0 END AS ceil_v,
CAST(v AS INTEGER) - CASE WHEN v < CAST(v AS INTEGER) THEN 1 ELSE 0 END AS floor_v
FROM vals
ORDER BY v;The results are the key part. CEIL always moves towards positive infinity regardless of sign, and FLOOR always moves towards negative infinity, which is the opposite of what "ceil" and "floor" intuitively suggest for negative numbers. If you want the magnitude rounded and the sign preserved, use ROUND, not CEIL or FLOOR.
ROUND has its own convention, and it differs between engines. SQLite's ROUND(2.5) returns 3.0 — a round-half-away-from-zero rule. PostgreSQL's round(numeric, int) also rounds half away from zero, but round(double precision) uses the C library's banker's rounding, where 2.5 becomes 2.0 and 3.5 becomes 4.0. MySQL's ROUND for exact-value numbers rounds half away from zero, while ROUND on a floating-point column uses the platform's rounding mode.
That inconsistency is why you should always specify the number of decimal places and never rely on the default: ROUND(x, 2) rather than ROUND(x). And when the value is money, cast to a fixed-precision decimal type first — ROUND(CAST(amount AS DECIMAL(12, 2)), 2) — so the function operates on an exact numeric value instead of a binary float.