Access 2010 vba Array vs. Query - sql

I maintain an Access DB at work that we use to send out hourly updates on employee productivity. Currently I have the form arranged into columns: The first column contains a ComboBox that we can select an employee's name from. Once a name is selected, the next column fills in automatically with the agent's employee ID (TID), through this code:
AgentName = rs.Fields("AgentName")
sqlString2 = "SELECT * FROM " & "AllAgents WHERE FullName ='" & AgentName & "'"
rs2.Open sqlString2, CurrentProject.Connection, adOpenKeyset, adLockOptimistic
AgentTID = rs2.Fields("TID").Value
rs2.Close
Everything works fine when working in the office on the corporate network, but I've just discovered that working over the VPN causes horrendous slowdown when using this form. Slowness is an issue I've fought with over the VPN forever, but I had a thought in this case that may potentially aleve the slowness, I judt want to know if I'm correct before I go to the trouble of re-coding everything.
My idea is that I could create an array in VBA that would be populated with the agents' name & ID's when the form first loads, or even when the DB is opened on each inidividual laptop. Then the DB would only need to read from the 'AllAgents' table once, and could simply use the array as a source instead. What I doin't know is if this would have an effect or not. Basically, if an Access DB is saved onto a network drive and accessed over a VPN, would the array be stored in the RAM of the laptop? If it is, I would assume this would alleviate the speed issues and would be worthwhile taking the time to re-code.
Thanks,

The thing about form-level or global variables in Access is that you better have good error handling in the application. If an uncaptured error occurs it can result in those variables being for lack of better word discarded. The result would be the next time you try to access the data in the array you get another exception.
Here are a few things you could try before going the array route:
Your combo box probably doesn't need to be bound to your recordset rs directly. Set the source of the combo box at design time to the underlying query or table.
This makes it possible to simply refer to the combo box's bound field using something like this: AgentName = cboAgentName.Value
(If you can eliminate an unnecessary recordset object from the form the better off you will be in the long run)
Your lookup code shouldn't need to use SELECT *. This just isn't a good practice to use in production code. Instead, use SELECT TID. Basically, only return in your query the fields you actually need.
You don't need to use the adOpenKeySet option, which is unnecessary overhead. You should be able to use adOpenForwardOnly.
I would also suggest looking at the AllAgents table to make sure that there is an index on the field you are using for the lookup. If there isn't, think about adding one.
You still might need to go the array route, but these are relatively simple things that you can use to try to troubleshoot performance without introducing massive code changes to the application.

Related

MS Access 2016 Woes - Lockups in MultiUser (Inserting Dummy Entries to Add New Item)

I found an interesting conundrum with a database I administer. To create a new Stock Item, it adds a dummy entry and then opens this new entry in the usual Form for editing. See the code below.
The code works perfectly fine until you have the database open on more than one PC.
If the user on the 1st PC adds a new item, the 2nd PC freaks out over the dummy entry. This causes 10-20 second delay on everything they do on the 2nd PC.
I'm trying to think of a simple / elegant way to achieve this without using a dummy entry (because it doesn't actually have a StockCode until the user enters one, I think on the 2nd PC the program chokes on the dummy entry with no StockCode)
I really have no idea at this point.
Case vbKeyF1 ' F1 Key to Add New Record
stokmastSQL = "INSERT INTO tblSTOKMAST (STOCKCODE, PER, SELL1, SELL2, SELL3, GST) VALUES ('', '', 0.00, 0.00, 0.00, 'N');"
DoCmd.SetWarnings False ' Turn off SQL warnings for Action Queries
DoCmd.RunSQL stokmastSQL
DoCmd.SetWarnings True ' Re-enable SQL warnings for Action Queries
[Forms]![frmSTOKMASTLIST]![lst_STOKMASTLIST].Requery ' Requery ListBox after change
SQL = "SELECT STOCKCODE, DESCR, PER, SELL1 FROM tblSTOKMAST ORDER BY STOCKCODE" ' Re-initialize Record Source of ListBox
[Forms]![frmSTOKMASTLIST]![lst_STOKMASTLIST].RowSource = SQL
[Forms]![frmSTOKMASTLIST]!lst_STOKMASTLIST.SetFocus ' Set Focus to ListBox and select first record
[Forms]![frmSTOKMASTLIST]!lst_STOKMASTLIST.Selected(0) = True
DoCmd.OpenForm "frmSTOKMASTEDIT", , , "[STOCKCODE] = '" & [Forms]![frmSTOKMASTLIST]![lst_STOKMASTLIST].Column(0) & "'" ' Open new record in frmSTOKMASTEDIT
KeyCode = 0
End Select
Well, there is a MASSIVE HUGE LARGE difference here between a long delay, and that of something not working.
What you described so far is something that is SLOW, not that it does not work.
About once a week on SO, this issue comes up, and has come up for about the last 20 years in a row!!!
You don't want to try and write a boatload of code to fix this issue, since virtually EVERYTHING you do will have to be fixed, and in 99% of cases, writing code will NOT fix this issue!!!
In other words, this is NOT due to the dummy entry, but something wrong.
You don't mention/note if the database is split - but lets leave that issue for a bit.
The first test/thing to try?
Do this:
launch the applcation. Now, open ANY table (and you not noted if you using linked tables here - a MASSIVE issue we need to know here).
Now, from the nav pane, open ANY table. Now minimize the table (assuming the application is in windowed mode. But, regardless, JUST OPEN any table.
At this point, now launch the form that creates the new record. is it slow?
The above is what we call a persistent connection. By opening a table (any table), then this forces access to keep the database open. And in multi-user mode, this "opining" process can be VERY slow (like 20 seconds).
So, first and foremost, try the above. So, you can open ANY table (and if using linked tables, then ANY linked table). Now, just leave that table open, and now again try your form. The delay (even with 2 users) should not be gone.
if you don't address this delay, then that dealy will start to appear everywhere, and will appear even without writing code.
And conversly, then writing code will not fix this issue.
If you determine the above fixes this issue? Then on application startup, you can in VBA code create what we call a persistent connection. Another way is to open a form (any form) bound to a existing table.
As noted, if the above does not fix this issue, then we have to look at how that new record is added, and perhaps it uses something like dmax() or some such to get the "max ID value". In that case? Then adding a index to that column will/can fix this issue.
So, try the above first (since you can do this without any code). Just open any table, and THEN launch/use the form(s) in question, and see if the long delay goes away.

Placing an undeclared variable in SQL query code generates an input box?

I recently posted this answer as a way to get user input when creating a MS Access query or report. The general behaviour is that if an undeclared variable is placed in SQL query code or a report (e.g. [UndeclaredVariable]), a small input/dialog box appears for the user to input the variables value.
I have been unable to find any mention of this functionality in documentation or elsewhere. All discussion is about using InputBox() in the standard way.
This functionality is unexpected/unusual for several reasons:
(In my limited knowledge) Using undeclared variables in MS Access generally causes an error
The input/dialog box is different than the one created when InputBox() is used
The functionality seems to transcend standard behavior (e.g. when an two undeclared variable are used in this way as the "ifTrue" and "ifFalse" components of an IIf() statement, BOTH dialog boxes are created sequentially!)
Does anyone know what this functionality is called or why it works in these ways?
To summarize the above comments:
the behavior is called a "parameter query" and is like normal parameterized queries (see here)
The behavior with IIf() is because Access requires a parameter to be given whether or not the value is used (in this case for both [ifTrue] and [ifFalse])
There seems to be no way to conditionally parameterize a query or report
Ok, as to why they work the way they do? Well a lot of people use Access for just working with data tables. They don't necessary build forms and reports. So, if you place a [parm] box in a query, it will prompt you, and you don't need code. So, a really handy feature.
And of course if you do build a report, and need a quick and dirty way to prompt for some criteria then once again, super handy, and again no code or forms need be written.
And if you do create a form, then you can place criteria in a query like this:
select * from tblCustomers where City = Forms!MyPromptForm!City
once again, no code. And using a forms! expression like above means no prompt when you run the query (or a report based on that query).
The only real downside of using these parameters is they are not without effort to make them optional.
As you are finding out, putting forms! expression in queries can get really messy real fast. And such queries can prompt you when you don't want to.
Even worse, is now that the query is now "married" and attached to that ONE form (if you use forms! expressions for parameters). Often, I have a nice query that I could use MANY times for different reports, and often even that same query could be used for reports...but then someone comes along and puts in a expression or paramter(s) that means the query is ONLY good when that form is opened, or you get nasty prompts for some parameter that you now don't want. Or you want DIFFERENT criteria on a different column.
So hard coding the number of parameters in such a query is REALLY painful, not flexible, and really starts to fall down.
So, if you have 5 combo boxes on a form, but the user only selects restrictions in 3 of the combo boxes...and wants the other 2 to be ignored?
I could probably write another 10 or pages as to why putting forms expressions in queries or shoving in parameters is bad (besides...it makes the queries real ugly, and hard to read. and, the sql then is not standard anymore (it will not work with server based systems either).
So, the solution use now is simply to take the values from the form, and build your own where clause in code. That way, you simply design the reports (or forms), and attached them to the query, BUT NO FORMS! conditions are placed in the query. And the SQL is clean without garbage in it.
To "send" the conditions to the report (or form), you simply use the "where" clause. This is exactly why ms-access has this feature…and it solves a zillion problems…and will reduce your development costs by a substantial amount.
Take a look at the following screen shots to see what I mean:
So in above we have a start date, and end date. And the user can select 1, or several tours and THEN click on the report view button. Trying to deal with a easy and clean "prompt" form for user and THEN attempt to use a query with parmaters is sheer folly.
The code to make those above screens work and launch the report with the selected restrictions when you hit the "print" button is easy:
Or take this screen:
So in the above we have a combo box to select the sales rep.
But, you can leave it blank.
So, code to deal with above will work like this:
dim strWhere as string
'select sales rep combo
if isnull(cboSalesRep) = false then
strWhere = "SalesRep = " & cboSalesRep & ""
end if
' select "status" for the report
if isnull(lstStatus) = false then
if strWhere <> "" then
strWhere = strWhere " and "
end if
strWhere = strWhere & "Status = '" & litStatus & "'"
end if
Note how the 2nd list box is checked for a value. You can add as "many" more conditions you want. Lets say we have a check box to only include Special Customers. We can add to our very nice prompt screen a check box to
[x] Show Only Special customers
The code we add would be:
if chkSpeicalOnly = True then
if strWhere <> "" then
strWhere = strWhere " and "
endif
strWhere = strWhere & "SpecialCust = true"
end if
For sure, each combo and control we add to the nice report screen takes a bit of code, but no more messy then the query builder..and this way, each query is nice and clean, and free of a bunch of HIGHLY un-maintainable forms! expressions or "optional" paramters.
Further, it means you can re-use the same query for different reports, and have no worries about some form that is supposed to be open or some nasty out of the blue prompt for a parameter. So, a tiny bit more code eliminates the messy query problem.. For me, this is very worth while trade.
So parameters are quick and dirty. But if REALLY going to start building a decent user interface and some nice report prompt forms? Dump the use of parameters - you be glad you did. And you only have to go to the ONE code spot, add another code block for a new and optional critera. You don't have to change the report, and and you don't have to go and modify the SQL either. So, a bit of code goes a LONG way to attacking this problem.
At the end of your code, we now have a valid SQL where clause. So, now just launch the report with with this:
Docmd.OpenReport "ReportName",acViewPreview,,strWhere
And the above also works for searching and launching a form. So the form can be based on such queries, and again no prompts or paramters means the form can be used to edit that data.
So the concept of filter data and launching forms (or reports) can and should be detached from the concept of some query that pulls data from a table. Over time, software will change, and then the boss will ask how about a filter or prompt for invoice number, but that again has to be optional. Over time your business rules will change. And you can often apply that filter to "many" reports. So in this form, the critea is built up, but the list box allows you to select one of "many" reports, and not all are based on the same SQL, but they all accept the same "filter" or better term used is the "where" clause of the open report or form command.
In above, the user can hold down the ctrl-key to select 1 or 2 or hit the "select all" button. In that form, the user is actually selecting the bus tour ID for a given tour. And then they can choose the report in the lower left. Again, attempting to acheive the above multiple critera simply can't work with sql paramets anyway.
I am using a TourID = some value and Busid in (LIST OF buses selected).
once again, you can simply pass that complex "where" clause to the report - and that where clause can even have a sub-query, or a Field in (LIST OF ID'S) criteria (which can't be done with parameters anyway).

MS Access 2010: "Cannot open any more databases."

While struggling with a single legacy MS Access application I faced this weird error:
Cannot open any more databases.
The application makes extensive use of UNION sql statements. So this seems to cause access hitting the limit of 2048 open tables. Any other chance than getting rid of these unions?
I had this problem when using linked external tables. The limit was reached because about 10 excel files were used by different queries over and over again. So the number of open tables was more or less the product of queries and tables.
I imagine using unions multiplies this problem as well.
The solution for me was to copy linked excel tables into Access native tables first. Then run the very same queries with the native tables.
Often, this occurs with big/complex forms with many subforms and/or comboboxes/listboxes.
Try to do what Saurabh says. Are good things anyway. But i think that these changes will not solve your problem.
Recently, i solve the same problem. I identified that always occurs when a given form were opened. This form had many subforms and combos.
First. Try to make your form or forms simpler: do you really need all subforms? All subforms must be loaded always?
I solve my problem distributing subforms in diferent pages of a tab control. Then load and unload subforms dynamically in Change event.
Initially, only subforms on the first page must have the "SourceObject" property assigned. The rest, has this property empty.
In change event, try to do something like this:
Private Sub TabControl_Change
Dim pgn As Access.Page
...
For Each varCtlSubform In Array(Me.Subform1, Me.Subform1, ...)
Set pgn = varCtlSubform.Parent
If pgn.PageIndex <> Me.TabControl.value Then
if varCtlSubform.SourceObject <> "" Then
varCtlSubform.SourceObject = ""
End if
Else
If varCtlSubform.SourceObject <> ctlSubform.Tag then
varCtlSubform.SourceObject = ctlSubform.Tag
End if
End If
Next
...
End sub
This is a generic function to iterate on all subform controls. If isn't in the active page, unload it. In other case, take source object from tag property.
You'll need to avoid references to unloaded subforms, i.e., if "Subform1" is unloaded you'll get an error with anything like this:
Me.Subform1.Form.InvoiceId
This change have other benefits. Your form will load faster and record navigation will be faster.
You need to evaluate each section of your UNION query, and any other queries that it depends upon. You may get improvement by creating a temp table that represents a query with many joined tables, and use the temp table instead.
When I started developing with Access I had a habit of making big denormalized snowflake queries and using them as a source for a reports and listboxes. I didn't have any tables with more than 100,000 records and the database ran fast. Later I started to get the annoying "Cannot open any more databases" error and discovered the errors of my ways.
I created a form that will help track how many databases connections you have used and how many remain. If you add this form to your database and click Requery after opening your queries and other objects, you will be able to find the objects that are using a significant number of connections.
Note that every reference to a local table or query object uses 1 connection. A reference to a linked table uses 2 connections. A query that joins two linked tables will use 5 connections. If that query is getting called by many other queries in your union, the number adds up fast. Maybe you don't need any of the fields from a joined table in a subquery. In that case you can make a new query.
I read a lot about this online and some people think Access/Jet has around 2,000 TableID’s but that number doesn’t match up with what my form reports. The numbers reported by my form align perfectly with the error. It may be counting something different than TableID’s but it provides an accurate gauge to measure the amount of connections being used as you open new objects.
You can read more and download it from here:
https://access.wordpress.com/2014/04/01/how-many-database-connections-are-still-available-in-an-access-database/
The only real way around this problem is to use a temporary set of tables. Insert the results from your unions into temp tables and then use those to limit the number of tables per query. I usually prefix my temp tables with an underscore ( _tmpCustomers ) and then destroy them when I'm done.
I want to thank ricardohzsz for his wonderful code! It really helped me improve my database performance as well as eliminate error 3048.
I would vote the post up but I don't have enough of a reputation on here to vote.
I had to make some modifications to get it to work for my needs (I needed the subforms to allow additions and edits and using this code made them read-only). I am posting the alterations here in case it may help somebody else, too:
Private Sub TabControlMain_Change()
Dim pgn As Access.Page
Dim sbf As SubForm
Dim strSubForm As String
Dim VarCtlSubform As Variant
For Each VarCtlSubform In Array(Me.sf1, Me.sf2, Me.sf3, etc)
Set pgn = VarCtlSubform.Parent
If pgn.PageIndex <> Me.TabControlMain.Value Then
If VarCtlSubform.SourceObject <> "" Then
VarCtlSubform.SourceObject = ""
End If
Else
If VarCtlSubform.SourceObject <> VarCtlSubform.Tag Then
VarCtlSubform.SourceObject = VarCtlSubform.Tag
strSubForm = VarCtlSubform.Name
Set sbf = Screen.ActiveForm.Controls(strSubForm)
sbf.Form.AllowAdditions = True
sbf.Form.AllowEdits = True
End If
End If
Next
End Sub
Your application is trying to open too many connections to the Access database. Its not just the tables in your sql statement that add up to 2048, even the forms, reports, comboboxes, unclosed recordsets etc add up to the number of connections used by your application. Few things you can try out here:
1. Close the resources (eg record sets) which you are not really using.
2. If you are making use of domain aggergate functions( eg DLookup), change it with Elookup as it explicitly cleans up after itself.
3. You can modify your sql code to make use of Temp Tables.
Hope it helps.

How does Access 2007's moveNext/moveFirst/, etc., feature work?

I'm not an Access expert, but am an SQL expert. I inherited an Access front-end referencing a SQL 2005 database that worked OK for about 5000 records, but is failing miserably for 800k records...
Behind the scenes in the SQL profiler & activity manager I see some kind of Access query like:
SELECT "MS1"."id" FROM "dbo"."customer" "MS1" ORDER BY "MS1"."id"
The MS prefix doesn't appear in any Access code I can see. I'm suspicious of the built-in Access navigation code:
DoCmd.GoToRecord , , acNext
The GoToRecord has AcRecord constant, which includes things like acFirst, acLast, acNext, acPrevious and acGoTo.
What does it mean in a database context to move to the "next" record? This particular table uses an identity column as the PK, so is it internally grabbing all the IDs and then moving to the one that is the next highest???
If so, how would it work if a table was comprised of three different fields for the PK?
Or am I on the wrong track, and something else in Access is calling that statement? Unfortunately I see a ton of prepared statements in the profiler.
THanks!
First is literally the first row in the Recordset. In general, Access accesses data via the equivalent of cursors. So, Next and Previous are moving forward and backwards in the Recordset one row at a time just as you can with SQL Server's cursors. Be careful about depending on the sequence of the rows without an ORDER BY statement in the construction of the Recordset. Although Access is an ISAM, you should not rely on the rows coming in any particular order. Depending on the cursor type, Access will not pull the entire table down but will generally ask for one record at a time. That said, I have seen Access pull entire tables for whatever reason.
You have to distinguish between automating Access objects and working with recordsets in code.
In a form, this command has meaning:
DoCmd.GoToRecord , , acNext
It is nonspecific, and it is not predictable what record it will go to unless you know the ordering of the underlying recordset in the form and the starting record. It navigates you through the recordset stored in the form's edit buffer (which is loaded in the OnOpen event of the form). The command would be used, for instance, in the code behind a command button whose purpose is to navigate records loaded into the form that currentlyl has the focus. I would never leave out the optional arguments if I were to use that command (I almost never do). Instead, I'd identify the specific form I wanted it to apply to:
DoCmd.GoToRecord acForm, "MyForm", acNext
In traversing a DAO recordset, .MoveNext likewise has no predefined meaning except if you know the ordering and starting record. When you are walking a recordset (something you shouldn't do very often, since it's pretty inefficient; but it depends on the task you need to perform) and need to hit each record, you'd certainly call .MoveNext as part of your loop:
With rs
.MoveFirst ' technically not required, as it's the default starting point
Do Until .EOF
[do something]
.MoveNext
Loop
End With
Nothing mysterious there. It's most likely going to be used in code with small numbers of records (large recordsets really oughtn't be navigated sequentially).
In answer to your specific question:
What does it mean in a database
context to move to the "next" record?
This particular table uses an identity
column as the PK, so is it internally
grabbing all the IDs and then moving
to the one that is the next highest???
...as I said, the next record is determined by the ordering of the recordset being traversed and the starting position. In the case of the form, it's the edit buffer that's being traversed, and as the current record bookmark changes in the edit buffer, the form is updated to load the data for that record. The dynaset is bound to the underlying data table, and when the form's edit buffer is saved, the edited data is written back to the server. While it's being edited, locks may or may not be maintained on the record on the server, but Access/Jet/ACE does keep track of the state of the existing record on the server and the record in the edit buffer and will inform you at save time in Access if the record on the server has been changed since it was loaded into the form's edit buffer.
Now, in a comment, you say the form is bound to the whole table. This is a terrible design no matter whether your data is stored in a Jet/ACE back end data file or in a server database like SQL Server. The only reason Access can get away with it is because it and Jet are rather efficient about pulling data from the data source.
I properly-designed client/server Access front end will not load full tables in forms, but instead ask what filtered recordset you want to load, either 1 or more records at a time. This is only marginally more complicated than binding to a whole table.
As to knowing what cursor types are being used, you shouldn't be worrying about it. By default, Access forms use what Access/Jet/ACE calls dynasets. Each form has a RecordsetType property, and it's set to dynaset by default (read the help file on the meaning of the different recordset types). If you want more control of that, you can (but likely shouldn't) create your recordsets in code and assign them to the form's .Recordset property. This is useful in a few circumstances, such as when you'd like to bind a form to a disconnected recordset, but the point of Access is leveraging its capabilities working with bound data. Assigning your own recordsets still gets you bound controls, and the form events, but is more work than is usually necessary.
Basically, change your forms to load only the subset of records the user needs to work with (that may be one record at a time), and then let everything else get done with Access's default behaviors. If something causes a bottleneck, then troubleshoot that and replace the default behavior with something more efficient.
In other words, avoid premature optimization -- let Access be Access.
And don't worry about what Access is doing behind the scenes unless/until Access does something inappropriate.

access report from dynamic crosstab query and vba to "manually" generate reports

I have come across the problem of generating complex access reports (by complex I mean with data processing, variable number of fields, among others).
Let me explain in deeper detail some of the things I need to implement:
Some fields should not show according to some values in a query
If a certain record does not exist, a nice colored (very noticeable) message should appear instead of the values that would be there (Suppose, for example, that a record with 03/04/2009 in the date field exists, a record with 03/06/2009 in the date field also exists but no record with 03/05/2009 exists. Before showing the data related to the last record, I should print something like "Didn't show up on 03/05/2009")
A bar chart that takes as data not the values in the records, but instead something else that is calculated over a set of records (like an average of all grades for a certain date). The number of series in this chart also varies according to values in the records, this chart would not be in the detail section, but instead in the page heading or some kind of group heading.
It should also be mentioned that the query is a TRANSFORM query (more precisely, an INNER JOIN of many TRANSFORM queries), and thus the number of columns returned by the query varies. While in the past I've been unable to bind this query as the recordsource for the report, somehow Access stopped complaining for now (can someone please clarify this? Is this normal, should I not worry about it and use it as a recordsource or should I avoid it?)
There are two options to achieve what I want (that I can see for now):
Create a report with no record source and lots of unbound fields, and through several events (Report_Open, Section_Format, etc.) and with the help of DAO, manually set the values of these fields. Changing the Data Series of the chart is also possible through VBA.
Set the record source to the query, and create some crazy and confusing VBA code to deal with the data and implement everything I need.
It seems to me that option 2 is going to be a huge headache and waste of time, and I recognize option 1 is pretty much like writing to an Excel file (since all the data is obtained with DAO), which would be much easier since I have much more control over almost everything there (but for many other reasons, we want everything in an access report)
While I'm biased and intend to go with option 1, I have found several problems with this option, for example:
I can't find a way to create new pages in the report with VBA, and thus I'm limited only to the first page.
Lack of some kind of free, online, decent and complete documentation on VBA and Access Reports
Also, if option 2 is more viable, I'm certainly willing to go with it, but I would also need some advice, and perhaps some tips to solving the problems I mentioned in this question.
So, the questions are:
Where can I find some decent and complete documentation on Access Reports and VBA?
How can I create pages in an access report, and choose which page I want to write to?
With the problem I have in my hands, will I reach any bottlenecks I should know about? Should I already be thinking of alternatives to Access Reports (writing to a spreadsheet, for example?)
Sounds like you want to dynamically create the report and avoid all the dummy text boxes.
In regard to:
I can't find a way to create new pages
in the report with VBA, and thus I'm
limited only to the first page.
Your solution #1 seems to assume an unbound report.
I think what I'd do is have the form the crosstab as the rowsource, so you'd have records to generate the pages, and then define your report's controls with no ControlSource (except for the controls that are bound to fields that are always present in the CrossTab). Then you could assign the ControlSources at runtime based on the particular columns. Here's the SQL for a crosstab grabbed from an app I'm working on now:
TRANSFORM First(impNoMatch.PersonID) AS FirstOfPersonID
SELECT impNoMatch.LastName, impNoMatch.FirstBame
FROM impNoMatch
GROUP BY impNoMatch.LastName, impNoMatch.FirstName
PIVOT impNoMatch.Status;
Now, you know that the fields in the SELECT clause will always be present, so if you opened a recordset on the SQL string you are using and count the number of fields in the recordset's Fields collection (you can't use the report's Recordset unless it's an ADO recordset, i.e., not bound to the Recordsource):
Dim strSQL As String
Dim rsFields As DAO.Recordset
Dim lngFieldCount As Long
strSQL = Me.Recordsource
Set rsFields = CurrentDB.OpenRecordset(strSQL)
lngFieldCount = rsFields.Fields.Count
From that, since you know the number of fields in the SELECT statement (i.e., the row headings), you can calculate the number of dynamic controls you want to assign, and you can use this recordset's fields collection to assign the ControlSources and unhide the controls.
You'd start out with all your controls that will display the dynamic fields set so their Visible property is FALSE. You'd also use a naming convention for those controls. In the code below, I've used txtNN, where NN is the numeric index in the Fields collection formatted as 2 digits. Here's the code (which adds lines to what's listed above, and is run in the OnOpen event):
Dim strSQL As String
Dim rsFields As DAO.Recordset
Dim lngFieldCount As Long
Dim l As Long
Dim strControlName As String
strSQL = Me.RecordSource
Set rsFields = CurrentDb.OpenRecordset(strSQL)
lngFieldCount = rsFields.Fields.Count
For l = 2 To lngFieldCount - 1
strControlName = "txt" & Format(l, "00")
Me(strControlName).ControlSource = rsFields.Fields(l).Name
Me(strControlName).Visible = True
Next l
rsFields.Close
Set rsFields = Nothing
Now, if you want to get fancy, you can reformat the controls, changing widths and horizontal/vertical position. If you do that, you have to do it in a different event, and it's a bit tricky to choose that. The only good place to put it is in a report group's header's OnFormat event. If you don't have any grouping, you can add one that doesn't do anything. In the case of my crosstab, a two-level sort on Lastname and Firstname and a header on the Firstname group with nothing in it is a good place to use the OnFormat event to change the appearance/layout of the controls on your report.
As to your question about how to learn how to do this, I recommend picking up an intermediate/advance Access programming book. The Access Developers Handbook is the gold standard on this, and includes tons of examples of programmatic control of reports.