Consulta de junção SQL para mostrar linhas com linhas inexistentes em uma tabela

12

Estou tentando obter alguns relatórios para registros de horas dos funcionários.

Temos duas tabelas especificamente para esta pergunta. Os funcionários são listados na Memberstabela e todos os dias eles inserem entradas de horas do trabalho que executaram e são armazenados na Time_Entrytabela.

Exemplo de configuração com o SQL Fiddle: http://sqlfiddle.com/#!3/e3806/7

O resultado final que vou buscar é uma tabela que mostra TODAS as Memberslistas de uma coluna e, em seguida, mostrará suas horas de soma para a data consultada nas outras colunas.

O problema parece ser que, se não houver linha na Time_Entrytabela para um membro em particular, agora haverá linha para esse membro. Eu tentei vários tipos de junção diferentes (esquerda, direita, interna, externa, externa completa, etc.), mas nenhum parece me dar o que eu quero, o que seria (com base no último exemplo no SQL Fiddle):

/*** Desired End Result ***/

Member_ID   | COUNTTime_Entry | TIMEENTRYDATE | SUMHOURS_ACTUAL | SUMHOURS_BILL
ADavis      | 0               | 11-10-2013    | 0               | 0
BTronton    | 0               | 11-10-2013    | 0               | 0
CJones      | 0               | 11-10-2013    | 0               | 0
DSmith      | 0               | 11-10-2013    | 0               | 0
EGirsch     | 1               | 11-10-2013    | 0.92            | 1
FRowden     | 0               | 11-10-2013    | 0               | 0

O que estou recebendo no momento em que consulta uma data específica de 11-1:

Member_ID   | COUNTTime_Entry | TIMEENTRYDATE | SUMHOURS_ACTUAL | SUMHOURS_BILL
EGirsch     | 1               | 11-10-2013    | 0.92            | 1

O que é correto com base na linha de entrada de tempo datada de 11/11/2013 para EGirsch, mas preciso ver zeros para os outros membros para obter relatórios e, eventualmente, um painel / relatório da Web para essas informações.

Esta é minha primeira pergunta e, enquanto eu procurava por Ingressar em consultas, etc. Sinceramente, não tenho certeza de como essa função pode ser chamada. Espero que isso não seja uma duplicata e ajude outras pessoas a tentar encontrar uma solução para problemas semelhantes.

despedida
fonte

Respostas:

11

Obrigado por SQLfiddle e dados de amostra! Gostaria que mais perguntas começassem dessa maneira.

Se você quiser todos os membros, independentemente de terem uma entrada para essa data, deseja a LEFT OUTER JOIN. Você estava muito próximo dessa versão, no entanto, um pequeno truque com junções externas é que, se você adicionar um filtro à tabela externa na WHEREcláusula, transformará uma junção externa em uma junção interna, porque excluirá todas as linhas NULLdesse lado (porque não sabe se NULLcorresponderia ao filtro ou não).

Modifiquei a primeira consulta para obter uma linha para cada membro:

SELECT Members.Member_ID
      ,Time_Entry.Date_Start
      ,Time_Entry.Hours_Actual
      ,Time_Entry.Hours_Bill
FROM dbo.Members
  LEFT OUTER JOIN dbo.Time_Entry
--^^^^ changed from FULL to LEFT
  ON Members.Member_ID = Time_Entry.Member_ID
  AND Time_Entry.Date_Start = '20131110';
--^^^ changed from WHERE to AND

Vou deixar como um exercício para o leitor tirá-lo de lá e adicionar as outras colunas, formatação COALESCEetc.

Algumas outras notas:

Aaron Bertrand
fonte
Aaron, muito obrigado pelo feedback. Novato SQL aqui, e não tinha idéia da diferença entre WHEREe AND. Eu tinha usado aliases originalmente, mas o sqlfiddle não parecia gostar, então apenas fui para o formato completo. Obrigado pelas outras dicas SQL também. Você recomendaria ISNULLou COALESCEtornaria os dados 0 em vez de NULL? Obrigado novamente!
farewelldave
11
@farewelldave Prefiro o COALESCE porque é padrão e não se desvia de sua funcionalidade em outros idiomas (compare como o ISNULL funciona no SQL Server vs. VB, por exemplo). Em quase todos os casos, a diferença de desempenho é inconseqüente, exceto uma. Muito mais detalhes aqui .
Aaron Bertrand
4

Quando me deparei com esse tipo de problema no passado, criei uma tabela de "números" para ajudar a lidar com as linhas ausentes.

Criei minha tabela de números especificamente para lidar com datas da seguinte forma:

CREATE TABLE Dates
(
    dDate DATETIME NOT NULL CONSTRAINT PK_Dates PRIMARY KEY CLUSTERED
);

INSERT INTO Dates (dDate)
SELECT TOP(73049) DATEADD(d, -1, ROW_NUMBER() OVER (ORDER BY o.object_id)) AS dDate
FROM master.sys.objects o, master.sys.objects o1, master.sys.objects o2

Isso cria uma tabela com uma única linha para cada data entre 1900-01-01 e 2099-12-31. Utilizo TOP(73049)para limitar o período gerado no meu exemplo a essas datas. Se você trabalha com um período diferente, pode ajustar esse número.

Em seguida, adiciono a dDatestabela à minha consulta para que uma linha seja retornada para cada data no intervalo desejado para cada member_id. O resultado é então associado à Time_Entrytabela como tal:

SELECT MD.Member_ID,
    MD.dDate,
    T.Date_Start,
    T.Hours_Actual,
    T.Hours_Bill
FROM 
    (
        SELECT M.Member_ID, D.dDate
        FROM dbo.Dates D, dbo.Members M
        WHERE D.dDate >= '20131110' AND D.dDate < '20131112'
    ) AS MD
    LEFT JOIN dbo.Time_Entry T ON MD.Member_ID = T.Member_ID AND MD.dDate = T.Date_Start
ORDER BY MD.Member_ID, MD.dDate

Isso permite especificar um período para o relatório.

Você pode refinar ainda mais os resultados adicionando COALESCE(...)e SUM(...)conforme:

SELECT MD.Member_ID,
    MD.dDate,
    T.Date_Start,
    SUM(COALESCE(T.Hours_Actual, 0)) AS TotalHoursActual,
    SUM(COALESCE(T.Hours_Bill, 0)) AS TotalHoursBill
FROM 
    (
        SELECT M.Member_ID, D.dDate
        FROM dbo.Dates D, dbo.Members M
        WHERE D.dDate >= '20131110' AND D.dDate < '20131112'
    ) AS MD
    LEFT JOIN dbo.Time_Entry T ON MD.Member_ID = T.Member_ID AND MD.dDate = T.Date_Start
GROUP BY MD.Member_ID, MD.dDate, T.Date_Start
ORDER BY MD.Member_ID, MD.dDate

Isso resulta na seguinte saída para seus dados de amostra:

insira a descrição da imagem aqui

Max Vernon
fonte
Obrigado, Max. Você pode encontrar muitas informações sobre essa técnica pesquisando "tabela de registro" em vez de "tabela de números". Eles são ótimos para melhorar o desempenho convertendo operações usando cursores / loops em operações usando conjuntos. Bancos de dados relacionais preferem conjuntos.
Suncat2000
11
@ Suncat2000 - concordou, embora eu prefira o nome "tabela de números", pois a contagem implica adição e, na minha experiência, esse padrão raramente é usado para operações matemáticas. Eles são ótimos para muitas coisas, mas certamente uma das maiores melhorias de desempenho que você pode obter está indo de uma abordagem RBAR, para uma abordagem baseada em conjuntos, usando uma tabela de números.
Max Vernon