Excel VBA - INNER JOIN Issue - sql

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

Related

Run query from FORM with variable

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

Querying multiple tables and displaying the outcome wtih SQL and Access

I need to display the customers that have purchased a product (based on a user search) in a list box. I have five different tables in Access which store different information and that relate to each other with IDs (using combox boxes in vb). I need to be able to search for a product, for example "White Bread", the program then should display the customer's full name and address as stored in the database.
Table: TransactionDetails
Fields: ID, stockID, custTransID
Table: CustomerTransaction
Fields: ID, custID, dateOfTransaction
Table: CustomerAccountDetails
Fields: ID, custFullName, custAddress, custLandline,
custMobile, custDOB, custCreditDetails
Table: StockDescription
Fields: ID, stockName, stockDesc, stockPrice
Table: SupplierDetails
Fields: ID, supplierName, supplier Address
I think I need to use INNER JOIN to query multiple tables at once but I am unsure of the syntax (I'm new to SQL). So far I have this:
Dim productSearch As String
productSearch = productSrchInput.Text
Dim databaseConnection As New OleDb.OleDbConnection
Dim counter As Integer
Dim array(10) As String
databaseConnection.ConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=assignment5database.accdb"
databaseConnection.Open()
Dim searchDatabase As OleDbCommand = New OleDbCommand("SELECT CustomerAccountDetails.custFullName, CustomerAccountDetails.custAddress " & _
"FROM CustomerAccountDetails " & _
"INNER JOIN StockDescription ON TransactionDetails.stockID = TransactionDetails.custTransID " & _
"WHERE StockDescription.stockName = '" & productSearch & "'", databaseConnection)
Dim searchResults As OleDbDataReader = searchDatabase.ExecuteReader
counter = 1
Do While searchResults.Read
srchResultsList.Items.Add(searchResults.Item(0))
counter += 1
Loop
databaseConnection.Close()
You are missing some of the joins that connect the customer to the stock details. Here is the SQL that Access will expect in order to pull the data based on your description. The parentheses may seem extraneous, if you are used to SQL server or MySQL, but Access will throw a fit if you leave them out.
SELECT CustomerAccountDetails.custFullName, CustomerAccountDetails.custAddress, StockDescription.stockName
FROM StockDescription
INNER JOIN ((CustomerAccountDetails
INNER JOIN CustomerTransaction ON CustomerAccountDetails.ID = CustomerTransaction.custID)
INNER JOIN TransactionDetails ON CustomerTransaction.ID = TransactionDetails.custTransID) ON StockDescription.ID = TransactionDetails.StockID
WHERE StockDescription.stockName="something"
As noted by Fionnuala, I will almost always build a query that has multiple joins in it using the Access query designer before putting it in code. I almost always leave out a set of parentheses or try to write the query in a structure that SQL Server would expect and get rejected by Access.
I think you could be able to use an inner join but perhaps a "union" may be more efficient.
http://www.w3schools.com/sql/sql_union.asp is good for improving sql knowledge, it helped me a lot.

Looping through multiple date records to find the number of days

need to loop through date records in order to calculate the number of days worked. The number of dates varies from each person, they could have as few as one or as many as 11, ending with a calculation from the last dat in the sequence to the current days date.
Example of Data:
Employee No Date
4522 08/11/2011
4522 09/06/2011
4522 05/04/2013
4522 07/15/2013
4522 01/31/2014
So today is 04/29/2014, so from 01/31/2014 to 04/29/2014 is 88 days, 05/04/2013 to 07/15/2013 is 72 days and 08/11/2011 to 09/06/2011 is 26 days. Add those all together and you get a total of 186 days worked.
Again the number of dates will vary in each case.
Here's what I have so far...
Sub getActualEmployment(empID As Variant)
Dim strconnection, strSQL As String
Dim conn As ADODB.Connection
Dim tbl As ADODB.Recordset
Dim OptimumCode As String
Dim orghrdt As Date
Dim intNumDays As Integer
strSQL = "SELECT EmployeeID,EffectiveDate, OriginalHireDate FROM "
strSQL = strSQL & "EmploymentStatusChanges WHERE EmployeeID= '" & empID & "'"
strSQL = strSQL & " Order by EffectiveDate ASC"
Set tbl = New ADODB.Recordset
With tbl
Set .ActiveConnection = conn
.Source = strSQL
.LockType = adLockOptimistic
.CursorType = adOpenKeyset
.CursorLocation = adUseClient
.Open
End With
With tbl
On Error Resume Next
.MoveFirst
Do Until tbl.EOF
If tbl!OriginalHireDate = tbl!EffectiveDate Then
orghrdt = tbl!EffectiveDate
Else
intNumDays = CInt(DateValue(tbl!EffectiveDate) - orghrdt)
End If
.MoveNext
Loop
End With
End Sub
This is the whole function code, hopefully this makes better sense.
SELECT Employees.[Employee No], Employees.Action, Employees.DDate AS Start, Min(IIf([employees_1].[DDate]>=[employees].[ddate],[employees_1].[ddate])) AS [End], IIf([end] Is Not Null,[end],Now())-[start] AS Days
FROM Employees LEFT JOIN Employees AS Employees_1 ON Employees.[Employee No] = Employees_1.[Employee No]
WHERE (((Employees_1.Action)="term"))
GROUP BY Employees.[Employee No], Employees.Action, Employees.DDate
HAVING (((Employees.Action)="hire" Or (Employees.Action)="rehire"));
This query gives a list of all the hire events and the corresponding terms, with the days calculated for each. You can use a second query to sum the days by employee after you see how the 1st query turns out.
Note - this assumes that you have an alternating set of hire / termination events. If not, you may want to add some validations for this. Also, I think Date is reserved so I used DDate as the field name.
Firstly always make sure that you check the number of records to ensure that there is at least one before calling the MoveFirst method and iterating a recordset. It's better practice than relying on an error handler.
When you say that you want the number of days between the first two dates, are you referring to the dates in column "Date" in rows 0 and 1 or are you referring to dates in the same row in other columns that we can't see in the image? I assume that there are other date columns "OriginalHireDate" and "EffectiveDate".
Use the DateDiff function in VB6 to calculate the difference between dates. It's outlined here:
http://www.chennaiiq.com/developers/reference/visual_basic/functions/datediff.asp
intNumDays = DateDiff("d", CDate(tbl!EffectiveDate), orghrdt)
This will return the number of days between tbl!EffectiveDate and orghrdt. It will return a positive number if the latter is after the former and a negative number if the former is after the latter.
If you could clarify your table structure and my previous queries, I'll see whether I can help you further. You should be able to achieve what you what without the need to iterate through the whole recordset.

Grouping joined lookup values in Access [duplicate]

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.

VB.NET How to do changes to rows selected using LINQ?

I have the following code which selects some rows using LINQ depending on some conditions.
Dim cname
Dim Category
Dim cprice
Try
For i = 0 To mainDatatable.Rows.Count - 1
cname = mainDatatable.Rows(i)("cname")
Category = mainDatatable.Rows(i)("ccategory")
cprice = mainDatatable.Rows(i)("cprice")
Dim nameparts() As String = cname.Split(New String() {" "}, StringSplitOptions.None)
Dim query = From products In mainDatatable.AsEnumerable()
Where products.Field(Of Integer)("ccategory") = Category And products.Field(Of Double)("cprice") = cprice And products.Field(Of String)("cname").Contains(nameparts(0)) And products.Field(Of String)("cname").Contains(nameparts(1))
Select products
If query.Count > 1 Then
'then i want to do some changes to those rows
End If
Next
Catch ex As Exception
MsgBox(ex.Message)
End Try
The code is working perfectly but I need to do changes on the selected rows. I'm not sure how to do it, for example i have a column called gprice and I want it to set it to some value for all the selected rows and save changes to mainDatatable ,would aprreciate your help.
To make it clear, suppose the row count in the query is 6 then I want to make all the values for some column in the query = 1 and then update the values of the selected rows in the mainDatatable
------update
If you don't get it forget the above code and here is what I am trying to do
-Ii have a datatable it only have 2 columns product name and product price, now there are products with same name but different colors
-I want to group those products by pharsing the string name and check for other products with identical words in the name, only one word is allowed to differ which is the color,
-Also the price have to be the same between the products in that group and this is what the above LINQ code does and it does it perfectly
-My problem is i have added a third column called groupnumber, I want to add 1 for each product in the 1st group, 2 for each product in group 2nd and so on.