We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Numeric Types
Choosing the right numeric type matters. Using FLOAT for currency will eventually cause rounding bugs. Using NUMERIC(20,10) everywhere wastes storage and slows queries. This lesson covers PostgreSQL's numeric types and when to use each one.
Integer Types
PostgreSQL offers three integer types that differ in their range:
| Type | Storage | Range |
|---|---|---|
SMALLINT |
2 bytes | -32,768 to 32,767 |
INTEGER (or INT) |
4 bytes | -2,147,483,648 to 2,147,483,647 |
BIGINT |
8 bytes | -9.2 quintillion to 9.2 quintillion |
When to Use Each
- SMALLINT: Quantities, ages, ratings, or status codes with small ranges
- INTEGER: The default choice for most whole-number data -- IDs, counts, quantities
- BIGINT: Very large numbers like social media post IDs, nanosecond timestamps, or financial amounts in cents at global scale
-- Integers in action
SELECT
42 AS an_integer,
-100 AS negative_int,
2147483647 AS max_integer;
SERIAL: Auto-Incrementing IDs
SERIAL is not a true data type -- it is a shortcut that creates an INTEGER column backed by an auto-incrementing sequence:
| Type | Underlying Type | Range |
|---|---|---|
SMALLSERIAL |
SMALLINT | 1 to 32,767 |
SERIAL |
INTEGER | 1 to 2,147,483,647 |
BIGSERIAL |
BIGINT | 1 to 9.2 quintillion |
When you define a column as SERIAL, PostgreSQL automatically:
- Creates the column as
INTEGER NOT NULL - Creates a sequence object
- Sets the column's default to
nextval('sequence_name')
-- Creating a table with a SERIAL primary key
CREATE TABLE example (
id SERIAL PRIMARY KEY,
name TEXT
);
-- When inserting, you can omit the id column
INSERT INTO example (name) VALUES ('Alice');
-- id will automatically be 1
INSERT INTO example (name) VALUES ('Bob');
-- id will automatically be 2
Modern alternative: PostgreSQL 10+ supports GENERATED ALWAYS AS IDENTITY, which is the SQL-standard way to achieve auto-incrementing IDs:
CREATE TABLE example (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT
);
Decimal/Numeric Types
For exact decimal numbers (especially money), use NUMERIC or DECIMAL:
NUMERIC(precision, scale)
- precision: Total number of digits (both sides of the decimal point)
- scale: Number of digits after the decimal point
| Definition | Example Values | Storage |
|---|---|---|
NUMERIC(10, 2) |
12345678.90 | Variable |
NUMERIC(5, 3) |
12.345 | Variable |
NUMERIC (no params) |
Any precision | Variable |
SELECT
123.456::NUMERIC(10, 2) AS rounded_to_cents,
123.456::NUMERIC(5, 3) AS three_decimals;
The first expression returns 123.46 (rounded to 2 decimal places). The second returns 123.456.
Key point: NUMERIC and DECIMAL are identical in PostgreSQL -- they are two names for the same type.
Floating-Point Types
Floating-point types store approximate numeric values using the IEEE 754 standard:
| Type | Storage | Precision |
|---|---|---|
REAL (or FLOAT4) |
4 bytes | ~6 decimal digits |
DOUBLE PRECISION (or FLOAT8) |
8 bytes | ~15 decimal digits |
SELECT
1.0 / 3.0 AS division_result,
(1.0 / 3.0)::REAL AS as_real,
(1.0 / 3.0)::DOUBLE PRECISION AS as_double;
The Floating-Point Trap
Floating-point arithmetic can produce surprising results:
SELECT 0.1::REAL + 0.2::REAL = 0.3::REAL AS is_equal;
-- This might return false!
This is not a PostgreSQL bug -- it is how floating-point math works in all programming languages. The binary representation of 0.1 is slightly inexact.
Rule of thumb: Use NUMERIC for money and any value that must be exact. Use FLOAT/DOUBLE PRECISION for scientific calculations where tiny rounding errors are acceptable and performance matters.
Type Casting
PostgreSQL provides two ways to convert between types:
The :: Operator (PostgreSQL-Specific)
SELECT
'42'::INTEGER AS text_to_int,
42::TEXT AS int_to_text,
'3.14'::NUMERIC AS text_to_numeric,
salary::NUMERIC(10, 0) AS salary_rounded
FROM employees
LIMIT 3;
The CAST Function (SQL Standard)
SELECT
CAST('42' AS INTEGER) AS text_to_int,
CAST(42 AS TEXT) AS int_to_text,
CAST('3.14' AS NUMERIC) AS text_to_numeric,
CAST(salary AS NUMERIC(10, 0)) AS salary_rounded
FROM employees
LIMIT 3;
Both produce the same result. The :: operator is more concise and is the convention in PostgreSQL code.
Arithmetic and Rounding
Integer Division
Be careful with integer division -- it truncates the decimal:
SELECT 7 / 2 AS integer_division;
-- Returns 3, not 3.5!
To get a decimal result, cast one operand:
SELECT 7.0 / 2 AS decimal_division;
-- Returns 3.5
SELECT 7::NUMERIC / 2 AS also_decimal;
-- Returns 3.5
Rounding Functions
SELECT
ROUND(3.7) AS rounded, -- 4
ROUND(3.14159, 2) AS two_dec, -- 3.14
CEIL(3.2) AS ceiling, -- 4
FLOOR(3.8) AS floor_val, -- 3
TRUNC(3.789, 1) AS truncated; -- 3.7
| Function | Behavior | ROUND(3.5) | Result |
|---|---|---|---|
ROUND |
Rounds to nearest | ROUND(3.5) |
4 |
CEIL |
Always rounds up | CEIL(3.1) |
4 |
FLOOR |
Always rounds down | FLOOR(3.9) |
3 |
TRUNC |
Truncates (removes decimals) | TRUNC(3.9) |
3 |
Practical Examples
Working with Salary Data
SELECT
name,
salary,
ROUND(salary / 12.0, 2) AS monthly_salary,
ROUND(salary / 52.0, 2) AS weekly_salary,
ROUND(salary / 260.0, 2) AS daily_rate
FROM employees
ORDER BY salary DESC
LIMIT 5;
Price Calculations
SELECT
name,
price,
ROUND(price * 1.08, 2) AS price_with_tax,
ROUND(price * 0.9, 2) AS discounted_price
FROM products
ORDER BY price DESC;
Try It Yourself
- Select all employees and show their salary as both an integer (no decimals) and rounded to the nearest thousand.
SELECT
name,
salary,
salary::INTEGER AS salary_int,
ROUND(salary, -3) AS salary_nearest_thousand
FROM employees;
- Calculate the total price and average price of products, showing results rounded to 2 decimal places.
SELECT
SUM(price) AS total_price,
ROUND(AVG(price), 2) AS avg_price,
MIN(price)::INTEGER AS cheapest_rounded,
MAX(price)::INTEGER AS priciest_rounded
FROM products;
- Show each order with its total_price and a column showing what 15% tax would be, rounded to 2 decimal places.
SELECT
customer_name,
total_price,
ROUND(total_price * 0.15, 2) AS tax_amount,
ROUND(total_price * 1.15, 2) AS total_with_tax
FROM orders
ORDER BY total_price DESC;