Consultas SQL para mostrar apenas os registros de compras mais recentes de itens alimentares individuais

8

Estou trabalhando com um sistema de compra / fatura de alimentos no MS Access 2013 e estou tentando criar uma consulta SQL que retornará o preço de compra mais recente para cada item de comida individual.

Aqui está um diagrama das tabelas com as quais estou trabalhando: Tabelas no banco de dados MS Access

Meu entendimento do SQL é muito básico e tentei a seguinte consulta (incorreta), na esperança de que ele retornasse apenas um registro por item (por causa do DISTINCToperador) e que retornasse apenas a compra mais recente (desde que eu fiz isso) ORDER BY [Invoice Date] DESC)

SELECT DISTINCT ([Food items].Item), 
    [Food items].Item, [Food purchase data].[Price per unit], [Food purchase data].[Purchase unit], Invoices.[Invoice Date]
FROM Invoices
INNER JOIN ([Food items] 
    INNER JOIN [Food purchase data] 
    ON [Food items].ID = [Food purchase data].[Food item ID]) 
ON Invoices.ID = [Food purchase data].[Invoice ID]
ORDER BY Invoices.[Invoice Date] DESC;

No entanto, a consulta acima simplesmente retorna todas as compras de alimentos (ou seja, vários registros para cada registro [Food items]), com os resultados classificados por data. Alguém pode me explicar o que estou entendendo mal sobre o DISTINCToperador? Ou seja, por que não está retornando apenas um registro para cada item [Food items]?

E, mais importante: qual é a maneira mais simples de extrair os dados mais recentes de compra de alimentos para cada item alimentar, dada a estrutura da tabela mostrada acima ? Eu realmente não me importo tanto com eficiência quanto com simplicidade (o banco de dados com o qual estou trabalhando é bastante pequeno - levará anos até que esteja na faixa de dezenas de milhares de registros). Preocupo-me mais com o fato de a consulta ser compreensível para alguém com pouco conhecimento de SQL.

UPDATE: Tentei, ambas as respostas sugeridas abaixo, e nenhuma delas funciona (elas apenas lançam erros de sintaxe).

Com base nas sugestões abaixo, e lendo mais on-line, escrevi a nova consulta a seguir, usando a função agregada max()e uma GROUP BYcláusula:

SELECT [Food purchase data].[Food item ID], [Food purchase data].[Price per unit], max(Invoices.[Invoice Date]) AS MostRecentInvoiceDate
FROM [Food purchase data], Invoices
GROUP BY [Food purchase data].[Food item ID], [Food purchase data].[Price per unit];

Mas ainda estou tendo o mesmo problema: ou seja, continuo vendo mais de um resultado para cada item alimentar. Alguém pode explicar por que essa consulta não está retornando apenas a compra mais recente para cada item de alimento?

ATUALIZAÇÃO 2 (RESOLVIDO!) :

Nenhuma das respostas abaixo deu muito certo, mas com base em algumas modificações pesadas da resposta de Vladimir abaixo , consegui criar as seguintes consultas, que parecem estar fornecendo os resultados corretos.

Primeiro, criei essa exibição e o nomeiei como "LatestInvoices":

SELECT InvoicesMaxDate.ItemID, InvoicesMaxDate.MaxDate, InvoicesMaxDate.MaxID
FROM [Food purchase data], Invoices, (SELECT [Food purchase data].[Food item ID] AS ItemID, MAX(Invoices.[Invoice Date]) AS MaxDate, MAX(Invoices.[Invoice ID]) AS MaxID
                FROM [Food purchase data], Invoices
                WHERE Invoices.[Invoice ID] = [Food purchase data].[Invoice ID]
                GROUP BY [Food purchase data].[Food item ID]
         )  AS InvoicesMaxDate
WHERE InvoicesMaxDate.MaxID = [Food purchase data].[Invoice ID] AND
                      InvoicesMaxDate.ItemID = [Food purchase data].[Food item ID] AND 
                      InvoicesMaxDate.MaxDate = Invoices.[Invoice Date]
GROUP BY InvoicesMaxDate.ItemID, InvoicesMaxDate.MaxDate,  InvoicesMaxDate.MaxID

Em seguida, escrevi outra consulta para obter os campos necessários:

SELECT [Food items].ID AS FoodItemID, [Food items].Item AS FoodItem, [Food purchase data].[Price], [Food purchase data].[Price per unit], [Food purchase data].[Purchase unit], LatestInvoices.MaxDate as InvoiceDate
FROM [Food items], [Food purchase data], LatestInvoices
WHERE LatestInvoices.[MaxID] = [Food purchase data].[Invoice ID] AND
             LatestInvoices.ItemID = [Food purchase data].[Food item ID] AND
             LatestInvoices.ItemID = [Food items].ID
ORDER BY [Food items].Item;

Obrigado a todos vocês que dedicaram um tempo para me ajudar com isso!

J. Taylor
fonte
2
DISTINCTretorna linhas que são distintas em todas as colunas da linha, não em colunas únicas.
Max Vernon
2
Apenas um conselho, evite usar espaços nos nomes de tabelas e colunas. Então você não precisará cercar tudo com [e]
Max Vernon
11
E é (sem dúvida) melhor incluir o nome da tabela em todas as IDcolunas, para que IDa Invoicestabela se torne InvoiceID.
Max Vernon
Oh, isso faz sentido - eu pensei que DISTINCTera por colunas únicas. Existe um operador análogo que selecionará apenas com base na exclusividade em uma única coluna? Além disso, obrigado pelas dicas sobre convenções de nomenclatura - sim, é muito chato ter que usar em [ ... ]qualquer lugar ... E posso ver como a inclusão do nome da tabela na coluna ID aumentaria a legibilidade.
J. Taylor

Respostas:

7

O MS Access é bastante limitado.

Presumo que seja possível ter mais de uma fatura para a mesma data. Nesse caso, selecionarei uma fatura com o ID mais alto.

Inicialmente, encontraremos a Data máxima da fatura para cada item alimentar.

SELECT
    FPD1.[Food item ID] AS ItemID
    ,MAX(I1.[Invoice Date]) AS MaxDate
FROM
    [Food purchase data] AS FPD1
    INNER JOIN Invoices AS I1 ON I1.ID = FPD1.[Invoice ID]
GROUP BY
    FPD1.[Food item ID]

Como é possível que haja várias faturas para a data máxima encontrada, escolheremos uma fatura com o ID máximo por item

Com base na sintaxe do MS Access de junções aninhadas e usando este exemplo dos documentos:

SELECT fields 
FROM 
  table1 INNER JOIN 
  (
      table2 INNER JOIN 
      (
          table3 INNER JOIN tablex ON table3.field3 = tablex.fieldx
      ) ON table2.field2 = table3.field3
  ) ON table1.field1 = table2.field2
;

Vamos tentar juntar tudo:

SELECT
    InvoicesMaxDate.ItemID
    ,InvoicesMaxDate.MaxDate
    ,MAX(I2.ID) AS MaxInvoiceID
FROM
    (
        SELECT
            FPD1.[Food item ID] AS ItemID
            ,MAX(I1.[Invoice Date]) AS MaxDate
        FROM
            [Food purchase data] AS FPD1
            INNER JOIN Invoices AS I1 ON I1.ID = FPD1.[Invoice ID]
        GROUP BY
            FPD1.[Food item ID]
    ) AS InvoicesMaxDate INNER JOIN
    (
        [Food purchase data] AS FPD2 
        INNER JOIN Invoices AS I2 ON I2.ID = FPD2.[Invoice ID]
    ) ON
        InvoicesMaxDate.ItemID = FPD2.[Food item ID] AND
        --- you may need to put extra "ON" here as well, not sure
        InvoicesMaxDate.MaxDate = I2.[Invoice Date]
GROUP BY
    InvoicesMaxDate.ItemID
    ,InvoicesMaxDate.MaxDate

Agora, temos o ItemID e o ID da última fatura para esse item. Junte-se a tabelas originais para buscar outros detalhes (colunas).

SELECT
    FI3.Item
    ,FI3.Item
    ,FPD3.[Price per unit]
    ,FPD3.[Purchase unit]
    ,I3.[Invoice Date]
FROM
    (
        SELECT
            InvoicesMaxDate.ItemID
            ,InvoicesMaxDate.MaxDate
            ,MAX(I2.ID) AS MaxInvoiceID
        FROM
            (
                SELECT
                    FPD1.[Food item ID] AS ItemID
                    ,MAX(I1.[Invoice Date]) AS MaxDate
                FROM
                    [Food purchase data] AS FPD1
                    INNER JOIN Invoices AS I1 ON I1.ID = FPD1.[Invoice ID]
                GROUP BY
                    FPD1.[Food item ID]
            ) AS InvoicesMaxDate INNER JOIN
            (
                [Food purchase data] AS FPD2 
                INNER JOIN Invoices AS I2 ON I2.ID = FPD2.[Invoice ID]
            ) ON
                InvoicesMaxDate.ItemID = FPD2.[Food item ID] AND
                InvoicesMaxDate.MaxDate = I2.[Invoice Date]
        GROUP BY
            InvoicesMaxDate.ItemID
            ,InvoicesMaxDate.MaxDate
    ) AS LastInvoices INNER JOIN
    (
        [Food items] AS FI3 INNER JOIN
        (
            [Food purchase data] AS FPD3
            INNER JOIN Invoices AS I3 ON I3.ID = FPD3.[Invoice ID]
        ) ON FI3.ID = FDP3.[Food item ID]
    ) ON
        LastInvoices.MaxInvoiceID = I3.ID AND
        LastInvoices.ItemID = FI3.ID

Na prática, eu criaria uma visualização para a primeira consulta com uma única junção. Depois, criava uma segunda visualização que une a primeira visualização às tabelas, depois a terceira e assim por diante, para evitar as junções aninhadas ou minimizá-las. A consulta geral seria mais fácil de ler.


Edite para esclarecer o que quero dizer com base na sua solução final que você colocou na pergunta.

Uma última tentativa de transmitir minha mensagem.

Isto é o que você escreveu com base nas minhas sugestões acima:

SELECT
    InvoicesMaxDate.ItemID
    ,InvoicesMaxDate.MaxDate
    ,Invoices.[Invoice ID]
FROM [Food purchase data], Invoices, 
    (
        SELECT 
            [Food purchase data].[Food item ID] AS ItemID
            ,MAX(Invoices.[Invoice Date]) AS MaxDate
        FROM [Food purchase data], Invoices
        WHERE Invoices.[Invoice ID] = [Food purchase data].[Invoice ID]
        GROUP BY [Food purchase data].[Food item ID]
    )  AS InvoicesMaxDate
WHERE
    Invoices.[Invoice ID] = [Food purchase data].[Invoice ID] AND
    InvoicesMaxDate.ItemID = [Food purchase data].[Food item ID] AND 
    InvoicesMaxDate.MaxDate = Invoices.[Invoice Date]
GROUP BY InvoicesMaxDate.ItemID, InvoicesMaxDate.MaxDate, Invoices.[Invoice ID];

Isto é o que eu quis dizer:

SELECT
    InvoicesMaxDate.ItemID
    ,InvoicesMaxDate.MaxDate
    ,MAX(Invoices.[Invoice ID]) AS [Invoice ID]
FROM [Food purchase data], Invoices, 
    (
        SELECT
            [Food purchase data].[Food item ID] AS ItemID
            ,MAX(Invoices.[Invoice Date]) AS MaxDate
        FROM [Food purchase data], Invoices
        WHERE Invoices.[Invoice ID] = [Food purchase data].[Invoice ID]
        GROUP BY [Food purchase data].[Food item ID]
    )  AS InvoicesMaxDate
WHERE
    Invoices.[Invoice ID] = [Food purchase data].[Invoice ID] AND
    InvoicesMaxDate.ItemID = [Food purchase data].[Food item ID] AND 
    InvoicesMaxDate.MaxDate = Invoices.[Invoice Date]
GROUP BY InvoicesMaxDate.ItemID, InvoicesMaxDate.MaxDate;

Você vê a diferença?

O InvoicesMaxDateretorno MAX Invoice Datepara cada um Food item ID. Se houver duas faturas para a mesma Food item IDcom o mesmo MAX Invoice Date, devemos escolher uma fatura entre elas. Isso é feito agrupando por InvoicesMaxDate.ItemID, InvoicesMaxDate.MaxDate. Não deve haver nenhum agrupamento por Invoices.[Invoice ID]aqui, porque queremos pegar a factura com o ID máximo.

Depois de salvar esta consulta como uma LatestInvoicesvisualização, ela será usada ainda mais conforme você escreveu corretamente (observe que a consulta final usa LatestInvoices.[Invoice ID]e LatestInvoices.ItemID, mas não usa LatestInvoices.MaxDate):

SELECT 
    [Food items].ID as FoodItemID
    ,[Food items].Item as FoodItem
    ,[Food purchase data].[Price]
    ,[Food purchase data].[Price per unit]
    ,[Food purchase data].[Purchase unit]
    ,Invoices.[Invoice Date]
FROM [Food items], [Food purchase data], Invoices, LatestInvoices
WHERE 
    Invoices.[Invoice ID] = [Food purchase data].[Invoice ID] AND
    [Food items].ID = [Food purchase data].[Food item ID] AND
    LatestInvoices.[Invoice ID] = Invoices.[Invoice ID] AND 
    LatestInvoices.ItemID = [Food items].ID
ORDER BY [Food items].Item

Quanto a, por que sua última consulta na pergunta retorna várias linhas por item:

SELECT 
    [Food purchase data].[Food item ID]
    , [Food purchase data].[Price per unit]
    , max(Invoices.[Invoice Date]) AS MostRecentInvoiceDate
FROM [Food purchase data], Invoices
GROUP BY [Food purchase data].[Food item ID], [Food purchase data].[Price per unit];

Você está agrupando aqui por [Food item ID]e [Price per unit], para obter tantas linhas quanto houver combinações únicas dessas duas colunas.

A consulta a seguir retornaria uma linha por [Food item ID].

SELECT 
    [Food purchase data].[Food item ID]
    , max(Invoices.[Invoice Date]) AS MostRecentInvoiceDate
FROM [Food purchase data], Invoices
GROUP BY [Food purchase data].[Food item ID];

Uma nota lateral, você realmente deve usar explícito em INNER JOINvez de ,. Essa sintaxe tem 20 anos.

SELECT 
    [Food purchase data].[Food item ID]
    , max(Invoices.[Invoice Date]) AS MostRecentInvoiceDate
FROM
    [Food purchase data]
    INNER JOIN Invoices ON Invoices.ID = [Food purchase data].[Invoice ID]
GROUP BY [Food purchase data].[Food item ID];
Vladimir Baranov
fonte
Obrigado pela sua resposta muito detalhada! A primeira consulta que você compartilhou funcionou e, de fato, obteve a data da fatura mais recente para cada item de alimento individual, o que é realmente útil. No entanto, quando tentei as próximas 2 consultas que você compartilhou, recebi "Syntax error (missing operator) in query expression"a expressão INNER JOIN Invoices AS I2 ON I2.ID = FPD2.[Invoice ID]... Vou brincar mais com ela para ver se consigo fazê-la funcionar.
J. Taylor
@JesseTaylor, aparentemente, é necessário colocar colchetes explicitamente (e )quando a consulta usa várias junções e mover a ONcláusula um pouco. Não tenho acesso para verificar, mas posso tentar adivinhar a sintaxe correta lendo os documentos ainda hoje.
Vladimir Baranov
@JesseTaylor, atualizei a resposta e espero adivinhar a sintaxe corretamente. Por favor, tente e deixe-me saber se funciona.
Vladimir Baranov
11
@JesseTaylor, de nada. Já faz um tempo desde que usei o Access e é difícil obter sua sintaxe correta. Uma observação sobre sua visão LatestInvoices: a final GROUPdeve ser BY InvoicesMaxDate.ItemID, InvoicesMaxDate.MaxDateapenas, sem Invoices.[Invoice ID]. Na SELECTparte deve haver MAX(Invoices.[Invoice ID]) AS [Invoice ID]. Este é o ponto inteiro. Inicialmente (na consulta interna), GROUP BY [Food item ID]encontramos a Data máxima da fatura. Pode haver várias faturas com essa data; portanto, existe um segundo GROUP BYpara escolher a fatura com o ID máximo entre elas.
Vladimir Baranov
11
@JesseTaylor, infelizmente, você me entendeu mal. Atualizei minha resposta para mostrar o que eu quis dizer. Para ver a diferença, adicione duas faturas aos dados (amostra) dos mesmos ItemIDcom a mesma data grande e tente as duas consultas.
Vladimir Baranov
3

Uma consulta que simplesmente funciona imediatamente:

SELECT Fi.Item, Fpd.[Price per unit], Fpd.[Purchase unit]
FROM [Food items] Fi INNER JOIN [Food purchase data] Fpd
ON Fpd.[Food item ID] = Fi.ID
WHERE Fpd.[Invoice ID] = (
  SELECT TOP 1 I.ID 
  FROM Invoices I INNER JOIN [Food purchase data] Fpd2
  ON Fpd2.[Invoice ID] = I.ID
  WHERE Fpd2.[Food item ID] = Fpd.[Food item ID]
  ORDER BY I.[Invoice Date] DESC
)
aksenoff
fonte
Quando executo essa consulta, recebo um erro: "No máximo um registro pode ser retornado por essa subconsulta". A exibição da folha de dados mostra apenas um registro com "#NAME?" em todos os campos.
J. Taylor
3

Eu poderia resolvê-lo com a seguinte consulta:

Select MAX(AllItemBuyings.[invoice date]) as RecentBuyingDate, AllItemBuyings.[Food Item Id]  From 
(    
    select fpd.[Invoice Id], fpd.[Food Item Id], I.[invoice date] From [Food purchase data]as fpd 
    inner join invoices I on fpd.[Invoice Id] = I.ID

) as AllItemBuyings    
Group By AllItemBuyings.[Food Item Id]

Como não tenho acesso, testei isso no SQL Server. Espero que isso funcione para você.

Editar / consulta adicional : para adicionar as outras colunas da tabela de itens alimentares, alterei a consulta. Fiz de uma maneira que realmente não gosto. Se tudo estiver bem para você, depende de seus dados e requisitos. Entrei na tabela FATURAS novamente usando a Data do pedido. Caso esta seja uma data que inclui o horário em que meu trabalho termina, esteja ciente disso. Não vejo outra maneira no seu cenário. Talvez exista uma solução melhor usando consulta recursiva ...?

Faça uma tentativa e deixe-me saber se funciona:

Select Recents.RecentBuyingDate, pd.* From 
(

   Select MAX(AllItemBuyings.[invoice date]) as RecentBuyingDate, AllItemBuyings.[Food Item Id]    From 
    (    
        select fpd.[Invoice Id], fpd.[Food Item Id], I.[invoice date], fpd.ID From [Food purchase data]as fpd 
        inner join invoices I on fpd.[Invoice Id] = I.ID

    ) as AllItemBuyings    
    Group By AllItemBuyings.[Food Item Id]

    ) as Recents    
    Join Invoices i on i.[invoice date] = Recents.RecentBuyingDate
    Join [Food purchase data] pd ON pd.[Invoice Id] = i.ID AND pd.[Food Item Id] = Recents.[Food Item Id]
Magier
fonte
Obrigado. Isso indica corretamente a data de compra mais recente para cada item. Como eu poderia usar isso, porém, a tração em todos os campos que eu mencionado na pergunta (por exemplo Item, Price per unit, etc)?
J. Taylor
A nova consulta que você sugeriu gera uma mensagem de erro que diz nada além de "Erro de sintaxe na cláusula FROM".
J. Taylor
Talvez o Access exija que a operação JOIN seja exatamente "INNER JOIN". Tente INNSER JOIN em vez de apenas JOIN.
Magier
2

Eu acredito que o abaixo deve funcionar.

SELECT fi.[Item], fd.[Price per unit], MAX(i.[Invoice Date])
FROM [Invoices] AS i
INNER JOIN [Food Purchase Data] AS fd
    ON i.ID = fd.[Invoice ID]
INNER JOIN [Food items] AS fi
    ON fd.[Food item ID] = fi.ID
GROUP BY fi.Item, fd.[Price per unit]
ORDER BY i.[Invoice Date] DESC

Quanto ao motivo pelo qual sua consulta não está retornando os resultados que você deseja:

SELECT [Food purchase data].[Food item ID], [Food purchase data].[Price per unit], max(Invoices.[Invoice Date]) AS MostRecentInvoiceDate
FROM [Food purchase data], Invoices
GROUP BY [Food purchase data].[Food item ID], [Food purchase data].[Price per unit];

O maior problema que vejo é que você realmente não está fazendo nada para se juntar às suas mesas. A "junção" implícita que está presente simplesmente listando as duas na cláusula FROM está fornecendo um produto cartesiano. Basicamente, ele retornará todas as combinações possíveis no seu banco de dados para os campos que você está consultando.

Por exemplo, se as duas tabelas tivessem três registros cada, em vez de retornar a data mais recente, sua consulta retornaria algo como: 1,1 1,2 1,3 2,1 2,2 2,3 3,1 3,2 3 3

É muito importante que você declare explicitamente suas associações. Duas maneiras de fazer isso na sua consulta seriam:

FROM [Food purchase data] AS fd, [Invoices] AS i
WHERE fd.[Invoice ID] = i.[ID]

OU

FROM [Food purchase data] AS fd
INNER JOIN [Invoices] AS i
    ON fd.[Invoice ID] = i.[ID]

Consultas atualizadas, se elas ainda não funcionarem, tente remover os aliases e usar os nomes de colunas totalmente qualificados.

agpoweredmg
fonte
Vamos continuar esta discussão no chat .
agpoweredmg
0

Concordo com as sugestões de Max sobre o seu modelo de dados. Implementá-las tornará seu SQL mais legível a longo prazo.

Com isso dito, DISTINCT exibirá linhas exclusivas. Portanto, para mostrar apenas os mais recentes, você deve limitar as colunas exibidas.

Tente algo como:

SELECT [Food purchase data].[Food item ID], max(Invoices.[Invoice Date]) AS MostRecentInvoiceDate
FROM Invoices 
INNER JOIN ([Food items] ON [Food items].ID = [Food purchase data].[Food item ID]) 
GROUP BY [Food purchase data].[Food item ID]

(Tradução: para cada item da loja, exiba a data da fatura mais recente.)

Você pode salvar isso como uma exibição e usá-lo em outra consulta, como faria em uma tabela. Assim, você pode fazer uma junção interna na fatura para o preço de compra e ingressar nas outras tabelas, se precisar desses detalhes.

(Teoricamente, você também pode fazer uma consulta aninhada, mas como solicitou simples, uma consulta salva é mais simples.)

ATUALIZAÇÃO com base na sua atualização:

Vou usar cláusulas WHERE em vez de JOINS porque não tenho o MS Access à mão. Você poderá usar a GUI para fazer as conexões entre as tabelas no MS Access com base nessas informações. (Forneça um SQLFiddle se você realmente precisar de ajuda com outras soluções de problemas.)

Etapa 1: Salve como uma VIEW (por exemplo, "MostRecentInvoice")

SELECT [Food purchase data].[Food item ID] AS FoodItemID, max(Invoices.[Invoice Date]) AS MostRecentInvoiceDate
FROM [Food purchase data], Invoices
WHERE [Food purchase data].[Food item ID] = Invoices.ID
GROUP BY [Food purchase data].[Food item ID];

Etapa 2: use a visualização em uma segunda consulta

SELECT (list all the fields you need here)
FROM MostRecentInvoice, Invoices, etc...
WHERE MostRecentInvoice.FoodItemID = [Food purchase data].[Food item ID] 
AND MostRecentInvoice.MostRecentInvoiceDate = Invoices.[Invoice Date]
AND (whatever else joins you'll need for the other tables)

... e para responder à sua pergunta: A segunda consulta na atualização não funciona porque a coluna [Preço por unidade] está nas suas instruções SELECT e GROUP BY. Isso significa essencialmente que você está pedindo para ver TODOS os valores possíveis de [Preço por unidade], mesmo que o que você realmente deseja seja apenas um: o valor mais recente.

chabzjo
fonte
Obrigado, mas quando tento executar a consulta que você compartilhou, recebo um erro: "Erro de sintaxe na operação JOIN".
J. Taylor
Desculpe, não tive tempo de criar as tabelas no Access. Supus que você teve algumas experiências com junções, pois havia algumas em sua pergunta. Você tentou criá-lo fazendo Criar -> Consulta no Access?
chabzjo
A primeira consulta que você compartilhou não fornece resultados corretos devido à linha WHERE [Food purchase data].[Food item ID] = Invoices.ID... Suponho que você quis dizer, WHERE [Food purchase data].[Invoice ID] = Invoices.[Invoice ID]mas que ainda retorna várias datas por item de comida, em vez das mais recentes.
J. Taylor