This question already has an answer here:
Combine values from related rows into a single concatenated string value
(1 answer)
Closed 8 years ago.
My database includes several lookup tables (shown as pulldown menus on the UI form).
For example,
customer_data - customer demographic info.
lookup_car - stores car descriptions (Pinto, Vega, Reliant Robin, Mustang, Corvette)
junction_car_customer - joins a customer with one or more cars
Customer Jeremy Clarkson (cust_id: 1) owns three cars. The dropdown for his record shows:
Pinto (car_id=100)
Reliant Robin (car_id=101)
Vega (car_id=102)
The junction_car_customer data looks like this:
cust_id car_id
1 100
1 101
1 102
I'm trying to return a row showing the customer name and the models owned (as a semi-colon delimited string).
Here's my query:
SELECT
cd.cust_id,
cd.name_first,
cd.name_last,
jcc.car_id,
lc.car_desc
FROM
((customer_data AS cd)
LEFT JOIN ju_cust_car AS jcc ON jcc.cust_id = cd.cust_id)
LEFT JOIN lookup_cars AS lc ON lc.car_id = jcc.car_id
ORDER BY
cd.name_last
This returns:
cust_id name_first name_last car_id car_desc
1 Jeremy Clarkson 100 Pinto
1 Jeremy Clarkson 101 Reliant Robin
1 Jeremy Clarkson 102 Vega
What I'd like is:
cust_id name_first name_last car_desc
1 Jeremy Clarkson Pinto;Reliant Robin;Vega
Is there an efficient way of returning the above result?
As HansUp says, you need to use a custom VBA function. If the data is fairly static, you can speed things up by caching the results. So...
1) In the VB editor, add a reference to the 'Microsoft Scripting Runtime' (we'll be needing the Dictionary class from this library).
2) Create a new standard module, and add code to it like the following:
Option Explicit
Private mCache As New Scripting.Dictionary
Sub ClearCarDescCache(Optional cust_id)
If IsMissing(cust_id) Then
mCache.RemoveAll
Else
mCache.Remove CInt(cust_id)
End If
End Sub
Function GetCarDescList(cust_id) As String
If mCache.Exists(cust_id) Then
GetCarDescList = mCache(cust_id)
Exit Function
End If
Dim RS As DAO.Recordset, S As String
Set RS = CurrentDb.OpenRecordset( _
" SELECT car_desc " + _
" FROM junction_car_customer INNER JOIN lookup_car " + _
" ON junction_car_customer.car_id = lookup_car.car_id " + _
" WHERE cust_id = " & cust_id & _
" ORDER BY car_desc", dbOpenForwardOnly)
While Not RS.EOF
If Len(S) = 0 Then
S = RS(0)
Else
S = S + ";" & RS(0)
End If
RS.MoveNext
Wend
mCache.Add cust_id, S
GetCarDescList = S
End Function
3) The main query can now look like this:
SELECT cust_id, name_first, name_last, GetCarDescList(cust_id) AS car_desc
FROM customer_data
ORDER BY name_last
4) Add explicit calls to ClearCarDescCache as appropriate.
Related
Using MS Access, I have a table called "Locations" and a table called "Routes". I want to create a form that will basically present the routes table to the user (i.e select "from" and select "to") and will filter the "to" locations based on what the user has selected for the "from" location, i.e. a cascading combo box. The corresponding "Route.ID" will be stored in a resultant table on completion of the form.
Location has the following columns -
ID
Name
Notes
1
London
Great Britain
2
Manchester
Great Britain
3
Alabama
USA
Routes has the following columns -
ID
From_LID
To_LID
KM
Notes
1
1
2
450
Using the M1 route
2
2
1
450
Using the M1 route
3
1
2
485
Using the inside routes
4
2
1
485
Using the inside routes
5
1
3
5450
Too far to consider
6
3
1
5450
Too far to consider
I want to create a form with a cascading combo box - "ComboFrom", "ComboTo". Where "ComboFrom" will search the table for a list of distinct IDs, and using a JOIN with the Locations table, I can display the location names. "ComboTo" is dependant on the ID value in "ComboFrom" and will therefore only return a list of locations where it matches the From_LocationID of the routes table.
I have the first part more or less done. ComboFrom has the below query:
SELECT DISTINCT Location.Location_ID, Location.[Location Name], Location.Description
FROM Location INNER JOIN Route ON Location.Location_ID = Route.From_LID
ORDER BY Location.[Location Name];
ComboTo has the below query:
SELECT Location.Location_ID, Location.[Location Name], Location.Description, Route.From_LID
FROM Location INNER JOIN Route ON Location.Location_ID = Route.To_LID
WHERE (((Route.From_LID)=[Forms]![fmrRoute1]![From_LID]))
ORDER BY Location.[Location Name];
The piece of code in the "Where" clause in the ComboTo field basically gets the input of the ID from ComboFrom and outputs the correct IDs from the corresponding To_LID list. I then add vba against the update events for ComboFrom to requery ComboTo field.
What I am having trouble figuring out is how I can get the corresponding route ID to display correctly. i.e if I choose "1" for ComboFrom, and "3" for ComboTo, the Route ID should show "5" and not "7" (where "7" is a new record in the Routes table). Do I have to have a separate query to search for ComboFrom and ComboTo and return the resultant Routes ID? If so, how do I do this and attach it to the field in the form that will automatically update every time the ComboFrom and ComboTo fields change?
Is there a better way to do what I am trying to do?
Thanks in advance for any assistance!
This looks like cascading combo boxes into filtering a form from those two unbound combo boxes.
You can google those two ideas.
The self join made this tricky. The key was ignoring the relationship between Location.ID and From_LID. That relationship is in the database but ignored (deleted) in the query.
this gives us the sql we will need for the second combo box record source.
SELECT Routes.From_LID, Routes.To_LID, First(Locations.LocationName) AS FirstOfLocationName
FROM Locations INNER JOIN Routes ON Locations.ID = Routes.To_LID
GROUP BY Routes.From_LID, Routes.To_LID
HAVING (((Routes.From_LID)=2))
ORDER BY First(Locations.LocationName)
'just need to replace the 2
Private Sub cmbFrom_AfterUpdate()
'cascade combobox
Dim strRowSource As String
strRowSource = "SELECT Routes.From_LID, Routes.To_LID, First(Locations.LocationName) AS FirstOfLocationName" & _
" FROM Locations INNER JOIN Routes ON Locations.ID = Routes.To_LID " & _
"GROUP BY Routes.From_LID, Routes.To_LID " & _
"HAVING (((Routes.From_LID) = " & Me.cmbFrom & "))" & _
"ORDER BY First(Locations.LocationName)"
Debug.Print strRowSource
Me.cmbTo.RowSource = strRowSource
Me.cmbTo.Visible = True
'a look and feel choice
End Sub
[![enter image description here][2]][2]
Private Sub cmbTo_AfterUpdate()
'filter form 'most of the time you want to create a search form where you filter to the records you want
Me.Filter = "From_LID = " & Me.cmbFrom & " AND To_LID = " & Me.cmbTo
Me.FilterOn = True
Me.Detail.Visible = True
End Sub
I need help in VBA code for access.
I have two tables of the same name and same number of fields located in two different backends. Two of the fields of the table indicates when was that particular record set has been Created and Modified (Date and Time). I need to compare all the record sets present in the Tables based on the fields Created and Modified and include the latest record set values in the current databank. If the record set has the same Created field values then it should check the Modified field values. The latest values present in the modified fields should be updated in the final table. Final table is what I am trying to reproduce. Table_1 from xxx_BE.accdb should resemble Final table after update. Table sample is shown below:
Table_1 from xxx_BE.accdb
Article Price Status Created Modified
A 500 Bad 10.07.2019 10:30 14.02.2020 15:45
B 6000 Good 21.05.2019 15:45
C 1500 Good 24.03.2018 08:30 25.03.2019 08:30
Table_1 from yyy_BE.accdb
Article Price Status Created Modified
A 5000 Bad 10.07.2019 10:30 19.05.2020 16:45
B 6000 Good 21.01.2019 15:45
C 9000 Bad 24.03.2018 08:30
D 14000 Bad 30.06.2018 08:30
Final Table
Article Price Status Created Modified
A 5000 Bad 10.07.2019 10:30 19.05.2020 16:45
B 6000 Good 21.01.2019 15:45
C 1500 Good 24.03.2018 08:30 25.03.2019 08:30
D 14000 Bad 30.06.2018 08:30
Please note:
Table_1 is present in xxx_BE.accdb and Table_2 is present in yyy_BE.accdb. I want the final table to appear in xxx_BE.accdb.
Assuming that you have the two source tables available in the front-end, then you can create a copy of one of them in the correct back-end. You can then use some VBA to use aUNION query as the basis for a recordset to get a list of all of the distinct Articles from the two source tables. You would then loop this recordset, creating a second UNION-based recordset to get the corresponding data from the two tables. This latter recordset would be sorted by the two date fields. You would then write all of this data to a recordset that is based on the final table.
Something like this works on the data supplied:
Sub sMergeData()
On Error GoTo E_Handle
Dim db As DAO.Database
Dim rsSteer As DAO.Recordset
Dim rsLookup As DAO.Recordset
Dim rsFinal As DAO.Recordset
Dim strSQL As String
Set db = CurrentDb
db.Execute "DELETE * FROM tblFinal;"
Set rsFinal = db.OpenRecordset("SELECT * FROM tblFinal WHERE 1=2;")
strSQL = "SELECT Article FROM Table1 " _
& " UNION SELECT Article FROM Table2;"
Set rsSteer = db.OpenRecordset(strSQL)
If Not (rsSteer.BOF And rsSteer.EOF) Then
Do
strSQL = "SELECT Price, Status, Created, Modified FROM Table1 WHERE Article='" & rsSteer!Article & "' " _
& " UNION SELECT Price, Status, Created, Modified FROM Table2 WHERE Article='" & rsSteer!Article & "' " _
& " ORDER BY Created DESC, Modified DESC;"
Set rsLookup = db.OpenRecordset(strSQL)
With rsFinal
.AddNew
!Article = rsSteer!Article
!Price = rsLookup!Price
!Status = rsLookup!Status
!Created = rsLookup!Created
!Modified = rsLookup!Modified
.Update
End With
rsSteer.MoveNext
Loop Until rsSteer.EOF
End If
sExit:
On Error Resume Next
rsFinal.Close
rsSteer.Close
rsLookup.Close
Set rsSteer = Nothing
Set rsLookup = Nothing
Set rsFinal = Nothing
Set db = Nothing
Exit Sub
E_Handle:
MsgBox Err.Description & vbCrLf & vbCrLf & "sMergeData", vbOKOnly + vbCritical, "Error: " & Err.Number
Resume sExit
End Sub
Regards,
Consider a pure SQL solution with multiple steps since you can directly query other Access database tables with period qualifying references or IN reference. The other needed steps are required since correlated aggregates in Access have performance issues on union queries. All can be run from the database holding final table.
Union Query (combine data sources)
SELECT Article, Price, Status, Created, Modified
FROM [C:\Path\To\xxx_BE.accdb].Table_1
UNION ALL
SELECT Article, Price, Status, Created, Modified
FROM [C:\Path\To\yyy_BE.accdb].Table_1
Alternatively:
SELECT Article, Price, Status, Created, Modified
FROM Table_1 IN "C:\Path\To\xxx_BE.accdb"
UNION ALL
SELECT Article, Price, Status, Created, Modified
FROM Table_1 IN "C:\Path\To\yyy_BE.accdb"
Make-Table Query (create a new local table)
SELECT q.Article, q.Price, q.Status, q.Created, q.Modified
INTO myTempTable
FROM myUnionQuery q
Correlated Aggregate Subquery (add rank by descending create/modified fields)
SELECT t.Article, t.Price, t.Status, t.Created, t.Modified,
(SELECT COUNT(*) FROM myTempTable sub
WHERE sub.Article = t.Article
AND sub.Created >= t.Created
AND sub.Modified >= t.Modified) AS [rank]
FROM myTempTable t
Final Insert-Select Query (filter using rank)
INSERT INTO myFinalTable (Article, Price, Status, Created, Modified)
SELECT q.Article, q.Price, q.Status, q.Created, q.Modified
FROM myCorrelAggQuery q
WHERE q.[rank] = 1
I need to calculate weighted price index between 2 retailers (weighted on retailer 1 turnover). I need to calculate this index using different Item compositions (different top n).
Table1 contains data on area-week-item level.
The basic approach of weighting is:
SUM(price1/price2 * turnover)/SUM(turnover)
I calculate it with the following Query statement:
SELECT week, SUM(price1/price2 * turnover)/SUM(turnover) AS [PriceIndex]
FROM Table1
GROUP BY week;
According to the business needs I usually need to calculate this index with different set of products - only private label, only vegetables or - the most simple example - for only TOP n products (based on turnover in Q1'16).
I want to make a simple form in Access using VBA with several parameters for my query.
Form example
I have Dictionary tables with item-code descriptions (to select only beer category) and TOP 500 ranking table. I've made a query, that works - it JOINS top 500 table (it consists just code column with needed codes. I can manually write number of TOP codes (hilighted the string with ***).
SELECT week, SUM(price1/price2 * turnover)/SUM(turnover) AS [PriceIndex]
FROM Table1 AS F INNER JOIN TopTable AS T ON F.code=T.code
WHERE F.code IN (
***SELECT TOP 500 code
FROM TopTable)
GROUP BY week;
Now the main question. How to make the illustrated form?
I've made the beginning:
Private Sub Command_Click()
Dim top_num As Long
Dim SQL As String
top_num = Top_number.Value
sSQL = _
"SELECT week, SUM(price1/price2 * turnover)/SUM(turnover) AS [PriceIndex]" & _
"FROM Table1 AS F INNER JOIN TopTable AS T ON F.code=T.code" & _
"WHERE F.code IN ( " & _
"***SELECT TOP top_num code" & _
"FROM TopTable)" & _
"GROUP BY week;"
' ????
End Sub
I need just to execute SQL statement with form variable top_num.
So if you want to display the results of you query in your form, you have to do it in a subform.
create the Subform
create a new form, name it Subform1 or anything else you like. click on the square which is top left of the form and bring the property window. Under "Format", "default display", select "Datasheet".
Your query returns 2 columns, so you need to add 2 textboxes on your form
You have to bound your textboxes to the column names returned by your query: select a textbox and bring the properties window, under "data" tab the first field is "control source", there you specify your query column names : week for one textbox, PriceIndex for the other
Your Subform is ready, save and close it.
Add the subform to the form
Open the main form in design mode and add a SubForm control to it. When you create the control the wizard ask you which will be the data source for it, select SubForm1
Adapt the code
Private Sub Command_Click()
Dim top_num As Long
Dim SQL As String
top_num = Top_number.Value
sSQL = _
"SELECT week, SUM(price1/price2 * turnover)/SUM(turnover) AS [PriceIndex]" & _
"FROM Table1 AS F INNER JOIN TopTable AS T ON F.code=T.code" & _
"WHERE F.code IN ( " & _
"***SELECT TOP " & top_num & " code" & _
"FROM TopTable)" & _
"GROUP BY week;"
' Add This :
Me!subform1.Form.RecordSource = sSQL
End Sub
You're done
I've got an issue with an Inner Join statement I am using to access data from my Access Database. What I am expecting to happen is that I run through the record set for each product. When a product was ordered more then once, I dont add it to the excel sheet, instead I increment the number ordered and the total cost.
The problem I am having is that instead of it working the way I described above, it is adding a product to the excel sheet for every time it was ordered. I have discovered that it is printing the products in the order that they were ordered (By their OrderID) which is not included in my code.
Any help?
Here is the code :
Public Sub WorksheetLoop()
Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
Dim stDB As String, stSQL As String, stSQLTwo As String, stProvider As String
Dim sheetName As String, stProdName As String
Dim suppNum As Integer, prodNum As Integer
Dim WS_Count As Integer
Dim I As Integer
suppNum = 1
prodNum = 1
stDB = "Data Source= " & ThisWorkbook.Path & "\obsDatabase.accdb"
stProvider = "Microsoft.ACE.OLEDB.12.0"
'Opening connection to database
With cn
.ConnectionString = stDB
.Provider = stProvider
.Open
End With
' Set WS_Count equal to the number of worksheets in the active
' workbook.
WS_Count = ActiveWorkbook.Worksheets.Count
' Begin the loop.
For I = 2 To WS_Count
ActiveWorkbook.Worksheets(I).Range("A1") = "Company Name - " + ActiveWorkbook.Worksheets(I).Name + ""
ActiveWorkbook.Worksheets(I).Range("A2") = "Item Number"
ActiveWorkbook.Worksheets(I).Range("B2") = "Description"
ActiveWorkbook.Worksheets(I).Range("C2") = "Unit"
ActiveWorkbook.Worksheets(I).Range("D2") = "Cost Per Unit"
ActiveWorkbook.Worksheets(I).Range("E2") = "Quantity"
ActiveWorkbook.Worksheets(I).Range("F2") = "Total Cost"
ActiveWorkbook.Worksheets(I).Range("G2") = "Amount Remaining"
'Function to retrieve info!
stSQL = "SELECT Products.ProductID, Products.ProductName, Products.ProductDescription, Products.ProductUnit, LineItems.UnitPrice, LineItems.Quantity, LineItems.TotalPrice " & _
"FROM Products INNER JOIN LineItems ON LineItems.ProductID = Products.ProductID WHERE Products.SupplierID = " & suppNum & " "
rs.Open stSQL, cn
With rs
Do Until .EOF
If ActiveWorkbook.Worksheets(I).Range("A65536").End(xlUp) = rs.Fields("ProductName") Then
If ActiveWorkbook.Worksheets(I).Range("D65536").End(xlUp) = rs.Fields("UnitPrice") Then
ActiveWorkbook.Worksheets(I).Range("E65536").End(xlUp) = ActiveWorkbook.Worksheets(I).Range("E65536").End(xlUp) + rs.Fields("Quantity")
ActiveWorkbook.Worksheets(I).Range("F65536").End(xlUp) = ActiveWorkbook.Worksheets(I).Range("F65536").End(xlUp) + rs.Fields("TotalPrice")
End If
Else
ActiveWorkbook.Worksheets(I).Range("A65536").End(xlUp).Offset(1, 0) = rs.Fields("ProductName")
ActiveWorkbook.Worksheets(I).Range("B65536").End(xlUp).Offset(1, 0) = rs.Fields("ProductDescription")
ActiveWorkbook.Worksheets(I).Range("C65536").End(xlUp).Offset(1, 0) = rs.Fields("ProductUnit")
ActiveWorkbook.Worksheets(I).Range("D65536").End(xlUp).Offset(1, 0) = rs.Fields("UnitPrice")
ActiveWorkbook.Worksheets(I).Range("E65536").End(xlUp).Offset(1, 0) = rs.Fields("Quantity")
ActiveWorkbook.Worksheets(I).Range("F65536").End(xlUp).Offset(1, 0) = rs.Fields("TotalPrice")
End If
rs.MoveNext
Loop
End With
rs.Close
suppNum = suppNum + 1
ActiveWorkbook.Worksheets(I).Columns("A:A").EntireColumn.AutoFit
ActiveWorkbook.Worksheets(I).Columns("B:B").EntireColumn.AutoFit
ActiveWorkbook.Worksheets(I).Columns("C:C").EntireColumn.AutoFit
ActiveWorkbook.Worksheets(I).Columns("D:D").EntireColumn.AutoFit
ActiveWorkbook.Worksheets(I).Columns("E:E").EntireColumn.AutoFit
ActiveWorkbook.Worksheets(I).Columns("F:F").EntireColumn.AutoFit
ActiveWorkbook.Worksheets(I).Columns("G:G").EntireColumn.AutoFit
Next I
cn.Close
End Sub
I'm not sure if I am missing the point. But could you clarify your end goal? Does it have to be done via Excel VBA?
If what you are trying to achieve is a tab with each suppliers orders on, with one row per product and a total quatity for that product, then I would consider creating a query in the database itself and pass the supplier id parameter and any other parameters to the query. The query could then handle the grouping and counting of products and quatitys as this would be an aggregrate query.
This way you can have a refreshable query on each tab and wrtie VBA to refresh them individually or all together whichever suits your needs.
I would always try to avoid complex VBA coding as it's buggy at the best of times and becomes difficult to maintain once distributed.
Another option would be pull all the product data into another tab which you could Hide via VBA and use formula like SUMPRODUCT to display the information on the various tabs. Or use a combo style box to select your supplier and dynamically change the result set.
As I said in the beginning I may be missing the point, but if not and you would like help with my option(s) let me know, and if I am please clarify your question.
For your INNER JOIN issue, you would need to use an agregate query not a stright forward select. This is because your database (I assume) can have one supplier which can order the same product more than once (A one to Many relationship), from what you have supplied you have a quantity column in the LineItems table so I assume the duplicate product ID is from two or more seperate orders from the same supplier. Here is an example of the query, also consider aliasing your tables names, it makes following the code easier.
SELECT
p.ProductID
,p.ProductName
,p.ProductDescription
,p.ProductUnit
,SUM(l.UnitPrice)
,SUM(l.Quantity)
,SUM(l.TotalPrice)
FROM Products p
INNER JOIN LineItems l
ON l.ProductID = p.ProductID
WHERE p.SupplierID = 1
GROUP BY
p.ProductID
,p.ProductName
,p.ProductDescription
,p.ProductUnit
Regards
Kevin
You're only ever comparing the current product name to the last line added to the worksheet but nothing in your SQL query enforces any ordering to the results.
This would be OK if the data was returned like this:
ProductName, Quantity
foo, 7
foo, 4
bar, 3
but would not be OK if the data was returned like this:
ProductName, Quantity
foo, 7
bar, 3
foo, 4
You could use an ORDER BY clause and work with your current macro but, as other people have pointed out, you could use SQL to combine the data and this should be a simpler solution
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Ms Access Query: Concatenating Rows through a query
I have a table that has many columns, but two of interest: Order Number and Product Type. Currently the table has multiple Product Types per Order. If the customer ordered phone service, TV service and Internet service, then there would be three records - one for each service but all having the same order number. I want to create a reference table to store a concatenated string with all of the services the customer ordered. This way I can summarize my data using this more logical method. I'm using a standard Access 2010 database.
**Current table:**
Order Number | Product Types
100001 | TV
100001 | Phone
100001 | Internet
100002 | Phone
100003 | TV
100003 | Internet
Desired reference table
100001 | TV/Phone/Internet
100002 | Phone
100003 | TV/Internet
Allen Browne provides a function which you may find useful for this: Concatenate values from related records. Save that function's code in a standard module.
SELECT DISTINCT
[Order Number],
ConcatRelated("[Product Types]",
"YourTable",
"[Order Number] = " & [Order Number],
"[Product Types]",
"/"
) AS All_Product_Types
FROM YourTable;
I tested that query in Access 2007 with your sample data saved in a table named "YourTable". It returned the results you asked for. However, this only works from within an Access session. If you wanted to run this query from outside Access (like from ASP), user-defined functions are not available, so you would get an error about ConcatRelated() not recognized.
So you can use a query to retrieve the concatenated values whenever you need them. However if you store those concatenated values, they can quickly be out of sync with changes to the base table's data.
If I understand the question, you're asking how to get the order numbers of just those orders who have TV, AND phone, AND internet. If you're just interested in those order numbers you could run a query like:
SELECT Distinct Table1.OrderNumber
FROM (Select OrderNumber from Table1 where [product types]= "Internet") AS i
INNER JOIN ((Select OrderNumber from Table1 where [product types]="Phone") AS p
INNER JOIN ((Select OrderNumber from Table1 Where [product types]= "TV") AS tv
INNER JOIN Table1 ON tv.OrderNumber = Table1.OrderNumber) ON p.OrderNumber = Table1.OrderNumber) ON i.OrderNumber = Table1.OrderNumber;
As was pointed out by onedaywhen in an early post on SO, this is easier with ADO:
Sample query:
SELECT [Order Number],
ConcatADO("SELECT [Product Types] FROM Orders
WHERE [Order Number]=" & [Order Number],", "," : ") AS Cat
FROM Orders
GROUP BY [Order Number], 2;
Function using ADO
Function ConcatADO(strSQL As String, strColDelim, _
strRowDelim, ParamArray NameList() As Variant)
Dim rs As New ADODB.Recordset
Dim strList As String
On Error GoTo Proc_Err
If strSQL <> "" Then
rs.Open strSQL, CurrentProject.Connection
strList = rs.GetString(, , strColDelim, strRowDelim)
strList = Mid(strList, 1, Len(strList) - Len(strRowDelim))
Else
strList = Join(NameList, strColDelim)
End If
ConcatADO = strList
Exit Function
Proc_Err:
ConcatADO = "***" & UCase(Err.Description)
End Function
You should not create a reference table that concatenates records. That is denormalizing the database.
You can try a crosstab query like below, but I have not tested it. You can read here for more information.
TRANSFORM First([Product Types]) AS Product
SELECT [Order Number], First([Product Types])
FROM CurrentTable
GROUP [Order Number]