Browse Templates/HR Management

HR Management

A complete Human Resources Management System (HRMS) database schema designed for managing the full employee lifecycle. Covers organizational hierarchy with departments and positions, employee records, leave request workflows with approval tracking, payroll processing, and performance review cycles. Suitable for building internal HR tools, HRIS platforms, or extending existing HR systems.

PostgreSQL10 tablesEnterprise
hrhuman-resourcesemployeespayrollleaveperformance
Created with ERD Studio

About this schema

employees holds where someone is now; employment_history holds everywhere they have been. The current department_id and position_id are on the employee for fast reads, and every change writes a history row with start_date, end_date and a change_reason — which is what makes tenure, promotion rates and internal mobility answerable.

The reporting line is a self-reference: employees.manager_id points back at employees. One nullable column gives the whole org chart, and a null manager is the top of the tree rather than an error — which also means nothing stops a cycle except the application that writes it.

Leave is two tables because entitlement and request are different facts. leave_types defines what exists and how many days it carries by default; leave_requests is one person asking for dates, with its own status and an approved_by who is another employee.

Key relationships

departments / positions → employees
Two independent dimensions. Position carries the salary band, so a transfer between departments does not imply a pay change.
employees → employees (manager_id)
The reporting line as a self-join. Null means no manager, which is how the top of the hierarchy is represented.
employees → employment_history
A dated row per role or department change, so the current values on the employee can always be reconstructed and explained.
leave_types → leave_requests → employees (approved_by)
The request names both the person asking and the person who decided, since approval is an act by a named manager.
employees → payroll
One row per pay period with base_salary, bonus and deductions kept separate, so net_pay is explainable rather than asserted.
employees → performance_reviews (employee_id, reviewer_id)
Two keys into the same table — the reviewed and the reviewer — which is why a manager's own reviews are just more rows.
departments.head_employee_id
A circular reference with employees, so it must be nullable for a department to be created before anyone leads it.

Design decisions

Current role on the employee, history in its own table

Deriving the current department from the latest history row is the normalised answer and makes every employee listing an ordered scan per person. Keeping both makes the common read one column and the historical question a join. The obligation is that a change must write both in one transaction, or the employee row and their history will disagree about where they work.

Payroll stores components, not just net pay

base_salary, bonus and deductions are kept apart so a payslip can be explained line by line and a disputed figure can be traced. One net_pay column would be smaller and would make every query about compensation structure impossible. It also means the arithmetic is frozen — a later salary change does not restate what was actually paid in March.

Leave balance is not stored

leave_types carries default_days and leave_requests carries the dates taken, so a balance is a calculation rather than a column. That avoids a counter that drifts whenever a request is cancelled or back-dated. The cost is that the calculation has to handle carry-over, pro-rata for new joiners and accrual rules, none of which the schema expresses — an entitlements table per employee per year is the usual addition.

Managers are employees, not a separate role

manager_id and reviewer_id both point at employees, which keeps one identity for a person who manages some people and is managed by another. The limitation is that the schema cannot express a dotted-line or a second manager, and nothing prevents a reporting cycle — both are application responsibilities here.

Tables in this template

departmentsDepartments
7 cols
positionsPositions
6 cols
employeesEmployees
10 cols
employment_historyEmployment History
8 cols
leave_typesLeave Types
5 cols
leave_requestsLeave Requests
9 cols
payrollPayroll
10 cols
benefitsBenefits
7 cols
performance_reviewsPerformance Reviews
8 cols
training_coursesTraining Courses
8 cols

Frequently asked

How do I build an org chart?
A recursive query over employees following manager_id. Starting from the rows where manager_id is null gives the top, and each level down is one more step of the recursion.
How is a leave balance calculated?
Take the leave type's default_days and subtract the days covered by that employee's approved requests for the year. Carry-over and accrual are not modelled, so any policy beyond a flat annual allowance needs an entitlements table.
Why keep employment_history when the employee has the current values?
Because HR questions are mostly historical — how long someone has been in a role, how often people move between departments, what the position was when a bonus was paid. The current columns cannot answer any of those.
Can an employee have two managers?
Not as modelled; manager_id is a single column. Matrix reporting needs a join table between employees, which also lets each relationship carry a type such as functional or project.
Is salary data protected here?
Not by the schema. payroll and positions hold compensation in ordinary columns, so access control is entirely the application's job — and an access audit log is the usual regulatory requirement this template does not include.