SQL query on Northwind multiple tables - sql

From Northwind database I want to get total revenue generated by emplyee sales
Employee -> Orders -> "Order Details"
I am not sure if my solution gives the right data (it was partly guessing)
SELECT
Employees.FirstName, Employees.LastName,
SUM(CONVERT(MONEY, ("Order Details".UnitPrice * Quantity * (1 - Discount) / 100)) * 100) AS ExtendedPrice
FROM
((Orders
INNER JOIN
"Order Details" ON Orders.OrderID = "Order Details".OrderID)
INNER JOIN
Employees ON Orders.EmployeeID = Employees.EmployeeID)
GROUP BY
LastName, FirstName;
Northwind database structure can be found here
Thank you in advance. It would be great to have a nice explanation as well

Chris, your effort is pretty good first effort, so there are a few things to change on this.
You don't need to divide by 100 and then multiply by 100. The discount is already a %. Your operation just truncates the numbers. I would avoid to this too early in a process as it introduces rounding errors. It is better to keep numbers raw and keep their precision as best you can for as long as you can. It is OK to display numbers as money in the GUI though i.e. to 2 decimals but not in intermediate calculations due to error introduced by truncating.
Table names and field names with spaces should be handled using [] rather than quotes. That makes it easier to find misspelling so use [Order Details]
When grouping and summing, make sure you use the keys. So name is not a key, so use EmployeeID if you are trying to group individual employees, this is because in real datasets you may have 2 employees with the same name and their sales will be grouped together incorrectly using your code.
Try this course/book, it is a good intro to querying databases. https://www.microsoft.com/en-au/learning/exam-70-461.aspx
The reason how this works? Select syntax has Select [fieldlist] from [table] inner join [jointable] on [join fields] group by [grouping fields]. fieldlist can be a calculation as well as actual field names to display. "inner join" means you want only those orders, order details, employees where there is actual matching data - Correct in your scenario. [table] and [jointable] is the actual tables that contain your data in a relational sense.
There is obvisouly a lot here to learn in one go. I would work through some of the different SQL Server querying courses that you can google.
Here's a revised version of the code:
SELECT Employees.EmployeeID, Employees.FirstName, Employees.LastName, Sum([Order Details].UnitPrice * Quantity * (1 - Discount)) AS ExtendedPrice
FROM Orders
INNER JOIN [Order Details] ON Orders.OrderID = [Order Details].OrderID
INNER JOIN Employees ON Orders.EmployeeID = Employees.EmployeeID
group by Employees.EmployeeID, Employees.FirstName, Employees.LastName
order by Employees.FirstName, Employees.LastName;

Related

Student Seeking Advice for a CSC Exam

I'm a student taking a course on SQL and DB. My question is this: how does one get good at hand writing queries? Our final exam will consist of many of these questions, and I want to do well. We aren't allowed any sort of reference sheet either, just fyi.
I suppose what I'm asking is: how would you approach this?
In short, You require practice aka hands on sql.
You will probably get many opinions on this from others. Aside from practice and reading, try to ensure you understand the absolute basics and sequence of query.
Always use table.column or alias.column to help prevent any ambiguity of where something is coming from.
Know the overall basic segments of writing a query such as
select
[all your alias.columns comma separated]
from
[your primary and/or JOIN/LEFT JOIN/etc tables]
where
[what is the criteria you are looking for]
AND [use proper parenthesis to prevent ambiguity if so needed]
group by
[any columns if doing aggregates such as count, min, max, avg, etc]
[you need to list all NON-AGGREGATE alias.columns]
having
[if any, such as count(*) > someValue]
order by
[any specific columns and ascending or descending order]
[such as orderDate DESC to put most recent order at top]
In my opinion, getting your FROM clause is one of the most important and I try to always list my table JOIN clauses on first table/alias = second table/alias. Indentation helps here so you can see how you get from one table to the next. At this point, do not think of your filtering (YET), just HOW the tables are related. Then you can add "AND" criteria for something you are specifically looking for from that source.
An example of orders. Looking for customers who ordered in the last 30 days. Start with that source as your first FROM table, everything else off of that. So I start with the orders because I care about WHEN something was ordered. I can then join to customers to get their name.
select
c.LastName,
c.FirstName,
o.OrderDate
from
Orders o
JOIN Customers c
on o.CustomerID = c.CustomerID
where
o.OrderDate > [sql-specific current date - 30 days]
order by
c.LastName,
c.FirstName
Another example of orders that ordered a specific item in the last 30 days. In this case, I could reverse the order of details as specific things being ordered might be smaller granularity vs everything. So, altering above such as
select
c.LastName,
c.FirstName,
o.OrderDate
from
Items i
JOIN OrderDetails od
on i.ItemID = od.ItemID
JOIN Orders o
on od.OrderID = o.OrderID
AND o.OrderDate > [sql-specific current date - 30 days]
JOIN Customers c
on o.CustomerID = c.CustomerID
where
i.ItemDescription = 'SomeThing'
order by
c.LastName,
c.FirstName
Notice my indentation nesting. Personal style preference, but at least you can see how alias i to od, od to o, o to c. In my preference, easier to see the trail of tables and how each are directly related. I also added the "AND" clause to filter out orders within the last 30 days directly in the JOIN to the orders table.
LEFT JOINs, I do the same and keep the criteria directly at the JOIN level. If you put a criteria of a left-join into the WHERE clause (without explicitly handling NULL OR [condition] it turns a left-join into an [INNER] join.
Hope this basic guidance helps you get more comfortable as you get more into writing your own queries and course/test preparation.

My question is about SQL, using a TOP function inside a sub-query in MS Access

Overall what I'm trying to achieve is a query that shows the most ordered item from a customer in a database. To achieve this I've tried making a query showing how many times a customer has ordered an item, and now I am trying to create a sub-query in it using TOP1 to discern the most bought items.
With the SQL from the first query (looking weird because I made it with the Access automatic creator):
SELECT
Customers.CustomerFirstName,
Customers.CustomerLastName,
Products.ProductName,
COUNT(SalesQuantity.ProductCode) AS CountOfProductCode
FROM (Employees
INNER JOIN (Customers
INNER JOIN Sales
ON Customers.CustomerCode = Sales.CustomerCode)
ON Employees.EmployeeCode = Sales.EmployeeCode)
INNER JOIN (Products
INNER JOIN SalesQuantity
ON Products.ProductCode = SalesQuantity.ProductCode)
ON Sales.SalesCode = SalesQuantity.SalesCode
GROUP BY
Customers.CustomerFirstName,
Customers.CustomerLastName,
Products.ProductName
ORDER BY
COUNT(SalesQuantity.ProductCode) DESC;
I have tried putting in a subquery after FROM line:
(SELECT TOP1 CountOfProduct(s)
FROM (.....)
ORDER by Count(SalesQuantity.ProductCode) DESC)
I'm just not sure what to put in for the "from"-every other tutorial has the data from an already created table, however this is from a query that is being made at the same time. Just messing around I've put "FROM" and then listed every table, as well as
FROM Count(SalesQuantity.ProductCode)
just because I've seen that in the order by from the other code, and assume that the query is discerning from this count. Both tries have ended with an error in the syntax of the "FROM" line.
I'm new to SQL so sorry if it's blatantly obvious, but any help would be greatly appreciated.
Thanks
As I understand, you want the most purchased product for each customer.
So, begin by building aggregate query that counts product purchases by customer (appears to be done in the posted image). Including customer ID in the query would simplify the next step which is to build another query with TOP N nested query.
Part of what complicates this is unique record identifier is lost because of aggregation. Have to use other fields from the aggregate query to provide unique identifier. Consider:
SELECT * FROM Query1 WHERE CustomerID & ProductName IN
(SELECT TOP 1 CustomerID & ProductName FROM Query1 AS Dupe
WHERE Dupe.CustomerID = Query1.CustomerID
ORDER BY Dupe.CustomerID, Dupe.CountOfProductCode DESC);
Overall what I'm trying to achieve is a query that shows the most ordered item from a customer in a database.
This answers your question. It does not modify your query which is only tangentially related.
SELECT s.CustomerCode, sq.ProductCode, SUM(sq.quantity) as qty
FROM Sales as s INNER JOIN
SalesQuantity as sq
ON s.SalesCode = sq.SalesCode
GROUP BY s.CustomerCode, sq.ProductCode;
To get the most ordered items, you can use this twice:
SELECT s.CustomerCode, sq.ProductCode, SUM(sq.quantity) as qty
FROM Sales as s INNER JOIN
SalesQuantity as sq
ON s.SalesCode = sq.SalesCode
GROUP BY s.CustomerCode, sq.ProductCode
HAVING sq.ProductCode IN (SELECT TOP 1 sq2.ProductCode
FROM Sales as s2 INNER JOIN
SalesQuantity as sq2
ON s2.SalesCode = sq2.SalesCode
WHERE s2.CustomerCode = s.CustomerCode
GROUP BY sq2.ProductCode
);
In almost any other database, this would be simpler, because you would be able to use window functions.

calculates between These two columns in SQL server [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 5 years ago.
Improve this question
I want to add a column to the query that calculates between These two columns In the same query ....................................................................
,isnull(sum(ORDERS.Net_Amount) + 0,0) as orders
,isnull(sum (convert(float,(RECEIPTS.Amount))) + 0,0) as recepts
IN SQL SERVER
SELECT CUSTOMERS.[ID_CUSTOMER]
,[FIRST_NAME]
,[TEL]
,[EMAIL]
,isnull(sum(ORDERS.Net_Amount) + 0,0) as orders
,isnull(sum (convert(float,(RECEIPTS.Amount))) + 0,0) as recepts
,[CRIDIT_LIMIT]
,[CUSTOMER_SINCE]
,[ADRESS]
,CUSTOMERS.[state]
FROM [CUSTOMERS]
LEFT JOIN ORDERS on CUSTOMERS.ID_CUSTOMER = ORDERS.CUSTOMER_ID
LEFT JOIN RECEIPTS on CUSTOMERS.ID_CUSTOMER = RECEIPTS.ID_CUSTOMER
GROUP BY CUSTOMERS.[ID_CUSTOMER]
,[FIRST_NAME]
,[TEL]
,[EMAIL]
,[CRIDIT_LIMIT]
,[CUSTOMER_SINCE]
,[ADRESS]
,CUSTOMERS.[state]
CUSTOMERS table
SELECT [ID_CUSTOMER]
,[FIRST_NAME]
,[TEL]
,[EMAIL]
,[IMAGE_CUSTOMER]
,[CRIDIT_LIMIT]
,[CUSTOMER_SINCE]
,[ADRESS]
,[Balance]
,[state]
FROM [CUSTOMERS]
ORDERS table
SELECT [ID_ORDER]
,[DATE_ORDER]
,[CUSTOMER_ID]
,[DESCRIPTION_ORDERS]
,[SALEMAN]
,[ORDER_TOTAL]
,[Discount_Of_Total]
,[Total_After_Discount]
,[Paid_Up]
,[Net_Amount]
,[state]
FROM [ORDERS]
RECEIPTS table
SELECT [image_state]
,[ID_RECEIPT]
,[ID_CUSTOMER]
,[Date]
,[Ref]
,[Amount]
,[Memo]
,[User_Name]
,[state]
,[Payment_Method]
,[Account_ID]
FROM [RECEIPTS]
Orders and receipts are not really related to each other (the receipt doesn't refer to a specific order), so don't join the two. What you want to do instead is find the order amount and the receipt amount per customer and show them. So aggregate the two tables per customer and outer-join the results to the customer table.
select
c.id_customer,
c.first_name,
c.tel,
c.email,
coalesce(o.sum_net_amount, 0) as order_amount,
coalesce(r.sum_amount, 0) as receipt_amount,
c.cridit_limit,
c.customer_since,
c.adress,
c.balance,
c.state
from customers c
left join
(
select customer_id, sum(net_amount) as sum_net_amount
from orders
group by customer_id
) o on c.id_customer = o.customer_id
left join
(
select id_customer, sum(amount) as sum_amount
from receipts
group by id_customer
) r on c.id_customer = r.id_customer;
I see you have updated your request now asking also for the difference of the sums. Well, the operator for subtraction in SQL is - little surprising - the minus sign:
coalesce(o.sum_net_amount, 0) - coalesce(r.sum_amount, 0) as diff
Please try the following...
SELECT Customers.ID_Customer,
first_name,
tel,
email,
SUM( net_amount ) AS OrdersTotal,
COALESCE( sumAmount, 0 ) AS PaymentsTotal,
SUM( net_amount ) - COALESCE( sumAmount, 0 ) AS DifferenceInTotals,
cridit_limit,
customer_since,
adress,
balance,
state
FROM Customers
INNER JOIN Orders ON Customers.ID_Customer = Orders.Customer_ID
LEFT JOIN ( SELECT ID_Customer,
SUM( amount ) AS sumAmount
FROM Receipts
GROUP BY ID_Customer
) AS sumAmountFinder ON Customers.ID_Customer = sumAmountFinder.ID_Customer
GROUP BY Customers.ID_Customer,
first_name,
tel,
email,
cridit_limit,
customer_since,
adress,
balance,
state;
This Answer is based on the assumption that every Customer will have at least one Order, but possibly no Receipts.
This statement is essentially the one that you supplied with the following modifications...
I have changed the JOIN to Orders to an INNER JOIN, since I am assuming that each Customer will have at least one Order. The LEFT JOIN is only necessary where you wish to retain all records from the left table that do not have at least one matching record from the right table as defined by the ON clause. (Note : If you wish to retain all the records from the right table where there is no matching records from the left table, use a RIGHT JOIN).
I have replaced the JOIN to the Receipts table with a subquery that calculates the total of the amount field for each Customer in the Receipts table. A LEFT JOIN is necessary between Customers and the results of this subquery as not all Customers will have a Receipt. In such situations the LEFT JOIN will set each of the fields from the subquery in the joined dataset to NULL.
Where the SUM() function encounters only NULL values it returns NULL, not 0. So that PaymentsTotal will be set to 0 for records where the Customer has no Receipts, I have used the COALESCE() function. This function will return the first non-NULL argument it encounters. Here I have set it to return the total of amount where it encounters one, and 0 where it encounters no total amount.
I have removed all of the square brackets from your field and table names. They are only required where you have used an otherwise disallowed name, such as names with spaces (use [Full Name] instead of Full Name) or names that are also reserved by SQL-Server (if you had decided to call PaymentsTotal Sum, then you would have had to use AS [Sum]). Many programmers consider giving fields such names to be bad practice, even when it is possible with []'s, but fortunately you have not used any otherwise names.
I have removed the table names from your SUM() calculations. Since only one table has a field called net_amount, then it will be a unique field name in the joined dataset, and you will be able to refer to them without specifying the name of the source table as well. Specifying the source table is still necessary in the case of Customers.ID_Customer as the joined dataset will have more than one field called ID_Customer. Also, you will need to specify the source tables names when creating the joined dataset using the JOIN's.
I have also taken the liberty of changing your capitalisation scheme. Having just about everything in constant upper-case is monotonous to the eye. Using different casing for SQL terms, table names and field names makes recognising each type of statement part much easier, and thus makes debugging code much easier.
Finally, and relatively trivially, cridit is actually spelt credit and adress is actually spelt address.
If you have any questions or comments, then please feel free to post a Comment accordingly.

Returning duplicates based on one value in SQL

So I've been working with SQL Server 2014 Express for the first time and I've encountered a problem.
Basically, what I've been asked to do (as part of an assignment in school) is to return values from a regular "customer-table", based entirely on the customer's stored location.
My query looks like this atm:
SELECT
City,
COUNT(Clients.ClientNo) AS AmountOfClients,
Orders.Pieces * products.Price TotalPrice
FROM
Clients
INNER JOIN
ClientOrder ON Clients.ClientNo = ClientOrder.ClientNo
INNER JOIN
Orders ON ClientOrder.OrderNo = Orders.OrderNo
INNER JOIN
products ON Orders.ProductNo = products.ProductNo
GROUP BY
City, ClientOrders.Pieces, products.Price
HAVING
COUNT(Clients.ClientNo) > 1
Now to explain it - I've been trying to find all customers where the contents of the "City" column re-occurs and return only those - along with the total amount of their orders (seen here as currency) based on the contents of three other tables (which is why the JOINs are there).
The latter part is working, but the wrong clients get returned and instead of returning the actual "ClientNo", only the amount of clients found get returned. I can't seem to find the right course of action here.
Ideally, the returned information/output would look somewhat like this:
City, ClientNo, TotalCost -- for each client that lives in the same City as another
Any input would be appreciated.
You should use only City in the group by clause and also use
SUM(Orders.Pieces * products.Price) TotalPrice
in the SELECT clause to get the total sum from each city order (Sum of Products).

When do you give up set operations in SQL and go procedural?

I was once given this task to do in an RDBMS:
Given tables customer, order, orderlines and product. Everything done with the usual fields and relationships, with a comment memo field on the orderline table.
For one customer retrieve a list of all products that customer has ever ordered with product name, year of first purchase, dates of three last purchases, comment of the latest order, sum of total income for that product-customer combination last 12 months.
After a couple of days I gave up doing it as a Query and opted to just fetch every orderline for a customer, and every product and run through the data procedurally to build the required table clientside.
I regard this a symptom of one or more of the following:
I'm a lazy idiot and should have seen how to do it in SQL
Set operations are not as expressive as procedural operations
SQL is not as expressive as it should be
Did I do the right thing? Did I have other options?
You definitely should be able to do this exercise without doing the work equivalent to a JOIN in application code, i.e. by fetching all rows from both orderlines and products and iterating through them. You don't have to be an SQL wizard to do that one. JOIN is to SQL what a loop is to a procedural language -- in that both are fundamental language features that you should know how to use.
One trap people fall into is thinking that the whole report has to be produced in a single SQL query. Not true! Most reports don't fit into a rectangle, as Tony Andrews points out. There are lots of rollups, summaries, special cases, etc. so it's both simpler and more efficient to fetch parts of the report in separate queries. Likewise, in a procedural language you wouldn't try do all your computation in a single line of code, or even in a single function (hopefully).
Some reporting tools insist that a report is generated from a single query, and you have no opportunity to merge in multiple queries. If so, then you need to produce multiple reports (and if the boss wants it on one page, then you need to do some paste-up manually).
To get a list of all products ordered (with product name), dates of last three purchases, and comment on latest order is straightforward:
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;
It's fine to iterate over the result row-by-row to extract the dates and comments on the latest orders, since you're fetching those rows anyway. But make it easy on yourself by asking the database to return the results sorted by date.
Year of first purchase is available from the previous query, if you sort by the order_date and fetch the result row-by-row, you'll have access to the first order. Otherwise, you can do it this way:
SELECT YEAR(MIN(o.order_date)) FROM Orders o WHERE o.customer_id = ?;
Sum of product purchases for the last 12 months is best calculated by a separate query:
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;
edit: You said in another comment that you'd like to see how to get the dates of the last three purchases in standard SQL:
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;
If you can use a wee bit of vendor-specific SQL features, you can use Microsoft/Sybase TOP n, or 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;
Set operations are not as expressive as procedural operations
Perhaps more like: "Set operations are not as familiar as procedural operations to a developer used to procedural languages" ;-)
Doing it iteratively as you have done now is fine for small sets of data, but simply doesn't scale the same way. The answer to whether you did the right thing depends on whether you are satisfied with the performance right now and/or don't expect the amount of data to increase much.
If you could provide some sample code, we might be able to help you find a set-based solution, which will be faster to begin with and scale far, far better. As GalacticCowboy mentioned, techniques such as temporary tables can help make the statements far more readable while largely retaining the performance benefits.
In most RDBMS you have the option of temporary tables or local table variables that you can use to break up a task like this into manageable chunks.
I don't see any way to easily do this as a single query (without some nasty subqueries), but it still should be doable without dropping out to procedural code, if you use temp tables.
This problem may not have been solvable by one query. I see several distinct parts...
For one customer
Get a list of all products ordered (with product name)
Get year of first purchase
Get dates of last three purchases
Get comment on latest order
Get sum of product purchases for the last 12 months
Your procedure is steps 1 - 5 and SQL gets you the data.
Sounds like a data warehouse project to me. If you need things like "three most recent things" and "sum of something over the last 12 months" then store them i.e. denormalize.
EDIT: This is a completely new take on the solution, using no temp tables or strange sub-sub-sub queries. However, it will ONLY work on SQL 2005 or newer, as it uses the "pivot" command that is new in that version.
The fundamental problem is the desired pivot from a set of rows (in the data) into columns in the output. While noodling on the issue, I recalled that SQL Server now has a "pivot" operator to deal with this.
This works on SQL 2005 only, using the Northwind sample data.
-- 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
SQL queries return results in the form of a single "flat" table of rows and columns. Reporting requirements are often more complex than this, demanding a "jagged" set of results like your example. There is nothing wrong with "going procedural" to solve such requirements, or using a reporting tool that sits on top of the database. However, you should use SQL as far as possible to get the best performance from the database.