Create flexible Excel exports with on-the-fly SQL

Kingsway Financial Assessments provides independent financial assessment reports and credit reports to support decision making when awarding contracts and tenders. Kingsway uses an Access database built by Hockley Computer Services using Goldsoft. Ken Hockley is today's guest blogger.

I’ve been using SQL statements in temporary Access tables to extract data to Excel for some time now (simple data dump).

The client has a form which shows a selection of the tables and fields in their system. In this example, the fields come from the main table t_job and related tables. The client selects which fields they want in the output file, and set some other parameters such as from date, to date, customer name, state, etc.

The client can choose any columns they wish. In the example that follows, the client is comparing certain pieces of financial data of a number of companies that they are evaluating. They only want to compare a subset of the data from the balance sheet and the profit and loss statement--they don’t want to see all the columns.

Once the client makes his selections, a VBA procedure writes all the SQL commands into a temp table named t_temp_sql, as shown here:

clip_image002

This might be kind of hard to read, so the table design looks like this:

Field Name Data Type
sql_id long
sql_wording text 255
column_name text 50
column_heading text 50
field_name text 50

The SQL statements in the sql_wording field are constructed from parts that are stored in an Excel workbook named Data_template_1.xls:

image

Based on the selections the client has made, the first part of the VBA procedure loops through this worksheet and uses the data to create the SQL statements and store them in the the Access temp table t_temp_sql.

The second part of the VBA procedure (starting with the comment "EXPORT PROCESS") loops through the Access temp table t_temp_sql and evaluates each of the SQL statements. It then populates an empty Excel sheet named raw_template.xls with the results and saves it as data_dump.xls, which is then sent to the client. (Apologies for the line wraps in the code—that's the only way to make it fit in this column.)

Private Sub dump_data()
  On Error GoTo e1
  
  'PURPOSE - dump data from fields in t_job and related tables to Excel
  'for further analysis
  
  Dim curr_db As Database
  Dim rs_sql As Recordset
  Dim rs_job As Recordset
  Dim rs_test As Recordset
  Dim file_name As Variant
  Dim hold_column_prefix As Variant
  Dim hold_sql As Variant
  Dim hold_crit As Variant
  Dim template_path As Variant
  Dim export_path As Variant
  Dim xlapp As Excel.Application
  Dim xlworkbook As Excel.Workbook
  Dim xlworksheet As Excel.Worksheet
  Dim xlrange As Excel.Range
  Dim screen_row As Long
  Dim num_of_jobs As Long
  Dim curr_job As Long
  Dim num_of_recs As Long
  Dim curr_rec As Long
  Dim c As Integer
  Dim num_of_sql_commands As Long
  Dim screen_col As String
  
  Set curr_db = DBEngine.Workspaces(0).Databases(0)
  
  'open an excel session
  Set xlapp = New Excel.Application
  
  'clear the temp table which will hold the sql commands
  hold_crit = "DELETE t_temp_sql.sql_id FROM t_temp_sql"
  DoCmd.RunSQL hold_crit
  
  'open a rs_on the empty temp table
  hold_crit = "select * from t_temp_sql"
  Set rs_sql = curr_db.OpenRecordset(hold_crit)
  
  'this file holds the specifications for the import
  template_path = "h:\job\production\data_templates\"
  file_name = "data_dump_financials.xls"
  
  'open the template Excel file
  Set xlworkbook = xlapp.Workbooks.Open(template_path & file_name)
  Set xlworksheet = xlworkbook.Worksheets(1)
  screen_row = 1     'row on excel sheet to start reading
  num_of_sql_commands = 1  'used to keep commands in a logical order
  
  'open a rs on the main table "t_job" in this case
  'use an on screen filter here to restrict the number of records
  'for analysis by date, client name, area etc
  hold_crit = "select * from t_job where job_id >= 14511 and job_id <= 14513"
  Set rs_job = curr_db.OpenRecordset(hold_crit)
  If rs_job.RecordCount > 0 Then
     rs_job.MoveLast
     num_of_jobs = rs_job.RecordCount
     rs_job.MoveFirst
     curr_job = 0
     While curr_job < num_of_jobs
       'write the job number in to the temp table on an otherwise empty record
       rs_sql.AddNew
       rs_sql!sql_id = num_of_sql_commands
       rs_sql!sql_wording = rs_job!job_id
       rs_sql.Update
       'loop through each populated column in the worksheet
       screen_col = "c"   'column on excel sheet to start reading
       hold_column_prefix = ""   'use this variable to allow for columns after z
       While xlworksheet.Range(hold_column_prefix & screen_col & Format("1")) <> ""
         'add an sql line
         rs_sql.AddNew
         num_of_sql_commands = num_of_sql_commands + 1
         rs_sql!sql_id = num_of_sql_commands
         'construct the sql command
         hold_sql = "select " & xlworksheet.Range(hold_column_prefix & screen_col _
            & "2")
         hold_sql = hold_sql & " from " & xlworksheet.Range(hold_column_prefix & _
            screen_col & "1")
         hold_sql = hold_sql & " where " & xlworksheet.Range(hold_column_prefix & _
            screen_col & "5")
         hold_sql = hold_sql & " = " & _
            rs_job.Fields(xlworksheet.Range(hold_column_prefix & screen_col & "5"))
         rs_sql!sql_wording = hold_sql
         rs_sql!column_name = _
            xlworksheet.Range(hold_column_prefix & screen_col & "4")
         rs_sql!column_heading = _
            xlworksheet.Range(hold_column_prefix & screen_col & "3")
         rs_sql!field_name = _
            xlworksheet.Range(hold_column_prefix & screen_col & "2")
         rs_sql.Update
         'move to the next column
         c = Asc(screen_col)
         If c = 122 Then
            c = 96
            If hold_column_prefix = "a" Then hold_column_prefix = "b"
            If hold_column_prefix = "" Then hold_column_prefix = "a"
            'allows for columns up to bz but can easily be extended
         End If
         screen_col = Chr(c + 1)
       Wend
       rs_job.MoveNext
       curr_job = curr_job + 1
     Wend
  End If
  rs_job.Close
  rs_sql.Close
  
  Set xlworksheet = Nothing
  xlworkbook.Close False
  Set xlworkbook = Nothing
  
  'EXPORT PROCESS
  
  'execute all the sql commands in t_temp_sql to the output template
  file_name = "raw_file.xls"
  Set xlworkbook = xlapp.Workbooks.Open(template_path & file_name)
  Set xlworksheet = xlworkbook.Worksheets(1)
  screen_row = 0
  
  'open a rs_on the temp table
  hold_crit = "select * from t_temp_sql order by sql_id"
  Set rs_sql = curr_db.OpenRecordset(hold_crit)
  If rs_sql.RecordCount > 0 Then
     rs_sql.MoveLast
     num_of_recs = rs_sql.RecordCount
     curr_rec = 0
     rs_sql.MoveFirst
     While curr_rec < num_of_recs
       'a new job starts where t_temp_sql.column name is null
       'the "if" allows for flexibility of more code here
       If IsNull(rs_sql!column_name) Then
          screen_row = screen_row + 1
          'write the job and column heading
          screen_col = "a"
          'write the column heading if this is the first record
          If curr_rec = 0 Then
             xlworksheet.Range(screen_col & "1") = "Job No"
          End If
          xlworksheet.Range(screen_col & screen_row + 1) = rs_sql!sql_wording
       End If
       'the "if" allows for flexibility of more code here
       If Not IsNull(rs_sql!column_name) Then
          'write the column heading if this is the first record
          If screen_row = 1 Then
             xlworksheet.Range(rs_sql!column_name & screen_row) = _
                rs_sql!column_heading
          End If
          'execute the sql command using a recordset.
          'allows more flexibility than docmd.slq
          Set rs_test = curr_db.OpenRecordset(rs_sql!sql_wording)
          'if data has been found write the data to the worksheet
          If rs_test.RecordCount > 0 Then
             xlworksheet.Range(rs_sql!column_name & screen_row + 1) = _
                rs_test.Fields(rs_sql!field_name)
          End If
          rs_test.Close
       End If
       curr_rec = curr_rec + 1
       rs_sql.MoveNext
     Wend
  End If
  rs_sql.Close
  
  export_path = "h:\temp\data_dump.xls"
  hold = Dir(export_path)
  If Len(hold) > 0 Then
     Kill export_path
  End If
  
  Set xlworksheet = Nothing
  xlworkbook.SaveAs (export_path)
  xlworkbook.Close False
  Set xlworkbook = Nothing
  
  Set xlapp = Nothing
  
  Me!working_now.Visible = False
  Me.Repaint
  
  hold = "The Excel file has been created"
  hold = hold & crlf & crlf & export_path
  MsgBox hold, 0, system_error
  
  Exit Sub
  
e1:
  MsgBox Error$, 0, system_error
  Err.Number = 0
  Resume

  
End Sub

This approach results in an easy-to-maintain, easy-to-use architecture that the client really appreciates. A stripped-down sample output file (data_dump.xls) is available here.

Office Blogs Comments

Comments: (2) Collapse

  • Why build the SQL and populate a temp table when you can populate Excel directly by using something like xlworksheet.Cells(rows,1).CopyFromRecordset rs_job rows can be any starting row you wish, as long as the total number of rows from the SQL doesn't exceed the limits for a particular version. I do this alot because my databases invariably hit the size limits and so, I found ways to avoid temp tables

  • Art: Why build...... Agree that the data can be exported in one go - thereby avoiding the need for a temp table However the reason for two stages is flexibility - one or both stages could be altered - for example if a currency value is negative the Excel cell is changed to vbWhite on vbRed or dates older than 7 years may need to be in bold etc Our experience is that very few data dumps are exactly the same so we re-use the basic code and then alter it to suit

Comments

Comments: (loading) Collapse