Adding unique data from Source Worksheet to Master Worksheet - vba

A row from the Source List Worksheet (SLW) columns (1, 2 & 3) needs pasted into the Master List Worksheet (MLW) columns (3, 4 & 5) [same order] if the unique ID number (SLW1 = MLW3) does NOT already exists in the "Master List" (same workbook).
My first Excel VBA project ever. So any and all advice/suggestions/corrections/short cuts would be great. This code is what I have fumbled creating. As you know, its not working.
Sub Transfer()
Dim SLR As Integer 'SourceList's Woksheets Last Row
Dim MLR As Integer 'MasterList's Woksheets Last Row
Dim SC As Integer 'SourceList Counting through the loop (ROW NUMBER)
Dim SR As Range 'SourceList A-C Row data
'(Source information 3 rows to be transfered)
Dim ID As Integer 'Unique code of Projects
Dim Found As Range
Sheets("SourceList").Activate
SLR = Cells(Rows.Count, "A").End(xlUp).Row
'Start loop to go through SourceList unique ID numbers
For SC = 2 To SLR
'Copy SourceList ID number into Variable "ID"
ID = Sheets("SourceList").Range(1, SC)
'Also, Save Range into Variable so it doesn't have to
'go back and forth between Worksheets
Set SR = Range(Cells(1, SC), Cells(3, SC))
Sheets("MasterList").Activate
Found = Columns("C:C").Find(What:=ID, After:=ActiveCell, LookIn:=xlFormulas, _
LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False).Activate
If Found Is Nothing Then
MLR = Cells(Rows.Count, "C").End(xlUp).Row + 1
Range(Cells(3, MLR)) = SR
SR.ClearContents
End If
Sheets("SourceList").Activate
Next SC
End Sub

Although I've posted a link for you to check out, I will post this solution which I've used before.
Sub ject()
Dim con As Object: Set con = CreateObject("ADODB.Connection")
Dim rec As Object: Set rec = CreateObject("ADODB.Recordset")
Dim datasource As String
datasource = ThisWorkbook.FullName ' returns the fullpath
Dim sconnect As String
sconnect = "Provider=Microsoft.ACE.OLEDB.12.0;" & _
"Data Source=" & datasource & ";" & _
"Extended Properties=""Excel 12.0;HDR=YES"";"
con.Open sconnect
Dim sqlstr As String
' This basically executes anti-join if you know SQL
sqlstr = "SELECT * "
sqlstr = sqlstr & "FROM [SWL$] e "
sqlstr = sqlstr & "LEFT JOIN [MWL$] u "
sqlstr = sqlstr & "ON e.ID = u.ID "
sqlstr = sqlstr & "WHERE u.ID IS NULL "
sqlstr = sqlstr & "AND e.ID IS NOT NULL;"
rec.Open sqlstr, con, 3, 1
' Dump data that meets your requirement
With Sheets("MWL")
Dim lr As Long
lr = .Range("D" & .Rows.Count).End(xlUp).Row + 1
.Range("D" & lr).CopyFromRecordset rec
End With
End Sub
Considerations:
Your SWL and MWL sheet data should start at Row 1 with headers.
Both should have the header name ID which contains the unique identifier. If not, you can adjust the code above.
So what the code does is access ADO (Active Data Objects) to be able to execute data comparison using SQL commands. It is way faster than the conventional Range to Range comparison (looping). I'm not sure if it is faster than Array to Array comparison but it is certainly easier to read and adjust once you get the hang of it. Anyways, this maybe a little bit too much at the moment (since you said it is your first project), but this is tried and tested and certainly works.
IMPORTANT: Notice the sconnect variable. You need to use the correct Connection String depending on the version of your Excel.

Related

VBA Copy Whole Column From SQL Recordset

I have a fixed data set of 6 rows being queried in SQL. I want to transfer the data in this query into Excel. My question is in two parts:
1 - Can I pull back multiple columns in one go or do I have to do it column by column? I have 17 columns and 6 rows. As this is transferred into my Excel template I'm looking to break this up into thirds so columns 1 to 7 go in one space 8 to 11 in another section and 12 to 17 in another section in Excel.
2 - When using my current code of:
Range("F2").Value = rst.Fields("ACCOUNT")
I only pull back a single row, is it possible to get the whole column (all 6 rows) or do I have to loop all 6 rows to get the full column?
Thanks in advance
Use .GetRows() method, it allows to set a number of records to retrieve, first record to begin, and single field name or ordinal position, or an array of field names or ordinal position numbers.
The below example shows how to get data from external Excel workbook into recordset, retrieve specified fields into array, and paste result array to the worksheet.
Option Explicit
Sub Test()
Dim sConnection As String
Dim sQuery As String
Dim oConnection As Object
Dim oRecordset As Object
Dim aData()
sConnection = _
"Provider=Microsoft.ACE.OLEDB.12.0;" & _
"User ID=Admin;" & _
"Data Source='" & ThisWorkbook.FullName & "';" & _
"Mode=Read;" & _
"Extended Properties=""Excel 12.0 Macro;"";"
sQuery = _
"SELECT * FROM [Sheet1$] " & _
"IN '" & ThisWorkbook.Path & "\Src1.xlsx' " & _
"[Excel 12.0;Provider=Microsoft.ACE.OLEDB.12.0;Mode=Read;Extended Properties='HDR=YES;'] " & _
"WHERE Country='UK';"
Set oConnection = CreateObject("ADODB.Connection")
oConnection.Open sConnection
Set oRecordset = oConnection.Execute(sQuery)
oRecordset.MoveFirst
aData = oRecordset.GetRows(, , Array("CustomerID", "ContactName"))
With ThisWorkbook.Sheets(1)
.Cells.Delete
Output2DArray .Cells(1, 1), WorksheetFunction.Transpose(aData)
.Cells.EntireColumn.AutoFit
End With
oConnection.Close
End Sub
Sub Output2DArray(oDstRng As Range, aCells As Variant)
With oDstRng
.Parent.Select
With .Resize( _
UBound(aCells, 1) - LBound(aCells, 1) + 1, _
UBound(aCells, 2) - LBound(aCells, 2) + 1)
.NumberFormat = "#"
.Value = aCells
End With
End With
End Sub
Also there is Src1.xlsx workbook containing Customers as data source in the same folder as this workbook:
The resulting worksheet is as follows, you can see there are CustomerID and ContactName fields only:

VB macro to identify different combinations of cells

I have 4 columns of data.
A is process step
C is Risk number
D is risk rating (high, moderate or low)
F is a risk moderator (control, monitor, test or blank).
I need to populate a presentation with the different combinations.
How many risks have controls and how many do not?
of the ones with controls, how many have monitors or tests, how many do not?
of the ones without controls how many have monitors or tests and how many do not?
within those different categories how many are high, moderate and low?
for the final box those without controls, monitors or tests, what process steps do the risks belong to?
Each risk effectively has a block occupied by column F containing mitigations. There can be several boxes with different items or they can be blank if there are no mitigators.
Below is code that identifies with or without controls but I am stuck on the next sections (counting the different risk ratings and breaking it further into with/without monitoring or testing.
Sub CountWithWithoutControlsv6()
Const RiskColumn As String = "C"
Const ControlColumn As String = "F"
Const StartAtRow As Long = 7
Dim r As Long
Dim ControlFound As Boolean
Dim control As Long
Dim WoControl As Long
Dim lastRow As Long
With ActiveSheet
lastRow = .Range(RiskColumn & .Rows.Count).End(xlUp).row
r = .Range(ControlColumn & .Rows.Count).End(xlUp).row
If lastRow < r Then
lastRow = r
End If
ControlFound = False
For r = StartAtRow To lastRow
If UCase(Trim(CStr(.Cells(r, ControlColumn).Value))) = "CONTROL" Then
ControlFound = True
End If
If Not IsEmpty(.Cells(r + 1, RiskColumn)) Then
'Store info for previous block each time we encounter a new "risk"
If ControlFound Then
control = control + 1
Else
WoControl = WoControl + 1
End If
ControlFound = False
End If
Next
'Store info for final "block"
If ControlFound Then
control = control + 1
Else
WoControl = WoControl + 1
End If
End With
MsgBox control & " = number of risks with controls, " & WoControl & " = number of risks without controls"
End Sub
I'd recommend you "flatten" your data so that column A contains the process step, C is Risk number, D is risk rating (high, moderate or low), F indicates whether a "control" risk moderator exists, G indicates whether a "monitor" risk moderator exists, and H indicates whether a "test" risk moderator exists.
So instead of having data such as
A C D F
Step1 Risk7 High control
monitor
test
Step2 Risk8 Low (blank)
you would instead have
A C D F G H
Step1 Risk7 High Yes Yes Yes
Step2 Risk8 Low No No No
Once in that format, it would be simple to use Excel formulae such as =COUNTIFS(F:F,"Yes",G:G,"Yes",H:H,"No").
If you followed your current approach to this problem you would ultimately achieve your goals - and the code itself would be straightforward to write. However, it would take an awfully long time to write to cover all of your cases and would be mind-numbingly tedious to write. (That you've only produced a few lines of what is, in all honesty, quite a trivial task is probably the reason for your downvote.)
However, before you launch into the remainder of the code, perhaps you would consider using ADO. It's ideally suited to this task and could complete your project in just a few lines. I've no desire to solve your problem for you, but I would like to offer some code to get you started together with a couple of query samples. I've used early binding which means I've referenced the Microsoft ActiveX Data Objects 6.1 Library (Tools ~> References...), but you could late bind if you wish.
Have a look at the code below and see if you could use it:
Dim conn As ADODB.Connection
Dim cmd As ADODB.Command
Dim rs As ADODB.Recordset
Dim connStr As String
Dim sqlStr As String
Dim tableName As String
Dim procField As String
Dim numField As String
Dim levelField As String
Dim controlField As String
connStr = "Provider=Microsoft.ACE.OLEDB.12.0;" & _
"Data Source='C:\Users\User\Documents\StackOverflow\ADOexample.xlsm';" & _
"Extended Properties=""Excel 12.0;HDR=YES;"";"
'Define the table and field names
With Sheet1 'change to your sheet
tableName = .Name
procField = CStr(.Cells(1, "A").Value)
numField = CStr(.Cells(1, "C").Value)
levelField = CStr(.Cells(1, "D").Value)
controlField = CStr(.Cells(1, "F").Value)
End With
'Open the connection
Set conn = New ADODB.Connection
conn.Open connStr
'Some sample queries
'Sample 1
Set rs = New Recordset
sqlStr = "SELECT * FROM [" & tableName & "$]" & _
" WHERE [" & controlField & "] IS NOT NULL;"
rs.Open sqlStr, conn
Do While Not rs.EOF
Debug.Print rs.Fields(numField)
rs.MoveNext
Loop
'Sample 2
Set rs = New Recordset
sqlStr = "SELECT * FROM [" & tableName & "$]" & _
" WHERE [" & levelField & "] = 'H'" & _
" AND ([" & controlField & "] = 'MONITOR'" & _
" OR [" & controlField & "] = 'TEST');"
rs.Open sqlStr, conn
Do While Not rs.EOF
Debug.Print rs.Fields(numField)
rs.MoveNext
Loop
'Clean up objects
rs.Close
Set rs = Nothing
conn.Close
Set conn = Nothing

Extracting data from over a million records

I have an Excel file in which I have set up a connection with an Access database. In the Excel file I have a list of names in column A, and I want to search these names in the Access database and return back two fields from that database. I need to do this for around 200-300 names.
Here is my code:
N = Cells(Rows.Count, "A").End(xlUp).Row
Application.DisplayAlerts = False
strDB = ThisWorkbook.Path & "file.accdb"
Set objConnection = New ADODB.Connection
objConnection.Open "Provider=Microsoft.ACE.OLEDB.12.0; Data Source=" & strDB
For i = 2 To N
Dim rstTable As ADODB.Recordset
Set rstTable = New ADODB.Recordset
lookup = Range("A" & i).Value
strSQL = "SELECT NAME1,NAME2 FROM DATA WHERE [Field2]= """ & lookup & """;"
'Store query output
rstTable.Open Source:=strSQL, ActiveConnection:=objConnection
'Paste results to Transactions sheet
Worksheets("Sheet1").Range("B" & i).CopyFromRecordset rstTable
'Close the record set & connection
rstTable.Close
objConnection.Close
Next i
This works (kindof) but it takes an extremely long time and randomly crashes. Any ideas how to improve this?
Making sure there is a key on the lookup field will help. I would suggest making a copy of the workbook and test external data from Access or MS Query to see if that gives a performance gain over VBA.
When using MS Query or data from Access, you can modify the command text in the connection properties and use ? in the where clause to specify the parameter in the worksheet (so you don't lose that functionality).
I modified your SQL statement. Replace the Where [Field2] = "xxx" by Where [Field2] IN ("xxx", "yyy", "zzz").
N = Cells(Rows.Count, "A").End(xlUp).Row
Application.DisplayAlerts = False
strDB = ThisWorkbook.Path & "file.accdb"
Set objConnection = New ADODB.Connection
objConnection.Open "Provider=Microsoft.ACE.OLEDB.12.0; Data Source=" & strDB
For i = 2 To N
lookup = lookup & "'" & Range("A" & i).Value & "', "
Next i
lookup = left(lookup, len(lookup) - 2)
Dim rstTable As ADODB.Recordset
Set rstTable = New ADODB.Recordset
strSQL = "SELECT NAME1,NAME2 FROM DATA WHERE [Field2] IN (" & lookup & ");"
'Store query output
rstTable.Open Source:=strSQL, ActiveConnection:=objConnection
'Paste results to Transactions sheet
Worksheets("Sheet1").Range("B" & i).CopyFromRecordset rstTable
'Close the record set & connection
rstTable.Close
objConnection.Close
You close the connection after the first iteration, so your next iteration -- which does not have code to open the connection -- would fail. So you should move the objConnection.Close out of the loop.
But, even then, to execute the same kind of query over and over again, just with a different argument, can be done in one go, using the IN (...) syntax:
' Declare all your variables
Dim N As Long
Dim strDB As String
Dim objConnection As ADODB.Connection
Dim rstTable As ADODB.Recordset
Dim strSQL As String
N = Cells(Rows.Count, "A").End(xlUp).Row
Application.DisplayAlerts = False
strDB = ThisWorkbook.Path & "file.accdb"
Set objConnection = New ADODB.Connection
objConnection.Open "Provider=Microsoft.ACE.OLEDB.12.0; Data Source=" & strDB
' collect the values in comma-separated string
lookup = ""
For i = 2 To N
lookup = lookup & ",""" & Range("A" & i).Value & """"
Next i
' Chop off the first comma
lookup = Mid(lookup, 2)
' Perform a single query, but also select the Field2 value
Set rstTable = New ADODB.Recordset
strSQL = "SELECT Field2, NAME1,NAME2 FROM DATA WHERE [Field2] IN (" & lookup & ");"
' query output
rstTable.Open Source:=strSQL, ActiveConnection:=objConnection
' Retrieve values
While Not rstTable.EOF
lookup = rstTable.Fields(0).Value
' Locate in which row to put the result
For i = 2 To N
If lookup = Range("A" & i).Value Then
Range("B" & i).Value = rstTable.Fields(1).Value
Range("C" & i).Value = rstTable.Fields(2).Value
End If
Next i
rstTable.MoveNext
Loop
' Close the record set & connection
rstTable.Close
objConnection.Close
You can do what you described, but I think it's far more efficient to do this in Access itself. Just create a table with your names and do an Inner Join to the table you want to find 2 fields. Should take less than a minute, and probably less than 30 seconds.

Shift Excel Cell down after executing a macro for second time

I've written a (below)macro that pulls data from the sql server 2008 r2. My issue is when the user runs the macro for first time by entering Jobnumber (say J0001) excel puts data on the spreadsheet starting from cell "A1" which is fine. The issue here is, when the user runs the macro for the second time by entering the jobnumber (say J0002), excel puts the data for Jobnumber (J0002) on cell "A1" and shifts the cells for J0001(first job) to cell "F" instead of moving down. How can I shift the previous entry down in the spreadsheet with the latest entry on top?
Here is my macro and attachment:
Sub Task()
Dim sqlstring As String
Dim connstring As String
Dim Strcode As String
Strcode = Trim(InputBox("Please enter a Job #", "Task history"))
sqlstring = "select distinct m.JobNumber , cast(m.ExpectedDate as DATE) 'Ship Date' ,m.CustLongName 'Customer' & _
" from ArchiveJobHeader m left join AuxiliaryInfoFile af (nolock) on af.jobnumber=m.jobnumber & _
" where m.JobNumber = '" & Trim(Strcode) & "'" & _
" order by 'Resulttime'"
connstring = "ODBC;DSN=SQLDSN;UID=test;PWD=test123"
Dim thisQT As QueryTable
Set thisQT = ActiveSheet.QueryTables.Add(Connection:=connstring, Destination:=Range("a1", "a1000"))
thisQT.BackgroundQuery = False
thisQT.Sql = sqlstring
thisQT.Refresh
End Sub][1]
If you incorporate a lastRow check and then assign a variable the Next Row number, you can concatenate your Range and it will be a new row every time.
Dim lastRow As Long, nextRow As Long
lastRow = Sheets("Sheet Name").Range("A" & Rows.count).End(xlUp).row
nextRow = lastRow + 1
Then when you set your Range, concatenate the variable with the string.
Set thisQT = ActiveSheet.QueryTables.Add( _
Connection:=connstring, _
Destination:=Range("A" & nextRow))
I'm not sure what you are doing with row 1000 as shown in your question. But this is the idea of using a variable with your normal Range Address.
You could have something like this:
Sub a()
'Must set Reference to "Microsoft ActiveX Data Objects 2.8 Library"
Dim ws As Worksheet
Dim n As Long ' Row To Write In
Dim sql As String
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim i As Long
Set ws = ThisWorkbook.Worksheets("Tab Name")
'Assuming you already have Headings in row 1 _
and start adding records from "A2" down...
n = ws.Range("A10000").End(xlUp).row + 1
'you sql string above is missing some double quotes...
sql = "select distinct m.JobNumber, cast(m.ExpectedDate as DATE) 'Ship Date', m.CustLongName 'Customer'" & _
" from ArchiveJobHeader m left join AuxiliaryInfoFile af (nolock) on af.jobnumber=m.jobnumber" & _
" where m.JobNumber = '" & Trim(Strcode) & "'" & _
" order by 'Resulttime'"
Set cn = New ADODB.Connection
' using an ODBC DSN... as in <http://msdn.microsoft.com/en-us/library/ms807027.aspx>
cn.Open "SQLDSN", "test", "test123"
Set rs = cn.Execute(sql) ' likely, in your case, to return one record only, _
so you are on there right away
For i = 0 To rs.Fields.Count
ws.Cells(n, i + 1) = rs(i)
Next
rs.Close
cn.Close
End Sub
You would need to put more work into this, I am afraid, but this is the direction you may consider.

Looking up Access database in Excel

I want to do something very simple: I have an Access database with one table mapping thousands of product IDs to product information fields. In an Excel worksheet, the user types in perhaps 100 product IDs in the first column. I need for the remaining columns to pull in information from the Access database for the corresponding IDs. Specifically:
if I use MS-Query, it seems to insist on the output being a table. I simply want the output to be inside a single cell. Preferably, a formula that involves a SQL-type query.
I don't want any of the values to be updated automatically, but rather want all the columns updated only on user demand (the user could either choose refresh through a menu, or a VBA-based refresh button on the sheet is fine as well).
I'm thinking this would be a straightforward use case, but it seems surprisingly hard to find a solution. Thank you in advance!
Working from Excel, you can use ADO to connect to a database. For Access and Excel 2007/2010, you might:
''Reference: Microsoft ActiveX Data Objects x.x Library
Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
''Not the best way to refer to a workbook, but convenient for
''testing. it is probably best to refer to the workbook by name.
strFile = ActiveWorkbook.FullName
''Connection string for 2007/2010
strCon = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & strFile _
& ";Extended Properties=""Excel 12.0 xml;HDR=Yes;"";"
cn.Open strCon
''In-line connection string for MS Access
scn = "[;DATABASE=Z:\Docs\Test.accdb]"
''SQL query string
sSQL = "SELECT a.Stuff, b.ID, b.AText FROM [Sheet5$] a " _
& "INNER JOIN " & scn & ".table1 b " _
& "ON a.Stuff = b.AText"
rs.Open sSQL, cn
''Write returned recordset to a worksheet
ActiveWorkbook.Sheets("Sheet7").Cells(1, 1).CopyFromRecordset rs
Another possibility returns a single field from MS Access. This example uses late binding, so you do not need a library reference.
Dim cn As Object
Dim rs As Object
Dim strFile As String
Dim strCon As String
Dim strSQL As String
Dim s As String
Dim i As Integer, j As Integer
strFile = "z:\docs\test.accdb"
strCon = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & strFile
''Late binding, so no reference is needed
Set cn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
cn.Open strCon
''Select a field based on a numeric reference
strSQL = "SELECT AText " _
& "FROM Table1 a " _
& "WHERE ID = " & Sheets("Sheet7").[A1]
rs.Open strSQL, cn, 3, 3
Sheets("Sheet7").[B1] = rs!AText
OK, this may seem a bit lengthy - Create an Excel-table - in the first row (from column two) you have the Fieldnames Exactly as you have them in the access-table, in the first column you have the desired key-values (e.g. CustomerIDs).
When you run the macro it fills in what it finds...
Sub RefreshData()
Const fldNameCol = 2 'the column with the first fieldname in it'
Dim db, rst As Object
Set db = DBEngine.workspaces(0).OpenDatabase("C:\path\to\db\name.accdb")
Set rst = db.openrecordset("myDBTable", dbOpenDynaset)
Dim rng As Range
Dim showfields() As Integer
Dim i, aRow, aCol As Integer
ReDim showfields(100)
Set rng = Me.Cells
aRow = 1 'if you have the fieldnames in the first row'
aCol = fldNameCol
'***** remove both '' to speed things up'
'On Error GoTo ExitRefreshData'
'Application.ScreenUpdating = False'
'***** Get Fieldnames from Excel Sheet'
Do
For i = 0 To rst.fields.Count - 1
If rst.fields(i).Name = rng(aRow, aCol).Value Then
showfields(aCol) = i + 1
Exit For
End If
Next
aCol = aCol + 1
Loop Until IsEmpty(rng(aRow, aCol).Value)
ReDim Preserve showfields(aCol - 1)
'**** Get Data From Databasetable'
aRow = 2 'startin in the second row'
aCol = 1 'key values (ID) are in the first column of the excel sheet'
Do
rst.FindFirst "ID =" & CStr(rng(aRow, aCol).Value) 'Replace ID with the name of the key field'
If Not rst.NoMatch Then
For i = fldNameCol To UBound(showfields)
If showfields(i) > 0 Then
rng(aRow, i).Value = rst.fields(showfields(i) - 1).Value
End If
Next
End If
aRow = aRow + 1
Loop Until IsEmpty(rng(aRow, aCol).Value)
ExitRefreshData:
Application.ScreenUpdating = True
On Error GoTo 0
End Sub
And if you dont want your fieldnames in the excel sheet replace the paragraph "Get Fieldnames From Excelsheet" with this:
fieldnames = Split("field1name", "", "", "field3name")
For j = 0 To UBound(fieldnames) - 1
For i = 0 To rst.fields.Count - 1
If rst.fields(i).Name = fieldnames(j) Then
showfields(j + fldNameCol) = i + 1
Exit For
End If
Next
Next
ReDim Preserve showfields(UBound(fieldnames) - 1 + fldNameCol)
and add this at the top
dim j as integer
dim fieldnames