Capa do Ebook gratuito SQL para Análise de Dados no Dia a Dia: Consultas, Relatórios e Insights com Dados Reais

SQL para Análise de Dados no Dia a Dia: Consultas, Relatórios e Insights com Dados Reais

Novo curso

26 páginas

Projeto aplicado com atendimento: SLAs, filas, resolução e qualidade de serviço

Capítulo 23

Tempo estimado de leitura: 0 minutos

+ Exercício

Neste capítulo, você vai construir um projeto aplicado de análise de atendimento (suporte/CS/central de serviços) com foco em SLAs, filas, resolução e qualidade de serviço. A ideia é sair de métricas soltas e chegar a um conjunto de consultas que sustentem um relatório operacional: onde estão os gargalos, quais filas estouram SLA, como evolui a resolução e como medir qualidade com consistência.

Modelo mental: o que medir em atendimento

Em operações de atendimento, quase tudo gira em torno de tempo e status. Para transformar isso em métricas confiáveis, você precisa separar quatro dimensões:

  • Demanda: quantos tickets entram (criados) por período, por fila, por canal, por prioridade.
  • Capacidade e fluxo: quantos tickets são atendidos e resolvidos, quanto tempo ficam aguardando, quantas reaberturas e transferências acontecem.
  • SLA: regras de tempo-alvo (primeira resposta, resolução) e se o ticket cumpriu ou não.
  • Qualidade: satisfação (CSAT), auditoria, FCR (resolução no primeiro contato), retrabalho (reabertura), e consistência de categorização.

O ponto crítico é que “tempo” em atendimento não é um único número: existe tempo de espera, tempo em atendimento, tempo total, tempo útil (horário comercial) e tempo por fila. Se você não definir claramente o que está medindo, o relatório vira uma disputa de interpretações.

Conjunto de dados do projeto (tabelas e campos)

Considere um esquema típico (nomes ilustrativos). Ajuste para o seu banco:

  • tickets: ticket_id, created_at, closed_at, status, channel, priority, customer_id, current_queue_id, assigned_agent_id
  • ticket_events: event_id, ticket_id, event_at, event_type, from_status, to_status, from_queue_id, to_queue_id, agent_id
  • sla_policy: policy_id, priority, channel, first_response_minutes_target, resolution_minutes_target
  • sla_tracking: ticket_id, first_response_at, first_response_breached, resolution_breached, resolution_at (ou calculado), policy_id
  • queues: queue_id, queue_name, queue_group
  • agents: agent_id, agent_name, team
  • csat_surveys: ticket_id, survey_sent_at, score (1–5), comment

Dois alertas práticos: (1) nem todo ticket tem pesquisa CSAT; (2) eventos podem ter granularidade alta e duplicidades (ex.: múltiplas mudanças de status em minutos). O projeto vai lidar com isso definindo “eventos relevantes” e regras de consolidação.

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

Definições operacionais (para não misturar conceitos)

SLA de primeira resposta

Tempo entre created_at e o primeiro momento em que o cliente recebeu uma resposta (ou o ticket saiu de “novo” para “em atendimento”, dependendo do seu sistema). Em muitos ambientes, isso é melhor representado por um campo calculado (first_response_at) ou por um evento específico (ex.: event_type = 'first_reply').

SLA de resolução

Tempo entre created_at e closed_at (ou o primeiro momento em que o ticket foi marcado como resolvido). Em operações com reabertura, você precisa decidir se o SLA “reinicia” ou se continua contando. Para relatórios executivos, é comum medir o tempo até a primeira resolução e também a taxa de reabertura.

Fila (queue) e responsabilidade

Fila é onde o ticket “está” aguardando ou sendo tratado. Como tickets podem ser transferidos, a análise por fila exige olhar eventos de mudança de fila e calcular tempo em cada etapa.

Qualidade

Qualidade pode ser aproximada por: CSAT médio, % de CSAT baixo (1–2), FCR (sem reabertura), e conformidade de categorização (ex.: motivo correto). Neste capítulo, vamos focar em CSAT e reabertura por serem comuns e mensuráveis.

Passo a passo prático: construir um relatório operacional de atendimento

Você vai montar o projeto em camadas: primeiro, uma base de tickets com tempos e flags; depois, métricas por fila e por agente; por fim, indicadores de qualidade e um “painel” em formato tabular.

Passo 1 — Base de tickets com tempos principais e SLA

Objetivo: ter uma linha por ticket com datas-chave, tempos (minutos) e status de SLA. Se você já tem uma tabela sla_tracking, aproveite. Se não tiver, você pode derivar first_response_at via eventos.

WITH first_response AS (  SELECT     te.ticket_id,    MIN(te.event_at) AS first_response_at  FROM ticket_events te  WHERE te.event_type IN ('first_reply','agent_reply','public_reply')  GROUP BY te.ticket_id), ticket_base AS (  SELECT     t.ticket_id,    t.created_at,    t.closed_at,    t.status,    t.channel,    t.priority,    t.current_queue_id,    t.assigned_agent_id,    fr.first_response_at,    CASE       WHEN fr.first_response_at IS NULL THEN NULL       ELSE EXTRACT(EPOCH FROM (fr.first_response_at - t.created_at)) / 60.0     END AS first_response_minutes,    CASE       WHEN t.closed_at IS NULL THEN NULL       ELSE EXTRACT(EPOCH FROM (t.closed_at - t.created_at)) / 60.0     END AS resolution_minutes  FROM tickets t  LEFT JOIN first_response fr    ON fr.ticket_id = t.ticket_id), ticket_with_policy AS (  SELECT     tb.*,    sp.policy_id,    sp.first_response_minutes_target,    sp.resolution_minutes_target,    CASE       WHEN tb.first_response_minutes IS NULL THEN NULL       WHEN tb.first_response_minutes <= sp.first_response_minutes_target THEN 0       ELSE 1     END AS first_response_breached,    CASE       WHEN tb.resolution_minutes IS NULL THEN NULL       WHEN tb.resolution_minutes <= sp.resolution_minutes_target THEN 0       ELSE 1     END AS resolution_breached  FROM ticket_base tb  LEFT JOIN sla_policy sp    ON sp.priority = tb.priority   AND sp.channel = tb.channel) SELECT * FROM ticket_with_policy;

Notas práticas:

  • Se o seu banco não tiver EXTRACT(EPOCH FROM ...), substitua pelo cálculo equivalente de diferença de tempo.
  • Se first_response_at não existir como evento, use o primeiro evento de “atribuição a agente” ou “mudança para em atendimento”, mas documente a definição.
  • Se a política de SLA depender também de cliente, contrato ou fila, inclua essas chaves no join da política.

Passo 2 — Volume, backlog e taxa de resolução por período

Objetivo: enxergar demanda (criados), saída (fechados) e backlog (abertos) para entender pressão operacional. Backlog é especialmente útil para explicar por que SLA piora mesmo com volume estável.

WITH daily AS (  SELECT     DATE(created_at) AS day,    COUNT(*) AS created_tickets  FROM tickets  GROUP BY DATE(created_at)), daily_closed AS (  SELECT     DATE(closed_at) AS day,    COUNT(*) AS closed_tickets  FROM tickets  WHERE closed_at IS NOT NULL  GROUP BY DATE(closed_at)), daily_join AS (  SELECT     d.day,    d.created_tickets,    COALESCE(dc.closed_tickets, 0) AS closed_tickets  FROM daily d  LEFT JOIN daily_closed dc    ON dc.day = d.day) SELECT   day,  created_tickets,  closed_tickets,  (created_tickets - closed_tickets) AS net_inflow_estimate FROM daily_join ORDER BY day;

Esse “net_inflow_estimate” é um proxy simples. Para backlog real por dia, você precisa contar tickets abertos em cada dia (ticket criado antes ou no dia e não fechado até o dia). Isso é mais pesado, mas viável em relatórios diários.

Passo 3 — SLA por fila: onde estoura e por quê

Objetivo: identificar filas com maior % de violação e também entender se o problema é primeira resposta ou resolução. Use a base do Passo 1 e agregue por fila.

WITH base AS (  SELECT *  FROM ticket_with_policy) SELECT   q.queue_name,  COUNT(*) AS tickets,  AVG(CASE WHEN first_response_breached = 1 THEN 1.0 ELSE 0.0 END) AS pct_first_response_breach,  AVG(CASE WHEN resolution_breached = 1 THEN 1.0 ELSE 0.0 END) AS pct_resolution_breach,  AVG(first_response_minutes) AS avg_first_response_minutes,  AVG(resolution_minutes) AS avg_resolution_minutes FROM base b LEFT JOIN queues q   ON q.queue_id = b.current_queue_id GROUP BY q.queue_name ORDER BY pct_resolution_breach DESC, tickets DESC;

Interpretação: filas com poucos tickets podem aparecer no topo por variância. Em um relatório real, aplique um corte mínimo de volume (ex.: pelo menos 30 tickets no período) para evitar decisões com base em amostras pequenas.

Passo 4 — Tempo em fila (análise de gargalo por transferência)

Objetivo: medir quanto tempo o ticket ficou em cada fila ao longo do ciclo, não apenas na fila atual. Isso exige transformar eventos de mudança de fila em intervalos.

Estratégia: para cada ticket, ordene eventos de fila por tempo e calcule o tempo entre um evento e o próximo. O tempo é atribuído à fila “depois” da mudança (ou “antes”, dependendo da sua convenção). Aqui vamos atribuir o intervalo à fila em que o ticket ficou após a mudança.

WITH queue_changes AS (  SELECT     te.ticket_id,    te.event_at,    te.to_queue_id AS queue_id  FROM ticket_events te  WHERE te.event_type = 'queue_changed'    AND te.to_queue_id IS NOT NULL), ordered AS (  SELECT     qc.*,    LEAD(qc.event_at) OVER (PARTITION BY qc.ticket_id ORDER BY qc.event_at) AS next_event_at  FROM queue_changes qc), intervals AS (  SELECT     ticket_id,    queue_id,    event_at AS interval_start,    next_event_at AS interval_end,    CASE       WHEN next_event_at IS NULL THEN NULL       ELSE EXTRACT(EPOCH FROM (next_event_at - event_at)) / 60.0     END AS minutes_in_queue  FROM ordered) SELECT   q.queue_name,  COUNT(*) AS intervals_count,  AVG(minutes_in_queue) AS avg_minutes_in_queue,  SUM(minutes_in_queue) AS total_minutes_in_queue FROM intervals i LEFT JOIN queues q   ON q.queue_id = i.queue_id WHERE minutes_in_queue IS NOT NULL GROUP BY q.queue_name ORDER BY total_minutes_in_queue DESC;

Pontos de atenção:

  • Se o ticket não tiver evento de fila inicial, você pode criar um “evento sintético” em created_at com a fila inicial (ex.: current_queue_id no momento de criação, se existir historicamente).
  • O último intervalo (sem next_event_at) pode ser fechado em closed_at do ticket para medir tempo até encerrar. Isso melhora muito a análise de gargalo.

Passo 5 — Produtividade e carga por agente (com cuidado)

Objetivo: entender volume resolvido por agente e tempos médios, sem cair na armadilha de comparar agentes com carteiras diferentes. O mínimo é segmentar por fila, prioridade ou tipo de ticket.

WITH base AS (  SELECT *  FROM ticket_with_policy  WHERE closed_at IS NOT NULL) SELECT   a.team,  a.agent_name,  COUNT(*) AS resolved_tickets,  AVG(resolution_minutes) AS avg_resolution_minutes,  AVG(CASE WHEN resolution_breached = 1 THEN 1.0 ELSE 0.0 END) AS pct_resolution_breach FROM base b LEFT JOIN agents a   ON a.agent_id = b.assigned_agent_id GROUP BY a.team, a.agent_name ORDER BY resolved_tickets DESC;

Boas perguntas para complementar:

  • O agente recebe mais tickets de alta prioridade?
  • O agente atua em filas com maior complexidade?
  • Há muitos tickets sem agente atribuído (indicando falha de roteamento)?

Passo 6 — Reabertura e FCR (First Contact Resolution)

Objetivo: medir retrabalho. Uma definição simples: ticket reaberto se, após fechado, volta para status aberto. FCR pode ser “tickets fechados sem reabertura em X dias”.

WITH status_events AS (  SELECT     te.ticket_id,    te.event_at,    te.to_status  FROM ticket_events te  WHERE te.event_type = 'status_changed'), reopened AS (  SELECT     se.ticket_id,    MIN(se.event_at) AS first_reopen_at  FROM status_events se  WHERE se.to_status IN ('reopened','open')  GROUP BY se.ticket_id), closed AS (  SELECT ticket_id, closed_at  FROM tickets  WHERE closed_at IS NOT NULL) SELECT   COUNT(*) AS closed_tickets,  SUM(CASE WHEN r.first_reopen_at IS NOT NULL AND r.first_reopen_at > c.closed_at THEN 1 ELSE 0 END) AS reopened_tickets,  AVG(CASE WHEN r.first_reopen_at IS NOT NULL AND r.first_reopen_at > c.closed_at THEN 0.0 ELSE 1.0 END) AS fcr_rate_estimate FROM closed c LEFT JOIN reopened r   ON r.ticket_id = c.ticket_id;

Aprimoramento: para FCR com janela, exija que a reabertura ocorra até X dias após o fechamento. Isso evita penalizar casos raros muito tardios.

Passo 7 — Qualidade via CSAT: cobertura, média e detratores

Objetivo: medir satisfação sem ignorar viés de resposta. Sempre reporte também a cobertura: % de tickets com pesquisa respondida.

WITH closed AS (  SELECT ticket_id, closed_at, current_queue_id, assigned_agent_id  FROM tickets  WHERE closed_at IS NOT NULL), csat AS (  SELECT ticket_id, score  FROM csat_surveys  WHERE score IS NOT NULL) SELECT   q.queue_name,  COUNT(*) AS closed_tickets,  COUNT(c.ticket_id) AS csat_responses,  (COUNT(c.ticket_id) * 1.0 / COUNT(*)) AS csat_coverage,  AVG(c.score * 1.0) AS csat_avg,  AVG(CASE WHEN c.score IN (1,2) THEN 1.0 ELSE 0.0 END) AS pct_low_csat FROM closed t LEFT JOIN csat c   ON c.ticket_id = t.ticket_id LEFT JOIN queues q   ON q.queue_id = t.current_queue_id GROUP BY q.queue_name ORDER BY pct_low_csat DESC, csat_coverage DESC;

Leitura correta: CSAT alto com cobertura baixa pode esconder problemas. CSAT baixo com cobertura alta é sinal mais confiável de dor real.

Passo 8 — Painel consolidado: SLA + fluxo + qualidade por fila

Objetivo: entregar uma tabela única por fila com os principais indicadores do período. Isso facilita consumo por gestores e vira base de dashboard.

WITH base AS (  SELECT *  FROM ticket_with_policy), closed AS (  SELECT *  FROM base  WHERE closed_at IS NOT NULL), csat AS (  SELECT ticket_id, score  FROM csat_surveys  WHERE score IS NOT NULL), reopened_flag AS (  SELECT     t.ticket_id,    CASE      WHEN EXISTS (        SELECT 1        FROM ticket_events te        WHERE te.ticket_id = t.ticket_id          AND te.event_type = 'status_changed'          AND te.to_status IN ('reopened','open')          AND te.event_at > t.closed_at      ) THEN 1 ELSE 0 END AS reopened  FROM tickets t  WHERE t.closed_at IS NOT NULL) SELECT   q.queue_name,  COUNT(*) AS tickets_created,  SUM(CASE WHEN b.closed_at IS NOT NULL THEN 1 ELSE 0 END) AS tickets_closed,  AVG(CASE WHEN b.first_response_breached = 1 THEN 1.0 ELSE 0.0 END) AS pct_first_response_breach,  AVG(CASE WHEN b.resolution_breached = 1 THEN 1.0 ELSE 0.0 END) AS pct_resolution_breach,  AVG(b.first_response_minutes) AS avg_first_response_minutes,  AVG(b.resolution_minutes) AS avg_resolution_minutes,  AVG(CASE WHEN rf.reopened = 1 THEN 1.0 ELSE 0.0 END) AS pct_reopened,  COUNT(c.score) AS csat_responses,  (COUNT(c.score) * 1.0 / SUM(CASE WHEN b.closed_at IS NOT NULL THEN 1 ELSE 0 END)) AS csat_coverage_on_closed,  AVG(c.score * 1.0) AS csat_avg FROM base b LEFT JOIN queues q   ON q.queue_id = b.current_queue_id LEFT JOIN reopened_flag rf   ON rf.ticket_id = b.ticket_id LEFT JOIN csat c   ON c.ticket_id = b.ticket_id GROUP BY q.queue_name ORDER BY pct_resolution_breach DESC, tickets_created DESC;

Como usar: essa tabela permite priorizar ações. Exemplo de leitura: uma fila com alta violação de primeira resposta pode precisar de triagem/roteamento; alta violação de resolução com reabertura alta pode indicar baixa qualidade ou falta de base de conhecimento; CSAT baixo com SLA bom pode indicar problema de comunicação ou solução inadequada.

Checklist de consistência específico para atendimento

1) Tickets sem datas-chave

Liste tickets sem first_response_at (quando esperado) e sem closed_at (quando deveriam estar fechados). Isso evita que médias fiquem artificialmente baixas/altas por muitos nulos.

2) Políticas de SLA não encontradas

Conte tickets que não casam com sla_policy. Normalmente é erro de cadastro (prioridade nova, canal novo) e gera NULL em metas e flags.

3) Eventos fora de ordem

Verifique se há eventos com event_at anterior ao created_at do ticket (problema de timezone/integração) ou múltiplos eventos idênticos (duplicidade de ingestão).

4) Transferências excessivas

Tickets com muitas mudanças de fila costumam correlacionar com SLA ruim e CSAT baixo. Uma consulta simples é contar queue_changed por ticket e segmentar por fila de origem/destino.

Extensões úteis (quando você quiser aprofundar)

  • Horário comercial: recalcular tempos de SLA considerando apenas janelas úteis (ex.: 09:00–18:00, dias úteis). Isso geralmente exige tabela de calendário e lógica adicional.
  • Segmentação por motivo: cruzar SLA/CSAT com categorias de contato (billing, técnico, cancelamento) para achar causas.
  • Capacidade: combinar tickets com escalas/turnos para medir carga por hora e dimensionamento.
  • Qualidade por auditoria: se houver tabela de auditoria (nota e checklist), integrar com SLA e reabertura para achar trade-offs.

Agora responda o exercício sobre o conteúdo:

Ao analisar SLA por fila em um relatório operacional, qual prática ajuda a evitar interpretações enganosas ao comparar filas?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Filas com poucos tickets podem aparecer com violacao alta por variancia. Um corte minimo de volume ajuda a evitar decisoes baseadas em amostras pequenas e torna a comparacao mais confiavel.

Próximo capitúlo

Projeto aplicado com logística: prazos, atrasos, custos e eficiência operacional

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.