(+351) 21 24 10006  ·  info@bconcepts.pt
Carnaxide, Lisboa

Como criar uma factless fact table num Data Warehouse

João Barros 12 de July de 2026 6 min de leitura

Uma factless fact table (tabela de factos sem factos) é uma tabela de factos que não tem medidas numéricas: guarda apenas as chaves das dimensões que participam num evento. Parece estranho, mas é a forma mais limpa de contar acontecimentos — aulas assistidas, candidaturas, cliques — e a única forma prática de responder à pergunta mais difícil de um Data Warehouse: o que não aconteceu. Segue o passo a passo para criar uma factless fact table num Data Warehouse, carregá-la e usá-la em consultas reais.

Pré-requisitos

  • Um Data Warehouse com esquema em estrela já iniciado (SQL Server, Azure SQL, Fabric Warehouse, Synapse ou outro motor SQL).
  • Pelo menos duas dimensões com surrogate keys (por exemplo DimStudent e DimCourse) e uma DimDate.
  • Uma tabela de staging com os eventos em bruto (stg_Attendance).
  • Permissões para criar tabelas e executar INSERT, e SQL básico (CREATE TABLE, JOIN, GROUP BY).

Passo 1: Perceber quando precisas de uma factless fact table

Há dois cenários clássicos, e vale a pena distingui-los antes de escrever código:

  • Eventos: algo aconteceu e não há nada para somar. Um aluno assistiu a uma aula. Não existe um "valor" da presença — a presença é o facto. A medida é simplesmente a contagem de linhas.
  • Cobertura: registas o que podia ter acontecido. Que alunos estão inscritos em que cursos, que produtos estão em promoção em que lojas. Sozinha diz pouco; cruzada com a tabela de eventos, revela as ausências.

Um erro comum é inventar uma coluna Quantity sempre igual a 1 só para "a tabela ter um facto". Não é preciso: COUNT(*) faz o mesmo trabalho e poupa espaço.

Passo 2: Definir o processo de negócio e o grão

O grão é a frase que descreve exatamente o que representa uma linha. Escreve-o antes do DDL — se não o consegues escrever numa frase, o modelo ainda não está pronto.

Grão: uma linha por aluno, por curso, por dia em que o aluno assistiu à aula.

Este grão diz-te logo três coisas: as dimensões envolvidas (aluno, curso, data), a chave primária da tabela e o que conta como duplicado.

Passo 3: Criar a tabela

A tabela só tem chaves estrangeiras. A chave primária é composta pelas três chaves — é isso que garante o grão e impede duplicados.

CREATE TABLE FactAttendance (
    StudentKey INT NOT NULL,
    CourseKey  INT NOT NULL,
    DateKey    INT NOT NULL,
    CONSTRAINT PK_FactAttendance
        PRIMARY KEY (StudentKey, CourseKey, DateKey),
    CONSTRAINT FK_FactAttendance_Student
        FOREIGN KEY (StudentKey) REFERENCES DimStudent(StudentKey),
    CONSTRAINT FK_FactAttendance_Course
        FOREIGN KEY (CourseKey) REFERENCES DimCourse(CourseKey),
    CONSTRAINT FK_FactAttendance_Date
        FOREIGN KEY (DateKey) REFERENCES DimDate(DateKey)
);

Repara: nenhuma coluna de medida. É normal, e é mesmo esse o objetivo.

Passo 4: Carregar os dados

A carga procura as surrogate keys nas dimensões e insere apenas o que ainda não existe, para poderes voltar a executá-la sem duplicar linhas (carga idempotente).

INSERT INTO FactAttendance (StudentKey, CourseKey, DateKey)
SELECT DISTINCT s.StudentKey, c.CourseKey, d.DateKey
FROM stg_Attendance a
JOIN DimStudent s ON s.StudentCode = a.StudentCode
JOIN DimCourse  c ON c.CourseCode  = a.CourseCode
JOIN DimDate    d ON d.FullDate    = CAST(a.AttendedAt AS date)
WHERE NOT EXISTS (
    SELECT 1
    FROM FactAttendance f
    WHERE f.StudentKey = s.StudentKey
      AND f.CourseKey  = c.CourseKey
      AND f.DateKey    = d.DateKey
);

Se um JOIN falhar (um aluno que ainda não existe em DimStudent), a linha é simplesmente descartada. Para não perderes eventos em silêncio, troca por LEFT JOIN e envia essas linhas para uma tabela de rejeitados, ou usa uma linha "Desconhecido" na dimensão.

Passo 5: Contar eventos

Sem medidas, a análise faz-se com contagens. É aqui que a tabela mostra o seu valor:

SELECT c.CourseName,
       d.MonthName,
       COUNT(*)                    AS Attendances,
       COUNT(DISTINCT f.StudentKey) AS DistinctStudents
FROM FactAttendance f
JOIN DimCourse c ON c.CourseKey = f.CourseKey
JOIN DimDate   d ON d.DateKey   = f.DateKey
WHERE d.CalendarYear = 2026
GROUP BY c.CourseName, d.MonthName
ORDER BY Attendances DESC;

Passo 6: Descobrir o que não aconteceu

Aqui entra a segunda factless: FactEnrollment, com o grão "uma linha por aluno inscrito num curso". Ao cruzar a cobertura (inscrições) com os eventos (presenças), aparecem os alunos que nunca assistiram a uma aula — uma pergunta impossível de responder só com a tabela de presenças, porque a ausência não gera linhas.

SELECT s.StudentName, c.CourseName
FROM FactEnrollment e
JOIN DimStudent s ON s.StudentKey = e.StudentKey
JOIN DimCourse  c ON c.CourseKey  = e.CourseKey
LEFT JOIN FactAttendance f
       ON f.StudentKey = e.StudentKey
      AND f.CourseKey  = e.CourseKey
WHERE f.StudentKey IS NULL
ORDER BY c.CourseName, s.StudentName;

Verificar o resultado

Três verificações rápidas antes de dares a tabela como pronta:

  1. Grão respeitado: o número de linhas tem de ser igual ao número de combinações distintas. Se criaste a PRIMARY KEY, o motor já o garante — uma segunda carga não consegue inserir duplicados.
  2. Sem chaves órfãs: SELECT COUNT(*) FROM FactAttendance f LEFT JOIN DimStudent s ON s.StudentKey = f.StudentKey WHERE s.StudentKey IS NULL; deve devolver 0.
  3. Totais certos: compara COUNT(*) na factless com o número de eventos distintos no staging. Se houver diferença, o erro está quase sempre num JOIN que falhou no Passo 4.

Corre a carga duas vezes seguidas: o total tem de ficar exatamente igual. Se aumentar, o problema está no NOT EXISTS ou no grão.

Conclusão

Com meia dúzia de colunas tens agora uma tabela que conta eventos de forma barata e que, com a tabela de cobertura, responde a perguntas de ausência que costumam ficar sem resposta. O próximo passo natural é ligar as duas ao Power BI e criar duas medidas em DAX — presenças e taxa de assiduidade — sobre este mesmo modelo. E no teu negócio, qual é o evento que hoje ninguém consegue contar só porque não tem nenhum valor associado?