SQLServerCentral Article

Build Great Looking Excel Spreadsheets

,

I am often asked to produce presentations containing data from database tables and views. Over the years I have developed two utilities that are indispensable time-savers:

  • a SQL-Server stored procedure that generates table/view column names, and
  • an Excel macro that colors alternating sets of rows based upon their "business key" values.

Emit a row full of column names

Excel is a great tool for formatting data that you've extracted from tables or views. You can copy such data into an Excel spreadsheet straight from SQL Management Studio's output grid. For years I didn't realize that there's an option to make SQL Management Studio also grab the column headers (see "Include Column Headers from SMSS"), so I created a stored procedure that would do this. The procedure remains useful if you don't want to have the SMSS "column headers" option turned on all the time, and it is also an interesting example of what you can do with SQL Server's schema - describing metadata.

Starting with SQL Server 2005, Microsoft implemented a set of ANSI-standard views that can help solve problems like this one. Using SQL Management Studio you can see them in every database under the "System Views" folder:

These views provide a simple, intuitive look into the schema of your database. In particular, INFORMATION_SCHEMA.COLUMNS provides a column-by-column listing of many interesting properties of each column in every user-defined table and view. Using just three of the columns in this view, you can query all of the column names in all of your tables and views, with the column names listed in declaration order:

select
        TABLE_NAME,
        COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
order by
        TABLE_NAME,
        ORDINAL_POSITION

We can use this query to build a stored procedure which, given the name of a table or view, returns a single row having the same schema as the passed-in table or view, in which each column contains its own name. The procedure uses a cursor that steps through the rows of the output of the above query, constructing a command by tacking on one column name with each iteration:

if object_id('ColumnHeaders', 'P') is not null drop procedure ColumnHeaders
go
create procedure ColumnHeaders
        @TableName varchar(128)
as
        -- test to ensure the argument is the name of a real table
        if @TableName not in (select distinct TABLE_NAME from INFORMATION_SCHEMA.COLUMNS)
        begin
                declare @ErrorMessage varchar(max)
                set @ErrorMessage = 'There is no table or view named ''' + @TableName + '''.'
                raiserror(@ErrorMessage, 16, 1)
        end
        else
        begin
                -- set up cursor
                declare #c cursor local for
                        select COLUMN_NAME + '=''' + COLUMN_NAME + ''',' ColumnAssignment
                        from INFORMATION_SCHEMA.COLUMNS
                        where TABLE_NAME = @TableName
                        order by ORDINAL_POSITION
                open #c
                -- construct an execution string
                declare @ExecString varchar(max)
                set @ExecString = 'select '
                declare @ColumnAssignment varchar(100)
                fetch next from #c into @ColumnAssignment
                while @@fetch_status = 0
                begin
                        set @ExecString = @ExecString + @ColumnAssignment
                        fetch next from #c into @ColumnAssignment
                end
                set @ExecString = left(@ExecString, len(@ExecString)-1)
                -- tear down cursor
                close #c
                deallocate #c
                -- execute execution string
                exec(@ExecString)
        end
go

We invoke this procedure by passing it the name of a table or view:

exec ColumnHeaders 'TBgeoSTATEPROVINCE'  -- supply your own table name as an argument

The output looks like this:

It consists of a single row having the same column names as the columns in the specified table, with each column containing its own name. This row can be pasted into the first row of an Excel spreadsheet. Subsequently you can query contents of the table or view itself, producing rows having the same schema, and append that query's output to the column-header row in your Excel sheet.

For example it takes only a few seconds to copy and paste the outputs of these two statements:

-- supply your own table name as an argument
exec ColumnHeaders 'TBgeoSTATEPROVINCE'
-- query some subset of your own table or view
select * from TBgeoSTATEPROVINCE where StateProvinceName like 'A%'

to produce a spreadsheet that looks like this:

StateProvinceIdStateProvinceNameStateProvinceCodeCountryIdNotesCreatedByIdCreationDateAuditDateAuditUserIdAuditStatus
1AlabamaAL222NULL19/5/2008 13:509/5/08 13:501I
2AlaskaAK222NULL19/5/2008 13:509/5/08 13:501I
3ArizonaAZ222NULL19/5/2008 13:509/5/08 13:501I
4ArkansasAR222NULL19/5/2008 13:509/5/08 13:501I

Color alternate sets of rows in a spreadsheet

Once you've gotten your column headers and query output into an Excel sheet, the sheer amount of data is often mind-numbing. Your data will generally contain groups of rows having the same business key. I like to paint such sets of rows in alternating colors so that readers can easily group the related rows. Coloring groups of rows by hand can be very tedious and error-prone, so here is some code that does it automatically.

First we have a comment. I strongly recommend keeping this at the top of your copy of this code because it tells everyone exactly how to incorporate the code into an Excel workbook:

' Automatically color alternating sets of rows of the current sheet, based on the data in one of the columns.
' This macro automatically figures out how wide and tall your data is.
' Instructions:
'    1. Open your spreadsheet.
'    2. Open "Tools|Macros|Visual Basic Editor"
'    3. Right click on "This Workbook" and select "View Code", which should open a page on the right.
'    4. Copy the entire contents of this file into that page.
'    5. Close the "Visual Basic Editor" by killing the whole interface that came up when you did step 3.
'    6. The macro will now operate on any page of your workbook:
'     * copy your data into an empty sheet
'     * select "Tools|Macros"
'     * select "ColorAlternateDataRows" from the list
'     * enter the requested row and column numbers
'
Option Explicit

Next is a function that returns the numeric index of a column, given its alphabetic name. This allows us to convert a user-specified column name into an index, so that we can invoke Excel library functions that take column indices as arguments:

' Return the 1-based index of a column, given its name.
' We presume fewer than 2**31 - 1 columns.
Private Function ColumnIndex(ColumnName As String) As Integer
    Dim ReturnValue As Integer
    ReturnValue = 0
    Dim CharIndex As Integer
    CharIndex = 1
    Do While CharIndex <= Len(ColumnName)
        Dim ThisChar As String
        ThisChar = Mid(ColumnName, CharIndex, 1)
        If ThisChar >= "A" And ThisChar <= "Z" Then
            ReturnValue = ReturnValue * 26 + (Asc(ThisChar) - Asc("A") + 1)
        ElseIf ThisChar >= "a" And ThisChar <= "z" Then
            ReturnValue = ReturnValue * 26 + (Asc(ThisChar) - Asc("a") + 1)
        Else
            ReturnValue = -1
            GoTo Ret
        End If
        CharIndex = CharIndex + 1
    Loop
Ret:
    ColumnIndex = ReturnValue
End Function

The following function tries to collect a positive integer from the user, cycling until it gets one or until the user cancels. This is used to collect the starting row number for the coloring (the user may have header rows that should not be colored):

' Retrieve a positive (long) integer from the user.
Private Function GetPositiveInteger(Text As String, Optional AllowColumnNames As Boolean = False) As Long
    Dim Data As Long
    Data = -1
    Dim FirstIteration As Boolean
    FirstIteration = True
    Do While Data < 0
        Dim S As String
        If FirstIteration Then
            S = InputBox(Text)
        Else
            If AllowColumnNames Then
                S = InputBox(Text & " We need a positive integer.")
            Else
                S = InputBox(Text & " We need a column name or a positive integer.")
            End If
        End If
        If Trim(S) = "" Then
            GoTo Ret    ' cancel
        ElseIf IsNumeric(S) Then
            ' See if it's an integer directly.
            If CInt(S) > 0 Then
                Data = CInt(S)
            End If
        ElseIf AllowColumnNames Then
            ' See if it's a column name.
            Dim ColNdx As Integer
            ColNdx = ColumnIndex(S)
            If ColNdx > 0 Then
                Data = ColNdx
            End If
        End If
        FirstIteration = False
    Loop
Ret:
    GetPositiveInteger = Data
End Function

This function is the actual macro that the user invokes. It does the following:

  • collects the desired starting row from the user
  • collects the column name of the (single) column whose changing value will cause a change in row color
  • computes the total "used range" (i.e. span of cells that are occupied) of the currently active workbook
  • colors the rows in alternating colors
' Color alternate data rows based upon business-key information provided
' interactively by the user.
Sub ColorAlternateDataRows()
    Dim FirstDataRow As Long
    Dim TestDataInColumn As Integer

    Dim S As Worksheet
    Set S = ThisWorkbook.ActiveSheet
    If Not (S Is Nothing) Then
            
        FirstDataRow = GetPositiveInteger("Enter the NUMBER of the FIRST ROW containing data to be colored.")
        If FirstDataRow <= 0 Then Exit Sub
        TestDataInColumn = GetPositiveInteger("Enter the COLUMN NAME of the column containing the changing data.", True)
        If TestDataInColumn <= 0 Then Exit Sub
    
        Dim CurrentData As String
        CurrentData = ""
        
        Dim Width As Integer
        Width = S.UsedRange.Columns.Count
        
        Const ColorIndex1 As Integer = 36   ' light yellow
        Const ColorIndex2 As Integer = 34   ' light blue
        
        Dim ColorIndex As Integer
        ColorIndex = ColorIndex2
        
        Dim RowIndex As Long
        Dim LastRow As Long
        LastRow = S.UsedRange.Rows.Count
        For RowIndex = FirstDataRow To LastRow
            ' change the color if necessary
            If CurrentData <> CStr(S.Cells(RowIndex, TestDataInColumn)) Then
                If ColorIndex = ColorIndex1 Then
                    ColorIndex = ColorIndex2
                Else
                    ColorIndex = ColorIndex1
                End If
                CurrentData = CStr(S.Cells(RowIndex, TestDataInColumn))
            End If
            ' apply the color
            S.Range(S.Cells(RowIndex, 1), S.Cells(RowIndex, Width)).Select
            With Selection.Interior
                .ColorIndex = ColorIndex
                .Pattern = xlSolid
            End With
        Next
        
    End If
   
End Sub

You can copy and paste all the above code into a single file that can be incorporated into any desired Excel workbook.

Now let's see what this looks like when we run it. We'll start with a trivial Excel sheet containing some data that came from a SQL Server table or view (header row colored by hand):

NumberLetter
1A
2A
3B
4B
5C

We execute the macro and it pops up this dialog. We've already colored the header row by hand, so to avoid having the macro re-color it we tell the macro to start coloring with row 2:

Up pops another dialog. Here we tell the macro that it should change row color every time the value in column A changes.

After hitting OK the macro produces this:

NumberLetter
1A
2A
3B
4B
5C

We can now rerun the macro, telling it to change colors on changes in column B instead, and we get this:

NumberLetter
1A
2A
3B
4B
5C

Note that the macro assumes only a single-column key, which I have found to be sufficient for the vast majority of my uses. The macro can of course be modified to accept a range of columns instead of just one. I leave this as an exercise for the reader.

Summary

Using these two macros I have quickly become the favored documenting person of almost every team in which I've worked. In a matter of seconds I can generate a well-colored Excel table that has column headers. It may seem like a small thing, but for me it's had a huge effect.

System Requirements

The stored procedure requires that you be running SQL Server 2005 or higher. The Excel macro was implemented using Excel 2003; it has not been tested in other Excel versions.

Author

You can contact me (Dave Ziffer) via the "Contact" link on my company's web site at www.ProjectPro.com. I'm also on LinkedIn. To keep abreast of my upcoming articles and projects, join the LinkedIn "Rapid Application Prototype" group.

Rate

2.42 (52)

You rated this post out of 5. Change rating

Share

Share

Rate

2.42 (52)

You rated this post out of 5. Change rating