PostgreSQL Introduction
Relational databases with PostgreSQL - when to choose SQL over NoSQL.
5 min read
SQL vs NoSQL#
| Aspect | SQL (PostgreSQL) | NoSQL (MongoDB) |
|---|---|---|
| Data structure | Tables, rows, columns | Documents, collections |
| Schema | Fixed, predefined | Flexible, dynamic |
| Relationships | JOINs, foreign keys | Embedded or referenced |
| Transactions | ACID guaranteed | Limited (multi-doc) |
| Scaling | Vertical (+ read replicas) | Horizontal (sharding) |
| Query language | SQL | MongoDB Query Language |
| Best for | Complex relations, transactions | Flexible schemas, rapid dev |
When to Use PostgreSQL#
Choose PostgreSQL for:#
- Complex relationships - Users, orders, products with many relations
- Financial data - Transactions must be ACID compliant
- Reporting/Analytics - Complex queries, aggregations
- Structured data - Schema is well-defined and stable
- Data integrity - Foreign keys, constraints matter
Choose MongoDB for:#
- Flexible schemas - Data structure changes often
- Document-oriented - Nested objects, arrays
- Rapid prototyping - Schema evolves with product
- High write volume - Logging, IoT, time-series
- Horizontal scaling - Massive datasets
PostgreSQL Basics#
Data Types#
sql
-- Common types
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- Auto-incrementing integer
uuid UUID DEFAULT gen_random_uuid(), -- UUID
email VARCHAR(255) NOT NULL, -- Variable-length string
name TEXT, -- Unlimited text
age INTEGER, -- Integer
balance DECIMAL(10, 2), -- Precise decimal
is_active BOOLEAN DEFAULT true, -- Boolean
metadata JSONB, -- JSON (binary, indexed)
tags TEXT[], -- Array
created_at TIMESTAMPTZ DEFAULT NOW(), -- Timestamp with timezone
updated_at TIMESTAMPTZ
);
Relationships#
sql
-- One-to-Many: User has many posts
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Many-to-Many: Posts have many tags
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL
);
CREATE TABLE post_tags (
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE,
tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
Indexes#
sql
-- Speed up queries
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
-- Unique index
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
-- Partial index (only active users)
CREATE INDEX idx_active_users ON users(email) WHERE is_active = true;
-- GIN index for JSONB
CREATE INDEX idx_users_metadata ON users USING GIN(metadata);
Basic Queries#
sql
-- Select with JOIN
SELECT
p.id,
p.title,
u.name as author_name,
u.email as author_email
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.created_at > NOW() - INTERVAL '7 days'
ORDER BY p.created_at DESC
LIMIT 10;
-- Aggregate
SELECT
u.id,
u.name,
COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id
HAVING COUNT(p.id) > 5;
-- Insert returning
INSERT INTO users (email, name)
VALUES ('user@example.com', 'John Doe')
RETURNING id, email, created_at;
-- Upsert (insert or update)
INSERT INTO users (email, name)
VALUES ('user@example.com', 'John Doe')
ON CONFLICT (email)
DO UPDATE SET name = EXCLUDED.name, updated_at = NOW()
RETURNING *;
Node.js Options#
| Library | Type | Best For |
|---|---|---|
| Prisma | ORM | Type-safe, modern DX |
| Drizzle | ORM | Lightweight, SQL-like |
| Knex | Query Builder | Raw SQL control |
| pg | Driver | Direct PostgreSQL |
| TypeORM | ORM | Decorators, enterprise |
Quick Start with pg (Raw Driver)#
bash
npm install pg
javascript
import pg from 'pg';
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
});
// Query
export async function getUsers() {
const { rows } = await pool.query('SELECT * FROM users');
return rows;
}
// Parameterized query (safe from SQL injection)
export async function getUserById(id) {
const { rows } = await pool.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return rows[0];
}
// Insert
export async function createUser(email, name) {
const { rows } = await pool.query(
'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *',
[email, name]
);
return rows[0];
}
// Transaction
export async function transferFunds(fromId, toId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, fromId]
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId]
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
Hosted PostgreSQL Options#
| Provider | Best For | Free Tier |
|---|---|---|
| Supabase | Full platform | 500MB |
| Neon | Serverless | 512MB |
| Railway | Simple deploy | $5 credit |
| Render | Managed | 90 days |
| AWS RDS | Enterprise | 12 months |
| PlanetScale | MySQL (similar) | 5GB |
Key Takeaways#
- Use PostgreSQL for relationships - Complex JOINs, foreign keys
- ACID for transactions - Financial, inventory operations
- Schema stability - When data structure is well-known
- Use an ORM - Prisma or Drizzle for type safety
- Index wisely - Speed up common queries
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.