How to call VBA-function for specifying columns in

2019-02-24 04:55发布

问题:

Here is my query:

PARAMETERS ...
TRANSFORM ...
SELECT ...
...
PIVOT Mytable.type In ("Other","Info");

This is a cross query and I need to set all the column headings with this row: PIVOT Mytable.type In ("Other","Info") and now I have hard-coded the headings, Other and Info.

But I want to do this dynamically. So what I want to do is to call a vba-function that returns all the headings I need.

Something like this:

PIVOT Mytable.type In (myVbaFunction());

So my question is: How to call a vba-function inside the sql-query?

回答1:

Yes, it is possible.
However, I don't think it's possible with WHERE IN (...).

Here is an example for a normal WHERE query:

Public Function Test() As String
    Test = "Smith"
End Function

...and then:

SELECT * FROM Users WHERE Name = Test();

It works, as long as the function only returns one value.
But I think it's not possible to let your function return something like "Smith, Miller" and use that like:

SELECT * FROM Users WHERE Name In (Test());

(at least I don't know how to do it)



回答2:

If the IN list is excluded from the PIVOT clause, the TRANSFORM query automatically creates columns for every PIVOT value generated from the SELECT statement. The final columns can be filtered by specifying the IN expression with "hard-coded" (i.e. literal) values. That is of course known from the other answers.

The exact same effect can be achieved by limiting the data from the SELECT query to start with... before the TRANSFORM needs to filter it. In this way, one is not limited to only pre-defined literal values--rather combinations of JOINS, sub-queries and/or VBA functions can also pre-filter the data, effectively choosing which columns appear in the transform table. Note that the HAVING clause is not allowed in a TRANSFORM query, but it could be used in another query which the TRANSFORM then selects on, so there are effectively no limits to how the data is prepared before the TRANSFORM.

All of the following produce equivalent results. First using PIVOT...IN:

TRANSFORM Count(Services.ID) AS [Schedules]
SELECT Agreement.City FROM Agreement INNER JOIN Services ON Agreement.Account = Services.Account
WHERE ( Services.Code = "IS" )
GROUP BY Agreement.City ORDER BY Agreement.City
PIVOT Month([ServiceDate]) In (1,4,12)

Using IN operator in WHERE clause:

TRANSFORM Count(Services.ID) AS [Schedules]
SELECT Agreement.City FROM Agreement INNER JOIN Services ON Agreement.Account = Services.Account
WHERE ( (Month([ServiceDate]) In (1,4,12)) AND Services.Code = "IS" )
GROUP BY Agreement.City ORDER BY Agreement.City
PIVOT Month([ServiceDate])

but unlike the PIVOT...IN clause, the list can also be another query:

WHERE ((Month([ServiceDate]) In (SELECT Values FROM PivotValues)) AND Services.Code = "IS" )

Finally, using a VBA function (which answers the original question):

TRANSFORM Count(Services.ID) AS [Schedules]
SELECT Agreement.City FROM Agreement INNER JOIN Services ON Agreement.Account = Services.Account
WHERE ( ReportMonth([ServiceDate]) AND Services.Code = "IS" )
GROUP BY Agreement.City ORDER BY Agreement.City
PIVOT Month([ServiceDate])

With the VBA function defined in a standard module:

Public Function ReportMonth(dt As Date) As Boolean
  Select Case Month(dt)
    Case 1, 4, 12: ReportMonth= True
    Case Else:     ReportMonth= False
  End Select
End Function

(iDevlop already suggested this solution in a comment, but I don't think it was understood and good examples are required.)



回答3:

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.



回答4:

Why not adding those headings in a table and join that table in your xtab query ?
That's probably easier to maintain that hard coding it in a function.



回答5:

As you have stated that you're using Access, then (out of the top of my head) yes, it's possible to use VBA functions in queries.

Check on the documentation and MSDN.