sql 384 lines · 2 tabs

Database design patterns and anti-patterns

Maria Garcia Feb 2026
2 tabs
-- Pattern: Single Table Inheritance (STI)
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  type VARCHAR(50) NOT NULL, -- 'admin', 'customer', 'vendor'
  username VARCHAR(100) UNIQUE NOT NULL,
  email VARCHAR(100) UNIQUE NOT NULL,
  -- Admin-specific
  permission_level INT,
  -- Customer-specific
  loyalty_points INT,
  -- Vendor-specific
  company_name VARCHAR(200),
  tax_id VARCHAR(50),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  CHECK (
    (type = 'admin' AND permission_level IS NOT NULL) OR
    (type = 'customer' AND loyalty_points IS NOT NULL) OR
    (type = 'vendor' AND company_name IS NOT NULL)
  )
);

-- Query specific type
SELECT * FROM users WHERE type = 'admin';

-- Pattern: Class Table Inheritance (CTI)
CREATE TABLE users_base (
  id SERIAL PRIMARY KEY,
  username VARCHAR(100) UNIQUE NOT NULL,
  email VARCHAR(100) UNIQUE NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE admins (
  user_id INT PRIMARY KEY REFERENCES users_base(id) ON DELETE CASCADE,
  permission_level INT NOT NULL
);

CREATE TABLE customers (
  user_id INT PRIMARY KEY REFERENCES users_base(id) ON DELETE CASCADE,
  loyalty_points INT DEFAULT 0
);

CREATE TABLE vendors (
  user_id INT PRIMARY KEY REFERENCES users_base(id) ON DELETE CASCADE,
  company_name VARCHAR(200) NOT NULL,
  tax_id VARCHAR(50)
);

-- Query with join
SELECT u.*, c.loyalty_points
FROM users_base u
JOIN customers c ON u.id = c.user_id
WHERE u.email = 'customer@example.com';

-- Pattern: Soft deletes
ALTER TABLE products ADD COLUMN deleted_at TIMESTAMP;
CREATE INDEX idx_products_deleted_at ON products(deleted_at) WHERE deleted_at IS NULL;

-- Delete (soft)
UPDATE products SET deleted_at = CURRENT_TIMESTAMP WHERE id = 123;

-- Query active records
SELECT * FROM products WHERE deleted_at IS NULL;

-- Create view for convenience
CREATE VIEW active_products AS
SELECT * FROM products WHERE deleted_at IS NULL;

SELECT * FROM active_products;

-- Pattern: Audit trail
CREATE TABLE products_audit (
  audit_id SERIAL PRIMARY KEY,
  product_id INT NOT NULL,
  operation VARCHAR(10) NOT NULL, -- INSERT, UPDATE, DELETE
  old_data JSONB,
  new_data JSONB,
  changed_by VARCHAR(100),
  changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE OR REPLACE FUNCTION audit_products()
RETURNS TRIGGER AS $$
BEGIN
  IF TG_OP = 'INSERT' THEN
    INSERT INTO products_audit (product_id, operation, new_data, changed_by)
    VALUES (NEW.id, 'INSERT', row_to_json(NEW)::jsonb, current_user);
  ELSIF TG_OP = 'UPDATE' THEN
    INSERT INTO products_audit (product_id, operation, old_data, new_data, changed_by)
    VALUES (NEW.id, 'UPDATE', row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb, current_user);
  ELSIF TG_OP = 'DELETE' THEN
    INSERT INTO products_audit (product_id, operation, old_data, changed_by)
    VALUES (OLD.id, 'DELETE', row_to_json(OLD)::jsonb, current_user);
  END IF;
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER products_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON products
FOR EACH ROW EXECUTE FUNCTION audit_products();

-- View audit history
SELECT
  audit_id,
  product_id,
  operation,
  old_data->>'name' AS old_name,
  new_data->>'name' AS new_name,
  changed_by,
  changed_at
FROM products_audit
WHERE product_id = 123
ORDER BY changed_at DESC;

-- Pattern: Optimistic locking with version
CREATE TABLE documents (
  id SERIAL PRIMARY KEY,
  title VARCHAR(200),
  content TEXT,
  version INT DEFAULT 1,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Update with version check
UPDATE documents
SET
  content = 'New content',
  version = version + 1,
  updated_at = CURRENT_TIMESTAMP
WHERE id = 123 AND version = 5;
-- Returns 0 rows if version changed (conflict)

-- Pattern: Materialized path for hierarchies
CREATE TABLE categories_path (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100),
  path VARCHAR(500), -- '/1/3/7/'
  depth INT
);

CREATE INDEX idx_categories_path ON categories_path(path);

-- Find all descendants
SELECT * FROM categories_path
WHERE path LIKE '/1/3/%';

-- Find all ancestors
-- For node '/1/3/7/15/', extract ancestors
WITH RECURSIVE ancestors AS (
  SELECT 1 AS level, '/1/' AS ancestor_path
  UNION ALL
  SELECT level + 1, ancestor_path || '3/'
  FROM ancestors WHERE level < 3
)
SELECT c.*
FROM ancestors a
JOIN categories_path c ON c.path = a.ancestor_path;
2 files · sql Explain with highlit

Database design patterns solve recurring problems. I use the Repository pattern to abstract data access. Active Record maps objects to tables. Unit of Work tracks changes for batch commits. Identity Map caches loaded entities. The Specification pattern encapsulates query logic. Avoid anti-patterns: EAV (Entity-Attribute-Value) creates query complexity. Polymorphic associations without constraints risk orphans. Massive denormalization causes update anomalies. God tables accumulate too many columns. Improper indexing degrades performance. Understanding patterns prevents technical debt, improves maintainability. Essential for scalable, clean database architecture.


Related snips

ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
rust
pub struct Server {
    host: String,
    port: u16,
    workers: usize,
}

Builder pattern for complex struct initialization

rust patterns builder
by Marcus Chen 1 tab
ruby
class UserRegistrationService
  class Result
    attr_reader :user, :errors

    def initialize(success:, user: nil, errors: [])
      @success = success

Service objects for business logic encapsulation

ruby rails service-objects
by Sarah Mitchell 2 tabs
typescript
import { ReactNode } from 'react'

interface CardProps {
  children: ReactNode
  className?: string
}

React component composition over inheritance

react patterns composition
by Maya Patel 2 tabs
sql
-- Pattern 1: Shared schema with tenant_id column

CREATE TABLE tenants (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  slug VARCHAR(50) UNIQUE NOT NULL,

Multi-tenancy database patterns and strategies

multi-tenancy saas row-level-security
by Maria Garcia 2 tabs
ruby
class User < ApplicationRecord
  has_many :posts, foreign_key: :author_id, dependent: :destroy

  before_validation :normalize_email
  before_create :generate_auth_token
  after_create :send_welcome_email

ActiveRecord callbacks for lifecycle hooks

rails activerecord patterns
by Alex Kumar 1 tab

Share this code

Here's the card — post it anywhere.

Database design patterns and anti-patterns — share card
Link copied