👋
Welcome to my blog!

PostgreSQL Deep Dive Part 6 - Money, Type Casting, and Boolean Operations

Master PostgreSQL's money handling, type casting operations, and boolean logic. Learn best practices for financial data storage and type conversion strategies.

PostgreSQL Deep Dive Part 6 - Money, Type Casting, and Boolean Operations
Database
Postgres
Development

Published At

12/28/2024

Reading Time

~ 8 min read

📚 Raw Postgres Study Notes

Access complete, unfiltered study notes maintained on GitHub for easily maintaing Second Brain Dir

We'll explore three critical aspects that often challenge developers: handling monetary values, type casting operations, and boolean logic. These features might seem straightforward at first glance, but they harbour subtle complexities that can impact your application's reliability and performance.

The Money Type: A Cautionary Tale 💰

PostgreSQL's MONEY type seems like an obvious choice for financial applications, but it comes with significant limitations that make it unsuitable for many real-world scenarios.

Basic Money Operations

sql
CREATE TABLE money_example (
    item_name TEXT,
    price MONEY
);
 
INSERT INTO money_example (item_name, price) VALUES
    ('Laptop', 1999.99),
    ('Smartphone', 799),
    ('Pen', .25),
    ('Headphone', '$199.99'),
    ('Smartwatch', 249.75),
    ('Gaming Console', 299.95);
 
SELECT * FROM money_example;
 
--| item_name      | price     |
--|----------------|-----------|
--| Laptop         | $1,999.99 |
--| Smartphone     |   $799.00 |
--| Pen            |     $0.25 |
--| Headphone      |   $199.99 |
--| Smartwatch     |   $249.75 |
--| Gaming Console |   $299.95 |
sql
CREATE TABLE money_example (
    item_name TEXT,
    price MONEY
);
 
INSERT INTO money_example (item_name, price) VALUES
    ('Laptop', 1999.99),
    ('Smartphone', 799),
    ('Pen', .25),
    ('Headphone', '$199.99'),
    ('Smartwatch', 249.75),
    ('Gaming Console', 299.95);
 
SELECT * FROM money_example;
 
--| item_name      | price     |
--|----------------|-----------|
--| Laptop         | $1,999.99 |
--| Smartphone     |   $799.00 |
--| Pen            |     $0.25 |
--| Headphone      |   $199.99 |
--| Smartwatch     |   $249.75 |
--| Gaming Console |   $299.95 |

The Problems with MONEY Type

  1. Currency Handling Issues:
sql
SHOW lc_monetary;-- Check current currency setting
 
--| lc_monetary |
--|-------------|
--| C           |
 
SET lc_monetary = 'en_GB.UTF-8';
-- Change to British Pounds-- The display changes but the underlying value remains in the original currency
 
SELECT * FROM money_example;
 
--| item_name      | price     |
--|----------------|-----------|
--| Laptop         | £1,999.99 |
--| Smartphone     |   £799.00 |
--| Pen            |     £0.25 |
--| Headphone      |   £199.99 |
--| Smartwatch     |   £249.75 |
--| Gaming Console |   £299.95 |
sql
SHOW lc_monetary;-- Check current currency setting
 
--| lc_monetary |
--|-------------|
--| C           |
 
SET lc_monetary = 'en_GB.UTF-8';
-- Change to British Pounds-- The display changes but the underlying value remains in the original currency
 
SELECT * FROM money_example;
 
--| item_name      | price     |
--|----------------|-----------|
--| Laptop         | £1,999.99 |
--| Smartphone     |   £799.00 |
--| Pen            |     £0.25 |
--| Headphone      |   £199.99 |
--| Smartwatch     |   £249.75 |
--| Gaming Console |   £299.95 |
  1. Precision Limitations:
sql
-- Money type rounds after two decimal places
INSERT INTO money_example VALUES ('Bitcoin', 29876.123456);
-- Rounds to 29876.12
sql
-- Money type rounds after two decimal places
INSERT INTO money_example VALUES ('Bitcoin', 29876.123456);
-- Rounds to 29876.12

This becomes problematic for:

  • Cryptocurrency values
  • Foreign exchange calculations
  • High-precision financial calculations

Best Practices for Monetary Values

  1. Store Everything in Integers (Cents):
sql
CREATE TABLE financial_transactions (
    id SERIAL PRIMARY KEY,
    description TEXT,
    amount_cents INTEGER NOT NULL,
    currency_code CHAR(3) NOT NULL
);
 
-- Converting dollars to cents
SELECT (100.78 * 100)::INTEGER as cents_value;
 
-- For display
SELECT
    description,
    amount_cents::NUMERIC / 100 as amount_dollars,
    currency_code
FROM financial_transactions;
sql
CREATE TABLE financial_transactions (
    id SERIAL PRIMARY KEY,
    description TEXT,
    amount_cents INTEGER NOT NULL,
    currency_code CHAR(3) NOT NULL
);
 
-- Converting dollars to cents
SELECT (100.78 * 100)::INTEGER as cents_value;
 
-- For display
SELECT
    description,
    amount_cents::NUMERIC / 100 as amount_dollars,
    currency_code
FROM financial_transactions;
  1. Using NUMERIC for Multi-Currency Support:
sql
CREATE TABLE precise_financial (
    id SERIAL PRIMARY KEY,
    amount NUMERIC(19,4),
    currency_code CHAR(3),
 
-- Enforce precision rules
    CONSTRAINT valid_amount
        CHECK (scale(amount) <= 4)
);
 
-- Handle different precision requirements
INSERT INTO precise_financial (amount, currency_code) VALUES
    (1234.5678, 'USD'),-- 4 decimal places for USD
    (1234.56, 'EUR'),-- 2 decimal places for EUR
    (1234.567, 'BTC');-- 3 decimal places for Bitcoin
sql
CREATE TABLE precise_financial (
    id SERIAL PRIMARY KEY,
    amount NUMERIC(19,4),
    currency_code CHAR(3),
 
-- Enforce precision rules
    CONSTRAINT valid_amount
        CHECK (scale(amount) <= 4)
);
 
-- Handle different precision requirements
INSERT INTO precise_financial (amount, currency_code) VALUES
    (1234.5678, 'USD'),-- 4 decimal places for USD
    (1234.56, 'EUR'),-- 2 decimal places for EUR
    (1234.567, 'BTC');-- 3 decimal places for Bitcoin

The Art of Type Casting: Beyond Simple Conversions 🔄

PostgreSQL provides multiple ways to convert between types, each with its own use cases and implications.

Casting Syntax Variations

sql
-- Double-colon syntax
SELECT
    100::MONEY,-- $100.00
    '2024-01-01'::DATE,  -- 2024-01-01
    '123.45'::NUMERIC;   -- 123.45
 
-- CAST function
SELECT
    CAST(100 AS MONEY),
    CAST('2024-01-01' AS DATE),
    CAST('123.45' AS NUMERIC);
 
-- Type inspection
SELECT
    pg_typeof(100::INT8),-- bigint
    pg_typeof(CAST(100 AS INT4)),-- integer
    pg_typeof('123.45'::NUMERIC(10,2));-- numeric
sql
-- Double-colon syntax
SELECT
    100::MONEY,-- $100.00
    '2024-01-01'::DATE,  -- 2024-01-01
    '123.45'::NUMERIC;   -- 123.45
 
-- CAST function
SELECT
    CAST(100 AS MONEY),
    CAST('2024-01-01' AS DATE),
    CAST('123.45' AS NUMERIC);
 
-- Type inspection
SELECT
    pg_typeof(100::INT8),-- bigint
    pg_typeof(CAST(100 AS INT4)),-- integer
    pg_typeof('123.45'::NUMERIC(10,2));-- numeric

Storage Size Analysis

sql
-- Compare storage requirements
SELECT
-- Integer sizes
    pg_column_size(100::INT2) as smallint_size,-- 2
    pg_column_size(100::INT4) as integer_size,-- 4
    pg_column_size(100::INT8) as bigint_size,-- 8
 
-- Numeric sizes vary with content
    pg_column_size(1000.12313::NUMERIC) as small_numeric,-- 8
    pg_column_size(1000313123.12313::NUMERIC) as medium_numeric,-- 22
    pg_column_size(10002432523523552.12313::NUMERIC) as large_numeric;-- 44
sql
-- Compare storage requirements
SELECT
-- Integer sizes
    pg_column_size(100::INT2) as smallint_size,-- 2
    pg_column_size(100::INT4) as integer_size,-- 4
    pg_column_size(100::INT8) as bigint_size,-- 8
 
-- Numeric sizes vary with content
    pg_column_size(1000.12313::NUMERIC) as small_numeric,-- 8
    pg_column_size(1000313123.12313::NUMERIC) as medium_numeric,-- 22
    pg_column_size(10002432523523552.12313::NUMERIC) as large_numeric;-- 44

Advanced Casting Scenarios

sql
-- Array casting
SELECT ARRAY[1,2,3]::TEXT[];  -- Converts integer array to text array
 
-- JSON casting
SELECT
    '{"key": "value"}'::JSON,
    '{"number": 123}'::JSONB;
 
-- Complex type casting
SELECT
-- UUID from text
    'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::UUID,
 
-- Array from string
    '{1,2,3}'::INTEGER[],
 
-- Custom type casting
    ROW(1, 'test')::custom_type;
sql
-- Array casting
SELECT ARRAY[1,2,3]::TEXT[];  -- Converts integer array to text array
 
-- JSON casting
SELECT
    '{"key": "value"}'::JSON,
    '{"number": 123}'::JSONB;
 
-- Complex type casting
SELECT
-- UUID from text
    'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::UUID,
 
-- Array from string
    '{1,2,3}'::INTEGER[],
 
-- Custom type casting
    ROW(1, 'test')::custom_type;

Boolean Logic: The Art of True and False ✅

PostgreSQL's boolean type might seem simple, but it offers surprising flexibility in input formats and operations.

Boolean Input Flexibility

sql
CREATE TABLE boolean_example (
    id SERIAL PRIMARY KEY,
    status BOOLEAN
);
 
-- PostgreSQL accepts various boolean representations
INSERT INTO boolean_example (status) VALUES
    (TRUE),-- TRUE
    (FALSE),-- FALSE
    ('t'),-- TRUE
    ('true'),-- TRUE
    ('f'),-- FALSE
    ('false'),-- FALSE
    ('1'),-- TRUE
    ('0'),-- FALSE
    ('on'),-- TRUE
    ('off'),-- FALSE
    ('yes'),-- TRUE
    ('no'),-- FALSE
    (NULL);-- NULL (unknown)
sql
CREATE TABLE boolean_example (
    id SERIAL PRIMARY KEY,
    status BOOLEAN
);
 
-- PostgreSQL accepts various boolean representations
INSERT INTO boolean_example (status) VALUES
    (TRUE),-- TRUE
    (FALSE),-- FALSE
    ('t'),-- TRUE
    ('true'),-- TRUE
    ('f'),-- FALSE
    ('false'),-- FALSE
    ('1'),-- TRUE
    ('0'),-- FALSE
    ('on'),-- TRUE
    ('off'),-- FALSE
    ('yes'),-- TRUE
    ('no'),-- FALSE
    (NULL);-- NULL (unknown)

Boolean Operations and Comparisons

sql
-- Three-valued logic with NULL
SELECT
    TRUE AND NULL,-- NULL
    FALSE AND NULL,-- FALSE
    TRUE OR NULL,-- TRUE
    FALSE OR NULL,-- NULL
    NOT NULL;-- NULL
 
-- Boolean aggregation
SELECT
    bool_and(status) as all_true,-- TRUE if all non-null values are TRUE
    bool_or(status) as any_true,-- TRUE if any value is TRUE
    count(*) FILTER (WHERE status) as true_count
FROM boolean_example;
 
--| all_true | any_true | true_count |
--|----------|----------|------------|
--| f        | t        |          6 |
 
-- Using booleans in constraints
CREATE TABLE inventory_items (
    id SERIAL PRIMARY KEY,
    name TEXT,
    in_stock BOOLEAN DEFAULT TRUE,
    discontinued BOOLEAN DEFAULT FALSE,
 
-- Logical constraints
    CONSTRAINT valid_status
        CHECK (
-- Can't be in stock if discontinued
            NOT (discontinued AND in_stock)
        )
);
 
sql
-- Three-valued logic with NULL
SELECT
    TRUE AND NULL,-- NULL
    FALSE AND NULL,-- FALSE
    TRUE OR NULL,-- TRUE
    FALSE OR NULL,-- NULL
    NOT NULL;-- NULL
 
-- Boolean aggregation
SELECT
    bool_and(status) as all_true,-- TRUE if all non-null values are TRUE
    bool_or(status) as any_true,-- TRUE if any value is TRUE
    count(*) FILTER (WHERE status) as true_count
FROM boolean_example;
 
--| all_true | any_true | true_count |
--|----------|----------|------------|
--| f        | t        |          6 |
 
-- Using booleans in constraints
CREATE TABLE inventory_items (
    id SERIAL PRIMARY KEY,
    name TEXT,
    in_stock BOOLEAN DEFAULT TRUE,
    discontinued BOOLEAN DEFAULT FALSE,
 
-- Logical constraints
    CONSTRAINT valid_status
        CHECK (
-- Can't be in stock if discontinued
            NOT (discontinued AND in_stock)
        )
);
 

Storage Efficiency:

sql
-- Boolean takes just 1 byte
SELECT pg_column_size(TRUE::BOOLEAN);-- 1-- Compare with alternatives
SELECT
    pg_column_size('Y'::CHAR(1)),-- 1
    pg_column_size(1::SMALLINT),-- 2
    pg_column_size('true'::TEXT);-- 5
sql
-- Boolean takes just 1 byte
SELECT pg_column_size(TRUE::BOOLEAN);-- 1-- Compare with alternatives
SELECT
    pg_column_size('Y'::CHAR(1)),-- 1
    pg_column_size(1::SMALLINT),-- 2
    pg_column_size('true'::TEXT);-- 5

Through this exploration of PostgreSQL's money type, casting operations, and boolean logic, we've uncovered the subtleties that make these features both powerful and potentially tricky. Remember:

  1. Avoid the MONEY type for serious financial applications
  2. Use explicit CAST when type conversion intention needs to be clear
  3. Leverage PostgreSQL's flexible boolean input formats for better user experience

This concludes our deep dive series into PostgreSQL's type system. Armed with this knowledge, you're better equipped to make informed decisions about data type selection and usage in your applications.

🏸

Do you have any questions, or simply wish to contact me privately? Don't hesitate to shoot me a DM on Twitter.

Have a wonderful day.
Abhishek 🙏

Join My Exclusive Newsletter Community

Step into a world where creativity intersects with technology. By subscribing, you'll get a front-row seat to my latest musings, full-stack development resources, and exclusive previews of future posts. Each email is a crafted experience that includes:

  • In-depth looks at my covert projects and musings to ignite your imagination.
  • Handpicked frontend development resources and current explorations, aimed at expanding your developer toolkit.
  • A monthly infusion of inspiration with my personal selection of quotes, books, and music.

Embrace the confluence of words and wonder, curated thoughtfully and sent straight to your inbox.

No fluff. Just the highest caliber of ideas.