Design optimized database schemas
-
Requirements Analysis and Data Modeling
- Analyze business requirements and data relationships
- Identify entities, attributes, and relationships
- Define data types, constraints, and validation rules
- Plan for scalability and future requirements
- Consider data access patterns and query requirements
-
Entity Relationship Design
- Create comprehensive entity relationship diagrams:
User Management Schema:
-- Users table with proper indexing and constraints CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, phone VARCHAR(20), date_of_birth DATE, email_verified BOOLEAN DEFAULT FALSE, phone_verified BOOLEAN DEFAULT FALSE, status user_status DEFAULT 'active', created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_login_at TIMESTAMP WITH TIME ZONE, deleted_at TIMESTAMP WITH TIME ZONE, -- Constraints CONSTRAINT users_email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), CONSTRAINT users_username_format CHECK (username ~* '^[a-zA-Z0-9_]{3,50}$'), CONSTRAINT users_names_not_empty CHECK (LENGTH(TRIM(first_name)) > 0 AND LENGTH(TRIM(last_name)) > 0) ); -- User status enum CREATE TYPE user_status AS ENUM ('active', 'inactive', 'suspended', 'pending_verification'); -- User profiles table for extended information CREATE TABLE user_profiles ( user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, avatar_url VARCHAR(500), bio TEXT, website VARCHAR(255), location VARCHAR(255), timezone VARCHAR(50) DEFAULT 'UTC', language VARCHAR(10) DEFAULT 'en', notification_preferences JSONB DEFAULT '{}', privacy_settings JSONB DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- User roles and permissions CREATE TABLE roles ( id SERIAL PRIMARY KEY, name VARCHAR(50) UNIQUE NOT NULL, description TEXT, permissions JSONB DEFAULT '[]', created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE user_roles ( user_id BIGINT REFERENCES users(id) ON DELETE CASCADE, role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE, assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, assigned_by BIGINT REFERENCES users(id), PRIMARY KEY (user_id, role_id) );
E-commerce Schema Example:
-- Categories with hierarchical structure CREATE TABLE categories ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, description TEXT, parent_id INTEGER REFERENCES categories(id), sort_order INTEGER DEFAULT 0, is_active BOOLEAN DEFAULT TRUE, meta_title VARCHAR(255), meta_description TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Products table with comprehensive attributes CREATE TABLE products ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, sku VARCHAR(100) UNIQUE NOT NULL, description TEXT, short_description TEXT, price DECIMAL(10,2) NOT NULL CHECK (price >= 0), compare_price DECIMAL(10,2) CHECK (compare_price >= price), cost_price DECIMAL(10,2) CHECK (cost_price >= 0), weight DECIMAL(8,2), dimensions JSONB, -- {length: x, width: y, height: z, unit: 'cm'} category_id INTEGER REFERENCES categories(id), brand_id INTEGER REFERENCES brands(id), vendor_id BIGINT REFERENCES vendors(id), status product_status DEFAULT 'draft', visibility product_visibility DEFAULT 'visible', inventory_tracking BOOLEAN DEFAULT TRUE, inventory_quantity INTEGER DEFAULT 0, low_stock_threshold INTEGER DEFAULT 5, allow_backorder BOOLEAN DEFAULT FALSE, requires_shipping BOOLEAN DEFAULT TRUE, is_digital BOOLEAN DEFAULT FALSE, tax_class VARCHAR(50) DEFAULT 'standard', featured BOOLEAN DEFAULT FALSE, tags TEXT[], attributes JSONB DEFAULT '{}', seo_title VARCHAR(255), seo_description TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, published_at TIMESTAMP WITH TIME ZONE, -- Full text search search_vector tsvector GENERATED ALWAYS AS ( to_tsvector('english', COALESCE(name, '') || ' ' || COALESCE(description, '') || ' ' || COALESCE(sku, '')) ) STORED ); -- Product status and visibility enums CREATE TYPE product_status AS ENUM ('draft', 'active', 'inactive', 'archived'); CREATE TYPE product_visibility AS ENUM ('visible', 'hidden', 'catalog_only', 'search_only'); -- Orders table with comprehensive tracking CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, order_number VARCHAR(50) UNIQUE NOT NULL, user_id BIGINT REFERENCES users(id), status order_status DEFAULT 'pending', currency CHAR(3) DEFAULT 'USD', subtotal DECIMAL(10,2) NOT NULL DEFAULT 0, tax_total DECIMAL(10,2) NOT NULL DEFAULT 0, shipping_total DECIMAL(10,2) NOT NULL DEFAULT 0, discount_total DECIMAL(10,2) NOT NULL DEFAULT 0, total DECIMAL(10,2) NOT NULL DEFAULT 0, -- Billing information billing_first_name VARCHAR(100), billing_last_name VARCHAR(100), billing_company VARCHAR(255), billing_address_line_1 VARCHAR(255), billing_address_line_2 VARCHAR(255), billing_city VARCHAR(100), billing_state VARCHAR(100), billing_postal_code VARCHAR(20), billing_country CHAR(2), billing_phone VARCHAR(20), -- Shipping information shipping_first_name VARCHAR(100), shipping_last_name VARCHAR(100), shipping_company VARCHAR(255), shipping_address_line_1 VARCHAR(255), shipping_address_line_2 VARCHAR(255), shipping_city VARCHAR(100), shipping_state VARCHAR(100), shipping_postal_code VARCHAR(20), shipping_country CHAR(2), shipping_phone VARCHAR(20), shipping_method VARCHAR(100), tracking_number VARCHAR(255), notes TEXT, internal_notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, shipped_at TIMESTAMP WITH TIME ZONE, delivered_at TIMESTAMP WITH TIME ZONE ); CREATE TYPE order_status AS ENUM ( 'pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded', 'on_hold' ); -- Order items with detailed tracking CREATE TABLE order_items ( id BIGSERIAL PRIMARY KEY, order_id BIGINT REFERENCES orders(id) ON DELETE CASCADE, product_id BIGINT REFERENCES products(id), product_variant_id BIGINT REFERENCES product_variants(id), quantity INTEGER NOT NULL CHECK (quantity > 0), unit_price DECIMAL(10,2) NOT NULL, total_price DECIMAL(10,2) NOT NULL, product_name VARCHAR(255) NOT NULL, -- Snapshot at time of order product_sku VARCHAR(100), -- Snapshot at time of order product_attributes JSONB, -- Snapshot of selected variants created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );
-
Advanced Schema Patterns
- Implement complex data patterns:
Audit Trail Pattern:
-- Generic audit trail for tracking all changes CREATE TABLE audit_log ( id BIGSERIAL PRIMARY KEY, table_name VARCHAR(255) NOT NULL, record_id BIGINT NOT NULL, operation audit_operation NOT NULL, old_values JSONB, new_values JSONB, changed_fields TEXT[], user_id BIGINT REFERENCES users(id), ip_address INET, user_agent TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Index for efficient querying INDEX idx_audit_log_table_record (table_name, record_id), INDEX idx_audit_log_user_time (user_id, created_at), INDEX idx_audit_log_operation_time (operation, created_at) ); CREATE TYPE audit_operation AS ENUM ('INSERT', 'UPDATE', 'DELETE'); -- Trigger function for automatic audit logging CREATE OR REPLACE FUNCTION audit_trigger_function() RETURNS TRIGGER AS $$ DECLARE old_data JSONB; new_data JSONB; changed_fields TEXT[]; BEGIN IF TG_OP = 'DELETE' THEN old_data = to_jsonb(OLD); INSERT INTO audit_log (table_name, record_id, operation, old_values, user_id) VALUES (TG_TABLE_NAME, OLD.id, 'DELETE', old_data, COALESCE(current_setting('app.current_user_id', true)::BIGINT, NULL)); RETURN OLD; ELSIF TG_OP = 'UPDATE' THEN old_data = to_jsonb(OLD); new_data = to_jsonb(NEW); -- Find changed fields SELECT array_agg(key) INTO changed_fields FROM jsonb_each(old_data) WHERE key IN (SELECT key FROM jsonb_each(new_data)) AND value IS DISTINCT FROM (new_data->key); INSERT INTO audit_log (table_name, record_id, operation, old_values, new_values, changed_fields, user_id) VALUES (TG_TABLE_NAME, NEW.id, 'UPDATE', old_data, new_data, changed_fields, COALESCE(current_setting('app.current_user_id', true)::BIGINT, NULL)); RETURN NEW; ELSIF TG_OP = 'INSERT' THEN new_data = to_jsonb(NEW); INSERT INTO audit_log (table_name, record_id, operation, new_values, user_id) VALUES (TG_TABLE_NAME, NEW.id, 'INSERT', new_data, COALESCE(current_setting('app.current_user_id', true)::BIGINT, NULL)); RETURN NEW; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql;
Soft Delete Pattern:
-- Add soft delete to any table ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP WITH TIME ZONE; ALTER TABLE products ADD COLUMN deleted_at TIMESTAMP WITH TIME ZONE; -- Create views that exclude soft-deleted records CREATE VIEW active_users AS SELECT * FROM users WHERE deleted_at IS NULL; CREATE VIEW active_products AS SELECT * FROM products WHERE deleted_at IS NULL; -- Soft delete function CREATE OR REPLACE FUNCTION soft_delete(table_name TEXT, record_id BIGINT) RETURNS VOID AS $$ BEGIN EXECUTE format('UPDATE %I SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1 AND deleted_at IS NULL', table_name) USING record_id; END; $$ LANGUAGE plpgsql; -- Restore function CREATE OR REPLACE FUNCTION restore_deleted(table_name TEXT, record_id BIGINT) RETURNS VOID AS $$ BEGIN EXECUTE format('UPDATE %I SET deleted_at = NULL WHERE id = $1', table_name) USING record_id; END; $$ LANGUAGE plpgsql;
-
Performance Optimization Schema Design
- Design for optimal query performance:
Strategic Indexing:
-- Single column indexes for frequently queried fields CREATE INDEX CONCURRENTLY idx_users_email ON users(email); CREATE INDEX CONCURRENTLY idx_users_username ON users(username); CREATE INDEX CONCURRENTLY idx_users_status ON users(status) WHERE status != 'active'; CREATE INDEX CONCURRENTLY idx_users_created_at ON users(created_at); -- Composite indexes for common query patterns CREATE INDEX CONCURRENTLY idx_products_category_status ON products(category_id, status) WHERE status = 'active'; CREATE INDEX CONCURRENTLY idx_products_featured_category ON products(featured, category_id) WHERE featured = true AND status = 'active'; CREATE INDEX CONCURRENTLY idx_orders_user_status_date ON orders(user_id, status, created_at); -- Partial indexes for specific conditions CREATE INDEX CONCURRENTLY idx_products_low_stock ON products(inventory_quantity) WHERE inventory_tracking = true AND inventory_quantity <= low_stock_threshold; -- Functional indexes for text search and computed values CREATE INDEX CONCURRENTLY idx_products_search_vector ON products USING gin(search_vector); CREATE INDEX CONCURRENTLY idx_users_full_name_lower ON users(lower(first_name || ' ' || last_name)); -- JSON/JSONB indexes for flexible data CREATE INDEX CONCURRENTLY idx_user_profiles_notifications ON user_profiles USING gin(notification_preferences); CREATE INDEX CONCURRENTLY idx_products_attributes ON products USING gin(attributes);
Partitioning Strategy:
-- Partition large tables by date for better performance CREATE TABLE orders_partitioned ( LIKE orders INCLUDING ALL ) PARTITION BY RANGE (created_at); -- Create monthly partitions CREATE TABLE orders_2024_01 PARTITION OF orders_partitioned FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); CREATE TABLE orders_2024_02 PARTITION OF orders_partitioned FOR VALUES FROM ('2024-02-01') TO ('2024-03-01'); -- Automatic partition management CREATE OR REPLACE FUNCTION create_monthly_partitions( table_name TEXT, start_date DATE, end_date DATE ) RETURNS VOID AS $$ DECLARE current_date DATE := start_date; partition_name TEXT; next_date DATE; BEGIN WHILE current_date < end_date LOOP next_date := current_date + INTERVAL '1 month'; partition_name := table_name || '_' || to_char(current_date, 'YYYY_MM'); EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)', partition_name, table_name, current_date, next_date); current_date := next_date; END LOOP; END; $$ LANGUAGE plpgsql; -- Schedule partition creation SELECT create_monthly_partitions('orders_partitioned', '2024-01-01'::DATE, '2025-01-01'::DATE);
-
Data Integrity and Constraints
- Implement comprehensive data validation:
Advanced Constraints:
-- Complex check constraints ALTER TABLE products ADD CONSTRAINT products_price_logic CHECK ( CASE WHEN compare_price IS NOT NULL THEN price <= compare_price ELSE true END ); ALTER TABLE products ADD CONSTRAINT products_inventory_logic CHECK ( CASE WHEN inventory_tracking = false THEN inventory_quantity IS NULL WHEN inventory_tracking = true THEN inventory_quantity >= 0 ELSE true END ); -- Custom domain types for reusable validation CREATE DOMAIN email_address AS VARCHAR(255) CHECK (VALUE ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'); CREATE DOMAIN phone_number AS VARCHAR(20) CHECK (VALUE ~* '^\+?[\d\s\-\(\)]{10,20}$'); CREATE DOMAIN positive_decimal AS DECIMAL(10,2) CHECK (VALUE >= 0); -- Use domains in table definitions CREATE TABLE contacts ( id BIGSERIAL PRIMARY KEY, email email_address NOT NULL, phone phone_number, balance positive_decimal DEFAULT 0 ); -- Foreign key constraints with cascading options ALTER TABLE order_items ADD CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE; ALTER TABLE order_items ADD CONSTRAINT fk_order_items_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT; -- Unique constraints for business logic ALTER TABLE user_roles ADD CONSTRAINT unique_user_role_active UNIQUE (user_id, role_id); -- Exclusion constraints for complex business rules ALTER TABLE product_promotions ADD CONSTRAINT no_overlapping_promotions EXCLUDE USING gist ( product_id WITH =, daterange(start_date, end_date, '[]') WITH && );
-
Temporal Data and Versioning
- Handle time-based data requirements:
Temporal Tables:
-- Product price history tracking CREATE TABLE product_price_history ( id BIGSERIAL PRIMARY KEY, product_id BIGINT REFERENCES products(id) ON DELETE CASCADE, price DECIMAL(10,2) NOT NULL, compare_price DECIMAL(10,2), effective_from TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, effective_to TIMESTAMP WITH TIME ZONE, created_by BIGINT REFERENCES users(id), reason TEXT, -- Ensure no overlapping periods EXCLUDE USING gist ( product_id WITH =, tstzrange(effective_from, effective_to, '[)') WITH && ) ); -- Function to get current price CREATE OR REPLACE FUNCTION get_current_price(p_product_id BIGINT) RETURNS DECIMAL(10,2) AS $$ DECLARE current_price DECIMAL(10,2); BEGIN SELECT price INTO current_price FROM product_price_history WHERE product_id = p_product_id AND effective_from <= CURRENT_TIMESTAMP AND (effective_to IS NULL OR effective_to > CURRENT_TIMESTAMP) ORDER BY effective_from DESC LIMIT 1; RETURN current_price; END; $$ LANGUAGE plpgsql; -- Trigger to update price history when product price changes CREATE OR REPLACE FUNCTION update_price_history() RETURNS TRIGGER AS $$ BEGIN IF OLD.price IS DISTINCT FROM NEW.price THEN -- Close current price period UPDATE product_price_history SET effective_to = CURRENT_TIMESTAMP WHERE product_id = NEW.id AND effective_to IS NULL; -- Insert new price period INSERT INTO product_price_history (product_id, price, compare_price, created_by) VALUES (NEW.id, NEW.price, NEW.compare_price, COALESCE(current_setting('app.current_user_id', true)::BIGINT, NULL)); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_product_price_history AFTER UPDATE ON products FOR EACH ROW EXECUTE FUNCTION update_price_history();
-
JSON/NoSQL Integration
- Leverage JSON columns for flexible data:
JSONB Schema Design:
-- Flexible product attributes using JSONB CREATE TABLE product_attributes ( product_id BIGINT REFERENCES products(id) ON DELETE CASCADE, attributes JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (product_id) ); -- JSONB indexes for efficient querying CREATE INDEX idx_product_attributes_gin ON product_attributes USING gin(attributes); CREATE INDEX idx_product_attributes_color ON product_attributes USING gin((attributes->'color')); CREATE INDEX idx_product_attributes_size ON product_attributes USING gin((attributes->'size')); -- Function to query products by attributes CREATE OR REPLACE FUNCTION find_products_by_attributes(search_attributes JSONB) RETURNS TABLE(product_id BIGINT, product_name VARCHAR, attributes JSONB) AS $$ BEGIN RETURN QUERY SELECT p.id, p.name, pa.attributes FROM products p JOIN product_attributes pa ON p.id = pa.product_id WHERE pa.attributes @> search_attributes; END; $$ LANGUAGE plpgsql; -- Usage examples: -- SELECT * FROM find_products_by_attributes('{"color": "red", "size": "large"}'); -- Settings table with JSONB for flexible configuration CREATE TABLE application_settings ( id SERIAL PRIMARY KEY, category VARCHAR(100) NOT NULL, key VARCHAR(100) NOT NULL, value JSONB NOT NULL, description TEXT, is_public BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, UNIQUE(category, key) ); -- Function to get setting value with type casting CREATE OR REPLACE FUNCTION get_setting(p_category VARCHAR, p_key VARCHAR, p_default ANYELEMENT DEFAULT NULL) RETURNS ANYELEMENT AS $$ DECLARE setting_value JSONB; BEGIN SELECT value INTO setting_value FROM application_settings WHERE category = p_category AND key = p_key; IF setting_value IS NULL THEN RETURN p_default; END IF; RETURN (setting_value #>> '{}')::TEXT::pg_typeof(p_default); END; $$ LANGUAGE plpgsql;
-
Database Security Schema
- Implement security at the schema level:
Row Level Security:
-- Enable RLS on sensitive tables ALTER TABLE orders ENABLE ROW LEVEL SECURITY; ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY; -- Create policies for data access CREATE POLICY orders_user_access ON orders FOR ALL TO authenticated_users USING (user_id = current_user_id()); CREATE POLICY orders_admin_access ON orders FOR ALL TO admin_users USING (true); -- Function to get current user ID from session CREATE OR REPLACE FUNCTION current_user_id() RETURNS BIGINT AS $$ BEGIN RETURN COALESCE(current_setting('app.current_user_id', true)::BIGINT, 0); END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- Create database roles with specific permissions CREATE ROLE app_readonly; GRANT CONNECT ON DATABASE myapp TO app_readonly; GRANT USAGE ON SCHEMA public TO app_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly; CREATE ROLE app_readwrite; GRANT app_readonly TO app_readwrite; GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_readwrite; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_readwrite; -- Sensitive data encryption CREATE EXTENSION IF NOT EXISTS pgcrypto; -- Function to encrypt sensitive data CREATE OR REPLACE FUNCTION encrypt_sensitive_data(data TEXT) RETURNS TEXT AS $$ BEGIN RETURN encode(encrypt(data::bytea, current_setting('app.encryption_key'), 'aes'), 'base64'); END; $$ LANGUAGE plpgsql; -- Function to decrypt sensitive data CREATE OR REPLACE FUNCTION decrypt_sensitive_data(encrypted_data TEXT) RETURNS TEXT AS $$ BEGIN RETURN convert_from(decrypt(decode(encrypted_data, 'base64'), current_setting('app.encryption_key'), 'aes'), 'UTF8'); END; $$ LANGUAGE plpgsql;
-
Schema Documentation and Maintenance
- Document and maintain schema design:
Database Documentation:
-- Add comments to tables and columns COMMENT ON TABLE users IS 'User accounts and authentication information'; COMMENT ON COLUMN users.email IS 'Unique email address for user authentication'; COMMENT ON COLUMN users.status IS 'Current status of user account (active, inactive, suspended, pending_verification)'; COMMENT ON COLUMN users.email_verified IS 'Whether the user has verified their email address'; COMMENT ON TABLE products IS 'Product catalog with inventory and pricing information'; COMMENT ON COLUMN products.search_vector IS 'Full-text search vector generated from name, description, and SKU'; COMMENT ON COLUMN products.attributes IS 'Flexible product attributes stored as JSONB (color, size, material, etc.)'; -- Create a view for schema documentation CREATE VIEW schema_documentation AS SELECT t.table_name, t.table_type, obj_description(c.oid) AS table_comment, col.column_name, col.data_type, col.is_nullable, col.column_default, col_description(c.oid, col.ordinal_position) AS column_comment FROM information_schema.tables t JOIN pg_class c ON c.relname = t.table_name JOIN information_schema.columns col ON col.table_name = t.table_name WHERE t.table_schema = 'public' ORDER BY t.table_name, col.ordinal_position;
-
Schema Testing and Validation
- Implement schema testing procedures:
Schema Validation Tests:
-- Test data integrity constraints DO $$ DECLARE test_result BOOLEAN; BEGIN -- Test email validation BEGIN INSERT INTO users (email, username, password_hash, first_name, last_name) VALUES ('invalid-email', 'testuser', 'hash', 'Test', 'User'); RAISE EXCEPTION 'Email validation failed - invalid email accepted'; EXCEPTION WHEN check_violation THEN RAISE NOTICE 'Email validation working correctly'; END; -- Test price constraints BEGIN INSERT INTO products (name, slug, sku, price, compare_price) VALUES ('Test Product', 'test-product', 'TEST-001', 100.00, 50.00); RAISE EXCEPTION 'Price validation failed - compare_price less than price accepted'; EXCEPTION WHEN check_violation THEN RAISE NOTICE 'Price validation working correctly'; END; -- Test foreign key constraints BEGIN INSERT INTO order_items (order_id, product_id, quantity, unit_price, total_price, product_name) VALUES (999999, 999999, 1, 10.00, 10.00, 'Test Product'); RAISE EXCEPTION 'Foreign key validation failed - non-existent order_id accepted'; EXCEPTION WHEN foreign_key_violation THEN RAISE NOTICE 'Foreign key validation working correctly'; END; END; $$; -- Performance test queries CREATE OR REPLACE FUNCTION test_query_performance() RETURNS TABLE(test_name TEXT, execution_time INTERVAL) AS $$ DECLARE start_time TIMESTAMP; end_time TIMESTAMP; BEGIN -- Test user lookup by email start_time := clock_timestamp(); PERFORM * FROM users WHERE email = 'test@example.com'; end_time := clock_timestamp(); test_name := 'User lookup by email'; execution_time := end_time - start_time; RETURN NEXT; -- Test product search start_time := clock_timestamp(); PERFORM * FROM products WHERE search_vector @@ to_tsquery('english', 'laptop'); end_time := clock_timestamp(); test_name := 'Product full-text search'; execution_time := end_time - start_time; RETURN NEXT; -- Test order history query start_time := clock_timestamp(); PERFORM o.* FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.user_id = 1 ORDER BY o.created_at DESC LIMIT 20; end_time := clock_timestamp(); test_name := 'User order history'; execution_time := end_time - start_time; RETURN NEXT; END; $$ LANGUAGE plpgsql; -- Run performance tests SELECT * FROM test_query_performance();