
Picture this: your company's sales data lives in a SQL Server database, your inventory is tracked in an Access file, and every Monday morning you spend two hours manually exporting CSVs, pasting them into Excel, and reformatting everything before you can even begin your actual analysis. You know there has to be a better way — and there is. With a few dozen lines of VBA, you can make Excel reach directly into any database, pull exactly the data you need, and drop it into a worksheet automatically, every time you open the file or click a button.
This lesson teaches you how to connect Excel to external databases using VBA and a technology called ADO (ActiveX Data Objects). You'll write code that opens a live connection to a database, fires SQL queries against it, and processes the results — all without touching a CSV file or copying and pasting anything. By the end, you'll understand not just how to write this code, but why it works, so you can adapt it to whatever database your organization uses.
What you'll learn:
This lesson assumes you know the basics of VBA: you can open the Visual Basic Editor (Alt + F11), write a Sub procedure, declare variables, and use basic loops and conditionals. You should also have a passing familiarity with SQL — specifically the SELECT statement — though we'll explain every query we write. If you've used Excel's built-in Power Query to pull data before, you'll recognize some of the concepts, but the approach here is entirely code-driven.
Before writing a single line of code, you need a mental model of what's actually happening when Excel talks to a database.
Think of ADO — ActiveX Data Objects — as a universal translator. Databases speak many different dialects: SQL Server has its own protocol, Microsoft Access has its own file format, Oracle has yet another interface. Without a translator, your VBA code would need to know each of those languages individually. ADO sits in the middle and handles the translation. You write VBA code that talks to ADO; ADO figures out how to talk to the specific database you've pointed it at.
ADO works through three primary objects you'll use constantly:
Connection — This represents the open pipe between your code and the database. Opening a connection is like picking up a phone and dialing. Nothing can happen until the connection is established.
Recordset — Once you're connected and you've asked for data, the results come back in a Recordset object. Think of a Recordset as a virtual table that lives in memory — rows and columns, just like a worksheet, but existing only in code.
Command — This object lets you send more sophisticated instructions to the database, including parameterized queries. For simple work, you can skip Command and query directly through the Connection, but for production automation you'll want Command for the safety it provides.
ADO doesn't come pre-loaded in VBA. You need to tell Excel to include the ADO library before your code can use it. This is called adding a reference — you're pointing VBA at a collection of code that someone else wrote, so you can use it in your own projects.
Open the Visual Basic Editor with Alt + F11. In the menu bar, click Tools, then References. A dialog box will appear with a long list of available libraries. Scroll down until you find Microsoft ActiveX Data Objects 6.1 Library (the version number may vary slightly depending on your Office installation — pick the highest 6.x version you see). Check the box next to it and click OK.
You'll know it worked if you can type Dim cn As ADODB.Connection in a module without getting a compile error.
Why a reference instead of late binding? You can use ADO without setting a reference, using a technique called late binding where you write
CreateObject("ADODB.Connection")instead of declaring typed variables. Late binding works, but you lose IntelliSense (VBA's autocomplete), which makes development painful. For learning, use the reference. For distributing code to machines where you can't control installed libraries, late binding is more portable.
Let's start with the simplest possible case: connecting to a local Microsoft Access database file (.accdb). Access is common in small business environments, and its connection string is straightforward — no server addresses, no authentication.
Suppose you have an Access database at C:\Data\SalesDB.accdb containing a table called Orders with columns OrderID, CustomerName, OrderDate, and TotalAmount.
Here's the complete procedure to pull all orders into your active worksheet:
Sub ImportOrdersFromAccess()
' Declare our ADO objects
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim ws As Worksheet
Dim i As Integer
' Point to the sheet where data will land
Set ws = ThisWorkbook.Sheets("Orders")
ws.Cells.Clear ' Clear any existing data
' Create and open the connection
Set cn = New ADODB.Connection
cn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Data\SalesDB.accdb;"
' Create and open the recordset with a SQL query
Set rs = New ADODB.Recordset
rs.Open "SELECT OrderID, CustomerName, OrderDate, TotalAmount FROM Orders ORDER BY OrderDate DESC", cn
' Write the column headers from the recordset field names
For i = 0 To rs.Fields.Count - 1
ws.Cells(1, i + 1).Value = rs.Fields(i).Name
Next i
' Dump all the data starting at row 2
ws.Cells(2, 1).CopyFromRecordset rs
' Clean up: always close objects and release memory
rs.Close
cn.Close
Set rs = Nothing
Set cn = Nothing
MsgBox "Import complete! " & ws.UsedRange.Rows.Count - 1 & " records loaded."
End Sub
Let's walk through the key moments:
The connection string is the most important line in this code: "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Data\SalesDB.accdb;". A connection string is just a semicolon-separated list of key-value pairs that tells ADO which driver to use (Provider) and where the database lives (Data Source). The ACE.OLEDB.12.0 provider is what handles Access files on modern Windows machines.
rs.Fields.Count - 1 might look odd. ADO counts fields starting at zero, so a recordset with four fields has indices 0, 1, 2, and 3 — hence Count - 1 as the upper bound.
CopyFromRecordset is a genuinely useful shortcut. Instead of looping through every row and column manually, this single method dumps the entire recordset into the spreadsheet starting at whatever cell you specify. It's dramatically faster than row-by-row code, especially with thousands of records.
SQL Server connections look different because there's no local file to point at — you're connecting across a network (or locally) to a running database engine. The connection string needs more information: the server name, the database name, and authentication details.
Sub ImportFromSQLServer()
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim ws As Worksheet
Dim sql As String
Set ws = ThisWorkbook.Sheets("SalesData")
ws.Cells.Clear
Set cn = New ADODB.Connection
' Windows Authentication (uses your Windows login - preferred in corporate environments)
cn.Open "Provider=SQLOLEDB;Data Source=CORP-SQL01;Initial Catalog=SalesDatabase;Integrated Security=SSPI;"
' Build the SQL query as a string variable for readability
sql = "SELECT " & _
" s.SalesRepName, " & _
" p.ProductCategory, " & _
" SUM(o.TotalAmount) AS TotalRevenue, " & _
" COUNT(o.OrderID) AS OrderCount " & _
"FROM Orders o " & _
"INNER JOIN SalesReps s ON o.SalesRepID = s.SalesRepID " & _
"INNER JOIN Products p ON o.ProductID = p.ProductID " & _
"WHERE o.OrderDate >= '2024-01-01' " & _
"GROUP BY s.SalesRepName, p.ProductCategory " & _
"ORDER BY TotalRevenue DESC"
Set rs = New ADODB.Recordset
rs.Open sql, cn
' Write headers
Dim i As Integer
For i = 0 To rs.Fields.Count - 1
ws.Cells(1, i + 1).Value = rs.Fields(i).Name
ws.Cells(1, i + 1).Font.Bold = True
Next i
' Write data
ws.Cells(2, 1).CopyFromRecordset rs
rs.Close
cn.Close
Set rs = Nothing
Set cn = Nothing
End Sub
Notice that the SQL here is a real aggregation query — joining three tables, grouping by rep and category, summing revenue. You can send any valid SQL that your database supports, including complex joins, subqueries, and window functions. Excel doesn't care what the query looks like; it just receives the results.
Windows vs. SQL Server Authentication: The connection string above uses
Integrated Security=SSPI, which means "log in using my Windows credentials." This is the preferred approach in corporate environments because nobody stores passwords in VBA code. If your database requires a SQL Server login instead, replace that withUser ID=yourlogin;Password=yourpassword;— but understand that anyone who can open your VBA editor can read those credentials.
Hard-coding dates and filter values into your SQL string is a maintenance nightmare. What happens when your manager wants last year's data instead of this year's? You don't want to edit VBA code every time. The solution is to make your query accept parameters — values that your code supplies at runtime.
The safe way to do this is with the ADODB.Command object and parameterized queries, which also protect against SQL injection (where malicious input could alter your query's behavior).
Sub ImportOrdersByDateRange()
Dim cn As ADODB.Connection
Dim cmd As ADODB.Command
Dim rs As ADODB.Recordset
Dim ws As Worksheet
Dim startDate As String
Dim endDate As String
' Get date range from named cells on a "Parameters" sheet
startDate = ThisWorkbook.Sheets("Parameters").Range("StartDate").Value
endDate = ThisWorkbook.Sheets("Parameters").Range("EndDate").Value
Set ws = ThisWorkbook.Sheets("Results")
ws.Cells.Clear
' Open connection
Set cn = New ADODB.Connection
cn.Open "Provider=SQLOLEDB;Data Source=CORP-SQL01;Initial Catalog=SalesDatabase;Integrated Security=SSPI;"
' Set up the Command object
Set cmd = New ADODB.Command
cmd.ActiveConnection = cn
cmd.CommandType = adCmdText
' Use ? as placeholders for parameters
cmd.CommandText = "SELECT OrderID, CustomerName, OrderDate, TotalAmount " & _
"FROM Orders " & _
"WHERE OrderDate BETWEEN ? AND ? " & _
"ORDER BY OrderDate"
' Append parameters in the same order as the ? placeholders
cmd.Parameters.Append cmd.CreateParameter("StartDate", adDate, adParamInput, , CDate(startDate))
cmd.Parameters.Append cmd.CreateParameter("EndDate", adDate, adParamInput, , CDate(endDate))
' Execute and get a recordset back
Set rs = cmd.Execute
' Write to sheet
Dim i As Integer
For i = 0 To rs.Fields.Count - 1
ws.Cells(1, i + 1).Value = rs.Fields(i).Name
ws.Cells(1, i + 1).Font.Bold = True
Next i
ws.Cells(2, 1).CopyFromRecordset rs
rs.Close
cn.Close
Set rs = Nothing
Set cmd = Nothing
Set cn = Nothing
MsgBox "Loaded orders from " & startDate & " to " & endDate
End Sub
The pattern here — reading parameters from named cells on a Parameters sheet — is one of the most practical patterns in Excel automation. Your users change the dates in clearly labeled cells, click a button, and get fresh data. They never touch any code.
The ? placeholders in the SQL are replaced, in order, by the parameters you append to cmd.Parameters. ADO handles the escaping and type conversion, so a date value gets formatted correctly for your specific database without you worrying about it.
Querying data is only half the picture. Sometimes you want Excel to serve as a data entry interface — users fill in a table, click a button, and rows get inserted into a database. Here's how to write data from a worksheet back into a database table.
Sub ExportNewOrdersToDatabase()
Dim cn As ADODB.Connection
Dim cmd As ADODB.Command
Dim ws As Worksheet
Dim lastRow As Long
Dim i As Long
Set ws = ThisWorkbook.Sheets("NewOrders")
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
If lastRow < 2 Then
MsgBox "No data found to export."
Exit Sub
End If
Set cn = New ADODB.Connection
cn.Open "Provider=SQLOLEDB;Data Source=CORP-SQL01;Initial Catalog=SalesDatabase;Integrated Security=SSPI;"
Set cmd = New ADODB.Command
cmd.ActiveConnection = cn
cmd.CommandType = adCmdText
cmd.CommandText = "INSERT INTO Orders (CustomerName, OrderDate, TotalAmount, SalesRepID) " & _
"VALUES (?, ?, ?, ?)"
' Loop through each data row (row 2 onward, assuming row 1 is headers)
For i = 2 To lastRow
' Clear previous parameters and append fresh ones for this row
cmd.Parameters.Delete 0 ' Remove first parameter and repeat
' Cleaner approach: recreate all parameters each iteration
Dim pCustomer As ADODB.Parameter
Dim pDate As ADODB.Parameter
Dim pAmount As ADODB.Parameter
Dim pRep As ADODB.Parameter
Set pCustomer = cmd.CreateParameter("CustomerName", adVarChar, adParamInput, 100, ws.Cells(i, 1).Value)
Set pDate = cmd.CreateParameter("OrderDate", adDate, adParamInput, , CDate(ws.Cells(i, 2).Value))
Set pAmount = cmd.CreateParameter("TotalAmount", adCurrency, adParamInput, , ws.Cells(i, 3).Value)
Set pRep = cmd.CreateParameter("SalesRepID", adInteger, adParamInput, , ws.Cells(i, 4).Value)
' Clear existing params and re-add
Do While cmd.Parameters.Count > 0
cmd.Parameters.Delete 0
Loop
cmd.Parameters.Append pCustomer
cmd.Parameters.Append pDate
cmd.Parameters.Append pAmount
cmd.Parameters.Append pRep
cmd.Execute
Next i
cn.Close
Set cmd = Nothing
Set cn = Nothing
MsgBox lastRow - 1 & " orders exported to database successfully."
End Sub
Wrap inserts in a transaction for production use. If you're inserting 500 rows and row 347 fails, do you want the first 346 committed? Probably not. Wrap your loop with
cn.BeginTransbefore it starts andcn.CommitTransafter it ends. If an error fires, callcn.RollbackTransto undo everything. This keeps your database in a consistent state.
The most dangerous thing an automation script can do is fail quietly. If your connection drops halfway through an import and VBA doesn't handle the error, you might end up with half a dataset in your sheet — and no idea anything went wrong. Here's a proper error-handling skeleton for any database procedure:
Sub RobustDataImport()
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
On Error GoTo ErrorHandler
Set cn = New ADODB.Connection
cn.Open "Provider=SQLOLEDB;Data Source=CORP-SQL01;Initial Catalog=SalesDatabase;Integrated Security=SSPI;"
Set rs = New ADODB.Recordset
rs.Open "SELECT * FROM Orders WHERE YEAR(OrderDate) = 2024", cn
ThisWorkbook.Sheets("Results").Cells(2, 1).CopyFromRecordset rs
' Normal cleanup
rs.Close
cn.Close
Set rs = Nothing
Set cn = Nothing
Exit Sub ' <-- Critical: skip the error handler on success
ErrorHandler:
MsgBox "Database error: " & Err.Number & " - " & Err.Description, vbCritical
' Safe cleanup even after an error
If Not rs Is Nothing Then
If rs.State = adStateOpen Then rs.Close
Set rs = Nothing
End If
If Not cn Is Nothing Then
If cn.State = adStateOpen Then cn.Close
Set cn = Nothing
End If
End Sub
The Exit Sub before ErrorHandler: is essential — without it, your code falls through into the error handler even when everything worked correctly. The cleanup code in the error handler checks State before trying to close objects, because calling .Close on something that's already closed raises another error.
Set up a practice scenario and work through all three layers:
Part 1 — Read: Create a Microsoft Access database called Practice.accdb with a table called Employees containing columns EmployeeID (AutoNumber), FirstName (Text), LastName (Text), Department (Text), and Salary (Currency). Add at least ten sample rows. Write a VBA procedure that connects to this database and pulls all employees in the "Marketing" department into a sheet called "Marketing Team."
Part 2 — Filter dynamically: Add a cell named FilterDept to a sheet called "Parameters." Modify your procedure to read the department name from that cell and use a parameterized query so the user can change the department without touching any code.
Part 3 — Write back: Add a second sheet called "NewHires" with columns matching your Employees table (minus the AutoNumber ID). Write a procedure that reads rows from that sheet and inserts them into your Access database using an INSERT statement. Test it by adding three new employees to the sheet and running the export.
"Provider cannot be found" error on the connection string. This almost always means the required database driver isn't installed on the machine. For Access files, you need the Microsoft Access Database Engine redistributable. For SQL Server, the SQLOLEDB provider ships with Windows, but older machines may need an updated driver. Check what's installed and match your provider name exactly.
The query returns zero rows, but you know data exists. Run your SQL query directly in the database management tool first. If it works there, the issue is in how VBA is constructing the string. Add a Debug.Print sql line before executing and read the query in the Immediate Window — you'll usually spot a missing space or misquoted value immediately.
Date comparisons return wrong results or no results. Dates are notorious in cross-database queries. Access expects dates wrapped in # symbols (#2024-01-01#), while SQL Server uses quoted strings ('2024-01-01'). Using parameterized queries with adDate type parameters sidesteps this problem entirely — let ADO format the date correctly for your database.
"Operation is not allowed when the object is closed" runtime error. You're trying to read from a recordset that either never opened successfully (your query had an error) or was already closed. Add an If Not rs Is Nothing And rs.State = adStateOpen Then check before accessing recordset data.
Memory grows over time in a scheduled process. If your macro runs on a timer or in a loop, always ensure Set rs = Nothing and Set cn = Nothing execute — even on errors. ADO objects hold database connections open until they're explicitly released or garbage collected, and leaked connections will eventually exhaust your database server's connection pool.
You've now learned how to use VBA and ADO to make Excel a genuine database client rather than a manual data dumping ground. The core pattern — open a connection with a provider-specific connection string, execute SQL through a Recordset or Command object, write results to a worksheet, and clean up — applies to virtually every relational database you'll encounter in a professional setting.
The specific skills you've built:
CopyFromRecordset for fast data transferADODB.Command with parameterized queries for dynamic, safe filteringWhere to go next: Once you're comfortable with basic querying, explore stored procedures — pre-written SQL that lives on the database server. Calling a stored procedure from VBA instead of sending raw SQL improves performance, centralizes business logic, and removes SQL text from your Excel files entirely. From there, look into refreshable data models where you combine this ADO technique with Excel Tables to build dashboards that self-update with a single button press.
The deeper you go into this, the more you'll realize that Excel isn't just a spreadsheet — it's a capable front-end for any data system your organization runs, and VBA is what connects those two worlds together.
Learning Path: Advanced Excel & VBA