Ocupação de Salas
Objetivo
Mede, por sala ativa, quanto do tempo disponível configurado foi efetivamente ocupado por agendamentos e quanto foi bloqueado, dentro de um período. O resultado é uma linha por sala com: horas de ocupação, horas de bloqueio, total de horas configuradas e os percentuais de aproveitamento.
Conceitos e regras de negócio
1. Disponibilidade da sala (denominador)
Cada sala tem horários de funcionamento configurados por dia da semana (room_time_slots). A disponibilidade total no período é calculada assim:
- Gera-se a lista de todos os dias do período (
:startDatea:endDate). - Para cada dia, identifica-se o dia da semana (
MONDAY,TUESDAY, ... — mesmo formato da colunaday_of_week). - Cruza-se com os horários configurados da sala naquele dia da semana.
- Soma-se
(end_time - start_time)de cada slot. As colunasstart_timeeend_timesão inteiros em horas (ex.: 8 e 18 = das 8h às 18h).
Exemplo: sala com slot de segunda a sexta das 8h às 18h, período de 7 dias (5 dias úteis) → total = 5 × 10 = 50 horas.
2. Ocupação e bloqueios (numeradores)
Vêm da tabela appointments, filtrando pelo período (start_date between :startDate and :endDate) e separando por situação:
| Conceito | Regra | Status considerados |
|---|---|---|
| Ocupação | status_id not in (6, 7) | Agendado (1), Confirmado (2), Espera (3), Andamento (4), Realizado (5) e Desmarcado (8) |
| Bloqueio | status_id = 7 | Bloqueio (7) |
| Excluído de tudo | — | Falta (6) |
Nota: pela regra original, agendamentos Desmarcados (8) contam como ocupação, pois a exclusão é apenas de Falta e Bloqueio. Manter assim para ficar fiel ao relatório; se o time de dados quiser excluir desmarcados, basta adicionar o 8 na lista de exclusão — mas isso divergirá do sistema.
Tabela de referência dos status de agendamento:
| id | Situação |
|---|---|
| 1 | Agendado |
| 2 | Confirmado |
| 3 | Espera |
| 4 | Andamento |
| 5 | Realizado |
| 6 | Falta |
| 7 | Bloqueio |
| 8 | Desmarcado |
3. Merge de sobreposições (regra mais importante)
Uma sala pode ter agendamentos simultâneos ou sobrepostos (ex.: dois clientes na mesma sala das 14h às 15h). Se simplesmente somarmos as durações, a ocupação fica inflada e pode passar de 100%.
Por isso, antes de somar, os intervalos de cada sala são fundidos: períodos que se sobrepõem contam apenas uma vez. A técnica usa window functions (funciona igual no Redshift):
- Ordena os agendamentos da sala por
start_date. - Calcula o "fim acumulado" até a linha anterior:
max(end_date) over (partition by room ordenado por start_date, rows between unbounded preceding and 1 preceding). - Compara cada agendamento com esse fim acumulado (
lag):- Se começa depois do fim acumulado → conta a duração inteira.
- Se começa dentro de um período já contado mas termina depois → conta só a parte que excede.
- Se está totalmente contido em um período já contado → conta zero.
Esse merge é feito separadamente para ocupação e para bloqueios.
4. Salas consideradas
Somente salas com status = 'ACTIVE' na tabela rooms.
Métricas finais
| Métrica | Cálculo |
|---|---|
| Ocupação (horas) | Soma das durações fundidas dos agendamentos de ocupação |
| Bloqueios (horas) | Soma das durações fundidas dos bloqueios |
| Ocupação + Bloqueios (horas) | Soma das duas anteriores |
| Total horários configurados (horas) | Disponibilidade da sala no período (item 1) |
| % Ocupação × Total | ocupacao / total × 100 |
| % Ocupação/Bloqueios × Total | (ocupacao + bloqueios) / total × 100 |
Usar nullif(total, 0) no denominador para evitar divisão por zero (sala sem horário configurado no período).
Totalizador: contagem de salas retornadas.
Adaptações necessárias para o Redshift
O sistema de origem usa recursos do PostgreSQL que não existem no Redshift. As substituições:
| PostgreSQL (origem) | Redshift (datalake) |
|---|---|
generate_series(inicio, fim, '1 day') | CTE recursiva (with recursive) gerando um dia por iteração — ver query de referência. Se futuramente existir uma tabela calendário no datalake, usá-la no lugar. |
Tipo interval e aritmética de intervalos | Trabalhar com segundos via datediff(second, start_date, end_date) e converter para horas (/ 3600.0) no final |
extract(isodow from data) (1=segunda ... 7=domingo) | date_part(dow, data) (0=domingo ... 6=sábado) — atenção ao mapeamento diferente ao converter para MONDAY, TUESDAY, etc. |
As window functions (lag, max ... rows between unbounded preceding and 1 preceding) e greatest funcionam normalmente no Redshift.
Tabelas envolvidas
| Tabela | Papel |
|---|---|
room_time_slots | Horários configurados por sala e dia da semana (day_of_week, start_time/end_time em horas) |
rooms | Cadastro das salas (name, status) |
appointments | Agendamentos, com room_id, start_date, end_date, status_id |
⚠️ Todo join deve incluir a coluna "schema" além dos ids, para não cruzar salas e agendamentos de franquias diferentes. Registros deletados não existem no datalake (o ETL não sincroniza soft deletes).
Escopo multi-franquia
O relatório faz sentido por franquia (as salas pertencem a uma unidade), então o resultado deve sempre incluir "schema" no agrupamento — seja exibindo a coluna, seja filtrando por uma franquia específica.
Colunas do relatório → Datalake
| Coluna do relatório | Origem |
|---|---|
| Nome da Sala | rooms.name |
| Ocupação (horas) | Soma fundida das durações de appointments (status ≠ 6 e ≠ 7) |
| % Ocupação × Total | ocupacao / total × 100 |
| Bloqueios (horas) | Soma fundida das durações de appointments (status = 7) |
| Ocupação + Bloqueios (horas) | Soma das duas |
| % Ocupação/Bloqueios × Total | (ocupacao + bloqueios) / total × 100 |
| Total horários configurados | room_time_slots cruzado com os dias do período |
Pontos de atenção
- Sobreposições: nunca somar durações direto — sempre aplicar o merge, senão salas com agendamentos simultâneos passam de 100%.
- Agendamentos que atravessam a meia-noite ou saem do período: o filtro é apenas por
start_datedentro do período; se oend_datecair fora, a duração inteira conta (fiel ao original). - Formato de exibição: o relatório original exibe as durações como texto de intervalo (ex.:
12:30:00); no datalake sugerimos horas decimais (ex.:12.5), mais prático para BI. date_part(dow)no Redshift retorna 0 para domingo — o mapeamento para os nomes dos dias é diferente doisodowdo PostgreSQL (que retorna 7 para domingo).