Normalization and Denormalization

Article in pdf format here

Helen Feddema approaches the same problem as Garry (Flexible Normalization and Denormalization of Data) , but this time manages her data to provide the users with the output that met their needs.

A reader asked me how he could convert a table with more than 100 questionnaire fields to a more manageable format, with the fields converted to records in a table to make it easier to tabulate the data. Effectively, this is the reverse of Garry’s problem where he converted multiple records into one: I’m converting a single record into multiple records. To make matters more interesting, my reader wanted to be able to save each survey’s results in a separate table, which forces me to re-create the table and query with each processing run.

In the Helen.mdb file that comes with this article, you’ll find the tblSurvey table (part of which is shown in Figure 1) that has the raw data from the questionnaires. It has 44 fields (cut down from the original table, which had more than 100 fields). There’s a Text field, ID, which is the key field, and the other fields are either Boolean or Text, with the Text fields taking a numeric value from 1 to 5.

To switch the fields to records, I first created a table (with the prefix zstbl to indicate that it’s a system table) with just three fields: SurveyID, a Long Integer field indexed Yes (Duplicates OK), Question and Answer

Public Function CreateResultsTable()

On Error Resume Next

Dim fld As DAO.Field

Dim flds As DAO.Fields

Dim rstSource As DAO.Recordset

Dim rstTarget As DAO.Recordset

Dim strPrompt As String

Dim strResultsTable As String

Dim strSourceTable As String

Dim strTableTemplate As String

Dim strTitle As String

Dim strReport As String

Dim strQuery As String

Dim strSQL As String

Dim lngCount As Long

Dim strCurrentDate As String

Dim intResult As Integer

Dim rpt As Access.Report

The next step is to delete the existing table for this Survey (if it exists) and re-create it, ready to accept the new data (the surveys were generated each day so I used the current date to name each version of the table):

strTableTemplate = "zstblSurveyResults"

strCurrentDate = Format(Date, "dd-mmm-yyyy")

strResultsTable = "tblSurveyResults_" & _

strCurrentDate

DoCmd.DeleteObject objecttype:=acTable, _

objectname:=strResultsTable

On Error GoTo ErrorHandler

'Make copy of table template

DoCmd.CopyObject newname:=strResultsTable, _


Figure 1. The table with raw survey data in numerous fields.


Figure 2.  A report based on a totals query giving the number of each answer for each question. (both text fields). This table is copied to create a results table that’s filled from code.

The CreateResultsTable function fills a results table with records containing field names and values from the original tblSurvey and creates a totals query based on it (qtotAnswers) that totals the number of Yes, No, and 1 through 5 answers for each question. For convenience, the function can be run from the mcrCreateResultsTable macro, or (for consistency with Garry’s database) the frmCreateResultsTable form. This query is the record source for a simple report, which is shown in Figure 2.

I begin by defining the variables that I’ll be needing:

sourceobjecttype:=acTable, sourceobjectname:=strTableTemplate

Now that the table is created, I can fill it with data. As in Garry’s example, I read each record and then process each field creating a record as I go:

strSourceTable = "tblSurvey"

Set dbs = CurrentDb

Set rstSource = dbs.OpenRecordset(strSourceTable)

Set rstTarget = dbs.OpenRecordset(strResultsTable)

Do While Not rstSource.EOF

Set flds = rstSource.Fields

For Each fld In flds

rstTarget.AddNew

If fld.Name <> "ID" Then

rstTarget![SurveyID] = rstSource![ID]

rstTarget![Question] = fld.Name

If fld.Type = dbBoolean Then

rstTarget![Answer] = _

IIf(fld.Value = True, "Yes", "No")

Else

rstTarget![Answer] = fld.Value

End If

rstTarget.Update

End If

Next fld

rstSource.MoveNext

Loop

rstSource.Close

Reporting on the data

However, the table was only part of the solution. I also needed to create a query that would total the results and serve as the basis for a report. I first deleted any existing copy of the query and then generated a new one:

'Delete old totals query

strQuery = "qtotAnswers"

DoCmd.DeleteObject objecttype:=acQuery, _

objectname:=strQuery

Set dbs = CurrentDb

strSQL = "SELECT [" & strResultsTable _

& "].[Question], " _

& "Sum(IIf([Answer]='Yes',1,0)) AS YesAnswer, " _

& "Sum(IIf([Answer]='No',1,0)) AS NoAnswer, " _

& "Sum(IIf([Answer]='1',1,0)) AS 1Answer, " _

& "Sum(IIf([Answer]='2',1,0)) AS 2Answer, " _

& "Sum(IIf([Answer]='3',1,0)) AS 3Answer, " _

& "Sum(IIf([Answer]='4',1,0)) AS 4Answer, " _

& "Sum(IIf([Answer]='5',1,0)) AS 5Answer " _

& "FROM [" & strResultsTable & _

"] GROUP BY [" & strResultsTable & "].[Question];"

Debug.Print "SQL for " & strQuery & ": " & strSQL

lngCount = CreateAndTestQuery(strQuery, strSQL)

Debug.Print "No. of records: " & lngCount

If lngCount = 0 Then

strPrompt = "No records found; canceling"

strTitle = "Canceling"

MsgBox strPrompt, vbOKOnly, strTitle

GoTo ErrorHandlerExit

End If

The final step is to update the report with the current date (stored in the Tag property for display in the txtTitle textbox) and ask the user if they want to view it:

strReport = "rptAnswers"

DoCmd.OpenReport reportname:=strReport, _

view:=acViewDesign, windowmode:=acHidden

Set rpt = Reports(strReport)

rpt.Tag = strCurrentDate

strTitle = "Finished"

strPrompt = strResultsTable & _

" results table created; open report?"

intResult = MsgBox(strPrompt, vbYesNo, strTitle)

If intResult = vbYes Then

DoCmd.OpenReport reportname:=strReport, _

view:=acViewPreview

Else

DoCmd.Close objecttype:=acReport, _

objectname:=strReport

End If

Finally, exit the function, with an error handler to take care of any errors:

ErrorHandlerExit:

Exit Function

ErrorHandler:

MsgBox "Error No: " & Err.Number & _

"; Description: " & Err.Description

Resume ErrorHandlerExit

End Function

The CreateAndTestQuery function listed below is handy for creating (and re-creating, as needed) a query in code. I use it to re-create the totals query qtotAnswers, based on the newly created results table (see Figure 2).

Public Function CreateAndTestQuery( _

strTestQuery As String, strTestSQL As String)

As Long

On Error Resume Next

Set dbs = CurrentDb

dbs.QueryDefs.Delete strTestQuery

On Error GoTo ErrorHandler

Set qdf = dbs.CreateQueryDef(strTestQuery, _

strTestSQL)

'Test whether there are any records

Set rst = dbs.OpenRecordset(strTestQuery)

With rst

.MoveFirst

.MoveLast

CreateAndTestQuery = .RecordCount

End With

ErrorHandlerExit:

Exit Function

ErrorHandler:

If Err.Number = 3021 Then

CreateAndTestQuery = 0

Resume ErrorHandlerExit

Else

MsgBox "Error No: " & Err.Number & _

"; Description: " & Err.Description

Resume ErrorHandlerExit

End If

End Function

You can find a sample database with all of the code in the accompanying download file. In addition to the Microsoft DAO 3.6 Object Library, my sample database also uses the Scripting Runtime Library.

 

Your download file is called   Feddema_Normalization.accdb

About Helen Feddema

Helen is the editor of Access Watch, a more or less biweekly ezine. She lives in the mid-Hudson area of New York state, with two cats and three computers.
This entry was posted in Reports and Graphs. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *


*


This site uses Akismet to reduce spam. Learn how your comment data is processed.