Database Standards
SQL and NoSQL database coding standards and best practices
Database coding standards ensure consistent, maintainable, and performant data access code. These guidelines cover naming conventions, query patterns, and database-specific best practices.
Core Naming Principles
Effective database naming follows five fundamental rules:
| Principle | Description |
|---|---|
| Singular Nouns | Tables and views use singular nouns without prefixes or suffixes |
| Lowercase Only | Use lowercase with underscores as separators |
| Meaning Over Implementation | Names describe domain meaning, not technical implementation |
| Avoid Vague Terms | Eliminate generic suffixes like _data, _info, _table |
| Self-Documenting | Names make the domain model immediately clear |
Naming Methodology
Before naming any database object, ask yourself these questions:
| Object Type | Question to Ask |
|---|---|
| Tables/Views | "What is one row?" |
| Columns | "What is one value?" |
If your answer requires more than one sentence, the object likely needs refactoring into smaller, more focused entities.
Tables
Use concrete domain nouns that describe the entity:
-- Good: Concrete domain nouns
user -- What is one row? A user.
device -- What is one row? A device.
policy -- What is one row? A policy.
audit_event -- What is one row? An audit event.
-- Good: Relationship tables with meaningful compound nouns
user_role -- What is one row? A user's role assignment.
account_membership -- What is one row? An account membership.
-- Bad: Vague or prefixed names
t_user -- Avoid t_ prefix
user_data -- Avoid _data suffix
user_info -- Avoid _info suffix
users_table -- Avoid _table suffixViews
Name views by the resulting concept, not the operation. Views representing "latest" or "active" states should reflect that semantic meaning:
-- Good: Semantic view names
active_user -- Users who are currently active
latest_device_state -- Most recent state for each device
pending_approval -- Items awaiting approval
-- Bad: Technical or prefixed view names
v_users -- Avoid v_ prefix
users_view -- Avoid _view suffix
user_join_orders -- Avoid describing the operationGeneral Naming Conventions
Tables and Collections
| Element | Convention | Example |
|---|---|---|
| Tables | snake_case, singular | user, order_item |
| Collections (NoSQL) | snake_case or camelCase | user, orderItem |
| Junction Tables | Meaningful compound nouns | user_role, account_membership |
Columns and Fields
| Element | Convention | Example |
|---|---|---|
| Primary Key | {table}_id | user_id, order_id |
| Foreign Key | {referenced_table}_id | user_id, order_id |
| Booleans | Natural conditional reads | is_active, has_permission, can_read |
| Status Fields | Descriptive enum names | status, approval_state |
Timestamp Conventions
Use explicit semantic prefixes to clarify the meaning of each timestamp:
| Timestamp | Meaning | Use Case |
|---|---|---|
created_at | When the record was created | Standard creation timestamp |
updated_at | When the record was last modified | Track modifications |
observed_at | When the data was observed/measured | Sensor data, metrics |
ingested_at | When data was imported into the system | ETL pipelines, data imports |
effective_at | When the record becomes effective | Policies, configurations |
expires_at | When the record expires | Sessions, tokens, licenses |
deleted_at | Soft delete timestamp | Soft delete pattern |
Abbreviations
Avoid abbreviations unless they are universally understood within your domain:
-- Bad: Unclear abbreviations
usr_id, ord_amt, txn_dt, cfg_val
-- Good: Full descriptive names
user_id, order_amount, transaction_date, config_value
-- Acceptable: Universal domain abbreviations
url, api, html, http, ip_addressIndexes and Constraints
Naming Patterns
| Object Type | Pattern | Example |
|---|---|---|
| Index | {table}_{column}_idx | user_email_idx |
| Composite Index | {table}_{col1}_{col2}_idx | order_user_id_created_at_idx |
| Unique Index | {table}_{column}_uniq | user_email_uniq |
| Primary Key | {table}_pkey | user_pkey |
| Foreign Key | {table}_{referenced}_fkey | order_user_fkey |
| Check Constraint | Describe the business rule | policy_effective_at_required |
Constraint Examples
-- Constraints should clarify business rules
ALTER TABLE policy
ADD CONSTRAINT policy_effective_at_required
CHECK (effective_at IS NOT NULL);
ALTER TABLE order_item
ADD CONSTRAINT order_item_quantity_positive
CHECK (quantity > 0);
ALTER TABLE user
ADD CONSTRAINT user_email_format
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');Vocabulary Consistency
Maintain consistent terminology across your entire schema:
| Principle | Example |
|---|---|
| Choose one term | Use either user or account, not both for the same concept |
| Consistent suffixes | If using _at for timestamps, use it everywhere |
| Domain alignment | Match terminology to your business domain |
-- Good: Consistent vocabulary
user, user_role, user_session, user_preference
-- Bad: Inconsistent vocabulary
user, account_role, member_session, customer_preferenceAvoid Encoding Technical Details
Names should not reveal query intent or implementation details:
-- Bad: Encodes implementation
user_denormalized
order_with_items_joined
fast_user_lookup
-- Good: Describes domain concept
user_summary
order_detail
active_userSQL Standards
Query Formatting
-- Use uppercase for SQL keywords
-- Indent for readability
SELECT
u.id,
u.email,
u.display_name,
COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.is_active = TRUE
AND u.created_at >= '2024-01-01'
GROUP BY u.id, u.email, u.display_name
HAVING COUNT(o.id) > 0
ORDER BY order_count DESC
LIMIT 100;Table Creation
-- Table naming: snake_case, plural
-- Include created_at and updated_at timestamps
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
display_name VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Index naming: idx_{table}_{column(s)}
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);
-- Foreign key naming: fk_{table}_{referenced_table}
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
total_amount DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT fk_orders_users FOREIGN KEY (user_id) REFERENCES users(id)
);Constraint Naming
| Constraint Type | Naming Convention | Example |
|---|---|---|
| Primary Key | pk_{table} | pk_users |
| Foreign Key | fk_{table}_{referenced} | fk_orders_users |
| Unique | uq_{table}_{column} | uq_users_email |
| Check | ck_{table}_{column} | ck_orders_status |
| Index | idx_{table}_{column(s)} | idx_users_email |
PostgreSQL Guidelines
Data Types
| Use Case | Recommended Type |
|---|---|
| Identifiers | UUID or BIGSERIAL |
| Timestamps | TIMESTAMP WITH TIME ZONE |
| Money | DECIMAL(precision, scale) |
| Text (variable) | VARCHAR(n) or TEXT |
| JSON | JSONB (not JSON) |
| Arrays | Native array types |
Best Practices
-- Use JSONB for flexible schemas
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type VARCHAR(100) NOT NULL,
payload JSONB NOT NULL DEFAULT '{}',
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes on JSONB fields
CREATE INDEX idx_events_payload_user_id
ON events USING GIN ((payload -> 'user_id'));
-- Use partial indexes for common queries
CREATE INDEX idx_orders_pending
ON orders (created_at)
WHERE status = 'pending';MySQL/MariaDB Guidelines
Data Types
| Use Case | Recommended Type |
|---|---|
| Identifiers | BIGINT UNSIGNED AUTO_INCREMENT or CHAR(36) for UUID |
| Timestamps | DATETIME or TIMESTAMP |
| Money | DECIMAL(precision, scale) |
| Text | VARCHAR(n) with explicit charset |
| JSON | JSON (MySQL 5.7+) |
Best Practices
-- Always specify charset and collation
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
display_name VARCHAR(100) NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_users_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Use covering indexes for common queries
CREATE INDEX idx_users_active_created
ON users (is_active, created_at);SQL Server Guidelines
Data Types
| Use Case | Recommended Type |
|---|---|
| Identifiers | UNIQUEIDENTIFIER or BIGINT IDENTITY |
| Timestamps | DATETIME2 or DATETIMEOFFSET |
| Money | DECIMAL(precision, scale) |
| Text | NVARCHAR(n) for Unicode |
Best Practices
-- Use schemas for organization
CREATE SCHEMA sales;
CREATE TABLE sales.orders (
id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
user_id UNIQUEIDENTIFIER NOT NULL,
status NVARCHAR(50) NOT NULL DEFAULT N'pending',
total_amount DECIMAL(18, 2) NOT NULL,
created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
updated_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
CONSTRAINT fk_orders_users FOREIGN KEY (user_id)
REFERENCES dbo.users(id)
);
-- Use filtered indexes
CREATE INDEX idx_orders_pending
ON sales.orders (created_at)
WHERE status = N'pending';NoSQL Guidelines
MongoDB
// Collection naming: camelCase or snake_case, plural
// Document structure
{
_id: ObjectId("..."),
email: "user@example.com",
displayName: "John Doe",
isActive: true,
profile: {
avatarUrl: "https://...",
bio: "..."
},
createdAt: ISODate("2024-01-15T10:30:00Z"),
updatedAt: ISODate("2024-01-15T10:30:00Z")
}
// Index creation
db.users.createIndex({ email: 1 }, { unique: true });
db.users.createIndex({ createdAt: -1 });
db.users.createIndex({ "profile.avatarUrl": 1 }, { sparse: true });Redis
# Key naming: colon-separated namespaces
user:123:profile # Hash for user profile
user:123:sessions # Set of session IDs
session:abc123 # Session data
cache:users:list # Cached user list
rate_limit:user:123:api # Rate limiting counterORM Best Practices
Avoiding N+1 Queries
// Bad: N+1 query problem
const users = await userRepository.find();
for (const user of users) {
const orders = await orderRepository.find({ userId: user.id }); // N queries!
}
// Good: Use eager loading / joins
const users = await userRepository.find({
relations: ['orders'],
});
// Good: Use query builder with join
const users = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.orders', 'order')
.getMany();Migration Best Practices
// Use descriptive migration names
// 20240115_create_users_table.ts
// 20240116_add_email_verified_to_users.ts
export class AddEmailVerifiedToUsers1705312200000 {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'users',
new TableColumn({
name: 'email_verified_at',
type: 'timestamp with time zone',
isNullable: true,
})
);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('users', 'email_verified_at');
}
}Query Optimization
Index Guidelines
| Query Pattern | Index Type |
|---|---|
| Equality lookups | B-tree (default) |
| Range queries | B-tree |
| Full-text search | Full-text index |
| JSON field access | GIN (PostgreSQL) |
| Geospatial queries | Spatial index |
Common Anti-Patterns
-- Anti-pattern: Function on indexed column
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
-- Better: Store normalized data or use functional index
CREATE INDEX idx_users_email_lower ON users (LOWER(email));
-- Anti-pattern: SELECT *
SELECT * FROM users WHERE id = 123;
-- Better: Select only needed columns
SELECT id, email, display_name FROM users WHERE id = 123;
-- Anti-pattern: Missing LIMIT on large tables
SELECT * FROM events ORDER BY created_at DESC;
-- Better: Always limit results
SELECT * FROM events ORDER BY created_at DESC LIMIT 100;Related Resources
- Coding Standards
- Security
- Surveilr Database Naming Guidelines
- SQL Style Guide
- PostgreSQL Wiki - Don't Do This
- Use The Index, Luke
- MongoDB Best Practices
Compliance
This section fulfills ISO 13485 requirements for control of records (4.2.4) and data integrity (4.2.5), and ISO 27001 requirements for information backup (A.8.13), data classification (A.5.12), and database security (A.8.11).
How is this guide?
Last updated on