Efficient PostgreSQL Database Design and Data Types
Although I’ve been working as a Software Engineer for several years and have been confident in using PostgreSQL as a primary database for my applications, I decided to take an additional course.
Here are some important takeaways from the course. Part I.
1. Designing Efficient Database Tables
When designing a database table, it’s essential to choose data types that are representative and compact:
Representative: The data type should cover the full range of expected values.
Compact: The type should not over-allocate space. For example, there’s no need to use a
BIGINTif the value range only requires aSMALLINT.
Example: Age
A person’s age typically falls between 0 and 150. So, instead of using INTEGER or BIGINT, which would use more space, it’s more efficient to use SMALLINT.
CREATE TABLE people (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
age SMALLINT
);Choosing the correct data type is crucial for both storage efficiency and query performance.
2. Understanding PostgreSQL Numeric Data Types
Numeric vs Floating-Point Types
In PostgreSQL, you can choose between numeric and floating-point types depending on the precision requirements of your application:
Numeric types (
NUMERIC,DECIMAL) are precise and can handle very large values, but they are slower for arithmetic operations.Floating-point types (
REAL,DOUBLE PRECISION) are fast but approximate values, making them suitable for scenarios where small precision loss is acceptable.
Numeric Example:
The NUMERIC(PRECISION, SCALE) type lets you define both the total number of digits (PRECISION) and the number of decimal places (SCALE).
SELECT 12345::NUMERIC(5, 3); -- Result: 123.450If you try to insert a value that exceeds the defined precision, PostgreSQL will raise an error:
SELECT 125345::NUMERIC(5, 3); -- Error: Numeric Field OverflowFloating Point Example:
Floating-point types are ideal for storing approximations where speed is more critical than absolute precision.
SELECT 6.0::float4 * (3.0 / 10.0) as float,
6.0::NUMERIC * (3.0 / 10.0) as numeric
Output:
+------------------+-------+
|float |numeric|
+------------------+-------+
|1.7999999999999998|1.8 |
+------------------+-------+3. Storing Money in PostgreSQL
Using the money Data Type
PostgreSQL has a built-in money data type, which is convenient for storing currency values in the default format (US Dollars). However, it has some limitations:
Advantages:
It’s simple to use and displays values with the dollar sign.
It can accept different numeric values in formats like
VARCHAR,INT, andDECIMALinINSERTstatements.
CREATE TABLE account (
id SERIAL PRIMARY KEY,
amount MONEY
);
INSERT INTO account (amount)
VALUES ('$123.00'),
('$23.99'),
(22.22),
(19.99),
(1200);Disadvantages:
Precision loss: When performing math operations, precision may be lost. It’s not ideal for high-precision financial calculations.
Currency handling: The currency is determined by the
lc_monetarysetting (default isen_US.utf8for USD), which can cause confusion if you need to handle multiple currencies. Changinglc_monetarywill change the display format but not convert the actual values.
Alternative Approaches:
Use
NUMERIC: This data type is flexible and precise, but it can be slower for arithmetic operations.Use
INTEGER: For better performance, consider storing monetary values as integers by converting the currency to the smallest unit (e.g., cents). This maintains precision and optimizes math operations.
-- Store $199.99 as 19999 cents
CREATE TABLE account (
id SERIAL PRIMARY KEY,
amount INTEGER -- Amount stored in cents
);If you’re working with multiple currencies, it’s best to store a separate currency column to differentiate between them:
CREATE TABLE account (
id SERIAL PRIMARY KEY,
amount INTEGER, -- In cents
currency CHAR(3) -- e.g., 'USD', 'EUR', 'AED'
);4. Special Values: Infinity and -Infinity
PostgreSQL allows you to store extremely large or small values using Infinity and -Infinity. These values are useful when dealing with scenarios where a value is beyond the representable range.
SELECT '-inf'::float; -- Result: -Infinity
SELECT 'inf'::float + 'inf'::float; -- Result: Infinity
SELECT 'inf'::float - 'inf'::float; -- Result: NaN (Not a Number)These values can be used to represent extremes like maximum stock values or when you want to indicate that a value is unbounded.
5. Type Casting in PostgreSQL
PostgreSQL offers powerful type casting functionality that allows you to convert values between different data types.
Example:
SELECT '100'::INTEGER, '23.99'::NUMERIC, pg_typeof('100'::INT2);This will cast the string '100' to an INTEGER and the string '23.99' to a NUMERIC. You can also check the type of a cast using the pg_typeof() function:
SELECT pg_typeof('100'::INT2); -- Result: smallint6. PG_COLUMN_SIZE Function
PostgreSQL offers the PG_COLUMN_SIZE() function, which can be helpful when you need to understand the storage size of different data types. This is important for optimizing table design and query performance.
Example:
SELECT PG_COLUMN_SIZE(200::int2) AS int2,
PG_COLUMN_SIZE(3000) AS int4,
PG_COLUMN_SIZE(299.99) AS numeric,
PG_COLUMN_SIZE(2922139.21199) AS numeric;Output:
+----+----+-------+-------+
|int2|int4|numeric|numeric|
+----+----+-------+-------+
|2 |4 |10 |14 |
+----+----+-------+-------+This can help you assess whether you’re using an unnecessarily large data type for a given value.
Conclusion
PostgreSQL offers a rich set of data types and functions that can significantly optimize your database design. By selecting the appropriate data type — whether it’s a numeric, floating-point, or special type like money or Infinity—you can make your applications more representative, efficient, precise and scalable.
Always choose compact and representative data types for your columns.
Consider using NUMERIC for financial data and INTEGER for money values in the smallest units (e.g., cents).
Leverage type casting and PG_COLUMN_SIZE to optimize your queries and understand how data types impact storage.
By keeping these considerations in mind, you’ll be well-equipped to design robust, high-performance PostgreSQL schemas.


