Guaranteed Column Inclusion in Transform
Andre pointed out that my last answer failed to address one feature of the explicit PIVOT column list, namely that it gaurantees columns even when the data does not include corresponding values. In many cases it might be just as well to generate the full SQL "on-the-fly" as David W Fenton commented. Here's some template code for doing so:
Public Function GenerateTransform(valueArray As Variant) As String
Dim sIN As String
Dim i As Integer, delimit As Boolean
If (VarType(valueArray) And vbArray) = vbArray Then
For i = LBound(valueArray) To UBound(valueArray)
sIN = sIN & IIf(delimit, ",", "") & valueArray(i)
delimit = True
Next i
If Len(sIN) > 0 Then sIN = "IN (" & sIN & ")"
End If
GenerateTransform = "TRANSFORM ... SELECT ... PIVOT ... " & sIN
End Function
Public Sub TestGenerateTransform()
Dim values(0 To 2) As Integer
values(0) = 1
values(1) = 4
values(2) = 12
Debug.Print GenerateTransform(values)
Debug.Print GenerateTransform(vbEmpty) 'No column list
End Sub
Like my other answer, the following queries allow one to utilize various^ techniques in selecting and filtering the criteria. Not only can this technique guarantee columns, this also allows more control of the rows^^.
^ Even though VBA functions can still be used to their usual extent in the SQL, Access does not allow new row data to be dynamically added during SQL execution using VBA... rows must be based on actual table rows. (Technically one can use a UNION SELECT with literal values to create rows, but this is prohibitive for a lot of data and does not facilitate any sort of dynamic column selection.) Hence, the following technique requires use of an auxillary table for defining/selecting column values.
The first query applies selection criteria and does initial grouping. If you compare to my other answer, this is essentially the same as the original TRANSFORM query--only without the TRANSFORM and PIVOT. Save and name this query [1 Initial Aggregate]:
SELECT Agreement.City, Month([ServiceDate]) AS [Month], Count(Services.ID) AS Schedules
FROM Agreement INNER JOIN Services ON Agreement.Account = Services.Account
WHERE (Services.Code = "IS")
GROUP BY Agreement.City, Month([ServiceDate])
ORDER BY Agreement.City
Next create a query that groups on all desired row values. In this example, I choose to include only the same values from the initial selection criteria. ^^ This set of values could also be decoupled from the previous selection criteria by basing it off the unfiltered table or another query. Save and name this query [2 Row Headings]:
SELECT RowSource.City AS City
FROM [1 Initial Aggregate] AS RowSource
GROUP BY RowSource.City
ORDER BY RowSource.City
Create a cross join of the row headings and the auxiliary table [PivotValues] containing the column headings. A cross join creates rows from every combination of the two tables--in Access SQL it is accomplished by excluding all JOIN keywords. Save and name this query [3 Cross Join]:
SELECT [2 Row Headings].City AS City, PivotValues.Values AS Months
FROM [2 Row Headings], PivotValues
ORDER BY [2 Row Headings].City, PivotValues.Values;
Finally, the transform: By using a LEFT JOIN, this will include all columns and rows that exist in the cross join query. For column and row pairs that are missing data in the joined select query, the column will still be included (i.e. guaranteed) with Null as the value. Even though we have already grouped on the initial query, the transform requires that we re-group anyway--perhaps a bit redundant but not a big a deal to obtain the desired control over the final crosstab results.
TRANSFORM Sum([1 Initial Aggregate].Schedules) AS SumOfSchedules
SELECT [3 Cross Join].City AS City
FROM [3 Cross Join] LEFT JOIN [1 Initial Aggregate] ON ([3 Cross Join].Months = [1 Initial Aggregate].Month) AND ([3 Cross Join].City = [1 Initial Aggregate].City)
GROUP BY [3 Cross Join].City
PIVOT [3 Cross Join].Months
This might seem like overkill just to make the crosstab columns dynamic, but it can be worth defining a few extra queries for complete control over the results. VBA code can be used to (re)define the values in the auxiliary table, thus satisfying the original question of using VBA to dynamically specify the columns.