How to Monitor Temp-Table Usage in Progress OpenEdge
2026 02 17 · 8 min readTemp-tables remain the most widely used data structure in Progress OpenEdge, with most applications and developers still relying on them despite the introduction of built-in collections in version 12.5.
Despite this, temp-table usage is rarely monitored during runtime. Proper monitoring can reveal excessive read and write operations, inefficient queries, and suboptimal index usage, helping you improve overall application performance.
In this article, I’ll outline practical ways to monitor temp-table usage in your code.
Monitoring Temp-Table Usage Using a Log File
Most applications already have client-side logging enabled. If that is the case, enabling temp-table usage statistics requires only a few additional session startup parameters.
Below is an example of a .pf file configuration:
-clientlog C:\Progress\workspace\logs\session.log
-logentrytypes 4GLMessages,4GLTrace,4GLTrans,TTStats
-logginglevel 3
-tttablerangesize 50
-ttindexrangesize 50
There are four important points to consider.
First, adding TTStats to the -logentrytypes parameter enables logging of temp-table and temp-table index usage statistics.
Second, -logginglevel must be set appropriately. Level 2 is sufficient if you only want to track temp-table statistics. If you also want to track index usage, you must set it to at least level 3.
Third, -tttablerangesize and -ttindexrangesize must be greater than zero. These values define how many temp-tables and indexes will be tracked, so adjust them according to your needs.
Once configured, you will see log entries marked with the TTSTATS tag. Below is an example output generated after running a simple test procedure:
[26/01/28@15:52:35.671+0200] P-031608 T-029564 3 4GL TTSTATS Index: ttCustomer.CustNum (test.p)
[26/01/28@15:52:35.671+0200] P-031608 T-029564 3 4GL TTSTATS Create: 1118 Delete: 0 Read: 1118 OS-Read: 0 Split: 2 Block-Delete: 0
[26/01/28@15:52:35.671+0200] P-031608 T-029564 3 4GL TTSTATS Index: ttCustomer.Name (test.p)
[26/01/28@15:52:35.671+0200] P-031608 T-029564 3 4GL TTSTATS Create: 1123 Delete: 0 Read: 0 OS-Read: 0 Split: 7 Block-Delete: 0
[26/01/28@15:52:35.671+0200] P-031608 T-029564 3 4GL TTSTATS Index: ttCustomer.SalesRep (test.p)
[26/01/28@15:52:35.671+0200] P-031608 T-029564 3 4GL TTSTATS Create: 1117 Delete: 0 Read: 0 OS-Read: 0 Split: 0 Block-Delete: 0
[26/01/28@15:52:35.671+0200] P-031608 T-029564 2 4GL TTSTATS Table: ttCustomer (test.p)
[26/01/28@15:52:35.671+0200] P-031608 T-029564 2 4GL TTSTATS Create: 1117 Update: 492 Delete: 0 Read: 1117 OS-Read: 0
While logging provides useful insights, it is not always ideal for quick troubleshooting. Log files grow faster, and TTSTATS entries are mixed with other log records. If you want a broader overview, using a log parser quickly becomes necessary.
This is where Virtual System Tables, or VSTs, become useful. In the next section, we will explore how to integrate them into your application code.
Using temp-table VSTs (Virtual System Tables)
By using VSTs, we gain more flexibility in how temp-table and index usage statistics are stored and processed. Instead of relying on log files, we can export the data into separate files and choose the format that best fits our needs.
This is where the Progress.Database.TempTableInfo class becomes useful. Its methods allow us to obtain VST handles and manipulate the data programmatically.
For this to work, the -tttablerangesize and -ttindexrangesize session startup parameters must be configured, as described in the previous section.
There are two main approaches:
- Collect statistics after temp-tables are no longer in memory
- Collect statistics during runtime while temp-tables are still active
1) Accessing usage statistics after the temp-tables are no longer in memory
In this scenario, we use the _TableStatHist and _IndexStatHist VSTs.
To archive statistics into these tables, the following properties must be enabled:
Progress.Database.TempTableInfo:ArchiveTableStatistics
Progress.Database.TempTableInfo:ArchiveIndexStatistics
Below is an example using a procedure called main.p:
/* main.p */
block-level on error undo, throw.
Progress.Database.TempTableInfo:ArchiveTableStatistics = true.
Progress.Database.TempTableInfo:ArchiveIndexStatistics = true.
run test.p.
run tableStats.p.
/* test.p */
block-level on error undo, throw.
define temp-table ttEmployee no-undo like Employee
field fullName as character
.
define temp-table ttCustomer no-undo like Customer.
for each Employee no-lock:
create ttEmployee.
buffer-copy Employee to ttEmployee.
ttEmployee.fullName = substitute("&1 &2", ttEmployee.FirstName, ttEmployee.LastName).
end.
for each Customer no-lock:
create ttCustomer.
buffer-copy Customer to ttCustomer.
end.
for each ttCustomer where
ttCustomer.Balance > 0:
ttCustomer.Balance = 1.
end.
/* tableStat.p */
block-level on error undo, throw.
using Progress.Database.TempTableInfo.
var handle hTTInfo, hIdxInfo.
var character cOutputFile = "tempTableStats.json", cOutputFile2 = "indexStats.json".
assign
hTTInfo = TempTableInfo:GetTableStatHistoryHandle()
hIdxInfo = TempTableInfo:GetIndexStatHistoryHandle()
.
hTTInfo:write-json("FILE", cOutputFile, true).
hIdxInfo:write-json("FILE", cOutputFile2, true).
Here we use GetTableStatHistoryHandle() and GetIndexStatHistoryHandle() to obtain handles for _TableStatHist and _IndexStatHist. We then export the data to JSON files.
{"_TableStatHist": [
{
"_Table-Name": "ttCustomer",
"_Prog-Name": "test.p",
"_Delete-Timestamp": "2026-01-28T16:10:31.966",
"_TableStat-read": 1117,
"_TableStat-update": 492,
"_TableStat-create": 1117,
"_TableStat-delete": 0,
"_TableStat-OsRead": 0
},
{
"_Table-Name": "ttEmployee",
"_Prog-Name": "test.p",
"_Delete-Timestamp": "2026-01-28T16:10:31.966",
"_TableStat-read": 0,
"_TableStat-update": 55,
"_TableStat-create": 55,
"_TableStat-delete": 0,
"_TableStat-OsRead": 0
}
]}
{"_IndexStatHist": [
{
"_Index-Name": "ttCustomer.Comments",
"_Prog-Name": "test.p",
"_Delete-Timestamp": "2026-01-28T16:10:31.966",
"_IndexStat-read": 0,
"_IndexStat-create": 213,
"_IndexStat-delete": 0,
"_IndexStat-split": 0,
"_IndexStat-blockdelete": 0,
"_IndexStat-OsRead": 0
},
{
"_Index-Name": "ttCustomer.CountryPost",
"_Prog-Name": "test.p",
"_Delete-Timestamp": "2026-01-28T16:10:31.966",
"_IndexStat-read": 0,
"_IndexStat-create": 1118,
"_IndexStat-delete": 0,
"_IndexStat-split": 2,
"_IndexStat-blockdelete": 0,
"_IndexStat-OsRead": 0
},
{
"_Index-Name": "ttCustomer.CustNum",
"_Prog-Name": "test.p",
"_Delete-Timestamp": "2026-01-28T16:10:31.966",
"_IndexStat-read": 1118,
"_IndexStat-create": 1118,
"_IndexStat-delete": 0,
"_IndexStat-split": 2,
"_IndexStat-blockdelete": 0,
"_IndexStat-OsRead": 0
},
{
"_Index-Name": "ttCustomer.Name",
"_Prog-Name": "test.p",
"_Delete-Timestamp": "2026-01-28T16:10:31.966",
"_IndexStat-read": 0,
"_IndexStat-create": 1123,
"_IndexStat-delete": 0,
"_IndexStat-split": 7,
"_IndexStat-blockdelete": 0,
"_IndexStat-OsRead": 0
},
{
"_Index-Name": "ttCustomer.SalesRep",
"_Prog-Name": "test.p",
"_Delete-Timestamp": "2026-01-28T16:10:31.966",
"_IndexStat-read": 0,
"_IndexStat-create": 1117,
"_IndexStat-delete": 0,
"_IndexStat-split": 0,
"_IndexStat-blockdelete": 0,
"_IndexStat-OsRead": 0
},
{
"_Index-Name": "ttEmployee.DeptCode",
"_Prog-Name": "test.p",
"_Delete-Timestamp": "2026-01-28T16:10:31.966",
"_IndexStat-read": 0,
"_IndexStat-create": 55,
"_IndexStat-delete": 0,
"_IndexStat-split": 0,
"_IndexStat-blockdelete": 0,
"_IndexStat-OsRead": 0
},
{
"_Index-Name": "ttEmployee.EmpNo",
"_Prog-Name": "test.p",
"_Delete-Timestamp": "2026-01-28T16:10:31.966",
"_IndexStat-read": 0,
"_IndexStat-create": 55,
"_IndexStat-delete": 0,
"_IndexStat-split": 0,
"_IndexStat-blockdelete": 0,
"_IndexStat-OsRead": 0
},
{
"_Index-Name": "ttEmployee.Name",
"_Prog-Name": "test.p",
"_Delete-Timestamp": "2026-01-28T16:10:31.966",
"_IndexStat-read": 0,
"_IndexStat-create": 55,
"_IndexStat-delete": 0,
"_IndexStat-split": 0,
"_IndexStat-blockdelete": 0,
"_IndexStat-OsRead": 0
}
]}
Exporting the data to JSON or XML is the simplest way to use this method. The file clearly reflects the structure of the _TableStatHist and _IndexStatHist tables.
If more control over the output format is required, the data can be exported using dynamic queries. Below is an alternative version of tableStat.p:
/* tableStat.p */
block-level on error undo, throw.
using Progress.Database.TempTableInfo.
var handle hTTInfo, hTTBuff, hIdxInfo, hIdxBuff, hQuery.
var character cOutputFile = "tempTableStats.csv", cOutputFile2 = "indexStats.csv".
assign
hTTInfo = TempTableInfo:GetTableStatHistoryHandle()
hTTBuff = hTTInfo:default-buffer-handle
hIdxInfo = TempTableInfo:GetIndexStatHistoryHandle()
hIdxBuff = hIdxInfo:default-buffer-handle
.
create query hQuery.
hQuery:set-buffers(hTTBuff).
hQuery:query-prepare("for each " + hTTBuff:name).
hQuery:query-open().
output to value(cOutputFile).
do while hQuery:get-next():
hTTBuff:buffer-export(?, ",", "_Delete-Timestamp,_TableStat-delete,_TableStat-OsRead").
end.
output close.
hQuery:query-close().
hQuery:set-buffers(hIdxBuff).
hQuery:query-prepare("for each " + hIdxBuff:name).
hQuery:query-open().
output to value(cOutputFile2).
do while hQuery:get-next():
hIdxBuff:buffer-export(?, ",", "_Delete-Timestamp,_IndexStat-split,_IndexStat-blockdelete,_IndexStat-OsRead").
end.
output close.
hQuery:query-close().
In this case the data is exported to .csv files. In addition in we have added a list of fields to be ignored as the third parameter in the buffer-export() method.
The tempTableStats.csv and indexStats.csv files that have been generated:
"ttCustomer","test.p",1117,492,1117
"ttEmployee","test.p",0,55,55
"ttCustomer.Comments","test.p",0,213,0
"ttCustomer.CountryPost","test.p",0,1118,0
"ttCustomer.CustNum","test.p",1118,1118,0
"ttCustomer.Name","test.p",0,1123,0
"ttCustomer.SalesRep","test.p",0,1117,0
"ttEmployee.DeptCode","test.p",0,55,0
"ttEmployee.EmpNo","test.p",0,55,0
"ttEmployee.Name","test.p",0,55,0
Exporting data this way using dynamic queries and buffer-export() allows us to customise the data we want to see in our files. We even go a step further and modify the queries to ignore certain temp-tables or indexes that are not relevant in our use case.
2) Accessing usage statistics during runtime
If we decide to use this method, there are a few differences in the initial setup. A new session startup parameter, -ttbasetable with the value of 1, must be added. This specifies from which temp-table the statistics will be tracked. Temp-tables are indexed in the order in which they are referenced in the code.
Also, setting the Progress.Database.TempTableInfo:ArchiveTableStatistics and Progress.Database.TempTableInfo:ArchiveIndexStatistics property values to true is no longer required in this case.
The startup parameters now look like this. The values of 50 are arbitrary, as we do not have this many temp-tables or indexes in our code. This does not meaningfully degrade performance, but it does create another issue, which is discussed below.
-tttablerangesize 50
-ttindexrangesize 50
-ttbasetable 1
This method is more complex. Unlike the first method, we do not use _TableStatHist and _IndexStatHist VSTs, but instead _TableStat and _IndexStat.
The issue with these tables is that they contain as many records as specified in the -tttablerangesize and -ttindexrangesize startup parameters, which is 50 in this case. However, only two records will actually contain usage statistics, as we reference only two temp-tables in our code.
Another limitation is that, unlike the historical VSTs, these tables do not contain table names or the program names that reference them. If we want this information to appear in our generated files, we need to use the GetVSTHandle() method from the TempTableInfo class. This approach also allows us to avoid processing the extra empty records in both _TableStat and _IndexStat VSTs.
The example presented here creates a copy of the _TableStat VST and adds the two missing fields. The workflow differs slightly from the first method. A new class, TableStatsClass.cls, is introduced to export the usage statistics, and its method is called directly from test.p.
Here is what that looks like:
/* test.p */
block-level on error undo, throw.
using vstTest.TableStatsClass.
define temp-table ttEmployee no-undo like Employee
field fullName as character
.
define temp-table ttCustomer no-undo like Customer.
for each Employee no-lock:
create ttEmployee.
buffer-copy Employee to ttEmployee.
ttEmployee.fullName = substitute("&1 &2", ttEmployee.FirstName, ttEmployee.LastName).
end.
for each Customer no-lock:
create ttCustomer.
buffer-copy Customer to ttCustomer.
end.
for each ttCustomer where
ttCustomer.CreditLimit lt 10:
delete ttCustomer.
end.
TableStatsClass:writeTempTableStatsToFile("tempTableStats.json").
/* TableStatsClass.cls */
using Progress.Database.TempTableInfo.
using Progress.Database.VSTTableID.
class vstTest.TableStatsClass:
method public static void writeTempTableStatsToFile(input pcFile as character):
var handle hTTInfo, hTTBuff, hQuery, hNewInfo, hNewBuff.
var character cTableName, cProgName.
hTTInfo = Progress.Database.TempTableInfo:GetVSTHandle(Progress.Database.VSTTableID:TableStatId).
hTTBuff = hTTInfo:default-buffer-handle.
create temp-table hNewInfo.
hNewInfo:create-like(hTTInfo).
hNewInfo:add-new-field("TableName", "character").
hNewInfo:add-new-field("ProgramName", "character").
hNewInfo:temp-table-prepare("ttTableStat").
hNewBuff = hNewInfo:default-buffer-handle.
create query hQuery.
hQuery:set-buffers(hTTBuff).
hQuery:query-prepare(substitute ("for each &1", hTTBuff:name)).
hQuery:query-open().
do while hQuery:get-next():
if TempTableInfo:GetTableInfoById(input hTTBuff::_TableStat-Id,
output cTableName,
output cProgName) and
cTableName ne "ttTableStat" then
do:
hNewBuff:buffer-create().
hNewBuff:buffer-copy(hTTBuff).
hNewBuff:buffer-field("TableName"):buffer-value() = cTableName.
hNewBuff:buffer-field("ProgramName"):buffer-value() = cProgName.
end.
end.
hQuery:query-close().
hNewInfo:write-json("FILE", pcFile, true).
end.
end class.
And here is the generated json file:
{"ttTableStat": [
{
"_TableStat-id": 1,
"_TableStat-read": 0,
"_TableStat-update": 55,
"_TableStat-create": 55,
"_TableStat-delete": 0,
"_TableStat-OsRead": 0,
"_TableStat-PartitionId": 0,
"TableName": "ttEmployee",
"ProgramName": "test.p"
},
{
"_TableStat-id": 2,
"_TableStat-read": 1117,
"_TableStat-update": 492,
"_TableStat-create": 1117,
"_TableStat-delete": 0,
"_TableStat-OsRead": 0,
"_TableStat-PartitionId": 0,
"TableName": "ttCustomer",
"ProgramName": "test.p"
}
]}
Wrapping Up: Monitoring Temp-Tables with VSTs
Virtual System Tables are a powerful tool for monitoring temp-table usage in a Progress OpenEdge application.
The examples shown here cover only basic use cases. Every application has unique requirements and performance challenges. Adding temp-table monitoring during development can help identify inefficiencies earlier and resolve issues more quickly, especially as the application grows in size and complexity.
Learning to apply temp-table monitoring in this way is a valuable addition to any OpenEdge developer’s skill set.
This article has been inspired by a presentation made by Peter Judge of Consultingwerk titled “What’s Happening with My Temp-Tables?”, which was presented at PUG Challenge 2025 Valencia. You can find the original slides for the presentation here.
Latest news
-
Progress OpenEdge Indexing: Best Practices for Large Data Sets
2025 12 17A clear explanation of indexing in Progress OpenEdge, including practical examples and best practices for improving performance with large data sets.
More -
Meet Buffer Handles: Faster Data Export in OpenEdge 12.8
Antanas Valenčius2025 11 13Learn how Progress OpenEdge 12.8 improves data export and import, explained by a Progress Developer at Baltic Amadeus.
More -
GitHub Actions CI/CD with Progress OpenEdge
2025 10 03This blog post explains how to implement GitHub actions CI/CD with Progress OpenEdge.
More