- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Database Normalization Explained With a Real Example: Tenants, Units and Contracts
About Post
Most databases start life as a spreadsheet. Someone in operations has been tracking tenancy contracts in Excel for years, it works, and now it's your job to "just put it in the system".
So you create one table with one column per spreadsheet column. It works on day one. By month three, a tenant has two different phone numbers depending on which row you look at, a building's address is spelled four ways, and deleting an old contract somehow deleted the only record of a unit.
Database normalization is the cure, and it's far less academic than the textbook definitions make it sound. Let's normalize that spreadsheet step by step, using a property management example, then talk about when to break the rules on purpose.
The starting point: one big table
Here's our imported spreadsheet, simplified:
| contract_no | tenant_name | tenant_phones | unit_code | unit_floor | building | building_address | rent | paid_jan | paid_feb |
|---|---|---|---|---|---|---|---|---|---|
| C-101 | A. Rahman | 5551 0001, 5551 0002 | B1-204 | 2 | Tower B1 | 12 Harbour Rd | 8000 | yes | no |
| C-102 | A. Rahman | 5551 0001 | B1-205 | 2 | Tower B1 | 12 Harbor Road | 7500 | yes | yes |
Already you can see the three classic problems, called anomalies:
- Update anomaly: the tenant's phone and the building's address are stored many times. Change one row, forget another, and the data now disagrees with itself.
- Insert anomaly: you can't record a new, empty unit, because a row needs a contract.
- Delete anomaly: delete the last contract in a building and the building's address disappears with it.
Normalization is just a set of steps that removes these, by making every fact live in exactly one place.
First normal form: one value per cell, no repeating columns
1NF says each cell holds a single value, and you don't have repeating groups of columns.
Our table breaks it twice. tenant_phones holds a list, so you can't easily search for a number or add a third one. And paid_jan, paid_feb… is a repeating group: what happens in year two? Add paid_jan_2?
The fix is to turn lists into rows in their own tables:
- A
tenant_phonestable, one row per phone number. - A
paymentstable, one row per payment, with acontract_id, a period, an amount and a paid date.
The test: if you ever find yourself splitting a cell by commas in code, or adding numbered columns, 1NF is asking for a new table.
Second normal form: depend on the whole key
2NF only matters for tables with a composite primary key (a key made of more than one column). It says every other column must depend on the whole key, not just part of it.
Say one contract can cover several units, like a shop plus a storage room. We create a contract_units table with the key (contract_id, unit_id):
| contract_id | unit_id | unit_floor | monthly_rent |
|---|---|---|---|
| 101 | 204 | 2 | 6500 |
| 101 | S-12 | B | 1500 |
monthly_rent is fine: the rent for this unit on this contract depends on both columns. But unit_floor depends only on unit_id. A unit is on the same floor no matter which contract it's in. That's a partial dependency, and it brings back the update anomaly.
The fix: unit_floor moves to a units table, where it depends on the unit's own key.
Third normal form: nothing but the key
3NF says a non-key column shouldn't depend on another non-key column. These are called transitive dependencies.
In our units table we have building and building_address. The address doesn't depend on the unit; it depends on the building, which depends on the unit. So the building gets its own table, and units keep only a building_id.
The same thinking pulls the tenant's details out of the contract. A tenant's name and email are facts about the tenant, not about each contract they sign.
There's a famous summary of the first three forms: every column should depend on the key, the whole key, and nothing but the key.
The normalized schema
Here's where we end up, as simplified Laravel migrations:
Schema::create('buildings', function (Blueprint $t) {
$t->id();
$t->string('name');
$t->string('address');
});
Schema::create('units', function (Blueprint $t) {
$t->id();
$t->foreignId('building_id')->constrained();
$t->string('code')->unique();
$t->string('floor');
});
Schema::create('contracts', function (Blueprint $t) {
$t->id();
$t->string('number')->unique();
$t->foreignId('tenant_id')->constrained();
$t->date('starts_on');
$t->date('ends_on');
});
Plus tenants, tenant_phones, contract_units (with the rent per unit) and payments. More tables, yes. But now the address is fixed in one place, an empty unit can exist, and deleting a contract can't take a building with it.
Getting data back together is a join away, and Eloquent relationships make that pleasant: $contract->units->first()->building->address, with eager loading to avoid N+1 queries.
Snapshot is not duplication. The rent on a signed contract and the tenant's name as printed on it are facts about that moment. If the tenant later changes their name, the signed contract must not change. Storing those values on the contract is correct modelling, not denormalization.
When to denormalize on purpose
Normalization optimises for correct writes. Sometimes reads matter more, and it's fine to bend the rules deliberately:
- Historical snapshots, as in the callout above: invoices, signed documents, audit logs.
- Counters and totals that would be expensive to compute on every page load, like the outstanding balance on a contract. Keep one source of truth and update the copy in the same transaction (or recalculate it in a job).
- Reporting tables that flatten many joins into one wide table for dashboards, rebuilt on a schedule.
- Search indexes and caches, which are denormalized copies by design.
The rule that keeps this safe: every copy has an owner. You always know which table is the truth, and how and when the copy is refreshed. Denormalization without an owner is just the original spreadsheet with extra steps.
The short version
- 1NF: one value per cell; lists become rows in their own table.
- 2NF: with a composite key, every column depends on the whole key.
- 3NF: columns don't depend on other non-key columns.
- Normalize by default. Denormalize for a measured reason, with a clear source of truth.
What's the strangest "spreadsheet turned database table" you've had to clean up? I suspect everyone has met a notes column holding three different kinds of data.

Be first to comment it...