Oracle HCM SQL Query Library
Production-ready SQL queries for Oracle Fusion Cloud HCM. Every query uses proper date-effective joins, correct table aliases, and handles the common gotchas that trip up Oracle developers.
PER_ALL_ASSIGNMENTS_M (the materialized table). If you're on an older release, swap _M for _F and add EFFECTIVE_LATEST_CHANGE = 'Y' to the date-effective filters.
Queries in This Library
1. Active Employee Roster
The foundation query. Returns all active employees with their department, location, job, position, grade, and hire date. This is the query you'll modify for 80% of your reports.
SELECT papf.PERSON_NUMBER, ppnf.FIRST_NAME, ppnf.LAST_NAME, paam.ASSIGNMENT_NUMBER, paam.ASSIGNMENT_STATUS_TYPE, haou.NAME AS DEPARTMENT, hla.LOCATION_NAME, pjfv.NAME AS JOB_NAME, hapf.NAME AS POSITION_NAME, pgf.NAME AS GRADE_NAME, paam.NORMAL_HOURS, paam.FREQUENCY, ppos.DATE_START AS HIRE_DATE, paam.EFFECTIVE_START_DATE FROM PER_ALL_PEOPLE_F papf JOIN PER_PERSON_NAMES_F ppnf ON papf.PERSON_ID = ppnf.PERSON_ID AND ppnf.NAME_TYPE = 'GLOBAL' AND TRUNC(SYSDATE) BETWEEN ppnf.EFFECTIVE_START_DATE AND ppnf.EFFECTIVE_END_DATE JOIN PER_ALL_ASSIGNMENTS_M paam ON papf.PERSON_ID = paam.PERSON_ID AND paam.ASSIGNMENT_STATUS_TYPE = 'ACTIVE' AND paam.EFFECTIVE_LATEST_CHANGE = 'Y' AND paam.PRIMARY_FLAG = 'Y' AND TRUNC(SYSDATE) BETWEEN paam.EFFECTIVE_START_DATE AND paam.EFFECTIVE_END_DATE JOIN PER_PERIODS_OF_SERVICE ppos ON paam.PERIOD_OF_SERVICE_ID = ppos.PERIOD_OF_SERVICE_ID LEFT JOIN HR_ALL_ORGANIZATION_UNITS haou ON paam.ORGANIZATION_ID = haou.ORGANIZATION_ID LEFT JOIN HR_LOCATIONS_ALL hla ON paam.LOCATION_ID = hla.LOCATION_ID LEFT JOIN PER_JOBS_F_VL pjfv ON paam.JOB_ID = pjfv.JOB_ID AND TRUNC(SYSDATE) BETWEEN pjfv.EFFECTIVE_START_DATE AND pjfv.EFFECTIVE_END_DATE LEFT JOIN HR_ALL_POSITIONS_F_VL hapf ON paam.POSITION_ID = hapf.POSITION_ID AND TRUNC(SYSDATE) BETWEEN hapf.EFFECTIVE_START_DATE AND hapf.EFFECTIVE_END_DATE LEFT JOIN PER_GRADES_F_VL pgf ON paam.GRADE_ID = pgf.GRADE_ID AND TRUNC(SYSDATE) BETWEEN pgf.EFFECTIVE_START_DATE AND pgf.EFFECTIVE_END_DATE WHERE TRUNC(SYSDATE) BETWEEN papf.EFFECTIVE_START_DATE AND papf.EFFECTIVE_END_DATE ORDER BY ppnf.LAST_NAME, ppnf.FIRST_NAME
Key tables: PER_ALL_PEOPLE_F, PER_PERSON_NAMES_F, PER_ALL_ASSIGNMENTS_M
2. Headcount by Department
Department-level headcount with FTE totals and full-time vs. part-time breakdown. Essential for workforce planning.
SELECT haou.NAME AS DEPARTMENT, hla.LOCATION_NAME, COUNT(DISTINCT papf.PERSON_ID) AS HEADCOUNT, SUM(CASE WHEN paam.ASSIGNMENT_CATEGORY = 'FR' THEN 1 ELSE 0 END) AS FULL_TIME, SUM(CASE WHEN paam.ASSIGNMENT_CATEGORY = 'PT' THEN 1 ELSE 0 END) AS PART_TIME, SUM(pawm.FTE) AS TOTAL_FTE FROM PER_ALL_PEOPLE_F papf JOIN PER_ALL_ASSIGNMENTS_M paam ON papf.PERSON_ID = paam.PERSON_ID AND paam.ASSIGNMENT_STATUS_TYPE = 'ACTIVE' AND paam.PRIMARY_FLAG = 'Y' AND paam.EFFECTIVE_LATEST_CHANGE = 'Y' AND TRUNC(SYSDATE) BETWEEN paam.EFFECTIVE_START_DATE AND paam.EFFECTIVE_END_DATE LEFT JOIN PER_ASSIGN_WORK_MEASURES_F pawm ON paam.ASSIGNMENT_ID = pawm.ASSIGNMENT_ID AND TRUNC(SYSDATE) BETWEEN pawm.EFFECTIVE_START_DATE AND pawm.EFFECTIVE_END_DATE LEFT JOIN HR_ALL_ORGANIZATION_UNITS haou ON paam.ORGANIZATION_ID = haou.ORGANIZATION_ID LEFT JOIN HR_LOCATIONS_ALL hla ON paam.LOCATION_ID = hla.LOCATION_ID WHERE TRUNC(SYSDATE) BETWEEN papf.EFFECTIVE_START_DATE AND papf.EFFECTIVE_END_DATE GROUP BY haou.NAME, hla.LOCATION_NAME ORDER BY haou.NAME
Key tables: PER_ASSIGN_WORK_MEASURES_F for FTE, HR_ALL_ORGANIZATION_UNITS for department names
3. Turnover Report (Last 12 Months)
Monthly termination counts with voluntary vs. involuntary breakdown. The query most executives ask for.
SELECT TO_CHAR(ppos.ACTUAL_TERMINATION_DATE, 'YYYY-MM') AS TERM_MONTH, haou.NAME AS DEPARTMENT, COUNT(*) AS TERMINATIONS, COUNT(CASE WHEN ppos.LEAVING_REASON = 'RESIGNATION' THEN 1 END) AS VOLUNTARY, COUNT(CASE WHEN ppos.LEAVING_REASON != 'RESIGNATION' THEN 1 END) AS INVOLUNTARY FROM PER_PERIODS_OF_SERVICE ppos JOIN PER_ALL_ASSIGNMENTS_M paam ON ppos.PERSON_ID = paam.PERSON_ID AND ppos.PERIOD_OF_SERVICE_ID = paam.PERIOD_OF_SERVICE_ID AND paam.PRIMARY_FLAG = 'Y' LEFT JOIN HR_ALL_ORGANIZATION_UNITS haou ON paam.ORGANIZATION_ID = haou.ORGANIZATION_ID WHERE ppos.ACTUAL_TERMINATION_DATE >= ADD_MONTHS(TRUNC(SYSDATE), -12) AND ppos.ACTUAL_TERMINATION_DATE IS NOT NULL GROUP BY TO_CHAR(ppos.ACTUAL_TERMINATION_DATE, 'YYYY-MM'), haou.NAME ORDER BY TERM_MONTH DESC, DEPARTMENT
LEAVING_REASON for your org's specific values.
Key tables: PER_PERIODS_OF_SERVICE — the only table with ACTUAL_TERMINATION_DATE
4. New Hires Report
Employees hired in the last 30 days with their assignment details. Modify the ADD_MONTHS parameter for different time ranges.
SELECT papf.PERSON_NUMBER, ppnf.FULL_NAME, ppos.DATE_START AS HIRE_DATE, haou.NAME AS DEPARTMENT, pjfv.NAME AS JOB_NAME, hapf.NAME AS POSITION_NAME, paam.ASSIGNMENT_NUMBER FROM PER_ALL_PEOPLE_F papf JOIN PER_PERSON_NAMES_F ppnf ON papf.PERSON_ID = ppnf.PERSON_ID AND ppnf.NAME_TYPE = 'GLOBAL' AND TRUNC(SYSDATE) BETWEEN ppnf.EFFECTIVE_START_DATE AND ppnf.EFFECTIVE_END_DATE JOIN PER_PERIODS_OF_SERVICE ppos ON papf.PERSON_ID = ppos.PERSON_ID JOIN PER_ALL_ASSIGNMENTS_M paam ON papf.PERSON_ID = paam.PERSON_ID AND paam.PRIMARY_FLAG = 'Y' AND paam.EFFECTIVE_LATEST_CHANGE = 'Y' AND TRUNC(SYSDATE) BETWEEN paam.EFFECTIVE_START_DATE AND paam.EFFECTIVE_END_DATE LEFT JOIN HR_ALL_ORGANIZATION_UNITS haou ON paam.ORGANIZATION_ID = haou.ORGANIZATION_ID LEFT JOIN PER_JOBS_F_VL pjfv ON paam.JOB_ID = pjfv.JOB_ID AND TRUNC(SYSDATE) BETWEEN pjfv.EFFECTIVE_START_DATE AND pjfv.EFFECTIVE_END_DATE LEFT JOIN HR_ALL_POSITIONS_F_VL hapf ON paam.POSITION_ID = hapf.POSITION_ID AND TRUNC(SYSDATE) BETWEEN hapf.EFFECTIVE_START_DATE AND hapf.EFFECTIVE_END_DATE WHERE ppos.DATE_START >= ADD_MONTHS(TRUNC(SYSDATE), -1) AND TRUNC(SYSDATE) BETWEEN papf.EFFECTIVE_START_DATE AND papf.EFFECTIVE_END_DATE ORDER BY ppos.DATE_START DESC
5. Employee Contact Directory
Work email, personal email, work phone, mobile, and home address for all employees. Uses LEFT JOINs since not every employee has all contact types.
SELECT papf.PERSON_NUMBER, ppnf.FIRST_NAME, ppnf.LAST_NAME, pea_work.EMAIL_ADDRESS AS WORK_EMAIL, pea_home.EMAIL_ADDRESS AS PERSONAL_EMAIL, pp_work.PHONE_NUMBER AS WORK_PHONE, pp_mobile.PHONE_NUMBER AS MOBILE_PHONE, pa.ADDRESS_LINE_1, pa.TOWN_OR_CITY, pa.REGION_2 AS STATE, pa.POSTAL_CODE FROM PER_ALL_PEOPLE_F papf JOIN PER_PERSON_NAMES_F ppnf ON papf.PERSON_ID = ppnf.PERSON_ID AND ppnf.NAME_TYPE = 'GLOBAL' AND TRUNC(SYSDATE) BETWEEN ppnf.EFFECTIVE_START_DATE AND ppnf.EFFECTIVE_END_DATE LEFT JOIN PER_EMAIL_ADDRESSES pea_work ON papf.PERSON_ID = pea_work.PERSON_ID AND pea_work.EMAIL_TYPE = 'W1' LEFT JOIN PER_EMAIL_ADDRESSES pea_home ON papf.PERSON_ID = pea_home.PERSON_ID AND pea_home.EMAIL_TYPE = 'H1' LEFT JOIN PER_PHONES pp_work ON papf.PERSON_ID = pp_work.PERSON_ID AND pp_work.PHONE_TYPE = 'W1' LEFT JOIN PER_PHONES pp_mobile ON papf.PERSON_ID = pp_mobile.PERSON_ID AND pp_mobile.PHONE_TYPE = 'M' LEFT JOIN PER_ADDRESSES_F pa ON papf.PERSON_ID = pa.PERSON_ID AND pa.ADDRESS_TYPE = 'HOME' AND TRUNC(SYSDATE) BETWEEN pa.EFFECTIVE_START_DATE AND pa.EFFECTIVE_END_DATE WHERE TRUNC(SYSDATE) BETWEEN papf.EFFECTIVE_START_DATE AND papf.EFFECTIVE_END_DATE
PHONE_TYPE and EMAIL_TYPE.
Key tables: PER_EMAIL_ADDRESSES, PER_PHONES, PER_ADDRESSES_F
6. Compensation / Salary Report
Current salary for each employee including annual salary, currency, and salary basis. Requires compensation data access.
SELECT papf.PERSON_NUMBER, ppnf.FULL_NAME, haou.NAME AS DEPARTMENT, pjfv.NAME AS JOB_NAME, pgf.NAME AS GRADE_NAME, cs.SALARY_AMOUNT, cs.ANNUAL_SALARY, cs.CURRENCY_CODE, cs.SALARY_BASIS_NAME, cs.DATE_FROM AS SALARY_START_DATE FROM PER_ALL_PEOPLE_F papf JOIN PER_PERSON_NAMES_F ppnf ON papf.PERSON_ID = ppnf.PERSON_ID AND ppnf.NAME_TYPE = 'GLOBAL' AND TRUNC(SYSDATE) BETWEEN ppnf.EFFECTIVE_START_DATE AND ppnf.EFFECTIVE_END_DATE JOIN PER_ALL_ASSIGNMENTS_M paam ON papf.PERSON_ID = paam.PERSON_ID AND paam.PRIMARY_FLAG = 'Y' AND paam.ASSIGNMENT_STATUS_TYPE = 'ACTIVE' AND paam.EFFECTIVE_LATEST_CHANGE = 'Y' AND TRUNC(SYSDATE) BETWEEN paam.EFFECTIVE_START_DATE AND paam.EFFECTIVE_END_DATE JOIN CMP_SALARY cs ON paam.ASSIGNMENT_ID = cs.ASSIGNMENT_ID AND TRUNC(SYSDATE) BETWEEN cs.DATE_FROM AND NVL(cs.DATE_TO, TO_DATE('4712-12-31','YYYY-MM-DD')) LEFT JOIN HR_ALL_ORGANIZATION_UNITS haou ON paam.ORGANIZATION_ID = haou.ORGANIZATION_ID LEFT JOIN PER_JOBS_F_VL pjfv ON paam.JOB_ID = pjfv.JOB_ID AND TRUNC(SYSDATE) BETWEEN pjfv.EFFECTIVE_START_DATE AND pjfv.EFFECTIVE_END_DATE LEFT JOIN PER_GRADES_F_VL pgf ON paam.GRADE_ID = pgf.GRADE_ID AND TRUNC(SYSDATE) BETWEEN pgf.EFFECTIVE_START_DATE AND pgf.EFFECTIVE_END_DATE WHERE TRUNC(SYSDATE) BETWEEN papf.EFFECTIVE_START_DATE AND papf.EFFECTIVE_END_DATE
DATE_FROM / DATE_TO instead of the usual EFFECTIVE_START_DATE / EFFECTIVE_END_DATE. The NVL(DATE_TO, '4712-12-31') handles open-ended current salaries.
Key tables: CMP_SALARY — joins on ASSIGNMENT_ID, not PERSON_ID
7. Absence Balances
Current absence balances (accrued, used, remaining) by absence type and plan. Essential for PTO reporting.
SELECT papf.PERSON_NUMBER, ppnf.FULL_NAME, aatb.NAME AS ABSENCE_TYPE, apb.ACCRUED_BALANCE, apb.USED_BALANCE, (apb.ACCRUED_BALANCE - apb.USED_BALANCE) AS REMAINING_BALANCE, apb.PLAN_NAME FROM PER_ALL_PEOPLE_F papf JOIN PER_PERSON_NAMES_F ppnf ON papf.PERSON_ID = ppnf.PERSON_ID AND ppnf.NAME_TYPE = 'GLOBAL' AND TRUNC(SYSDATE) BETWEEN ppnf.EFFECTIVE_START_DATE AND ppnf.EFFECTIVE_END_DATE JOIN ANC_PER_ABS_PLAN_BAL apb ON papf.PERSON_ID = apb.PERSON_ID JOIN ANC_ABSENCE_TYPES_B aatb ON apb.ABSENCE_TYPE_ID = aatb.ABSENCE_TYPE_ID WHERE TRUNC(SYSDATE) BETWEEN papf.EFFECTIVE_START_DATE AND papf.EFFECTIVE_END_DATE
Key tables: ANC_PER_ABS_PLAN_BAL for balances, ANC_ABSENCE_TYPES_B for type names
SQL Best Practices for Oracle HCM
- Always use date-effective filters on
_Ftables —TRUNC(SYSDATE) BETWEEN EFFECTIVE_START_DATE AND EFFECTIVE_END_DATE - Use
PRIMARY_FLAG = 'Y'on assignments to avoid duplicates from secondary assignments - Use
EFFECTIVE_LATEST_CHANGE = 'Y'onPER_ALL_ASSIGNMENTS_Mto get the latest version of each assignment - Join names with
NAME_TYPE = 'GLOBAL'— otherwise you get separate rows for legal/display names - Use
_VLviews (likePER_JOBS_F_VL) instead of joining_F+_TLmanually - Use
LEFT JOINfor optional lookups (grade, position, location) — not all assignments have all fields populated - Prefer
PER_ALL_ASSIGNMENTS_Mover_F— Oracle deprecated_Fin 24B and_Mis faster for reporting
Search All 35,000+ Tables
Find columns, types, and schema for any Oracle Fusion Cloud table instantly.
Search Tables