Domanda

Una volta mi è stato assegnato questo compito da svolgere in un RDBMS:

Dati tabelle cliente, ordine, righe ordine e prodotto. Tutto fatto con i soliti campi e relazioni, con un campo memo commento sulla tabella della linea d'ordine.

Per un cliente recuperare un elenco di tutti i prodotti che il cliente ha mai ordinato con nome del prodotto, anno del primo acquisto, date degli ultimi tre acquisti, commento dell'ultimo ordine, somma delle entrate totali per quella combinazione prodotto-cliente ultime 12 mesi.

Dopo un paio di giorni ho rinunciato a farlo come una query e ho optato per recuperare tutte le righe dell'ordine per un cliente e ogni prodotto ed eseguire i dati proceduralmente per costruire il lato client della tabella richiesto.

Lo considero un sintomo di uno o più dei seguenti:

  • Sono un idiota pigro e avrei dovuto vedere come farlo in SQL
  • Le operazioni impostate non sono espressive come le operazioni procedurali
  • SQL non è espressivo come dovrebbe essere

Ho fatto la cosa giusta? Avevo altre opzioni?

È stato utile?

Soluzione

Dovresti sicuramente essere in grado di fare questo esercizio senza fare il lavoro equivalente a un JOIN nel codice dell'applicazione, cioè recuperando tutte le righe da entrambe le linee d'ordine e i prodotti e iterando attraverso di esse. Non devi essere un mago SQL per farlo. JOIN è per SQL ciò che un loop è per un linguaggio procedurale - in quanto entrambe sono caratteristiche linguistiche fondamentali che dovresti sapere come usare.

Una trappola in cui cadono le persone sta pensando che l'intero report debba essere prodotto in una singola query SQL. Non vero! La maggior parte dei rapporti non si inserisce in un rettangolo, come sottolinea Tony Andrews. Esistono molti rollup, riepiloghi, casi speciali, ecc. Pertanto è più semplice ed efficiente recuperare parti del report in query separate. Allo stesso modo, in un linguaggio procedurale non proveresti a fare tutto il tuo calcolo in una singola riga di codice, o anche in una singola funzione (si spera).

Alcuni strumenti di reporting insistono sul fatto che un report viene generato da una singola query e che non hai l'opportunità di unire più query. Se è così, allora devi produrre più rapporti (e se il capo lo vuole su una pagina, allora devi fare un po 'di incollaggio manualmente).

Per ottenere un elenco di tutti i prodotti ordinati (con il nome del prodotto), le date degli ultimi tre acquisti e i commenti sull'ultimo ordine è semplice:

SELECT o.*, l.*, p.*
FROM Orders o
 JOIN OrderLines l USING (order_id)
 JOIN Products p USING (product_id)
WHERE o.customer_id = ?
ORDER BY o.order_date;

Va ??bene scorrere i risultati riga per riga per estrarre le date e i commenti sugli ultimi ordini, poiché stai comunque recuperando quelle righe. Ma renditi facile chiedendo al database di restituire i risultati ordinati per data.

Anno del primo acquisto è disponibile dalla query precedente, se si ordina per order_date e si recupera il risultato riga per riga, si avrà accesso a il primo ordine. Altrimenti, puoi farlo in questo modo:

SELECT YEAR(MIN(o.order_date)) FROM Orders o WHERE o.customer_id = ?;

La somma degli acquisti di prodotti negli ultimi 12 mesi è calcolata al meglio da una query separata:

SELECT SUM(l.quantity * p.price)
FROM Orders o
 JOIN OrderLines l USING (order_id)
 JOIN Products p USING (product_id)
WHERE o.customer_id = ?
 AND o.order_date > CURDATE() - INTERVAL 1 YEAR;

modifica: in un altro commento hai detto che ti piacerebbe vedere come ottenere le date degli ultimi tre acquisti in SQL standard:

SELECT o1.order_date
FROM Orders o1
  LEFT OUTER JOIN Orders o2 
  ON (o1.customer_id = o2.customer_id AND (o1.order_date < o2.order_date 
      OR (o1.order_date = o2.order_date AND o1.order_id < o2.order_id)))
WHERE o1.customer_id = ?
GROUP BY o1.order_id
HAVING COUNT(*) <= 3;

Se puoi usare un po 'di funzionalità SQL specifiche del fornitore, puoi usare Microsoft / Sybase TOP n o MySQL / PostgreSQL LIMIT :

SELECT TOP 3 order_date
FROM Orders
WHERE customer_id = ?
ORDER BY order_date DESC;

SELECT order_date
FROM Orders
WHERE customer_id = ?
ORDER BY order_date DESC
LIMIT 3;

Altri suggerimenti

  

Le operazioni impostate non sono espressive come le operazioni procedurali

Forse più come: " Le operazioni di set non sono così familiari come le operazioni procedurali per uno sviluppatore abituato ai linguaggi procedurali " ; -)

Farlo iterativamente come hai fatto ora va bene per piccoli insiemi di dati, ma semplicemente non si ridimensiona allo stesso modo. La risposta al fatto che tu abbia fatto la cosa giusta dipende se sei soddisfatto delle prestazioni in questo momento e / o non prevedi che la quantità di dati aumenterà molto.

Se potessi fornire del codice di esempio, potremmo essere in grado di aiutarti a trovare una soluzione basata su set, che sarà più veloce per iniziare e scalare molto, molto meglio. Come accennato da GalacticCowboy, tecniche come le tabelle temporanee possono aiutare a rendere le dichiarazioni molto più leggibili, pur mantenendo in gran parte i vantaggi in termini di prestazioni.

Nella maggior parte dei RDBMS hai la possibilità di utilizzare tabelle temporanee o variabili di tabella locali che puoi utilizzare per suddividere un'attività come questa in blocchi gestibili.

Non vedo alcun modo per farlo facilmente come una singola query (senza alcune sottoquery cattive ), ma dovrebbe comunque essere fattibile senza abbandonare la procedura codice, se usi le tabelle temporanee.

Questo problema potrebbe non essere stato risolvibile da una query. Vedo diverse parti distinte ...

Per un cliente

  1. Ottieni un elenco di tutti i prodotti ordinati (con il nome del prodotto)
  2. Ottieni l'anno del primo acquisto
  3. Ottieni le date degli ultimi tre acquisti
  4. Ottieni commenti sull'ultimo ordine
  5. Ottieni la somma degli acquisti di prodotti negli ultimi 12 mesi

La procedura prevede i passaggi da 1 a 5 e SQL fornisce i dati.

Mi sembra un progetto di data warehouse. Se hai bisogno di cose come " tre cose più recenti " e "somma di qualcosa negli ultimi 12 mesi" quindi memorizzarli, ovvero denormalizzare.

EDIT: questa è una versione completamente nuova della soluzione, che non utilizza tabelle temporanee o strane query sub-sub-sub. Tuttavia, funzionerà SOLO su SQL 2005 o versioni successive, in quanto utilizza il "pivot" comando che è nuovo in quella versione.

Il problema fondamentale è il pivot desiderato da una serie di righe (nei dati) in colonne nell'output. Mentre risolvo il problema, ho ricordato che SQL Server ora ha un "pivot" operatore per far fronte a questo.

Funziona su SQL 2005 solo , utilizzando i dati di esempio Northwind.

-- This could be a parameter to a stored procedure
-- I picked this one because he has products that he ordered 4 or more times
declare @customerId nchar(5)
set @customerId = 'ERNSH'

select c.CustomerID, p.ProductName, products_ordered_by_cust.FirstOrderYear,
    latest_order_dates_pivot.LatestOrder1 as LatestOrderDate,
    latest_order_dates_pivot.LatestOrder2 as SecondLatestOrderDate,
    latest_order_dates_pivot.LatestOrder3 as ThirdLatestOrderDate,
    'If I had a comment field it would go here' as LatestOrderComment,
    isnull(last_year_revenue_sum.ItemGrandTotal, 0) as LastYearIncome
from
    -- Find all products ordered by customer, along with first year product was ordered
    (
        select c.CustomerID, od.ProductID,
            datepart(year, min(o.OrderDate)) as FirstOrderYear
        from Customers c
            join Orders o on o.CustomerID = c.CustomerID
            join [Order Details] od on od.OrderID = o.OrderID
        group by c.CustomerID, od.ProductID
    ) products_ordered_by_cust
    -- Find the grand total for product purchased within last year - note fudged date below (Northwind)
    join (
        select o.CustomerID, od.ProductID, 
            sum(cast(round((od.UnitPrice * od.Quantity) - ((od.UnitPrice * od.Quantity) * od.Discount), 2) as money)) as ItemGrandTotal
        from
            Orders o
            join [Order Details] od on od.OrderID = o.OrderID
        -- The Northwind database only contains orders from 1998 and earlier, otherwise I would just use getdate()
        where datediff(yy, o.OrderDate, dateadd(year, -10, getdate())) = 0
        group by o.CustomerID, od.ProductID
    ) last_year_revenue_sum on last_year_revenue_sum.CustomerID = products_ordered_by_cust.CustomerID
        and last_year_revenue_sum.ProductID = products_ordered_by_cust.ProductID
    -- THIS is where the magic happens.  I will walk through the individual pieces for you
    join (
        select CustomerID, ProductID,
            max([1]) as LatestOrder1,
            max([2]) as LatestOrder2,
            max([3]) as LatestOrder3
        from
        (
            -- For all orders matching the customer and product, assign them a row number based on the order date, descending
            -- So, the most recent is row # 1, next is row # 2, etc.
            select o.CustomerID, od.ProductID, o.OrderID, o.OrderDate,
                row_number() over (partition by o.CustomerID, od.ProductID order by o.OrderDate desc) as RowNumber
            from Orders o join [Order Details] od on o.OrderID = od.OrderID
        ) src
        -- Now, produce a pivot table that contains the first three row #s from our result table,
        -- pivoted into columns by customer and product
        pivot
        (
            max(OrderDate)
            for RowNumber in ([1], [2], [3])
        ) as pvt
        group by CustomerID, ProductID
    ) latest_order_dates_pivot on products_ordered_by_cust.CustomerID = latest_order_dates_pivot.CustomerID
        and products_ordered_by_cust.ProductID = latest_order_dates_pivot.ProductID
    -- Finally, join back to our other tables to get more details
    join Customers c on c.CustomerID = products_ordered_by_cust.CustomerID
    join Orders o on o.CustomerID = products_ordered_by_cust.CustomerID and o.OrderDate = latest_order_dates_pivot.LatestOrder1
    join [Order Details] od on od.OrderID = o.OrderID and od.ProductID = products_ordered_by_cust.ProductID
    join Products p on p.ProductID = products_ordered_by_cust.ProductID
where c.CustomerID = @customerId
order by CustomerID, p.ProductID

Le query SQL restituiscono risultati sotto forma di un singolo "quotato" tabella di righe e colonne. I requisiti di segnalazione sono spesso più complessi di questo, richiedendo un "frastagliato" insieme di risultati come il tuo esempio. Non c'è nulla di sbagliato nel "procedere procedurale" per risolvere tali requisiti o utilizzando uno strumento di reporting che si trova nella parte superiore del database. Tuttavia, è necessario utilizzare SQL per quanto possibile per ottenere le migliori prestazioni dal database.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top