Como criar uma factless fact table num Data Warehouse
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
DimStudenteDimCourse) e umaDimDate. - 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:
- 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.
- 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. - 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?