RustProof Labs: blogging for education (logo)
My book Mastering PostGIS and OpenStreetMap is available!

Excel and VBA for SQL Server Reporting

By Ryan Lambert -- Published April 03, 2015

Let’s imagine that you have a database that contains some data in it that you want to share. Maybe you’ve inherited a SQL Server database and want to provide some basic reports to other users. Does that scenario sounds somewhat familiar?

Add Data to SQL

If you aren’t interested in creating the test database, feel free to skip this section by scrolling down until you see the chart. My feelings won’t be hurt, I promise!

To create a report in Excel using data in SQL, so we need some data to query against, and before we can have data, we need tables. Let’s create an employee and sales table.

CREATE TABLE dbo.employee
    name VARCHAR(200) NOT NULL,
    title VARCHAR(100) NOT NULL

CREATE TABLE dbo.sales
    employeeID INT REFERENCES dbo.employee (employeeID),
    amount NUMERIC (6,2) NOT NULL,

Now insert some data. The first part is a straightforward INSERT statement to create the employees of this family ran sales business. The second part loops through and inserts 500 sales records to random employees. Change the @loop variable if you want to add more or fewer records, keep in mind more records will take more time.

INSERT INTO dbo.employee (name, title)
    ('Bob Smith','Sales Manger'),
    ('Jane Smith','Sales Associate'),
    ('Susan Doe','Sales Associate'),
    ('Suzy Salamander','Sales Associate'),
    ('Walker Texas-Ranger','Sales Cowboy'),
    ('John Doe','Sales Associate')

DECLARE @loop INT = 500;
DECLARE @employeeID INT;
DECLARE @amount NUMERIC(6,2);

WHILE @i < @loop
    SELECT @employeeID = employeeID FROM dbo.employee ORDER BY NEWID();
    SELECT @amount = (RAND() * 10) + (RAND() * 100) + (RAND() * 150);
    INSERT INTO dbo.sales (employeeID, amount)
        VALUES (@employeeID, @amount);
    SET @i = @i + 1;

Query Data

Now we have some data in our sales and employee tables so let’s run a query to get sales by employee.

SELECT, e.title, SUM(s.amount) AS totalSales
    FROM dbo.sales s
    INNER JOIN dbo.employee e
        ON s.employeeID = e.employeeID
    GROUP BY, e.title
    ORDER BY SUM(s.amount) DESC

You should see something like the following results, the totalSales will obviously vary.

SQL Employee Sales query results screenshot

Now you could copy and paste those results in Excel and make a chart off the data manually and you might end up with something that looks like this:

Sales by Employee Bar Chart

We could do that manually each day, week, or hour that they want the report updated, or we could automate away a lot of that headache.

Manual Processes Suck

I don’t like copying and pasting things. In my opinion, that’s low hanging fruit for automation! Below I include some VBA to help automate reports from SQL. You don’t have to “know” VBA to get this to work, the changes you have to make to the code should be minimal.

In Excel 2007 you now must save the file as a “Macro Enabled Workbook” that gets the file extensions “.xlsm”. You can find this option when you do “File > Save As”. In older versions of Excel they’re still stored in the same .xls files as regular files. To access the VBA editor in any version of Excel hit “Alt + F11″.

Don’t forget References

Because I know we’re going to be using the ADODB library, we need to add that to VBA’s references. This is done in the VBA editor under “Tools > References”. In the dialog that comes up, find and check the “Microsoft ActiveX Data Objects 2.8 Library” then click “Ok”. Without this, VBA will throw an error on the first line that references ADODB.

On the left hand side of the VBA editor, Right click on the VBA Project that contains your file’s name in it then select “Insert > Module”. Your “project” now has a module named “Module1″. Rename the module to “SQL” in the name field of the properties pane shown at the bottom of the screenshot.

VBA Modules - SQL Screenshot

Now copy and paste the VBA code below into the code editor of your SQL module. Update the values for “db” and “srv” to match your environment. If you don’t know what values to put there, ask your database administrator. This assumes you’re using Windows Authentication to control access to MS SQL Server. Check out the code, I’ll explain after.

Option Explicit

Sub RunQry(dest As Range, SQL As String)

    Dim conn As New ADODB.Connection
    Dim cmd As New ADODB.Command
    Dim rs As ADODB.Recordset
    Dim connString  As String
    Dim db As String
    Dim srv As String

    ' Update the values between the double quotes (" ") to match your database server and name.
    db = "YourDatabaseName"

    connString = "Provider=SQLOLEDB;Initial Catalog=" & db & ";Data Source =" & srv & ";Integrated Security=SSPI"
    conn.Open connString
    Set cmd.ActiveConnection = conn

    cmd.CommandText = SQL
    cmd.CommandType = adCmdText
    Set rs = cmd.Execute
    dest.CopyFromRecordset rs

    Set rs = Nothing
    Set conn = Nothing

End Sub

The RunQry module accepts two parameters, a Range for the returned data to be stored at and arbitrary SQL code to be executed. It creates an ADODB connection and executes the SQL query passed to the method against the server and database defined. The results from the executed SQL are then pasted at the range defined as a starting point.

Warning: This has potential security implications. Queries must be designed with SQL Injection in mind! Don’t ever pass raw text from the user in as query parameters without sanitizing them first! Ideally users have very limited permissions, and are limited to only executing stored procedures!

Execute our Query from Excel

Now that we have the SQL module with a RunQry, we can call it from other code and simply tell it what query to run, and where to put the data. Create a new module by right clicking on the Modules folder icon and again “Insert > Module” and name this one “main”.

Paste the following VBA code into the main module. Notice that I created a stored procedure in SQL Server to execute the sales by employee query because I hate having multi-line SQL escaped in VBA. This provides a cleaner and more secure approach.

Option Explicit

Sub run_sales_query()
    Dim data_start As Range
    Dim sql_qry As String

    ' Change to clear out the number of rows/columns your data will fill

    ' Change to move where data is placed
    Set data_start = Sheet1.Range("A6")

    ' Perfect example of why stored procedures are better for this!
    sql_qry = "EXEC dbo.getSalesByPerson"

    Call SQL.RunQry(data_start, sql_qry)
End Sub

The code pastes the results from our query (without headers) to cell A6 on Sheet1. This will leave room for the Refresh button above the data. I also manually add the headers above the data (row 5) since they don’t come in with the results from SQL.

To test run the report hit “Alt + F8″ and run the run_sales_query macro.

An error that highlights an ADODB line is probably a missing reference! The sql_qry variable can be set to any regular SELECT or EXEC statement that returns data. I recommend always retrieving data from stored procedures, but hey, that’s just me!

SQL Enabled Report

I added a simple button that triggers the run_sales_query module to refresh the data. I can now add to the sales data by running a couple thousand more sales transactions (see the Loop code above) and see the updated report by simply clicking the “Refresh Data” button. Pretty slick!

Sales by Employee report in Excel

Final Thoughts

As your reports grow beyond more than one simple query, I find it’s best to have each distinct query going to its own dedicated worksheet to avoid potential overlap of data. Create your various polished reports for display that have links to the raw data worksheets. When the polished reports are ready, the raw data worksheets can be hidden to clean up the file. You can get fancier by providing drop down menus, but that’s beyond the scope of this post.

By Ryan Lambert
Published April 03, 2015
Last Updated April 17, 2015