GuideApril 23, 2026·By Crew

How We Calculate Room Utilization Rate — The Technical Breakdown

Technical companion to Room Utilization Rate in The Numbers That Matter series.


Room utilization rate = total occupied hours / total available hours. Both sides need careful definition.

Available hours

Available isn't just rooms × operating hours. Subtract maintenance downtime:

SELECT
  r.id AS room_id, r.name,
  (club.closingHour - club.openingHour)
    - COALESCE(SUM(
        EXTRACT(EPOCH FROM (LEAST(d.endAt, :dateEnd) - GREATEST(d.startAt, :dateStart))) / 3600
      ), 0) AS available_hours
FROM rooms r
CROSS JOIN clubs club
LEFT JOIN room_downtime d ON d.roomId = r.id
  AND d.startAt < :dateEnd AND d.endAt > :dateStart
WHERE r.isRentable = true AND r.clubId = :clubId
GROUP BY r.id, r.name, club.closingHour, club.openingHour;

Occupied hours

From rental records, with boundary clamping for rentals spanning the query window:

SELECT r.id AS room_id,
  COALESCE(SUM(
    EXTRACT(EPOCH FROM (LEAST(rental.endAt, :dateEnd) - GREATEST(rental.startAt, :dateStart))) / 3600
  ), 0) AS occupied_hours
FROM rooms r
LEFT JOIN rentals rental ON rental.roomId = r.id
  AND rental.startAt < :dateEnd AND rental.endAt > :dateStart
  AND rental.status = 'completed'
WHERE r.isRentable = true AND r.clubId = :clubId
GROUP BY r.id;

Combined utilization

SELECT
  a.room_id, a.name, a.available_hours, o.occupied_hours,
  CASE WHEN a.available_hours > 0
    THEN ROUND((o.occupied_hours / a.available_hours) * 100, 1) ELSE 0
  END AS utilization_pct
FROM available a
JOIN occupied o ON o.room_id = a.room_id
ORDER BY utilization_pct DESC;

Hourly heatmap

For dead-zone detection, bucket by hour across 30 days to show which hours are consistently underutilized. Group by EXTRACT(HOUR FROM rental.startAt) and count distinct rooms occupied per hour vs. total available rooms.

By room type

SELECT r.roomType,
  ROUND(AVG(o.occupied_hours / NULLIF(a.available_hours, 0)) * 100, 1) AS avg_utilization_pct
FROM available a
JOIN occupied o ON o.room_id = a.room_id
JOIN rooms r ON r.id = a.room_id
GROUP BY r.roomType ORDER BY avg_utilization_pct DESC;

Above 85% is a candidate for expansion or price increase. Below 40% needs investigation.

Storage

For real-time: compute on demand from the rental table. For historical trends: a nightly job snapshots per-room utilization to a summary table, keeping trend queries fast.


Back to the operator post: Room Utilization Rate — what to do with this metric →