David Kurtz
This site will remain for now, but no new posts will appear here.David Kurtzhttp://www.blogger.com/profile/00468908370233805717noreply@blogger.comBlogger193125
Managing Ad Hoc PS/Queries in PeopleSoft
- Do not increase the PIA inactivity timeout or PS/Query timeouts. They are both delivered set at 20 minutes. They are primarily there to protect users and the system as a whole.
- If you have already increased them, you may need to bring them down in stages, allowing users to get used to the new limits and handle exceptional queries.
- If you permit users to use PS/Query, then the PIA inactivity timeout should be the same ICQuery Tuxedo service timeout set on the PSQRYSRV server (default 20 minutes)
- If you do not use online PS/Query, then the PIA timeout can be reduced as low as the PSAPPSRV timeout (default 5 minutes).
- It is not possible to regulate when users run queries in the PIA. Users, quite legitimately, also need to run queries that may take longer than the timeouts. Encourage users to schedule such queries on the process scheduler.
- You can restrict the number of online queries that execute concurrently on the database by restricting the maximum number of PSQRYSRV processes. There is still nothing to prevent users from starting queries which then just queue up on the application server. Users cannot distinguish between a query that is queuing in the application server or executing on the database. The service timeout starts from when the service request is queued in the application server.
- However, you still don’t want even scheduled queries to run for hours on end. Set a maximum processing time in the process definition for the PSQUERY application engine. Say 4 hours. The process scheduler will terminate the PSQUERY process after this amount of time.
- A common cause of long-running PS/Queries is a mistake in the query definition, resulting in missing or incorrect joins in the SQL.
- A SQL query can continue to run on the database server after the client-side process, the PSQRYSRV server process or the PSQUERY application engine process, has terminated, at least until the current fetch returns. A common cause is that a large sort or hash operation must complete before the first fetch returns.
- Enable dead connect detection by setting SQLNET.EXPIRE_TIME in SQLNET.ORA. The idle Oracle shadow process will periodically poll the client process to check that it is still responsive. If a PSQUERY process has been terminated by the process scheduler, this mechanism should also terminate the session if it is still running on the database.
- Use the database resource manager to allocate and manage CPU allocation and limit run time.
- The resource manager only intervenes when the system runs out of free CPU.
- Define a hierarchy of resource manager consumer groups. Map database sessions to consumer groups using session instrumentation. In general, online processing is higher priority than scheduled processes, which are higher priority than ad-hoc queries.
- Set a maximum CPU/runtime limit for scheduled queries. Using different consumer groups for online and scheduled batch queries.
- For scheduled queries, match the consumer group timeout to the maximum runtime for the PSQUERY application engine.
- For online queries, match the consumer group timeout to the PIA inactivity timeout (which should also be the same as the ICQuery Tuxedo service timeout).
- See A Resource Manager CPU Time Limit for PS/Queries Executed Online in the PIA
- NB: Resource Manager is an Oracle Enterprise Edition Feature.
- On Exadata, use SQL Quarantine to identify queries with a track record of exceeding the consumer group timeout.
- Enable SQL Quarantine for scheduled queries only using a trigger on PSPRQSRQST – see Github: set_prcs_sess_parm_trg.sql
- Quarantined queries are immediately terminated with error ORA-00040: active time limit exceeded - call aborted. For scheduled queries, this error can be found in the process scheduler message log.
- Enable SQL Quarantine for SQL Developer, etc, using an on-logon trigger – see Github: set_sess_parm_trg.sql
- Add run control logging for scheduled PS/Queries
- Whenever a scheduled PS/Query, run via PSQUERY, terminates other than successfully, the SQL query can be found in the message log. However, bind variable values are not recorded.
- Bind variable values may be changed between executions of the same query using the same run control by the same user. Enable run control logging so that it is possible to reconstruct the query and bind variables run by the user.
- There is nothing to stop the user from altering the PS/Query between executions, in which case you may need to get the SQL of a long-running query that completes successfully from AWR.
- It is not possible to allocate PS/Queries to a different temporary tablespace because that is determined by the database user ID. Everything in PeopleSoft runs as the Owner ID (usually SYSADM). Unless you are faking a reporting database with a second schema pointed to by a second entry in PS.PSDBOWNER.
Determining Optimal Index Key Compression Length
In 2008, Richard Foote wrote this still excellent 4-part guide to index key compression.
- Index Compression Part I (Low)
- Index Compression Part II (Down Is The New Up)
- Index Compression Part III (2+2=5)
- Index Compression Part IV (Packt Like Sardines In a Crushd Tin Box)
He started with this comment, that I think is just as valid as it was then:
“Index compression is perhaps one of the most under used and neglected index options available. It has the potential to substantially reduce the overall size of non-Unique indexes and multi-column unique indexes, in some scenarios dramatically so... Not only will it potentially save storage, but if the resultant index contains fewer leaf blocks, that’s potentially fewer LIOs and from the Cost Based Optimizer’s point of view, potentially a cheaper execution plan option.”
Index key compression is a highly effective option for reducing index size and improving index performance.
“Oracle stores each distinct combination of compressed column values found within a specific index leaf block in a ‘Prefix’ table within the leaf block and assigns each combination a unique prefix number.” If the prefix length (the number of leading columns to be compressed) is too great, then the prefix table will contain more entries, ultimately one for every row in the index. The compressed index could end up being larger than the uncompressed index! If the prefix length is too small, then you might not get as much compression as you might with a longer prefix length.
In other words, there is a sweet spot where you will achieve optimal compression. That sweet spot can vary from no compression to compressing all the columns. It will vary from index to index, from partition to partition, and potentially over time as the data in an index changes.
Test Every Option to Determine Optimal CompressionOne way to determine optimal compression is through exhaustive testing. Each index could be rebuilt at each possible compression prefix length, and the size of the index could be compared, and the performance of application processes could be tested.
The following PL/SQL script (available on GitHub) rebuilds each index on a named table at each possible compression length, collects statistics and stores them in a table.
REM index_compression_test.sql
create table gfc_index_compression_stats
(table_name varchar2(128)
,index_name varchar2(128)
,num_rows number
,last_analyzed date
,prefix_length number
,blevel number
,leaf_blocks number
,avg_leaf_blocks_per_key number
,avg_data_blocks_per_key number
,clustering_factor number
,constraint gfc_index_compression_stats_pk primary key (table_name, index_name, prefix_length)
);
DECLARE
l_table_name VARCHAR2(128) := 'PSTREENODE';
l_num_cols INTEGER;
l_sql CLOB;
e_invalid_compress_length EXCEPTION;
PRAGMA EXCEPTION_INIT(e_invalid_compress_length,-25194);
BEGIN
FOR i IN (
SELECT table_name, index_name, column_position prefix_length FROM user_ind_columns
WHERE table_name = l_table_name
UNION
SELECT table_name, index_name, 0 FROM user_indexes
WHERE table_name = l_table_name
ORDER BY table_name, index_name, prefix_length DESC
) LOOP
IF i.prefix_length > 0 THEN
l_sql := 'ALTER INDEX '||i.index_name||' REBUILD COMPRESS '||i.prefix_length;
ELSE
l_sql := 'ALTER INDEX '||i.index_name||' REBUILD NOCOMPRESS';
END IF;
BEGIN
dbms_output.put_line(l_sql);
EXECUTE IMMEDIATE l_sql;
dbms_stats.gather_index_stats(user,i.index_name);
MERGE INTO gfc_index_compression_stats u
USING (SELECT * FROM user_indexes WHERE table_name = i.table_name And index_name = i.index_name) s
ON (u.table_name = s.table_name AND u.index_name = s.index_name AND u.prefix_length = NVL(s.prefix_length,0))
WHEN MATCHED THEN UPDATE SET u.num_rows = s.num_rows, u.last_analyzed = s.last_analyzed, u.blevel = s.blevel, u.leaf_blocks = s.leaf_blocks, u.avg_leaf_blocks_per_key = s.avg_leaf_blocks_per_key, u.avg_data_blocks_per_key = s.avg_data_blocks_per_key, u.clustering_factor = s.clustering_factor
WHEN NOT MATCHED THEN INSERT (table_name, index_name, num_rows, last_analyzed, prefix_length, blevel, leaf_blocks, avg_leaf_blocks_per_key, avg_data_blocks_per_key, clustering_factor)
VALUES (s.table_name, s.index_name, s.num_rows, s.last_analyzed, NVL(s.prefix_length,0), s.blevel, s.leaf_blocks, s.avg_leaf_blocks_per_key, s.avg_data_blocks_per_key, s.clustering_factor);
EXCEPTION
WHEN e_invalid_compress_length THEN NULL;
END;
END LOOP;
END;
/The following chart presents the data collected by the script above for the PSTREENODE table in PeopleSoft. The number of leaf blocks is graphed against the compression prefix length. The left-hand end of each line shows the uncompressed size of the index. ANALYZE INDEX … VALIDATE STRUCTURE CASCADE;
ANALYZE TABLE emp VALIDATE STRUCTURE CASCADE;
REM calc_opt_comp.sql
REM (c)Go-Faster Consultancy Ltd. 2014
REM see https://blog.psftdba.com/2016/02/implementing-index-compression-and.html
set serveroutput on autotrace off
clear columns
SPOOL calc_opt_comp
REM DROP TABLE sysadm.gfc_index_stats PURGE;
--create working storage table with same structure as INDEX_STATS
CREATE TABLE sysadm.gfc_index_stats
AS SELECT * FROM index_stats
WHERE 1=2
/
ALTER TABLE sysadm.gfc_index_stats
MODIFY name NOT NULL
/
CREATE UNIQUE INDEX sysadm.gfc_index_stats
ON sysadm.gfc_index_stats (name, partition_name)
/
undefine table_name
DECLARE
l_sql VARCHAR2(100);
l_owner VARCHAR2(8) := 'SYSADM';
l_table_name VARCHAR2(30) := '&&table_name';
BEGIN
FOR i IN (
SELECT i.index_name, ip.partition_name
FROM all_indexes i
, all_ind_partitions ip
WHERE i.index_type like '%NORMAL'
AND i.table_owner = l_owner
AND i.partitioned = 'YES'
AND i.table_name = l_table_name
AND ip.index_owner = i.owner
AND ip.index_name = i.index_name
AND ip.subpartition_count = 0
AND ip.segment_created = 'YES'
UNION
SELECT i.index_name, isp.subpartition_name
FROM all_indexes i
, all_ind_subpartitions isp
WHERE i.index_type like '%NORMAL'
AND i.table_owner = l_owner
AND i.partitioned = 'YES'
AND i.table_name = l_table_name
AND isp.index_owner = i.owner
AND isp.index_name = i.index_name
AND isp.segment_created = 'YES'
UNION
SELECT i.index_name, NULL
FROM all_indexes i
WHERE i.index_type like '%NORMAL'
AND i.table_owner = l_owner
AND i.table_name = l_table_name
AND i.partitioned = 'NO'
AND i.segment_created = 'YES'
MINUS
SELECT name, partition_name
FROM sysadm.gfc_index_stats
) LOOP
IF i.partition_name IS NULL THEN
l_sql := 'ANALYZE INDEX '||l_owner||'.'||i.index_name||' VALIDATE STRUCTURE';
ELSE
l_sql := 'ANALYZE INDEX '||l_owner||'.'||i.index_name||' PARTITION ('||i.partition_name||') VALIDATE STRUCTURE';
END IF;
dbms_output.put_line(l_sql);
EXECUTE IMMEDIATE l_sql;
DELETE FROM sysadm.gfc_index_stats g
WHERE EXISTS(
SELECT 'x'
FROM index_stats i
WHERE i.name = g.name
AND (i.partition_name = g.partition_name OR (i.partition_name IS NULL AND g.partition_name IS NULL)));
INSERT INTO sysadm.gfc_index_stats
SELECT i.* FROM index_stats i;
COMMIT;
END LOOP;
END;
/
…The script produces reports of its analysis. The summary report shows the optimal compression length for each index and lists the columns that are and are not compressed. We can see that the result of the ANALYZE command agrees with the result of the previous test that rebuilt each index at each compression length and measured the size of the index.
Summary Report
Opt Comp Weighted Est.
Prefix Num Average Comp
Table Name Index Name Length FREQ Parts Blocks Saving % Blocks
------------------ ------------------ -------- ---- ----- ------------ -------- ------------
Compress Columns Do Not Compress Columns
----------------------------------------------------------- -----------------------------------------------------------
PSTREENODE PSAPSTREENODE 4 1 0 2,048 41.0 1,208
SETID, TREE_NAME, EFFDT, TREE_BRANCH TREE_NODE, TREE_NODE_NUM, TREE_NODE_NUM_END, TREE_NODE_TYPE
PSBPSTREENODE 8 1 0 1,920 34.0 1,267
SETID, TREE_NAME, TREE_BRANCH, TREE_NODE_NUM, TREE_NODE, TR
EE_NODE_NUM_END, TREE_LEVEL_NUM, TREE_NODE_TYPE
PSDPSTREENODE 3 1 0 1,280 61.0 499
SETID, TREE_NAME, EFFDT PARENT_NODE_NUM
PSFPSTREENODE 2 1 0 1,024 67.0 338
TREE_NAME, EFFDT
PSGPSTREENODE 2 1 0 2,304 35.0 1,498
PARENT_NODE_NAME, TREE_NAME EFFDT, TREE_NODE, SETID
PSHPSTREENODE 2 1 0 2,048 24.0 1,556
TREE_NODE, TREE_NAME EFFDT, SETID, SETCNTRLVALUE, TREE_NODE_NUM
PSIPSTREENODE 3 1 0 1,152 .0 1,152
SETID, TREE_NAME, EFFDT TREE_NODE, TREE_NODE_NUM, TREE_NODE_NUM_END
PS_PSTREENODE 4 1 0 1,792 46.0 968
SETID, SETCNTRLVALUE, TREE_NAME, EFFDT TREE_NODE_NUM, TREE_NODE, TREE_BRANCH
****************** ----- ------------ ------------
sum 0 13,568 8,486Compression of Partitioned Indexes Detail Report
Opt Comp Est.
Prefix Saving Comp
Table Name Index Name Partition Name Length Blocks % Blocks
------------------ ------------------ ------------------------------ -------- ------------ ------ ------------
…
PSHJRNL_LN JRNL_LNH201612 1 143,264 142.0 -60,171
JRNL_LNH201712 0 88,192 74.0 22,930
JRNL_LNH201812 6 12,240 .0 12,240
JRNL_LNH201912 6 11,104 .0 11,104
…
JRNL_LNH202201 6 13,752 .0 13,752
JRNL_LNH202202 6 5,496 .0 5,496
JRNL_LNH202203 6 6,504 .0 6,504
JRNL_LNH202204 6 5,920 .0 5,920
JRNL_LNH202205 6 6,864 .0 6,864
JRNL_LNH202206 6 13,584 .0 13,584
JRNL_LNH202207 6 12,408 .0 12,408
JRNL_LNH202208 3 212,904 121.0 -44,710
JRNL_LNH202209 0 262,472 111.0 -28,872
JRNL_LNH202210 3 228,552 102.0 -4,571
****************** ------------ ------------
sum 1,625,328 574,550- Choosing to compress the entire index at a shorter compression. In which case, most of the partitions will be larger, the exception partitions will be small, but the net effect is that the index will be larger.
- Disabling compression on these partitions. Over-compressed indexes are generally only slightly larger than uncompressed indexes, so the benefit is probably only small
- Leave compression at the length that is optimal for most of the partitions, accepting that a few partitions will be over-compressed. This usually results in the smallest index overall.
Optimising Journal Line Queries: 5. Conclusion
This is the last of five articles that examine the challenges posed by typical queries on the journal line table (PS_JRNL_LN).
After introducing Exadata system statistics, partitioning, and compression and also archiving some historical data, we arrived at the point where the execution plan of the statement changes without needing to use hints.
Original Execution Plan (With Hints)I have used hints to force the original execution plan.
ALTER SESSION SET STATISTICS_LEVEL=ALL;
SELECT /*+LEADING(A) USE_NL(B)
INDEX(B (PS_JRNL_LN.BUSINESS_UNIT PS_JRNL_LN.JOURNAL_ID PS_JRNL_LN.JOURNAL_DATE PS_JRNL_LN.UNPOST_SEQ PS_JRNL_LN.JOURNAL_LINE PS_JRNL_LN.LEDGER))*/
A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, B.BUSINESS_UNIT, B.JOURNAL_ID, TO_CHAR(B.JOURNAL_DATE,'YYYY-MM-DD'), B.LEDGER, B.ACCOUNT
, B.PRODUCT, B.PROJECT_ID, B.CHARTFIELD1, B.CHARTFIELD2, B.CURRENCY_CD, B.AFFILIATE, SUM( B.MONETARY_AMOUNT), B.FOREIGN_CURRENCY
, SUM( B.FOREIGN_AMOUNT), A.REVERSAL_CD, A.REVERSAL_ADJ_PER, A.JRNL_HDR_STATUS, TO_CHAR(A.POSTED_DATE,'YYYY-MM-DD'), A.OPRID, A.DESCR254
, B.DEPTID, A.SOURCE, B.ALTACCT, TO_CHAR(CAST((A.DTTM_STAMP_SEC) AS TIMESTAMP),'YYYY-MM-DD-HH24.MI.SS.FF'), B.LINE_DESCR
FROM PS_JRNL_HEADER A, PS_JRNL_LN B
WHERE (A.BUSINESS_UNIT = B.BUSINESS_UNIT
AND A.JOURNAL_ID = B.JOURNAL_ID
AND A.JOURNAL_DATE = B.JOURNAL_DATE
AND A.UNPOST_SEQ = B.UNPOST_SEQ
AND A.JRNL_HDR_STATUS IN('P','V','U')
AND A.FISCAL_YEAR IN (2024)
AND A.ACCOUNTING_PERIOD BETWEEN 1 AND 12
AND B.CHARTFIELD1 IN ('1234567','1234568','1234569')
AND B.LEDGER IN ('LEDGER'))
GROUP BY A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, B.BUSINESS_UNIT, B.JOURNAL_ID, B.JOURNAL_DATE, B.LEDGER, B.ACCOUNT, B.PRODUCT, B.PROJECT_ID, B.CHARTFIELD1,
B.CHARTFIELD2, B.CURRENCY_CD, B.AFFILIATE, B.FOREIGN_CURRENCY, A.REVERSAL_CD, A.REVERSAL_ADJ_PER, A.JRNL_HDR_STATUS, A.POSTED_DATE, A.OPRID, A.DESCR254,
B.DEPTID, A.SOURCE, B.ALTACCT, A.DTTM_STAMP_SEC, B.LINE_DESCR
/
select * from table(dbms_xplan.display_cursor(null,null,'ADVANCED +ADAPTIVE'));
Rather than get the execution plan from EXPLAIN PLAN, I have executed the query with STATISTICS_LEVEL set to ALL, and then displayed the cursor.
The execution plan starts with the PS_JRNL_HEADER table and uses a nested loop join with a lookup of the unique index on PS_JRNL_LN. Although note that the optimizer costs were produced with Exadata system statistics.
Plan hash value: 4030641493 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows |E-Bytes|E-Temp | Cost (%CPU)| E-Time | Pstart| Pstop | A-Rows | A-Time | Buffers | Reads | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | | | 1740K(100)| | | | 209K|00:06:41.85 | 238M| 15M| | 1 | HASH GROUP BY | | 1 | 498K| 108M| 129M| 1740K (1)| 00:01:08 | | | 209K|00:06:41.85 | 238M| 15M| | 2 | NESTED LOOPS | | 1 | 498K| 108M| | 1722K (1)| 00:01:08 | | | 495K|01:03:03.80 | 238M| 15M| | 3 | NESTED LOOPS | | 1 | 498K| 108M| | 1722K (1)| 00:01:08 | | | 459M|00:11:20.66 | 5549K| 4259K| |* 4 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 1 | 430K| 41M| | 1135 (8)| 00:00:01 | | | 430K|00:00:00.34 | 88642 | 88637 | | 5 | PARTITION RANGE ITERATOR | | 430K| 1 | | | 3 (0)| 00:00:01 | KEY | KEY | 459M|00:10:38.60 | 5460K| 4170K| |* 6 | INDEX RANGE SCAN | PS_JRNL_LN | 430K| 1 | | | 3 (0)| 00:00:01 | KEY | KEY | 459M|00:09:55.80 | 5460K| 4170K| |* 7 | TABLE ACCESS BY LOCAL INDEX ROWID| PS_JRNL_LN | 459M| 1 | 127 | | 4 (0)| 00:00:01 | 1 | 1 | 495K|00:50:25.33 | 233M| 11M| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- … Predicate Information (identified by operation id): --------------------------------------------------- 4 - storage(("A"."FISCAL_YEAR"=2024 AND INTERNAL_FUNCTION("A"."JRNL_HDR_STATUS") AND "A"."ACCOUNTING_PERIOD"<=12 AND "A"."ACCOUNTING_PERIOD">=1)) filter(("A"."FISCAL_YEAR"=2024 AND INTERNAL_FUNCTION("A"."JRNL_HDR_STATUS") AND "A"."ACCOUNTING_PERIOD"<=12 AND "A"."ACCOUNTING_PERIOD">=1)) 6 - access("A"."BUSINESS_UNIT"="B"."BUSINESS_UNIT" AND "A"."JOURNAL_ID"="B"."JOURNAL_ID" AND "A"."JOURNAL_DATE"="B"."JOURNAL_DATE" AND "A"."UNPOST_SEQ"="B"."UNPOST_SEQ" AND "B"."LEDGER"='LEDGER') filter("B"."LEDGER"='LEDGER') 7 - filter(("B"."CHARTFIELD1"='1234567' OR "B"."CHARTFIELD1"='1234568' OR "B"."CHARTFIELD1"='1234569')) Hint Report (identified by operation id / Query Block Name / Object Alias): Total hints for statement: 3 --------------------------------------------------------------------------- 1 - SEL$1 - LEADING(A) 6 - SEL$1 / B@SEL$1 - INDEX(B (PS_JRNL_LN.BUSINESS_UNIT PS_JRNL_LN.JOURNAL_ID PS_JRNL_LN.JOURNAL_DATE PS_JRNL_LN.UNPOST_SEQ PS_JRNL_LN.JOURNAL_LINE PS_JRNL_LN.LEDGER)) - USE_NL(B)
The cost of this execution plan depends mainly upon how many journal header rows are selected. There is a cost of 3 per index lookup, plus another 1 for the table access, making a total of 4 per journal header row. Here we selected 430K rows from PS_JRNL_HEADER, so 430K rows * 4/row = 1720K. We got an actual cost of 1722K. The discrepancy is because the 430K was rounded off by the representation of numbers in the execution plan. Then the cost of the GROUP BY operation is 18K. Hence, the overall cost is 1740K.
The actual execution time of the query was 63 minutes (78 minutes with time taken to fetch 211K rows across the network to the client).
New Execution Plan (Without Hints)Plan hash value: 1053505630 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows |E-Bytes|E-Temp | Cost (%CPU)| E-Time | Pstart| Pstop | A-Rows | A-Time | Buffers | Reads | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | | | 535K(100)| | | | 209K|00:00:10.06 | 27M| 27M| | 1 | HASH GROUP BY | | 1 | 498K| 108M| 129M| 535K (24)| 00:00:21 | | | 209K|00:00:10.06 | 27M| 27M| | * 2 | HASH JOIN | | 1 | 498K| 108M| 46M| 517K (24)| 00:00:21 | | | 495K|00:00:09.23 | 27M| 27M| | 3 | PART JOIN FILTER CREATE | :BF0000 | 1 | 498K| 108M| | 517K (24)| 00:00:21 | | | 430K|00:00:00.66 | 88642 | 88638 | |- 4 | NESTED LOOPS | | 1 | 498K| 108M| 46M| 517K (24)| 00:00:21 | | | 430K|00:00:00.30 | 88642 | 88638 | |- 5 | NESTED LOOPS | | 1 | | | | | | | | 430K|00:00:00.24 | 88642 | 88638 | |- 6 | STATISTICS COLLECTOR | | 1 | | | | | | | | 430K|00:00:00.19 | 88642 | 88638 | | * 7 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 1 | 430K| 41M| | 1135 (8)| 00:00:01 | | | 430K|00:00:00.13 | 88642 | 88638 | |- 8 | PARTITION RANGE ITERATOR | | 0 | | | | | | KEY | KEY | 0 |00:00:00.01 | 0 | 0 | |- * 9 | INDEX RANGE SCAN | PS_JRNL_LN | 0 | | | | | | KEY | KEY | 0 |00:00:00.01 | 0 | 0 | |- * 10 | TABLE ACCESS BY LOCAL INDEX ROWID| PS_JRNL_LN | 0 | 1 | 127 | | 515K (24)| 00:00:21 | 1 | 1 | 0 |00:00:00.01 | 0 | 0 | | 11 | PARTITION RANGE JOIN-FILTER | | 1 | 498K| 60M| | 515K (24)| 00:00:21 |:BF0000|:BF0000| 815K|00:00:07.65 | 27M| 27M| | * 12 | TABLE ACCESS STORAGE FULL | PS_JRNL_LN | 18 | 498K| 60M| | 515K (24)| 00:00:21 |:BF0000|:BF0000| 815K|00:00:07.55 | 27M| 27M| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Outline Data ------------- /*+ BEGIN_OUTLINE_DATA IGNORE_OPTIM_EMBEDDED_HINTS OPTIMIZER_FEATURES_ENABLE('19.1.0') DB_VERSION('19.1.0') ALL_ROWS OUTLINE_LEAF(@"SEL$1") FULL(@"SEL$1" "A"@"SEL$1") FULL(@"SEL$1" "B"@"SEL$1") LEADING(@"SEL$1" "A"@"SEL$1" "B"@"SEL$1") USE_HASH(@"SEL$1" "B"@"SEL$1") USE_HASH_AGGREGATION(@"SEL$1") END_OUTLINE_DATA */ Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("A"."JOURNAL_DATE"="B"."JOURNAL_DATE" AND "A"."BUSINESS_UNIT"="B"."BUSINESS_UNIT" AND "A"."JOURNAL_ID"="B"."JOURNAL_ID" AND "A"."UNPOST_SEQ"="B"."UNPOST_SEQ") 7 - storage(("A"."FISCAL_YEAR"=2024 AND INTERNAL_FUNCTION("A"."JRNL_HDR_STATUS") AND "A"."ACCOUNTING_PERIOD"<=12 AND "A"."ACCOUNTING_PERIOD">=1)) filter(("A"."FISCAL_YEAR"=2024 AND INTERNAL_FUNCTION("A"."JRNL_HDR_STATUS") AND "A"."ACCOUNTING_PERIOD"<=12 AND "A"."ACCOUNTING_PERIOD">=1)) 9 - access("A"."BUSINESS_UNIT"="B"."BUSINESS_UNIT" AND "A"."JOURNAL_ID"="B"."JOURNAL_ID" AND "A"."JOURNAL_DATE"="B"."JOURNAL_DATE" AND "A"."UNPOST_SEQ"="B"."UNPOST_SEQ" AND "B"."LEDGER"='LEDGER') filter("B"."LEDGER"='LEDGER') 10 - filter(("B"."CHARTFIELD1"='1234567' OR "B"."CHARTFIELD1"='1234568' OR "B"."CHARTFIELD1"='1234569')) 12 - storage(("B"."LEDGER"='LEDGER' AND INTERNAL_FUNCTION("B"."CHARTFIELD1"))) filter(("B"."LEDGER"='LEDGER' AND INTERNAL_FUNCTION("B"."CHARTFIELD1")))
Optimising Journal Line Queries: 5. Conclusion
This is the last of five articles that examine the challenges posed by typical queries on the journal line table (PS_JRNL_LN).
After introducing Exadata system statistics, partitioning, and compression and also archiving some historical data, we arrived at the point where the execution plan of the statement changes without needing to use hints.
Original Execution Plan (With Hints)I have used hints to force the original execution plan.
ALTER SESSION SET STATISTICS_LEVEL=ALL;
SELECT /*+LEADING(A) USE_NL(B)
INDEX(B (PS_JRNL_LN.BUSINESS_UNIT PS_JRNL_LN.JOURNAL_ID PS_JRNL_LN.JOURNAL_DATE PS_JRNL_LN.UNPOST_SEQ PS_JRNL_LN.JOURNAL_LINE PS_JRNL_LN.LEDGER))*/
A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, B.BUSINESS_UNIT, B.JOURNAL_ID, TO_CHAR(B.JOURNAL_DATE,'YYYY-MM-DD'), B.LEDGER, B.ACCOUNT
, B.PRODUCT, B.PROJECT_ID, B.CHARTFIELD1, B.CHARTFIELD2, B.CURRENCY_CD, B.AFFILIATE, SUM( B.MONETARY_AMOUNT), B.FOREIGN_CURRENCY
, SUM( B.FOREIGN_AMOUNT), A.REVERSAL_CD, A.REVERSAL_ADJ_PER, A.JRNL_HDR_STATUS, TO_CHAR(A.POSTED_DATE,'YYYY-MM-DD'), A.OPRID, A.DESCR254
, B.DEPTID, A.SOURCE, B.ALTACCT, TO_CHAR(CAST((A.DTTM_STAMP_SEC) AS TIMESTAMP),'YYYY-MM-DD-HH24.MI.SS.FF'), B.LINE_DESCR
FROM PS_JRNL_HEADER A, PS_JRNL_LN B
WHERE (A.BUSINESS_UNIT = B.BUSINESS_UNIT
AND A.JOURNAL_ID = B.JOURNAL_ID
AND A.JOURNAL_DATE = B.JOURNAL_DATE
AND A.UNPOST_SEQ = B.UNPOST_SEQ
AND A.JRNL_HDR_STATUS IN('P','V','U')
AND A.FISCAL_YEAR IN (2024)
AND A.ACCOUNTING_PERIOD BETWEEN 1 AND 12
AND B.CHARTFIELD1 IN ('1234567','1234568','1234569')
AND B.LEDGER IN ('LEDGER'))
GROUP BY A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, B.BUSINESS_UNIT, B.JOURNAL_ID, B.JOURNAL_DATE, B.LEDGER, B.ACCOUNT, B.PRODUCT, B.PROJECT_ID, B.CHARTFIELD1,
B.CHARTFIELD2, B.CURRENCY_CD, B.AFFILIATE, B.FOREIGN_CURRENCY, A.REVERSAL_CD, A.REVERSAL_ADJ_PER, A.JRNL_HDR_STATUS, A.POSTED_DATE, A.OPRID, A.DESCR254,
B.DEPTID, A.SOURCE, B.ALTACCT, A.DTTM_STAMP_SEC, B.LINE_DESCR
/
select * from table(dbms_xplan.display_cursor(null,null,'ADVANCED +ADAPTIVE'));
Rather than get the execution plan from EXPLAIN PLAN, I have executed the query with STATISTICS_LEVEL set to ALL, and then displayed the cursor.
The execution plan starts with the PS_JRNL_HEADER table and uses a nested loop join with a lookup of the unique index on PS_JRNL_LN. Although note that the optimizer costs were produced with Exadata system statistics.
Plan hash value: 4030641493 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows |E-Bytes|E-Temp | Cost (%CPU)| E-Time | Pstart| Pstop | A-Rows | A-Time | Buffers | Reads | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | | | 1740K(100)| | | | 209K|00:06:41.85 | 238M| 15M| | 1 | HASH GROUP BY | | 1 | 498K| 108M| 129M| 1740K (1)| 00:01:08 | | | 209K|00:06:41.85 | 238M| 15M| | 2 | NESTED LOOPS | | 1 | 498K| 108M| | 1722K (1)| 00:01:08 | | | 495K|01:03:03.80 | 238M| 15M| | 3 | NESTED LOOPS | | 1 | 498K| 108M| | 1722K (1)| 00:01:08 | | | 459M|00:11:20.66 | 5549K| 4259K| |* 4 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 1 | 430K| 41M| | 1135 (8)| 00:00:01 | | | 430K|00:00:00.34 | 88642 | 88637 | | 5 | PARTITION RANGE ITERATOR | | 430K| 1 | | | 3 (0)| 00:00:01 | KEY | KEY | 459M|00:10:38.60 | 5460K| 4170K| |* 6 | INDEX RANGE SCAN | PS_JRNL_LN | 430K| 1 | | | 3 (0)| 00:00:01 | KEY | KEY | 459M|00:09:55.80 | 5460K| 4170K| |* 7 | TABLE ACCESS BY LOCAL INDEX ROWID| PS_JRNL_LN | 459M| 1 | 127 | | 4 (0)| 00:00:01 | 1 | 1 | 495K|00:50:25.33 | 233M| 11M| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- … Predicate Information (identified by operation id): --------------------------------------------------- 4 - storage(("A"."FISCAL_YEAR"=2024 AND INTERNAL_FUNCTION("A"."JRNL_HDR_STATUS") AND "A"."ACCOUNTING_PERIOD"<=12 AND "A"."ACCOUNTING_PERIOD">=1)) filter(("A"."FISCAL_YEAR"=2024 AND INTERNAL_FUNCTION("A"."JRNL_HDR_STATUS") AND "A"."ACCOUNTING_PERIOD"<=12 AND "A"."ACCOUNTING_PERIOD">=1)) 6 - access("A"."BUSINESS_UNIT"="B"."BUSINESS_UNIT" AND "A"."JOURNAL_ID"="B"."JOURNAL_ID" AND "A"."JOURNAL_DATE"="B"."JOURNAL_DATE" AND "A"."UNPOST_SEQ"="B"."UNPOST_SEQ" AND "B"."LEDGER"='LEDGER') filter("B"."LEDGER"='LEDGER') 7 - filter(("B"."CHARTFIELD1"='1234567' OR "B"."CHARTFIELD1"='1234568' OR "B"."CHARTFIELD1"='1234569')) Hint Report (identified by operation id / Query Block Name / Object Alias): Total hints for statement: 3 --------------------------------------------------------------------------- 1 - SEL$1 - LEADING(A) 6 - SEL$1 / B@SEL$1 - INDEX(B (PS_JRNL_LN.BUSINESS_UNIT PS_JRNL_LN.JOURNAL_ID PS_JRNL_LN.JOURNAL_DATE PS_JRNL_LN.UNPOST_SEQ PS_JRNL_LN.JOURNAL_LINE PS_JRNL_LN.LEDGER)) - USE_NL(B)
The cost of this execution plan depends mainly upon how many journal header rows are selected. There is a cost of 3 per index lookup, plus another 1 for the table access, making a total of 4 per journal header row. Here we selected 430K rows from PS_JRNL_HEADER, so 430K rows * 4/row = 1720K. We got an actual cost of 1722K. The discrepancy is because the 430K was rounded off by the representation of numbers in the execution plan. Then the cost of the GROUP BY operation is 18K. Hence, the overall cost is 1740K.
The actual execution time of the query was 63 minutes (78 minutes with time taken to fetch 211K rows across the network to the client).
New Execution Plan (Without Hints)Plan hash value: 1053505630 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Starts | E-Rows |E-Bytes|E-Temp | Cost (%CPU)| E-Time | Pstart| Pstop | A-Rows | A-Time | Buffers | Reads | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | | | | 535K(100)| | | | 209K|00:00:10.06 | 27M| 27M| | 1 | HASH GROUP BY | | 1 | 498K| 108M| 129M| 535K (24)| 00:00:21 | | | 209K|00:00:10.06 | 27M| 27M| | * 2 | HASH JOIN | | 1 | 498K| 108M| 46M| 517K (24)| 00:00:21 | | | 495K|00:00:09.23 | 27M| 27M| | 3 | PART JOIN FILTER CREATE | :BF0000 | 1 | 498K| 108M| | 517K (24)| 00:00:21 | | | 430K|00:00:00.66 | 88642 | 88638 | |- 4 | NESTED LOOPS | | 1 | 498K| 108M| 46M| 517K (24)| 00:00:21 | | | 430K|00:00:00.30 | 88642 | 88638 | |- 5 | NESTED LOOPS | | 1 | | | | | | | | 430K|00:00:00.24 | 88642 | 88638 | |- 6 | STATISTICS COLLECTOR | | 1 | | | | | | | | 430K|00:00:00.19 | 88642 | 88638 | | * 7 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 1 | 430K| 41M| | 1135 (8)| 00:00:01 | | | 430K|00:00:00.13 | 88642 | 88638 | |- 8 | PARTITION RANGE ITERATOR | | 0 | | | | | | KEY | KEY | 0 |00:00:00.01 | 0 | 0 | |- * 9 | INDEX RANGE SCAN | PS_JRNL_LN | 0 | | | | | | KEY | KEY | 0 |00:00:00.01 | 0 | 0 | |- * 10 | TABLE ACCESS BY LOCAL INDEX ROWID| PS_JRNL_LN | 0 | 1 | 127 | | 515K (24)| 00:00:21 | 1 | 1 | 0 |00:00:00.01 | 0 | 0 | | 11 | PARTITION RANGE JOIN-FILTER | | 1 | 498K| 60M| | 515K (24)| 00:00:21 |:BF0000|:BF0000| 815K|00:00:07.65 | 27M| 27M| | * 12 | TABLE ACCESS STORAGE FULL | PS_JRNL_LN | 18 | 498K| 60M| | 515K (24)| 00:00:21 |:BF0000|:BF0000| 815K|00:00:07.55 | 27M| 27M| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Outline Data ------------- /*+ BEGIN_OUTLINE_DATA IGNORE_OPTIM_EMBEDDED_HINTS OPTIMIZER_FEATURES_ENABLE('19.1.0') DB_VERSION('19.1.0') ALL_ROWS OUTLINE_LEAF(@"SEL$1") FULL(@"SEL$1" "A"@"SEL$1") FULL(@"SEL$1" "B"@"SEL$1") LEADING(@"SEL$1" "A"@"SEL$1" "B"@"SEL$1") USE_HASH(@"SEL$1" "B"@"SEL$1") USE_HASH_AGGREGATION(@"SEL$1") END_OUTLINE_DATA */ Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("A"."JOURNAL_DATE"="B"."JOURNAL_DATE" AND "A"."BUSINESS_UNIT"="B"."BUSINESS_UNIT" AND "A"."JOURNAL_ID"="B"."JOURNAL_ID" AND "A"."UNPOST_SEQ"="B"."UNPOST_SEQ") 7 - storage(("A"."FISCAL_YEAR"=2024 AND INTERNAL_FUNCTION("A"."JRNL_HDR_STATUS") AND "A"."ACCOUNTING_PERIOD"<=12 AND "A"."ACCOUNTING_PERIOD">=1)) filter(("A"."FISCAL_YEAR"=2024 AND INTERNAL_FUNCTION("A"."JRNL_HDR_STATUS") AND "A"."ACCOUNTING_PERIOD"<=12 AND "A"."ACCOUNTING_PERIOD">=1)) 9 - access("A"."BUSINESS_UNIT"="B"."BUSINESS_UNIT" AND "A"."JOURNAL_ID"="B"."JOURNAL_ID" AND "A"."JOURNAL_DATE"="B"."JOURNAL_DATE" AND "A"."UNPOST_SEQ"="B"."UNPOST_SEQ" AND "B"."LEDGER"='LEDGER') filter("B"."LEDGER"='LEDGER') 10 - filter(("B"."CHARTFIELD1"='1234567' OR "B"."CHARTFIELD1"='1234568' OR "B"."CHARTFIELD1"='1234569')) 12 - storage(("B"."LEDGER"='LEDGER' AND INTERNAL_FUNCTION("B"."CHARTFIELD1"))) filter(("B"."LEDGER"='LEDGER' AND INTERNAL_FUNCTION("B"."CHARTFIELD1")))
Optimising Journal Line Queries: 4. Compression
This is the fourth of five articles that examine the challenges posed by typical queries on the journal line table (PS_JRNL_LN).
SELECT p.partition_position, m.*, p.high_value
FROM dba_tab_modifications m
INNER JOIN dba_tab_partitions p
ON p.table_owner = m.table_owner AND p.table_name = m.table_name AND p.partition_name = m.partition_name
WHERE m.table_owner = 'SYSADM' AND m.table_name = 'PS_JRNL_LN'
ORDER BY 1
/
Part Table Drop
Pos# Owner TABLE_NAME PARTITION_NAME S INSERTS UPDATES DELETES TIMESTAMP TRU Segs HIGH_VALUE
---- -------- ------------ -------------------- - --------- --------- --------- ------------------- --- ---- --------------------------------------------------------------------------------
…
40 SYSADM PS_JRNL_LN JRNL_LN_202212 0 0 0 13/10/2024 10:08:56 NO 0 TO_DATE(' 2023-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
41 SYSADM PS_JRNL_LN JRNL_LN_202301 0 0 0 29/09/2024 10:01:16 NO 0 TO_DATE(' 2023-02-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
42 SYSADM PS_JRNL_LN JRNL_LN_202302 0 0 0 29/09/2024 10:01:16 NO 0 TO_DATE(' 2023-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
43 SYSADM PS_JRNL_LN JRNL_LN_202303 0 0 0 29/09/2024 10:01:16 NO 0 TO_DATE(' 2023-04-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
44 SYSADM PS_JRNL_LN JRNL_LN_202304 0 0 0 29/09/2024 10:01:16 NO 0 TO_DATE(' 2023-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
45 SYSADM PS_JRNL_LN JRNL_LN_202305 0 0 0 29/09/2024 10:01:16 NO 0 TO_DATE(' 2023-06-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
46 SYSADM PS_JRNL_LN JRNL_LN_202306 0 0 0 29/09/2024 10:01:16 NO 0 TO_DATE(' 2023-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
47 SYSADM PS_JRNL_LN JRNL_LN_202307 0 0 0 29/09/2024 10:01:16 NO 0 TO_DATE(' 2023-08-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
48 SYSADM PS_JRNL_LN JRNL_LN_202308 0 0 0 29/09/2024 10:01:16 NO 0 TO_DATE(' 2023-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
49 SYSADM PS_JRNL_LN JRNL_LN_202309 0 0 0 13/10/2024 10:08:56 NO 0 TO_DATE(' 2023-10-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
50 SYSADM PS_JRNL_LN JRNL_LN_202310 0 0 0 27/10/2024 10:59:45 NO 0 TO_DATE(' 2023-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
51 SYSADM PS_JRNL_LN JRNL_LN_202311 0 0 0 29/09/2024 10:01:16 NO 0 TO_DATE(' 2023-12-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
52 SYSADM PS_JRNL_LN JRNL_LN_202312 34 193 34 10/12/2024 14:21:38 NO 0 TO_DATE(' 2024-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
53 SYSADM PS_JRNL_LN JRNL_LN_202401 42374 127736 210 12/12/2024 05:27:31 NO 0 TO_DATE(' 2024-02-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
54 SYSADM PS_JRNL_LN JRNL_LN_202402 34803 92215 0 12/12/2024 05:26:30 NO 0 TO_DATE(' 2024-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
55 SYSADM PS_JRNL_LN JRNL_LN_202403 54940 166263 0 12/12/2024 05:12:29 NO 0 TO_DATE(' 2024-04-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
56 SYSADM PS_JRNL_LN JRNL_LN_202404 5900 13730 0 13/12/2024 05:29:32 NO 0 TO_DATE(' 2024-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
57 SYSADM PS_JRNL_LN JRNL_LN_202405 6151 13869 0 13/12/2024 05:31:06 NO 0 TO_DATE(' 2024-06-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
58 SYSADM PS_JRNL_LN JRNL_LN_202406 18317 58263 0 13/12/2024 16:15:49 NO 0 TO_DATE(' 2024-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
59 SYSADM PS_JRNL_LN JRNL_LN_202407 5067792 14937405 0 13/12/2024 16:02:36 NO 0 TO_DATE(' 2024-08-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
60 SYSADM PS_JRNL_LN JRNL_LN_202408 5217744 15378822 0 13/12/2024 18:02:57 NO 0 TO_DATE(' 2024-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
61 SYSADM PS_JRNL_LN JRNL_LN_202409 65389 243360 160 13/12/2024 12:45:25 NO 0 TO_DATE(' 2024-10-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
62 SYSADM PS_JRNL_LN JRNL_LN_202410 44839 152210 0 13/12/2024 00:28:54 NO 0 TO_DATE(' 2024-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
63 SYSADM PS_JRNL_LN JRNL_LN_202411 28279594 53637873 27478940 13/12/2024 18:18:00 NO 0 TO_DATE(' 2024-12-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
64 SYSADM PS_JRNL_LN JRNL_LN_202412 34761590 53485631 27484239 13/12/2024 19:16:11 NO 0 TO_DATE(' 2025-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
65 SYSADM PS_JRNL_LN JRNL_LN_202501 137138 473452 0 13/12/2024 19:18:09 NO 0 TO_DATE(' 2025-02-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORI1
66 SYSADM PS_JRNL_LN JRNL_LN_202502 0 0 0 10/11/2024 10:08:21 NO 0 TO_DATE(' 2025-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
67 SYSADM PS_JRNL_LN JRNL_LN_202503 466 0 0 13/12/2024 03:59:20 NO 0 TO_DATE(' 2025-04-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
68 SYSADM PS_JRNL_LN JRNL_LN_202504 0 0 0 17/11/2024 10:03:01 NO 0 TO_DATE(' 2025-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
69 SYSADM PS_JRNL_LN JRNL_LN_202505 0 0 0 17/11/2024 10:03:01 NO 0 TO_DATE(' 2025-06-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIA
…
ALTER TABLE ps_jrnl_ln ADD CLUSTERING BY LINEAR ORDER (business_unit, journal_id, journal_date, unpost_Seq);
ALTER TABLE ps_jrnl_ln MODIFY CLUSTERING YES ON LOAD YES ON DATA MOVEMENT;
…
ALTER TABLE ps_jrnl_ln MOVE PARTITION jrnl_ln_202012 COMPRESS FOR QUERY HIGH UPDATE INDEXES ONLINE PARALLEL;
ALTER TABLE ps_jrnl_ln MOVE PARTITION jrnl_ln_202101 COMPRESS FOR QUERY LOW UPDATE INDEXES ONLINE PARALLEL;
…
set serveroutput on timi on
DECLARE
l_table_name VARCHAR2(18) := 'PS_JRNL_LN';
l_part_update BOOLEAN := FALSE;
BEGIN
FOR i IN(
select t.table_name, t.partition_name, t.num_rows, t.blocks stat_blocks, s.blocks seg_blocks
, s.tablespace_name, p.compress_for, t.num_rows/NULLIF(LEAST(t.blocks,s.blocks),0) rpb
from user_segments s
inner join user_tab_partitions p ON p.table_name = s.segment_name AND p.partition_name = s.partition_name
inner join user_tab_statistics t ON s.segment_name = t.table_name AND s.partition_name = t.partition_name and t.blocks>s.blocks
where s.segment_type = 'TABLE PARTITION' and p.compress_for IS NOT NULL and s.segment_name = l_table_name
) LOOP
l_part_update := TRUE;
dbms_output.put_line(i.table_name||' ('||i.partition_name||') '||i.stat_blocks||' => '||i.seg_blocks||' blocks');
dbms_stats.set_table_stats(ownname=>'SYSADM',tabname=>i.table_name,partname=>i.partition_name,numblks=>i.seg_blocks,force=>TRUE);
END LOOP;
IF l_part_update THEN
FOR i IN (
SELECT table_name, sum(blocks) blocks
FROM user_tab_statistics
WHERE table_name = l_table_name AND partition_name IS NOT NULL
GROUP BY table_name
) LOOP
dbms_output.put_line(i.table_name||' = '||i.blocks||' blocks');
dbms_stats.set_table_stats(ownname=>'SYSADM',tabname=>i.table_name,numblks=>i.blocks,force=>TRUE);
END LOOP;
ELSE
dbms_output.put_line(l_table_name||' - no action required');
END IF;
END;
/
ALTER INDEX psdjrnl_ln REBUILD ONLINE TABLESPACE psindex PARALLEL;
ALTER INDEX psdjrnl_ln COALESCE CLEANUP PARALLEL;
ALTER INDEX psdjrnl_ln SHRINK SPACE;
…
ALTER INDEX ps_jrnl_ln REBUILD PARTITION jrnl_ln_202012 ONLINE;
ALTER INDEX ps_jrnl_ln REBUILD PARTITION jrnl_ln_202101 ONLINE;
…
Optimising Journal Line Queries: 3. Partitioning
This is the third of a series of five articles that examine the challenges posed by typical queries on the journal line table (PS_JRNL_LN).
PeopleSoft does not partition tables by default. Application Designer does not support partitioning, mainly because different databases implement partitioning differently. Thus, it is always left to the customer to implement as a customisation. In this article, I am only going to discuss partitioning on Oracle.
Interval PartitioningThis is the sample query that I started with.
SELECT A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, B.BUSINESS_UNIT, B.JOURNAL_ID, TO_CHAR(B.JOURNAL_DATE,'YYYY-MM-DD'), B.LEDGER, B.ACCOUNT
, B.PRODUCT, B.PROJECT_ID, B.CHARTFIELD1, B.CHARTFIELD2, B.CURRENCY_CD, B.AFFILIATE, SUM( B.MONETARY_AMOUNT), B.FOREIGN_CURRENCY
, SUM( B.FOREIGN_AMOUNT), A.REVERSAL_CD, A.REVERSAL_ADJ_PER, A.JRNL_HDR_STATUS, TO_CHAR(A.POSTED_DATE,'YYYY-MM-DD'), A.OPRID, A.DESCR254
, B.DEPTID, A.SOURCE, B.ALTACCT, TO_CHAR(CAST((A.DTTM_STAMP_SEC) AS TIMESTAMP),'YYYY-MM-DD-HH24.MI.SS.FF'), B.LINE_DESCR
FROM PS_JRNL_HEADER A, PS_JRNL_LN B
WHERE (A.BUSINESS_UNIT = B.BUSINESS_UNIT
AND A.JOURNAL_ID = B.JOURNAL_ID
AND A.JOURNAL_DATE = B.JOURNAL_DATE
AND A.UNPOST_SEQ = B.UNPOST_SEQ
AND A.JRNL_HDR_STATUS IN('P','V','U')
AND A.FISCAL_YEAR IN (2024)
AND A.ACCOUNTING_PERIOD BETWEEN 1 AND 12
AND B.CHARTFIELD1 IN ('1234567','1234568','1234569')
AND B.LEDGER IN ('LEDGER'))
GROUP BY A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, B.BUSINESS_UNIT, B.JOURNAL_ID, B.JOURNAL_DATE, B.LEDGER, B.ACCOUNT, B.PRODUCT
, B.PROJECT_ID, B.CHARTFIELD1, B.CHARTFIELD2, B.CURRENCY_CD, B.AFFILIATE, B.FOREIGN_CURRENCY, A.REVERSAL_CD, A.REVERSAL_ADJ_PER
, A.JRNL_HDR_STATUS, A.POSTED_DATE, A.OPRID, A.DESCR254, B.DEPTID, A.SOURCE, B.ALTACCT, A.DTTM_STAMP_SEC, B.LINE_DESCR
It would have been desirable to have been able to partition PS_JRNL_LN by FISCAL_YEAR. However, that column is only present on the parent table, PS_JRNL_HEADER. Oracle can do referential partitioning, where the child table is partitioned by an attribute of a column in the parent table. The parent table must also be partitioned similarly, thus producing a 1:1 mapping of partitions between the parent and child tables. However, this feature also requires the presence of an enforced foreign key constraint between parent and child tables.
PeopleSoft has never implemented or supported database-enforced referential integrity constraints (again, mainly because it was done differently on different databases). Although it is tempting to add a foreign key constraint between these tables, that would be a customisation to PeopleSoft that Oracle would not support. The application would then have to insert parent rows before child rows and delete child rows before deleting parent rows. It has never been tested against these constraints.
Therefore, it is only possible to consider partitioning by a column on PS_JRNL_LN. A column in the unique key is an obvious choice.
- Depending on how BUSINESS_UNIT is set up and used, you might be able to list sub-partition by this column, and split journal lines down into several subpartitions. However, it is almost inevitable that the volumes will be heavily skewed.
- It is tempting to range partition on JOURNAL_ID. Although this column usually contains an entirely numeric value, it is in fact defined as a character (VARCHAR2) data type. Therefore, it is not possible to interval partition upon it. Periodically, it would be necessary to add partitions manually.
- The alternative is to interval range partition on JOURNAL_DATE. I chose to define a monthly interval. I specified the first few partitions for whole years because at this customer, these partitions contained less data after archiving. Thereafter, Oracle automatically creates monthly partitions as data is inserted.
CREATE TABLE PS_JRNL_LN
(BUSINESS_UNIT VARCHAR2(5 CHAR) NOT NULL
,JOURNAL_ID VARCHAR2(10 CHAR) NOT NULL
,JOURNAL_DATE DATE NOT NULL
,UNPOST_SEQ NUMBER(*,0) NOT NULL
,JOURNAL_LINE NUMBER(9,0) NOT NULL
,LEDGER VARCHAR2(10 CHAR) NOT NULL
...
) PARTITION BY RANGE (JOURNAL_DATE) INTERVAL (NUMTOYMINTERVAL(1, 'MONTH'))
(PARTITION JRNL_LN_2016 VALUES LESS THAN (TO_DATE('2017-01-01', 'YYYY-MM-DD'))
,PARTITION JRNL_LN_2017 VALUES LESS THAN (TO_DATE('2018-01-01', 'YYYY-MM-DD'))
,PARTITION JRNL_LN_2018 VALUES LESS THAN (TO_DATE('2019-01-01', 'YYYY-MM-DD'))
,PARTITION JRNL_LN_2019 VALUES LESS THAN (TO_DATE('2020-01-01', 'YYYY-MM-DD'))
)
/
rem rename_jrnl_ln_partitions.sql
rem requires https://github.com/davidkurtz/psscripts/blob/master/psftapi.sql
spool rename_jrnl_ln_partitions.lst
set serveroutput on
DECLARE
l_high_value DATE;
l_sql CLOB;
l_new_partition_name VARCHAR2(30);
BEGIN
psft_ddl_lock.set_ddl_permitted(TRUE);
FOR i IN (
select /*+LEADING(r upt upkc utc)*/ r.recname, upt.table_name, utp.partition_name, utp.high_value, upt.interval interval_size
from sysadm.psrecdefn r
INNER JOIN user_part_tables upt ON upt.table_name = DECODE(r.sqltablename,' ','PS_'||r.recname,r.sqltablename)
AND upt.partitioning_type = 'RANGE' and upt.interval IS NOT NULL
INNER JOIN user_part_key_columns upkc ON upkc.name = upt.table_name AND upkc.object_Type = 'TABLE' and upkc.column_position = 1
INNER JOIN user_tab_columns utc ON utc.table_name = upkc.name AND utc.column_name = upkc.column_name
INNER JOIN user_tab_partitions utp ON utp.table_name = upt.table_name AND utp.partition_name like 'SYS_P%'
WHERE r.recname = 'JRNL_LN' AND r.rectype = 0
AND (utc.data_type = 'DATE' OR utc.data_type like 'TIMESTAMP%')
) LOOP
l_sql := 'SELECT '||i.high_value||'-'||i.interval_size||' FROM DUAL';
EXECUTE IMMEDIATE l_sql INTO l_high_value;
l_new_partition_name := i.recname||'_'||TO_CHAR(l_high_value,'YYYYMM');
l_sql := 'ALTER TABLE '||i.table_name||' RENAME PARTITION '||i.partition_name||' TO '||l_new_partition_name;
IF i.partition_name != l_new_partition_name THEN
dbms_output.put_line(l_sql);
EXECUTE IMMEDIATE l_sql;
END IF;
END LOOP;
FOR i IN (
select /*+LEADING(r upi upkc utc)*/ r.recname, upi.index_name, uip.partition_name, uip.high_value, upi.interval interval_size
from sysadm.psrecdefn r
INNER JOIN user_part_indexes upi ON upi.table_name = DECODE(r.sqltablename,' ','PS_'||r.recname,r.sqltablename)
AND upi.partitioning_type = 'RANGE' and upi.interval IS NOT NULL
INNER JOIN user_part_key_columns upkc ON upkc.name = upi.index_name AND upkc.object_Type = 'INDEX' and upkc.column_position = 1
INNER JOIN user_tab_columns utc ON utc.table_name = upi.table_name AND utc.column_name = upkc.column_name
INNER JOIN user_ind_partitions uip ON uip.index_name = upi.index_name
AND (uip.partition_name like 'SYS_P%' OR SUBSTR(uip.partition_name,1+LENGTH(r.recname),1) != SUBSTR(upi.index_name,3,1))
WHERE r.recname = 'JRNL_LN' AND r.rectype = 0
AND (utc.data_type = 'DATE' OR utc.data_type like 'TIMESTAMP%')
) LOOP
l_sql := 'SELECT '||i.high_value||'-'||i.interval_size||' FROM DUAL';
EXECUTE IMMEDIATE l_sql INTO l_high_value;
l_new_partition_name := i.recname||SUBSTR(i.index_name,3,1)||TO_CHAR(l_high_value,'YYYYMM');
l_sql := 'ALTER INDEX '||i.index_name||' RENAME PARTITION '||i.partition_name||' TO '||l_new_partition_name;
IF i.partition_name != l_new_partition_name THEN
dbms_output.put_line(l_sql);
EXECUTE IMMEDIATE l_sql;
END IF;
END LOOP;
psft_ddl_lock.set_ddl_permitted(FALSE);
END;
/
spool off Optimising Journal Line Queries: 2. Exadata System Statistics
This is the second of a series of five articles that examine the challenges posed by typical queries on the journal line table (PS_JRNL_LN).
Exadata System StatisticsMany other people have written notes about how Oracle's optimizer costs a full table scan. This is a selection:
- Neil Chandler: Oracle Optimizer System Statistics
- John Brady: Oracle Optimizer Plan Costing - Full Table Scans
- Randolf Geist: Understanding the different modes of System Statistics aka. CPU Costing and the effects of multiple block sizes - part 4
- Martin Widlake: Cost of Full Table Scans
Roughly speaking, the cost calculated by the optimizer that we see in an execution plan is an estimate of the time taken to perform an operation, where the unit of time is the duration of a single block read. Although that statement is an oversimplification. There are various guesses and assumptions built into the optimizer's calculation. The cost-based optimizer looks for the cheapest plan, that ought to be the fastest to execute. However, in many cases, cost does not correspond to execution time.
- See also Jonathan Lewis: Cost is Time
The cost of a full table scan is made up of an I/O cost (the time taken to read the blocks from disk) and a CPU cost (the time taken to process the rows). The I/O cost is the number of multi-block read operations, multiplied by the ratio of the duration of a multi-block read to a single-block read.
- IO Cost = (HWM / MBRC) . (MREADTIM / SREADTIM)
Where
- HWM = the high water mark of the segment expressed as a number of blocks
- MBRC = average multi-block read count for sequential read, in blocks (see parameter DB_FILE_MULTIBLOCK_READ_COUNT).
- MREADTIME = average time to perform a multi-block read at once (sequential read), in milliseconds
- SREADTIME = average time to read a single block (random read), in milliseconds
See PL/SQL Packages and Types Reference: DBMS_STATS
The single and multi-block read times are derived from two system statistics, the block size and the multi-block read count.
- SREADTIM = IOSEEKTIM + DB_BLOCK_SIZE / IOTFRSPEED
- MREADTIM = IOSEEKTIM + (DB_BLOCK_SIZE * MBRC) / IOTFRSPEED
Where
- IOSEEKTIM = Seek time + latency time + operating system overhead time, in milliseconds (default 10ms).
- IOTFRSPEED = I/O transfer speed in bytes per millisecond (or if you prefer KBytes/second)
- DB_BLOCK_SIZE = block size of the segment (usually 8Kb)
System statistics can be gathered based on actual system behaviour using DBMS_STATS, or set to pre-defined values using DBMS_STATS.GATHER_SYSTEM_STATS. Over the years many blogs, forums and presentations have discussed the merits or otherwise of collecting or setting system statistics.
Oracle's position is set out in the Oracle Optimizer Blog: Should You Gather System Statistics? It can be summarised as:
- Do not gather your own system statistics.
- Use the Oracle-provided defaults.
- Except on Exadata, where you can consider using the Exadata defaults, and perhaps not even then on a mixed workload. You will have to test this for yourself.
On any Oracle system, the default system statistics can be reset with
exec DBMS_STATS.GATHER_SYSTEM_STATS('NOWORKLOAD');
This sets the system statistics as follows:
- MBRC=8
- IOSEEKTIM=10
- IOTFRSPEED=10
Thus:
SREADTIM = IOSEEKTIM + DB_BLOCK_SIZE / IOTFRSPEED = 10 + 8192 / 4096 = 12 (ms)MREADTIM = IOSEEKTIM + (DB_BLOCK_SIZE * MBRC) / IOTFRSPEED = 10 + (8192 * 8 ) / 4096 = 10 + 16 = 26 (ms)However, on Exadata, you can set 'system statistics take into account the unique capabilities of Oracle Exadata, such as large I/O size and high I/O throughput'
exec DBMS_STATS.GATHER_SYSTEM_STATS('EXADATA');
Some system statistics are then set differently:
- MBRC=128
- IOSEEKTIM=10
- IOTFRSPEED=204800
Thus
SREADTIM = IOSEEKTIM + DB_BLOCK_SIZE / IOTFRSPEED = 10 + 8192 / 204800 = 10.04 (ms)MREADTIM = IOSEEKTIM + (DB_BLOCK_SIZE * MBRC) / IOTFRSPEED = 10 + (8192 . 128 ) / 204800 = 10 + 5.1200 = 15.12000 (ms)Now, I can put these numbers back into the formula Oracle uses to calculate the I/O cost of a full scan.
- IO Cost = (HWM / MBRC) . (MREADTIM / SREADTIM)
Let us suppose that we are going to read 100M blocks. The I/O cost of that scan will be very different with Exadata system statistics rather than the normal default system statistics.
Normal IO Cost = (100000000/8) . (26/12) = 27,083,333.3Thus, introducing Exadata system statistics significantly reduces the cost of the full scan, making the database more likely to use a full scan than index lookups. That may or may not be desirable.
-------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | Pstart| Pstop |
-------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 428K| 93M| | 2834K (8)| 00:01:51 | | |
| 1 | HASH GROUP BY | | 428K| 93M| 111M| 2834K (8)| 00:01:51 | | |
|* 2 | HASH JOIN | | 428K| 93M| 46M| 2819K (8)| 00:01:51 | | |
| 3 | JOIN FILTER CREATE | :BF0001 | 428K| 41M| | 1476 (7)| 00:00:01 | | |
| 4 | PART JOIN FILTER CREATE | :BF0000 | 428K| 41M| | 1476 (7)| 00:00:01 | | |
|* 5 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 428K| 41M| | 1476 (7)| 00:00:01 | | |
| 6 | JOIN FILTER USE | :BF0001 | 1120K| 136M| | 2817K (8)| 00:01:51 | | |
| 7 | PARTITION RANGE JOIN-FILTER| | 1120K| 136M| | 2817K (8)| 00:01:51 |:BF0000|:BF0000|
|* 8 | TABLE ACCESS STORAGE FULL | PS_JRNL_LN | 1120K| 136M| | 2817K (8)| 00:01:51 |:BF0000|:BF0000|
-------------------------------------------------------------------------------------------------------------------------
If I look at the same example query that I used earlier, then with the Exadata default system statistics, the cost has come down significantly (from 66M to 2817K). It is a significant improvement, but it is still greater than the cost of the nested loop (1730K). Therefore, for this query, I still only get this execution plan if I hint the statement to force it. I still need to make the full scan cheaper.
Different queries will have different costs and will flip between the nested loop and Full scan/Bloom filter/hash join at different points.
Non-Exadata System Statisticsexec DBMS_STATS.GATHER_SYSTEM_STATS('NOWORKLOAD');Optimising Journal Line Queries: 2. Exadata System Statistics
This is the second of a series of five articles that examine the challenges posed by typical queries on the journal line table (PS_JRNL_LN).
Exadata System StatisticsMany other people have written notes about how Oracle's optimizer costs a full table scan. This is a selection:
- Neil Chandler: Oracle Optimizer System Statistics
- John Brady: Oracle Optimizer Plan Costing - Full Table Scans
- Randolf Geist: Understanding the different modes of System Statistics aka. CPU Costing and the effects of multiple block sizes - part 4
- Martin Widlake: Cost of Full Table Scans
Roughly speaking, the cost calculated by the optimizer that we see in an execution plan is an estimate of the time taken to perform an operation, where the unit of time is the duration of a single block read. Although that statement is an oversimplification. There are various guesses and assumptions built into the optimizer's calculation. The cost-based optimizer looks for the cheapest plan, that ought to be the fastest to execute. However, in many cases, cost does not correspond to execution time.
- See also Jonathan Lewis: Cost is Time
The cost of a full table scan is made up of an I/O cost (the time taken to read the blocks from disk) and a CPU cost (the time taken to process the rows). The I/O cost is the number of multi-block read operations, multiplied by the ratio of the duration of a multi-block read to a single-block read.
- IO Cost = (HWM / MBRC) . (MREADTIM / SREADTIM)
Where
- HWM = the high water mark of the segment expressed as a number of blocks
- MBRC = average multi-block read count for sequential read, in blocks (see parameter DB_FILE_MULTIBLOCK_READ_COUNT).
- MREADTIME = average time to perform a multi-block read at once (sequential read), in milliseconds
- SREADTIME = average time to read a single block (random read), in milliseconds
See PL/SQL Packages and Types Reference: DBMS_STATS
The single and multi-block read times are derived from two system statistics, the block size and the multi-block read count.
- SREADTIM = IOSEEKTIM + DB_BLOCK_SIZE / IOTFRSPEED
- MREADTIM = IOSEEKTIM + (DB_BLOCK_SIZE * MBRC) / IOTFRSPEED
Where
- IOSEEKTIM = Seek time + latency time + operating system overhead time, in milliseconds (default 10ms).
- IOTFRSPEED = I/O transfer speed in bytes per millisecond (or if you prefer KBytes/second)
- DB_BLOCK_SIZE = block size of the segment (usually 8Kb)
System statistics can be gathered based on actual system behaviour using DBMS_STATS, or set to pre-defined values using DBMS_STATS.GATHER_SYSTEM_STATS. Over the years many blogs, forums and presentations have discussed the merits or otherwise of collecting or setting system statistics.
Oracle's position is set out in the Oracle Optimizer Blog: Should You Gather System Statistics? It can be summarised as:
- Do not gather your own system statistics.
- Use the Oracle-provided defaults.
- Except on Exadata, where you can consider using the Exadata defaults, and perhaps not even then on a mixed workload. You will have to test this for yourself.
On any Oracle system, the default system statistics can be reset with
exec DBMS_STATS.GATHER_SYSTEM_STATS('NOWORKLOAD');
This sets the system statistics as follows:
- MBRC=8
- IOSEEKTIM=10
- IOTFRSPEED=10
Thus:
SREADTIM = IOSEEKTIM + DB_BLOCK_SIZE / IOTFRSPEED = 10 + 8192 / 4096 = 12 (ms)MREADTIM = IOSEEKTIM + (DB_BLOCK_SIZE * MBRC) / IOTFRSPEED = 10 + (8192 * 8 ) / 4096 = 10 + 16 = 26 (ms)However, on Exadata, you can set 'system statistics take into account the unique capabilities of Oracle Exadata, such as large I/O size and high I/O throughput'
exec DBMS_STATS.GATHER_SYSTEM_STATS('EXADATA');
Some system statistics are then set differently:
- MBRC=128
- IOSEEKTIM=10
- IOTFRSPEED=204800
Thus
SREADTIM = IOSEEKTIM + DB_BLOCK_SIZE / IOTFRSPEED = 10 + 8192 / 204800 = 10.04 (ms)MREADTIM = IOSEEKTIM + (DB_BLOCK_SIZE * MBRC) / IOTFRSPEED = 10 + (8192 . 128 ) / 204800 = 10 + 5.1200 = 15.12000 (ms)Now, I can put these numbers back into the formula Oracle uses to calculate the I/O cost of a full scan.
- IO Cost = (HWM / MBRC) . (MREADTIM / SREADTIM)
Let us suppose that we are going to read 100M blocks. The I/O cost of that scan will be very different with Exadata system statistics rather than the normal default system statistics.
Normal IO Cost = (100000000/8) . (26/12) = 27,083,333.3Thus, introducing Exadata system statistics significantly reduces the cost of the full scan, making the database more likely to use a full scan than index lookups. That may or may not be desirable.
-------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | Pstart| Pstop |
-------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 428K| 93M| | 2834K (8)| 00:01:51 | | |
| 1 | HASH GROUP BY | | 428K| 93M| 111M| 2834K (8)| 00:01:51 | | |
|* 2 | HASH JOIN | | 428K| 93M| 46M| 2819K (8)| 00:01:51 | | |
| 3 | JOIN FILTER CREATE | :BF0001 | 428K| 41M| | 1476 (7)| 00:00:01 | | |
| 4 | PART JOIN FILTER CREATE | :BF0000 | 428K| 41M| | 1476 (7)| 00:00:01 | | |
|* 5 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 428K| 41M| | 1476 (7)| 00:00:01 | | |
| 6 | JOIN FILTER USE | :BF0001 | 1120K| 136M| | 2817K (8)| 00:01:51 | | |
| 7 | PARTITION RANGE JOIN-FILTER| | 1120K| 136M| | 2817K (8)| 00:01:51 |:BF0000|:BF0000|
|* 8 | TABLE ACCESS STORAGE FULL | PS_JRNL_LN | 1120K| 136M| | 2817K (8)| 00:01:51 |:BF0000|:BF0000|
-------------------------------------------------------------------------------------------------------------------------
If I look at the same example query that I used earlier, then with the Exadata default system statistics, the cost has come down significantly (from 66M to 2817K). It is a significant improvement, but it is still greater than the cost of the nested loop (1730K). Therefore, for this query, I still only get this execution plan if I hint the statement to force it. I still need to make the full scan cheaper.
Different queries will have different costs and will flip between the nested loop and Full scan/Bloom filter/hash join at different points.
Non-Exadata System Statisticsexec DBMS_STATS.GATHER_SYSTEM_STATS('NOWORKLOAD');Optimising Journal Line Queries: 1. Problem Statement
In each PeopleSoft product, certain tables usually grow to become the largest in the implementations at most customers. The challenges they present and the options for dealing with them are also common to most systems. Most PeopleSoft Financials systems use General Ledger. In General Ledger, the ledger, summary ledger and journal line tables are usually the largest tables, and present the biggest challenges.
This is the first of five articles that examine the challenges typically presented by queries on the journal line table (PS_JRNL_LN).
Problem Statement In General Ledger, we typically see many queries on the ledger (or summary ledger) tables and also queries in the application, drill-down queries in nVision reporting, and ad-hoc PS/Queries that query details of journals posted to the ledger. Below is part of a typical query. The statement and execution plans below were taken from a PeopleSoft Financials system. It is running on Oracle 19c on Exadata. Making use of Exadata features will also be a topic.SELECT A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, B.BUSINESS_UNIT, B.JOURNAL_ID, TO_CHAR(B.JOURNAL_DATE,'YYYY-MM-DD'), B.LEDGER, B.ACCOUNT
, B.PRODUCT, B.PROJECT_ID, B.CHARTFIELD1, B.CHARTFIELD2, B.CURRENCY_CD, B.AFFILIATE, SUM( B.MONETARY_AMOUNT), B.FOREIGN_CURRENCY
, SUM( B.FOREIGN_AMOUNT), A.REVERSAL_CD, A.REVERSAL_ADJ_PER, A.JRNL_HDR_STATUS, TO_CHAR(A.POSTED_DATE,'YYYY-MM-DD'), A.OPRID, A.DESCR254
, B.DEPTID, A.SOURCE, B.ALTACCT, TO_CHAR(CAST((A.DTTM_STAMP_SEC) AS TIMESTAMP),'YYYY-MM-DD-HH24.MI.SS.FF'), B.LINE_DESCR
FROM PS_JRNL_HEADER A, PS_JRNL_LN B
WHERE (A.BUSINESS_UNIT = B.BUSINESS_UNIT
AND A.JOURNAL_ID = B.JOURNAL_ID
AND A.JOURNAL_DATE = B.JOURNAL_DATE
AND A.UNPOST_SEQ = B.UNPOST_SEQ
AND A.JRNL_HDR_STATUS IN('P','V','U')
AND A.FISCAL_YEAR IN (2024)
AND A.ACCOUNTING_PERIOD BETWEEN 1 AND 12
AND B.CHARTFIELD1 IN ('1234567','1234568','1234569')
AND B.LEDGER IN ('LEDGER'))
GROUP BY A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, B.BUSINESS_UNIT, B.JOURNAL_ID, B.JOURNAL_DATE, B.LEDGER, B.ACCOUNT, B.PRODUCT
, B.PROJECT_ID, B.CHARTFIELD1, B.CHARTFIELD2, B.CURRENCY_CD, B.AFFILIATE, B.FOREIGN_CURRENCY, A.REVERSAL_CD, A.REVERSAL_ADJ_PER
, A.JRNL_HDR_STATUS, A.POSTED_DATE, A.OPRID, A.DESCR254, B.DEPTID, A.SOURCE, B.ALTACCT, A.DTTM_STAMP_SEC, B.LINE_DESCR
- The journal line table (PS_JRNL_LN) is joined to its parent, the journal header table (PS_JRNL_HEADER), by the 4 key columns on the journal header (BUSINESS_UNIT, JOURNAL_ID, JOURNAL_DATE, UNPOST_SEQ).
- There are criteria on both the journal header and line tables.
- The number of journal line rows per header is usually highly variable, and it also varies from customer to customer depending on the shape of their data. It is not unusual to see thousands of journal line rows per header row. Filtering it by FISCAL_YEAR and perhaps also ACCOUNTING_PERIOD could be very effective. However, these columns are on PS_JRNL_HEADER, and not on PS_JRNL_LN.
- Queries often include criteria on other attribute columns on PS_JRNL_LN. However, these columns are not indexed by default, though many customers add such indexes.
------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | Pstart| Pstop |
------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 428K| 93M| | 1730K (1)| 00:01:08 | | |
| 1 | HASH GROUP BY | | 428K| 93M| 111M| 1730K (1)| 00:01:08 | | |
| 2 | NESTED LOOPS | | 428K| 93M| | 1715K (1)| 00:01:07 | | |
| 3 | NESTED LOOPS | | 428K| 93M| | 1715K (1)| 00:01:07 | | |
|* 4 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 428K| 41M| | 1476 (7)| 00:00:01 | | |
| 5 | PARTITION RANGE ITERATOR | | 1 | | | 3 (0)| 00:00:01 | KEY | KEY |
|* 6 | INDEX RANGE SCAN | PS_JRNL_LN | 1 | | | 3 (0)| 00:00:01 | KEY | KEY |
|* 7 | TABLE ACCESS BY LOCAL INDEX ROWID| PS_JRNL_LN | 1 | 128 | | 4 (0)| 00:00:01 | 1 | 1 |
------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | Pstart| Pstop |
-------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 428K| 93M| | 66M (1)| 00:43:30 | | |
| 1 | HASH GROUP BY | | 428K| 93M| 111M| 66M (1)| 00:43:30 | | |
|* 2 | HASH JOIN | | 428K| 93M| 46M| 66M (1)| 00:43:30 | | |
| 3 | JOIN FILTER CREATE | :BF0001 | 428K| 41M| | 32501 (1)| 00:00:02 | | |
| 4 | PART JOIN FILTER CREATE | :BF0000 | 428K| 41M| | 32501 (1)| 00:00:02 | | |
|* 5 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 428K| 41M| | 32501 (1)| 00:00:02 | | |
| 6 | JOIN FILTER USE | :BF0001 | 1132K| 137M| | 66M (1)| 00:43:28 | | |
| 7 | PARTITION RANGE JOIN-FILTER| | 1132K| 137M| | 66M (1)| 00:43:28 |:BF0000|:BF0000|
|* 8 | TABLE ACCESS STORAGE FULL | PS_JRNL_LN | 1132K| 137M| | 66M (1)| 00:43:28 |:BF0000|:BF0000|
-------------------------------------------------------------------------------------------------------------------------
- Why is the full scan so expensive?
- How can I make it cheaper?
Optimising Journal Line Queries: 1. Problem Statement
In each PeopleSoft product, certain tables usually grow to become the largest in the implementations at most customers. The challenges they present and the options for dealing with them are also common to most systems. Most PeopleSoft Financials systems use General Ledger. In General Ledger, the ledger, summary ledger and journal line tables are usually the largest tables, and present the biggest challenges.
This is the first of five articles that examine the challenges typically presented by queries on the journal line table (PS_JRNL_LN).
Problem Statement In General Ledger, we typically see many queries on the ledger (or summary ledger) tables and also queries in the application, drill-down queries in nVision reporting, and ad-hoc PS/Queries that query details of journals posted to the ledger. Below is part of a typical query. The statement and execution plans below were taken from a PeopleSoft Financials system. It is running on Oracle 19c on Exadata. Making use of Exadata features will also be a topic.SELECT A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, B.BUSINESS_UNIT, B.JOURNAL_ID, TO_CHAR(B.JOURNAL_DATE,'YYYY-MM-DD'), B.LEDGER, B.ACCOUNT
, B.PRODUCT, B.PROJECT_ID, B.CHARTFIELD1, B.CHARTFIELD2, B.CURRENCY_CD, B.AFFILIATE, SUM( B.MONETARY_AMOUNT), B.FOREIGN_CURRENCY
, SUM( B.FOREIGN_AMOUNT), A.REVERSAL_CD, A.REVERSAL_ADJ_PER, A.JRNL_HDR_STATUS, TO_CHAR(A.POSTED_DATE,'YYYY-MM-DD'), A.OPRID, A.DESCR254
, B.DEPTID, A.SOURCE, B.ALTACCT, TO_CHAR(CAST((A.DTTM_STAMP_SEC) AS TIMESTAMP),'YYYY-MM-DD-HH24.MI.SS.FF'), B.LINE_DESCR
FROM PS_JRNL_HEADER A, PS_JRNL_LN B
WHERE (A.BUSINESS_UNIT = B.BUSINESS_UNIT
AND A.JOURNAL_ID = B.JOURNAL_ID
AND A.JOURNAL_DATE = B.JOURNAL_DATE
AND A.UNPOST_SEQ = B.UNPOST_SEQ
AND A.JRNL_HDR_STATUS IN('P','V','U')
AND A.FISCAL_YEAR IN (2024)
AND A.ACCOUNTING_PERIOD BETWEEN 1 AND 12
AND B.CHARTFIELD1 IN ('1234567','1234568','1234569')
AND B.LEDGER IN ('LEDGER'))
GROUP BY A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, B.BUSINESS_UNIT, B.JOURNAL_ID, B.JOURNAL_DATE, B.LEDGER, B.ACCOUNT, B.PRODUCT
, B.PROJECT_ID, B.CHARTFIELD1, B.CHARTFIELD2, B.CURRENCY_CD, B.AFFILIATE, B.FOREIGN_CURRENCY, A.REVERSAL_CD, A.REVERSAL_ADJ_PER
, A.JRNL_HDR_STATUS, A.POSTED_DATE, A.OPRID, A.DESCR254, B.DEPTID, A.SOURCE, B.ALTACCT, A.DTTM_STAMP_SEC, B.LINE_DESCR
- The journal line table (PS_JRNL_LN) is joined to its parent, the journal header table (PS_JRNL_HEADER), by the 4 key columns on the journal header (BUSINESS_UNIT, JOURNAL_ID, JOURNAL_DATE, UNPOST_SEQ).
- There are criteria on both the journal header and line tables.
- The number of journal line rows per header is usually highly variable, and it also varies from customer to customer depending on the shape of their data. It is not unusual to see thousands of journal line rows per header row. Filtering it by FISCAL_YEAR and perhaps also ACCOUNTING_PERIOD could be very effective. However, these columns are on PS_JRNL_HEADER, and not on PS_JRNL_LN.
- Queries often include criteria on other attribute columns on PS_JRNL_LN. However, these columns are not indexed by default, though many customers add such indexes.
------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | Pstart| Pstop |
------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 428K| 93M| | 1730K (1)| 00:01:08 | | |
| 1 | HASH GROUP BY | | 428K| 93M| 111M| 1730K (1)| 00:01:08 | | |
| 2 | NESTED LOOPS | | 428K| 93M| | 1715K (1)| 00:01:07 | | |
| 3 | NESTED LOOPS | | 428K| 93M| | 1715K (1)| 00:01:07 | | |
|* 4 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 428K| 41M| | 1476 (7)| 00:00:01 | | |
| 5 | PARTITION RANGE ITERATOR | | 1 | | | 3 (0)| 00:00:01 | KEY | KEY |
|* 6 | INDEX RANGE SCAN | PS_JRNL_LN | 1 | | | 3 (0)| 00:00:01 | KEY | KEY |
|* 7 | TABLE ACCESS BY LOCAL INDEX ROWID| PS_JRNL_LN | 1 | 128 | | 4 (0)| 00:00:01 | 1 | 1 |
------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | Pstart| Pstop |
-------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 428K| 93M| | 66M (1)| 00:43:30 | | |
| 1 | HASH GROUP BY | | 428K| 93M| 111M| 66M (1)| 00:43:30 | | |
|* 2 | HASH JOIN | | 428K| 93M| 46M| 66M (1)| 00:43:30 | | |
| 3 | JOIN FILTER CREATE | :BF0001 | 428K| 41M| | 32501 (1)| 00:00:02 | | |
| 4 | PART JOIN FILTER CREATE | :BF0000 | 428K| 41M| | 32501 (1)| 00:00:02 | | |
|* 5 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 428K| 41M| | 32501 (1)| 00:00:02 | | |
| 6 | JOIN FILTER USE | :BF0001 | 1132K| 137M| | 66M (1)| 00:43:28 | | |
| 7 | PARTITION RANGE JOIN-FILTER| | 1132K| 137M| | 66M (1)| 00:43:28 |:BF0000|:BF0000|
|* 8 | TABLE ACCESS STORAGE FULL | PS_JRNL_LN | 1132K| 137M| | 66M (1)| 00:43:28 |:BF0000|:BF0000|
-------------------------------------------------------------------------------------------------------------------------
- Why is the full scan so expensive?
- How can I make it cheaper?
Logging Run Controls and Bind Variables for Scheduled PS/Queries
This blog proposes additional logging for scheduled PS/Queries so that long-running queries can be reconstructed and analysed.
Previous blog posts have discussed limiting PS/Query runtime with the resource manager (see Management of Long Running PS/Queries Cancelled by Resource Manager CPU Limit). From 19c, on Engineered Systems only, the 'Oracle Database automatically quarantines the plans for SQL statements terminated by … the Resource Manager for exceeding resource limits'. SQL Quarantine is enabled by default in Oracle 19c on Exadata (unless patch 30104721 is applied that backports the new 23c parameters, see Oracle Doc ID 2635030.1: 19c New Feature SQL Quarantine - How To Stop Automatic SQL Quarantine).
What is the Problem?SQL Quarantine prevents a query from executing. Therefore, AWR will not capture the execution plan. AWR will also purge execution plans where an execution has not been captured within the AWR retention period. The original long-running query execution that was quarantined, if captured by AWR, will be aged out because it will not execute again.
If we want to investigate PS/Queries that produced execution plans that exceeded the runtime limit and were then quarantined, we need to reproduce the execution plan, either with the EXPLAIN PLAN FOR command or by executing the query in a session where the limited resource manager consumer group does not apply.
However, PS/Queries with bind variables present a challenge. A PS/Query run with different bind variables can produce different execution plans. One execution plan might be quarantined and so never complete, while another may complete within an acceptable time.
In AWR, a plan is only captured once for each statement. Therefore, it is possible to find one set of bind variables for each plan, although there may be many sets of bind variables that all produce the same execution plan. However, we cannot obtain Oracle bind variables for quarantined execution plans that did not execute. To regenerate their execution plans, we need another way to obtain their bind variables.
This problem occurs more generally where the Diagnostics Pack is not available, then it is not possible to reconstruct long-running queries without additional logging or tracing.
Solution- PS_QUERY_RUN_CNTRL: Scheduled Query Run Control. This record identifies the query executed. Rows in this table will be copied to PS_QRYRUN_CTL_HST.
- PS_QUERY_RUN_PARM: Scheduled Query Run Parameters. This record holds the bind variables and the values passed to the query. The table contains a row for each bind variable for each execution. Rows in this table will be copied to PS_QRYRUN_PARM_HST
Two database triggers manage the history tables:
- A database trigger that fires when the run status of the request is updated to '7' (processing). It copies rows for the current run control into two corresponding history tables. Thus, we will have a log of every bind variable for every scheduled query.
- A second database trigger will fire when a PSQUERY request record is deleted. It deletes the corresponding rows from these history tables.
When a PS/Query produces a quarantined execution plan, the PSQUERY process terminates with error ORA-56955: quarantined plan used (see Quarantined SQL Plans for PS/Queries). Now we can obtain the bind variables that resulted in attempts to execute a quarantined query execution plan.
The following script (ps_query_run_cntrl_hist_trigger.sql) creates the tables and triggers.
- Application Designer record definitions should be created for the two history tables by importing the project QRYRUN_HST (download QRYRUN_HST.zip from GitHub).
REM ps_query_run_cntrl_hist_trigger.sql
REM 21.4.2025 - trigger and history tables to capture
set echo on serveroutput on timi on
clear screen
spool ps_query_run_cntrl_hist_trigger
rollback;
CREATE TABLE SYSADM.PS_QRYRUN_CTL_HST
(PRCSINSTANCE INTEGER DEFAULT 0 NOT NULL,
OPRID VARCHAR2(30) DEFAULT ' ' NOT NULL,
RUN_CNTL_ID VARCHAR2(30) DEFAULT ' ' NOT NULL,
DESCR VARCHAR2(30) DEFAULT ' ' NOT NULL,
QRYTYPE SMALLINT DEFAULT 1 NOT NULL,
PRIVATE_QUERY_FLAG VARCHAR2(1) DEFAULT 'N' NOT NULL,
QRYNAME VARCHAR2(30) DEFAULT ' ' NOT NULL,
URL VARCHAR2(254) DEFAULT ' ' NOT NULL,
ASIAN_FONT_SETTING VARCHAR2(3) DEFAULT ' ' NOT NULL,
PTFP_FEED_ID VARCHAR2(30) DEFAULT ' ' NOT NULL) TABLESPACE PTTBL
/
CREATE UNIQUE iNDEX SYSADM.PS_QRYRUN_CTL_HST
ON SYSADM.PS_QRYRUN_CTL_HST (PRCSINSTANCE) TABLESPACE PSINDEX PARALLEL NOLOGGING
/
ALTER INDEX SYSADM.PS_QRYRUN_CTL_HST NOPARALLEL LOGGING
/
CREATE TABLE SYSADM.PS_QRYRUN_PARM_HST
(PRCSINSTANCE INTEGER DEFAULT 0 NOT NULL,
OPRID VARCHAR2(30) DEFAULT ' ' NOT NULL,
RUN_CNTL_ID VARCHAR2(30) DEFAULT ' ' NOT NULL,
BNDNUM SMALLINT DEFAULT 0 NOT NULL,
FIELDNAME VARCHAR2(18) DEFAULT ' ' NOT NULL,
BNDNAME VARCHAR2(30) DEFAULT ' ' NOT NULL,
BNDVALUE CLOB) TABLESPACE PSIMAGE2
/
CREATE UNIQUE iNDEX SYSADM.PS_QRYRUN_PARM_HST
ON SYSADM.PS_QRYRUN_PARM_HST (PRCSINSTANCE, BNDNUM) TABLESPACE PSINDEX PARALLEL NOLOGGING
/
ALTER INDEX SYSADM.PS_QRYRUN_PARM_HST NOPARALLEL LOGGING
/
- PSQUERY is not a restartable Application Engine program. Therefore, there is no risk of duplicate inserts into the history tables.
- The exception handlers in the triggers deliberately suppress any error, in case that causes the process scheduler to crash.
CREATE OR REPLACE TRIGGER sysadm.query_run_cntrl_hist_ins
BEFORE UPDATE OF runstatus ON sysadm.psprcsrqst
FOR EACH ROW
WHEN (new.runstatus ='7' AND old.runstatus != '7' AND new.prcsname = 'PSQUERY' AND new.prcstype = 'Application Engine')
BEGIN
INSERT INTO PS_QRYRUN_CTL_HST
(PRCSINSTANCE, OPRID, RUN_CNTL_ID, DESCR ,QRYTYPE, PRIVATE_QUERY_FLAG, QRYNAME, URL, ASIAN_FONT_SETTING, PTFP_FEED_ID)
SELECT :new.prcsinstance, OPRID, RUN_CNTL_ID, DESCR ,QRYTYPE, PRIVATE_QUERY_FLAG, QRYNAME, URL, ASIAN_FONT_SETTING, PTFP_FEED_ID
FROM ps_query_run_cntrl WHERE oprid = :new.oprid AND run_cntl_id = :new.runcntlid;
INSERT INTO PS_QRYRUN_PARM_HST
(PRCSINSTANCE, OPRID, RUN_CNTL_ID, BNDNUM, FIELDNAME, BNDNAME, BNDVALUE)
SELECT :new.prcsinstance prcsinstance, OPRID, RUN_CNTL_ID, BNDNUM, FIELDNAME, BNDNAME, BNDVALUE
FROM ps_query_run_parm WHERE oprid = :new.oprid AND run_cntl_id = :new.runcntlid;
EXCEPTION WHEN OTHERS THEN NULL; --exception deliberately coded to suppress all exceptions
END;
/
CREATE OR REPLACE TRIGGER sysadm.query_run_cntrl_hist_del
BEFORE DELETE ON sysadm.psprcsrqst
FOR EACH ROW
WHEN (old.prcsname = 'PSQUERY' AND old.prcstype = 'Application Engine')
BEGIN
DELETE FROM PS_QRYRUN_CTL_HST WHERE prcsinstance = :old.prcsinstance;
DELETE FROM PS_QRYRUN_PARM_HST WHERE prcsinstance = :old.prcsinstance;
EXCEPTION WHEN OTHERS THEN NULL; --exception deliberately coded to suppress all exceptions
END;
/
show errors
spool offExampleWhen a query is scheduled to run on the process scheduler, the bind variables are specified through this generic dialogue.
Once the PSQUERY process has started (it immediately commits its update to RUNSTATUS), these values are written to the new history tables.
select * from ps_qryrun_ctl_hst; PRCSINSTANCE OPRID RUN_CNTL_ID DESCR QRYTYPE P QRYNAME URL ASI PTFP_FEED_ID ------------ -------- ------------ ------------------------------ ---------- - ------------------------------ -------------------------------------------------- --- ------------------------------ 12345678 ABCDEF 042225 Journal Line Detail - Account 1 N XXX_JRNL_LINE_DTL_ACCT https://xxxxxxx.yyyyy.com/psp/XXXXXXX/EMPLOYEE/ERP select * from ps_qryrun_ctl_hst; PRCSINSTANCE OPRID RUN_CNTL_ID BNDNUM FIELDNAME BNDNAME BNDVALUE ------------ -------- ------------ ------ ------------------ -------------------- ------------------------------ 12345678 ABCDEF 042225 1 bind1 BUSINESS_UNIT 354XX 12345678 ABCDEF 042225 2 bind2 BUSINESS_UNIT 354XX 12345678 ABCDEF 042225 3 FISCAL_YEAR FISCAL_YEAR 2025 12345678 ABCDEF 042225 4 ACCOUNTING_PD_FROM ACCOUNTING_PD_FROM 2 12345678 ABCDEF 042225 5 ACCOUNTING_PD_TO ACCOUNTING_PD_TO 2 12345678 ABCDEF 042225 6 bind6 ACCOUNT 23882XXXXX 12345678 ABCDEF 042225 7 bind7 ACCOUNT 23882XXXXX 12345678 ABCDEF 042225 8 bind8 ALTACCOUNT 23882XXXXX 12345678 ABCDEF 042225 9 bind9 ALTACCOUNT 23882XXXXXConclusion
If the query is quarantined, PSQUERY will terminate with error ORA-56955: quarantined plan used. The SQL statement can be extracted from the message log, and the execution plan can be generated with the EXPLAIN PLAN FOR command, using the bind variable values captured in the history tables.
Note: The signature of the SQL Quarantine directive is the exact matching signature of the SQL text (it can be generated from the SQL text with dbms_sqltune.sqltext_to_signature). There can be multiple PLAN_HASH_VALUEs for the same signature (because there can be multiple execution plans for the same SQL). Verify that the FULL_PLAN_HASH_VALUE of the execution plan generated with the captured bind variables corresponds to the PLAN_HASH_VALUE of a SQL Quarantine directive.
A Resource Manager CPU Time Limit for PS/Queries Executed On-line in the PIA
In previous posts, I have proposed:
- A Sample Oracle Database Resource Manager Plan for PeopleSoft. It can be downloaded from Github: PSFT_PLAN.
- Setting a maximum CPU time for PS/Queries run on the process scheduler (see Management of Long Running PS/Queries Cancelled by Resource Manager CPU Limit).
This blog looks at whether a limit could also be set for PS/Queries run via the PeopleSoft Internet Architecture (PIA).
The main objection to having the database terminate a PS/Query running in the PIA is that the resulting Oracle error message will be displayed in the browser without any further explanation. Therefore, I think it is better to allow the PIA to handle termination of the query. However, I also think that it would be prudent to prevent queries continuing to run in background after the client session has been terminated.
Timeout Settings for PS/QueryThe inactivity timeout in the PIA, is delivered at 20 minutes (1200 seconds), with a warning 2 minutes earlier. Then the user's session in the PIA is terminated.
There are timeouts on every Tuxedo service. In the PeopleSoft Application Server configuration, the service timeout is specified for each type of application server/queue.
…
[PSQRYSRV]
;=========================================================================
; Settings for PSQRYSRV
;=========================================================================
;-------------------------------------------------------------------------
; UBBGEN settings
Min Instances=3
Max Instances=3
Service Timeout=1200
…
…
ICQuery SRVGRP=APPSRV
LOAD=50 PRIO=50
{QUERYSRV}
SVCTIMEOUT={$PSQRYSRV\Service Timeout}
{QUERYSRV}
{!QUERYSRV}
SVCTIMEOUT={$PSAPPSRV\Service Timeout}
{!QUERYSRV}
BUFTYPE="ALL"
…
PS/Queries are run by the ICQuery service that is advertised on PSQRYSRV server if configured, and whose service timeout is also delivered set to 1200s. Otherwise, it is advertised on PSAPPSRV whose timeout is 300s.
- PSQRYSRV should always be configured if PS/Queries are to be run online in an application server domain. Partly, so that it has the longer timeout, and partly so that long running PS/Queries do not block short on-line requests.
- Please avoid the temptation to increase either of these timeouts. If a query needs to run for more than 20 minutes, then it should be run on the process scheduler.
When the service timeout is reached, the Tuxedo server process will terminate. This may not terminate the query on the database until the current fetch operation completes. If a query involves a large group or sort operation, it can be a long time before the first fetch returns.
Oracle Terminated Connection Timeout (sometimes known as Dead Connect Detection) should be configured by setting SQLNET.EXPIRE_TIME in the SQLNET.ORA file. Then database shadow process periodically sends a probe to the otherwise idle client, and if the client doesn't respond the session terminates. However, this process is not perfect.
Therefore, it would be prudent to set a timeout in the consumer group for online PS/Query sessions. In PSFT_PLAN, that is the PSQUERY_ONLINE_GROUP consumer group. We don't want the timeout to terminate the session before either ICQuery service times out, nor do we want the query to run on afterwards. Therefore, the consumer group timeout should be set to the same value as the PSQRYSRV timeout, so also 1200 seconds.
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'PSQUERY_ONLINE_GROUP'
,mgmt_p6 => 90
,switch_group => 'CANCEL_SQL'
,switch_time => 1200
,switch_for_call => TRUE
);If you change the default inactivity timeout, these three settings should all be set to the same value.
If you are not running on Exadata, then it is safe to set this timeout without any further configuration.
Disabling SQL Quarantine on Exadata for PS/Query While Using it ElsewhereHowever, on Exadata on 19c, the database will automatically create SQL quarantine directives for statements that exceed the CPU time limit specified in the consumer group and are terminated with error ORA-00040: active time limit exceeded - call aborted. It may take a few minutes for the database to create the quarantine directive - see 2634990.1: 19c New Feature SQL Quarantine Not Work. Then, the next time the same query generates the same execution plan, it will immediately be terminated with error ORA-56955: quarantined plan used. Again, we don't want such SQL errors produced in the PIA.
In Oracle 21c, two new parameters have been introduced to control SQL Quarantine.
- OPTIMIZER_CAPTURE_SQL_QUARANTINE enables or disables the automatic creation of SQL Quarantine configurations. The default value is FALSE.
- OPTIMIZER_USE_SQL_QUARANTINE determines whether the optimizer considers SQL Quarantine configurations when choosing an execution plan for a SQL statement. The default value is TRUE.
The parameters can be backported to Oracle 19.3 or later by applying patch 30104721 (see Oracle Doc ID 2635030.1: 19c New Feature SQL Quarantine - How To Stop Automatic SQL Quarantine, and Go-Faster Oracle Blog: New Parameters In 21c To Control Automatic SQL Quarantine Can Be Backported To 19c). Both these parameters can be set at system and session level.
If you want to take advantage of SQL Quarantine, you have to enable it. However, I suggest leaving the new parameters at their default values at system level, even though this means a change of behaviour in 19c when the patch is applied.
- OPTIMIZER_CAPTURE_SQL_QUARANTINE = FALSE
- OPTIMIZER_USE_SQL_QUARANTINE = TRUE
Then set both parameters to TRUE at session level in the session for
- PSQUERY processes run on the process scheduler.
- SQL*Plus and SQL Developer processes.
Ensure they are not set for PIA application server processes
- PSAPPSRV
- PSQRYSRV
In my sample resource plan for PeopleSoft, three consumer groups now have timeouts. SQL Quarantine works in conjunction with consumer groups that have CPU timeouts. I want SQL Quarantine disabled in PSQUERY_ONLINE_GROUP, but enabled in PSQUERY_BATCH_GROUP and LOW_LIMITED_GROUP.
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'PSQUERY_ONLINE_GROUP'
,mgmt_p6 => 90
,switch_group => 'CANCEL_SQL'
,switch_time => 1200 /*same as ICQuery service timeout*/
,switch_elapsed_time => 1200
,switch_estimate => FALSE /*do not timeout on basis of estimated time*/
,switch_for_call => TRUE
);
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'PSQUERY_BATCH_GROUP'
,mgmt_p6 => 1
,switch_group => 'CANCEL_SQL'
,switch_time => 14400
,switch_estimate => TRUE
,switch_for_call => TRUE
);
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'LOW_LIMITED_GROUP'
,mgmt_p8 => 1
,switch_group => 'CANCEL_SQL'
,switch_time => 7200
,switch_elapsed_time => 7200
,switch_estimate => TRUE
,switch_for_call => TRUE
);The consumer groups are mapped to sessions by program name, module and action.
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.CLIENT_PROGRAM , value => 'PSQRYSRV%' , consumer_group => 'PSQUERY_ONLINE_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.MODULE_NAME_ACTION, value => 'QUERY_MANAGER.QUERY_VIEWER', consumer_group => 'PSQUERY_ONLINE_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.MODULE_NAME , value => 'PSQUERY' , consumer_group => 'PSQUERY_BATCH_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.MODULE_NAME , value => 'PSAE.PSQUERY.%' , consumer_group => 'PSQUERY_BATCH_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.CLIENT_PROGRAM , value => 'SQL Developer%' , consumer_group => 'LOW_LIMITED_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.CLIENT_PROGRAM , value => 'sqlplus%' , consumer_group => 'LOW_LIMITED_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.CLIENT_PROGRAM , value => 'Toad%' , consumer_group => 'LOW_LIMITED_GROUP');Initialisation parameters can be set for processes run on the process scheduler using a trigger on the PSPRCSRQST table. This technique was described in a previous blog post: Setting Oracle Session Parameters for Specific Process Scheduler Processes.
When a PeopleSoft process is initiated on the process scheduler, the first thing it does is to update its RUNSTATUS on PSPRCSRQST to '7' indicating that it is processing. The SET_PRCS_SESS_PARM_TRIG trigger fires on that transition. It dynamically generates ALTER SESSION commands for the metadata that matches the current process.
- Script to create trigger set_prcs_sess_parm_trg.sql
- Example script to create metadata: set_prcs_sess_parm.sql. It includes the following statement that will create metadata for PSQUERY to set the new SQL Quarantine parameters if they are available and if running on Exadata.
----------------------------------------------------------------------------------------------------
--Settings for SQL Quarantine in PSQuery on Exadata -- note SQL checks that parameter is valid
----------------------------------------------------------------------------------------------------
DELETE FROM sysadm.PS_PRCS_SESS_PARM where prcsname = 'PSQUERY'
AND param_name IN('optimizer_capture_sql_quarantine','optimizer_use_sql_quarantine')
/
----------------------------------------------------------------------------------------------------
INSERT INTO sysadm.PS_PRCS_SESS_PARM (prcstype, prcsname, oprid, runcntlid, keyword, param_name, parmvalue)
with n as ( --returns a row on Exadata only
SELECT COUNT(DISTINCT cell_name) num_exadata_cells
FROM v$cell
HAVING COUNT(DISTINCT cell_name)>0
), x (param_name, keyword, parmvalue) as ( --returns rows if parameters available
select name, 'SET', 'TRUE'
from v$parameter, n
where name IN('optimizer_capture_sql_quarantine','optimizer_use_sql_quarantine')
), y (prcstype, prcsname, oprid, runcntlid) as (
select prcstype, prcsname, ' ', ' '
from ps_prcsdefn
where prcsname = 'PSQUERY'
)
select y.prcstype, y.prcsname, y.oprid, y.runcntlid, x.keyword, x.param_name, x.parmvalue
from x,y
/This is done with a AFTER LOGON trigger and another metadata data table that is similar to the scheduled process trigger. The mappings in the metadata in this script must match the mappings for the consumer groups where automatic SQL quarantine capture is required.
- Script to create trigger and metadata: set_sess_parm_trg.sql
- Set the timeout for the consumer group for PS/Query to be the same value as the ICQuery Tuxedo service timeout (usually also the same value as the PIA inactivity timeout).
- SQL Quarantine is only available on Exadata. On other platforms, the query just runs to the CPU timeout, by which time the ICQuery service has already timed out.
- If on Exadata, then
- Apply patch 30104721 on 19c to backport new 21c parameters.
- Leave the quarantine parameters at their default values at system level
- Set both parameters to TRUE at session level in the session for
- PSQUERY processes run on the process scheduler.
- SQL*Plus and SQL Developer processes.
A Resource Manager CPU Time Limit for PS/Queries Executed On-line in the PIA
In previous posts, I have proposed:
- A Sample Oracle Database Resource Manager Plan for PeopleSoft. It can be downloaded from Github: PSFT_PLAN.
- Setting a maximum CPU time for PS/Queries run on the process scheduler (see Management of Long Running PS/Queries Cancelled by Resource Manager CPU Limit).
This blog looks at whether a limit could also be set for PS/Queries run via the PeopleSoft Internet Architecture (PIA).
The main objection to having the database terminate a PS/Query running in the PIA is that the resulting Oracle error message will be displayed in the browser without any further explanation. Therefore, I think it is better to allow the PIA to handle termination of the query. However, I also think that it would be prudent to prevent queries continuing to run in background after the client session has been terminated.
Timeout Settings for PS/QueryThe inactivity timeout in the PIA, is delivered at 20 minutes (1200 seconds), with a warning 2 minutes earlier. Then the user's session in the PIA is terminated.
There are timeouts on every Tuxedo service. In the PeopleSoft Application Server configuration, the service timeout is specified for each type of application server/queue.
…
[PSQRYSRV]
;=========================================================================
; Settings for PSQRYSRV
;=========================================================================
;-------------------------------------------------------------------------
; UBBGEN settings
Min Instances=3
Max Instances=3
Service Timeout=1200
…
…
ICQuery SRVGRP=APPSRV
LOAD=50 PRIO=50
{QUERYSRV}
SVCTIMEOUT={$PSQRYSRV\Service Timeout}
{QUERYSRV}
{!QUERYSRV}
SVCTIMEOUT={$PSAPPSRV\Service Timeout}
{!QUERYSRV}
BUFTYPE="ALL"
…
PS/Queries are run by the ICQuery service that is advertised on PSQRYSRV server if configured, and whose service timeout is also delivered set to 1200s. Otherwise, it is advertised on PSAPPSRV whose timeout is 300s.
- PSQRYSRV should always be configured if PS/Queries are to be run online in an application server domain. Partly, so that it has the longer timeout, and partly so that long running PS/Queries do not block short on-line requests.
- Please avoid the temptation to increase either of these timeouts. If a query needs to run for more than 20 minutes, then it should be run on the process scheduler.
When the service timeout is reached, the Tuxedo server process will terminate. This may not terminate the query on the database until the current fetch operation completes. If a query involves a large group or sort operation, it can be a long time before the first fetch returns.
Oracle Terminated Connection Timeout (sometimes known as Dead Connect Detection) should be configured by setting SQLNET.EXPIRE_TIME in the SQLNET.ORA file. Then database shadow process periodically sends a probe to the otherwise idle client, and if the client doesn't respond the session terminates. However, this process is not perfect.
Therefore, it would be prudent to set a timeout in the consumer group for online PS/Query sessions. In PSFT_PLAN, that is the PSQUERY_ONLINE_GROUP consumer group. We don't want the timeout to terminate the session before either ICQuery service times out, nor do we want the query to run on afterwards. Therefore, the consumer group timeout should be set to the same value as the PSQRYSRV timeout, so also 1200 seconds.
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'PSQUERY_ONLINE_GROUP'
,mgmt_p6 => 90
,switch_group => 'CANCEL_SQL'
,switch_time => 1200
,switch_for_call => TRUE
);If you change the default inactivity timeout, these three settings should all be set to the same value.
If you are not running on Exadata, then it is safe to set this timeout without any further configuration.
Disabling SQL Quarantine on Exadata for PS/Query While Using it ElsewhereHowever, on Exadata on 19c, the database will automatically create SQL quarantine directives for statements that exceed the CPU time limit specified in the consumer group and are terminated with error ORA-00040: active time limit exceeded - call aborted. It may take a few minutes for the database to create the quarantine directive - see 2634990.1: 19c New Feature SQL Quarantine Not Work. Then, the next time the same query generates the same execution plan, it will immediately be terminated with error ORA-56955: quarantined plan used. Again, we don't want such SQL errors produced in the PIA.
In Oracle 21c, two new parameters have been introduced to control SQL Quarantine.
- OPTIMIZER_CAPTURE_SQL_QUARANTINE enables or disables the automatic creation of SQL Quarantine configurations. The default value is FALSE.
- OPTIMIZER_USE_SQL_QUARANTINE determines whether the optimizer considers SQL Quarantine configurations when choosing an execution plan for a SQL statement. The default value is TRUE.
The parameters can be backported to Oracle 19.3 or later by applying patch 30104721 (see Oracle Doc ID 2635030.1: 19c New Feature SQL Quarantine - How To Stop Automatic SQL Quarantine, and Go-Faster Oracle Blog: New Parameters In 21c To Control Automatic SQL Quarantine Can Be Backported To 19c). Both these parameters can be set at system and session level.
If you want to take advantage of SQL Quarantine, you have to enable it. However, I suggest leaving the new parameters at their default values at system level, even though this means a change of behaviour in 19c when the patch is applied.
- OPTIMIZER_CAPTURE_SQL_QUARANTINE = FALSE
- OPTIMIZER_USE_SQL_QUARANTINE = TRUE
Then set both parameters to TRUE at session level in the session for
- PSQUERY processes run on the process scheduler.
- SQL*Plus and SQL Developer processes.
Ensure they are not set for PIA application server processes
- PSAPPSRV
- PSQRYSRV
In my sample resource plan for PeopleSoft, three consumer groups now have timeouts. SQL Quarantine works in conjunction with consumer groups that have CPU timeouts. I want SQL Quarantine disabled in PSQUERY_ONLINE_GROUP, but enabled in PSQUERY_BATCH_GROUP and LOW_LIMITED_GROUP.
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'PSQUERY_ONLINE_GROUP'
,mgmt_p6 => 90
,switch_group => 'CANCEL_SQL'
,switch_time => 1200 /*same as ICQuery service timeout*/
,switch_elapsed_time => 1200
,switch_estimate => FALSE /*do not timeout on basis of estimated time*/
,switch_for_call => TRUE
);
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'PSQUERY_BATCH_GROUP'
,mgmt_p6 => 1
,switch_group => 'CANCEL_SQL'
,switch_time => 14400
,switch_estimate => TRUE
,switch_for_call => TRUE
);
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'LOW_LIMITED_GROUP'
,mgmt_p8 => 1
,switch_group => 'CANCEL_SQL'
,switch_time => 7200
,switch_elapsed_time => 7200
,switch_estimate => TRUE
,switch_for_call => TRUE
);The consumer groups are mapped to sessions by program name, module and action.
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.CLIENT_PROGRAM , value => 'PSQRYSRV%' , consumer_group => 'PSQUERY_ONLINE_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.MODULE_NAME_ACTION, value => 'QUERY_MANAGER.QUERY_VIEWER', consumer_group => 'PSQUERY_ONLINE_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.MODULE_NAME , value => 'PSQUERY' , consumer_group => 'PSQUERY_BATCH_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.MODULE_NAME , value => 'PSAE.PSQUERY.%' , consumer_group => 'PSQUERY_BATCH_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.CLIENT_PROGRAM , value => 'SQL Developer%' , consumer_group => 'LOW_LIMITED_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.CLIENT_PROGRAM , value => 'sqlplus%' , consumer_group => 'LOW_LIMITED_GROUP');
DBMS_RESOURCE_MANAGER.set_consumer_group_mapping (attribute => DBMS_RESOURCE_MANAGER.CLIENT_PROGRAM , value => 'Toad%' , consumer_group => 'LOW_LIMITED_GROUP');Initialisation parameters can be set for processes run on the process scheduler using a trigger on the PSPRCSRQST table. This technique was described in a previous blog post: Setting Oracle Session Parameters for Specific Process Scheduler Processes.
When a PeopleSoft process is initiated on the process scheduler, the first thing it does is to update its RUNSTATUS on PSPRCSRQST to '7' indicating that it is processing. The SET_PRCS_SESS_PARM_TRIG trigger fires on that transition. It dynamically generates ALTER SESSION commands for the metadata that matches the current process.
- Script to create trigger set_prcs_sess_parm_trg.sql
- Example script to create metadata: set_prcs_sess_parm.sql. It includes the following statement that will create metadata for PSQUERY to set the new SQL Quarantine parameters if they are available and if running on Exadata.
----------------------------------------------------------------------------------------------------
--Settings for SQL Quarantine in PSQuery on Exadata -- note SQL checks that parameter is valid
----------------------------------------------------------------------------------------------------
DELETE FROM sysadm.PS_PRCS_SESS_PARM where prcsname = 'PSQUERY'
AND param_name IN('optimizer_capture_sql_quarantine','optimizer_use_sql_quarantine')
/
----------------------------------------------------------------------------------------------------
INSERT INTO sysadm.PS_PRCS_SESS_PARM (prcstype, prcsname, oprid, runcntlid, keyword, param_name, parmvalue)
with n as ( --returns a row on Exadata only
SELECT COUNT(DISTINCT cell_name) num_exadata_cells
FROM v$cell
HAVING COUNT(DISTINCT cell_name)>0
), x (param_name, keyword, parmvalue) as ( --returns rows if parameters available
select name, 'SET', 'TRUE'
from v$parameter, n
where name IN('optimizer_capture_sql_quarantine','optimizer_use_sql_quarantine')
), y (prcstype, prcsname, oprid, runcntlid) as (
select prcstype, prcsname, ' ', ' '
from ps_prcsdefn
where prcsname = 'PSQUERY'
)
select y.prcstype, y.prcsname, y.oprid, y.runcntlid, x.keyword, x.param_name, x.parmvalue
from x,y
/This is done with a AFTER LOGON trigger and another metadata data table that is similar to the scheduled process trigger. The mappings in the metadata in this script must match the mappings for the consumer groups where automatic SQL quarantine capture is required.
- Script to create trigger and metadata: set_sess_parm_trg.sql
- Set the timeout for the consumer group for PS/Query to be the same value as the ICQuery Tuxedo service timeout (usually also the same value as the PIA inactivity timeout).
- SQL Quarantine is only available on Exadata. On other platforms the query just runs to the CPU timeout, by which time the ICQuery service has already timed out.
- If on Exadata, then
- Apply patch 30104721 on 19c to backport new 21c parameters.
- Leave the quarantine parameters at their default values at system-level
- Set both parameters to TRUE at session level in the session for
- PSQUERY processes run on the process scheduler.
- SQL*Plus and SQL Developer processes.
Quarantined SQL Plans for PS/Queries
This follows on from my previous post, Management of Long Running PS/Queries Cancelled by Resource Manager.
From 19c, on Engineered Systems only (such as Exadata and Exadata Cloud Service) the 'Oracle Database automatically quarantines the plans for SQL statements terminated by … the Resource Manager for exceeding resource limits.
The Resource Manager can set a maximum estimated execution time for a SQL statement, for example, 20 minutes. If a statement execution exceeds this limit, then the Resource Manager terminates the statement. However, the statement may run repeatedly before being terminated, wasting 20 minutes of resources each time it is executed.
Starting in Oracle Database 19c, if a statement exceeds the specified resource limit, then the Resource Manager terminates the execution and “quarantines” the plan. To quarantine the plan means to put it on a blacklist of plans that the database will not execute. Note that the plan is quarantined, not the statement itself.'
[Oracle SQL Tuning Guide: 4.7 About Quarantined SQL Plans]
- See also Tim Hall's article: SQL Quarantine in Oracle Database 19c
When an attempt is made to execute a quarantined execution plan an error is produced: ORA-56955: quarantined plan used.
Oracle does not log timed-out or quarantined queries. On V$SQL and V$SQLSTAT, AVOIDED_EXECUTIONS records the number of times a SQL query has been prevented from running. However, this will not stay in the library cache long on a PeopleSoft system, due to the continual parse of dynamically generated SQL statements. As of Oracle 19.20, it is not recorded in AWR on DBA_HIST_SQLSTAT.
If an error condition occurs during a PSQUERY process run on the process scheduler, the process terminates with an error. The SQL statement and the error message are recorded in the Application Engine message log. As demonstrated in the previous blog, we can detect such failures by inspecting the message log of the PSQUERY process that did not finish successfully (ie. it has an end time, but the run status does not indicate success).
Matching Quarantine Directives to Cancelled QueriesQuarantine directives are visible via DBA_SQL_QUARANTINE, including SQL text and execution plan hash value.
It would be useful to know which quarantine directive relates to which query. However, it is not easy to match the SQL in the PeopleSoft message log entries with that in the quarantine entries. The SQL text in the message log can have multiple spaces. These are stripped out in the DBA_SQL_QUARANTINE view where the normalised SQL statement is visible.
The timestamp of creation and last execution of the quarantine directive is stored on it, but matching these to when the query was running can result in false positives.
Also, you cannot tell which quarantine directive was created by which consumer group. The maximum CPU timeout is recorded on DBA_SQL_QUARANTINE. In my example, it is only possible to distinguish the originating consumer group because the two consumer groups happen to have different timeouts.
A method that matches exactly, but only returns partial rows is to:
- Obtain ASH data for queries terminated by the resource manager. It can be matched by timestamp, MODULE, and ACTION (provided that EnableAEMonitoring is enabled).
- Profile the ASH to find the statement that took the longest during each PSQUERY process, and that is almost certain to be the SQL query. Thus obtaining the SQL_ID, SQL Plan Hash Value and consumer group ID. It is also possible to determine the total database time for the query, and the database time spent on CPU.
- The consumer group name can then be obtained from DBA_HIST_RSRC_CONSUMER_GROUP
- Obtain the SQL text for the long-running query. It would also have to be captured by an AWR snapshot. This does often occur because it was a long-running SQL, but it is not certain.
- The signature for the SQL statement (not the force-matching signature) can be derived using the SQLTEXT_TO_SIGNATURE function in DBMS_SQLTUNE. This can be matched to the signature recorded in DBA_SQL_QUARANTINE.
- You can have multiple quarantine directives for the same signature (i.e. the same SQL statement), each with a different plan hash value.
- NB: The plan hash value on DBA_SQL_QUARANTINE is the adaptive plan hash value (with all of its possible plan alternatives), and therefore it matches SQL_FULL_PLAN_HASH_VALUE in the ASH data, and not SQL_PLAN_HASH_VALUE (the plan that actually executed).
- When a query executes until timed-out, producing ORA-00040, you usually can find the SQL statement in the AWR repository and so generate the signature to exactly match the quarantine record.
- When an attempt is made to run a quarantined statement and execution plan, you usually cannot find the SQL statement because it hasn't run for long enough to produce an ASH sample. Even when it has, you also have to rely on the statement having been captured previously by AWR. Those conditions only come together occasionally.
- We can see the quarantine directives that were created when the resource manager cancelled a query, raising error ORA-00040: active time limit exceeded - call aborted.
- However, where quarantine directives have prevented SQL from executing, raising error ORA-56955: quarantined plan used, the ASH data from the event that originally created the directive has since been purged, so we cannot use it to match directives.
Mon Feb 24 page 1
PS/Queries terminated by Resource Manager/quarantined Execution Plan
Public/ ASH
Private Ru Oracle Exec ASH CPU Message Log
P.I. DBNAME OPRID RUNCNTLID Query QRYNAME St Err. # Secs Secs Secs Date/Time Stamp SQL_ID
--------- ------- -------- ------------------------------ ------- ------------------------------ -- --------- ------ ------ ------ ---------------------------- -------------
SQL Plan Full Plan CPU
Hash Value Hash Value Consumer Group Name SIGNATURE Quarantine Name Time Quarantine Created Quarantine Last Executed
----------- ----------- ------------------------- --------------------- ------------------------------------ ----- ---------------------------- ----------------------------
…
31452465 FSPROD USR0001 GBR_JRNL_LN_DETAIL_ACCT Public GBR_JRNL_LN_DETAIL_ACCT 10 ORA-56955 36 10 10 20-FEB-25 06.28.03.578218 PM 0wm9g6xkys12h
4009529842 653370716 PSQUERY_BATCH_GROUP 5584654620166156419
31451318 FSPROD USR0002 GBR_JRNL_LN_DETAIL_ACCT Public GBR_JRNL_LN_DETAIL_ACCT 10 ORA-56955 36 20-FEB-25 02.36.38.590841 PM
31451292 FSPROD USR0002 GBR_JRNL_LN_DETAIL_ACCT Public GBR_JRNL_LN_DETAIL_ACCT 10 ORA-56955 36 20-FEB-25 02.30.51.777351 PM
31438602 FSPROD USR0003 1 Private DK_GBR_GL_DETAIL_NEW 10 ORA-00040 28316 28275 14203 18-FEB-25 11.39.19.502412 PM 5qrbrf775whky
3446094907 3491308607 PSQUERY_BATCH_GROUP 16266909742923016361 SQL_QUARANTINE_f3gxc76u48u59d019243f 14400 18-FEB-25 11.49.33.091081 PM
31437925 FSPROD USR0004 16 Private TB_TEST2 10 ORA-00040 17684 17654 17541 18-FEB-25 06.09.14.060615 PM 06xqrgj18wp05
4256462904 2062199471 PSQUERY_BATCH_GROUP 6341132966559464532 SQL_QUARANTINE_5h01uuscnrg2n7aeaaaaf 14400 18-FEB-25 06.17.20.679769 PM
31437907 FSPROD USR0004 16 Private TB_TEST2 10 ORA-00040 17694 17695 17592 18-FEB-25 06.04.05.942470 PM 4yurn75y2p0t2
3232504707 121066138 PSQUERY_BATCH_GROUP 4966087806133732884 SQL_QUARANTINE_49usqjjc001hn0737529a 14400 18-FEB-25 06.17.24.869185 PM
…Management of Long Running PS/Queries Cancelled by Resource Manager CPU Limit
I have written previously about using the Oracle database resource manager to prioritise the allocation of CPU to different processes in a PeopleSoft system. I proposed a sample resource plan that can be used as a starting point to build a resource plan that meets a system's specific objectives and requirements.
- see PSFT_PLAN: A Sample Oracle Database Resource Manager Plan for PeopleSoft
- The script to implement the sample PSFT_PLAN resource plan is available from GitHub.
This post looks at
- How to configure the resource manager to cancel long-running queries,
- What happens when it does and what PeopleSoft users experience,
- How the system administrators can monitor such queries,
- What action could they take.
- PSQUERY_BATCH_GROUP
- Applies to scheduled PSQUERY Application Engine Programs
- Limited to 4 hours on CPU (or estimated at >= 4 hours)
- PSQUERY_ONLINE
- Applies to queries run online via the PeopleSoft Internet Architecture (PIA).
- There is no resource manager limit for this consumer group.
- The PIA has a session timeout (default 20 minutes).
- The ICQuery Tuxedo service that runs the queries also has a timeout (default 20 minutes)
- When the resource manager cancels a SQL call, it simply raises an Oracle error that appears in a message box in the PIA without any further explanation. I think it is better to let the PIA timeouts handle online queries in a more controlled fashion.
- Users should generally be encouraged to schedule queries that will take more than a few minutes to run on the process scheduler.
- Resist the temptation to increase either the PIA or ICQuery service timeout from the delivered setting of 20 minutes.
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'PSQUERY_BATCH_GROUP'
,mgmt_p6 => 1
,switch_group => 'CANCEL_SQL'
,switch_time => 14400
,switch_estimate => TRUE
,switch_for_call => TRUE
);
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'PSQUERY_ONLINE_GROUP'
,mgmt_p6 => 90
);
DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(
'PSFT_PLAN', 'LOW_LIMITED_GROUP'
,mgmt_p8 => 1
,switch_group => 'CANCEL_SQL'
,switch_time => 7200
,switch_elapsed_time => 7200
,switch_estimate => TRUE
,switch_for_call => TRUE
);- SWITCH_GROUP specifies the consumer group to which the session is switched when a switch condition is met. If the group switches to CANCEL_SQL the current call is cancelled, raising error ORA-00400.
- SWITCH_TIME specified the number of seconds on CPU (not elapsed time).
- If SWITCH_ESTIMATE is true, the resource manager also switches group if the estimated run time is greater than the switch time
- SWITCH_FOR_CALL is set to true so that if the consumer group is switched, it is then restored to the original consumer group at the end of the top call. Thus a persistent session is not permanently switched. This is important if switching an application engine server (PSAESRV) session.
- The query may be cancelled immediately because the estimated execution time is greater than the limit.
- Otherwise, it is quite likely to run for an elapsed time that is greater than the CPU time limit. Some time will be consumed in the client process, during which the database will be idle waiting for the next fetch request from the client,
- Some of the database time may not be on CPU because it may be doing something else, such as physical IO (in which case the session will report being on an event other than NULL).
- The database session may be held back by the resource manager allocating CPU to higher priority processes, in which case the session will again not be on CPU, and will report being on event resmgr: cpu quantum.
For this analysis, I have made some alterations to the message log query (see message_log_checker-psquery.sql).
- This query is restricted to messages generated by PSQUERY processes that did not run to success (not run status 9).
- PeopleSoft messages are typically defined with up to 9 substitution variables, but long SQL statements can have many more entries in PS_MESSAGE_LOGPARM. So the PL/SQL function in this query simply appends any additional log parameter rows beyond the 9 substitution variables to the end of the generated string.
- Once the message has been generated we can look for one of the error messages associated with the resource manager terminating a query:
- ORA-00040: active time limit exceeded - call aborted
- ORA-56955: quarantined plan used
It is necessary to filter by message number because even in PS/Query users can write invalid SQL that produces other error messages. However, all this text processing for each row retrieved makes the query quite slow.
Public/
Private Ru Exec Msg Msg Msg
P.I. DBNAME OPRID RUNCNTLID Query QRYNAME St Secs DTTM_STAMP_SEC Seq Set Nbr
--------- ------- -------- ------------------------------ ------- ------------------------------ -- ------ ---------------------------- --- --- ----
MSG
----------------------------------------------------------------------------------------------------------------------------------------------------
12395311 FSPROD USR001 42 Private MY_TEST2 10 20772 10-FEB-25 04.41.47.384694 PM 1 65 30
File: C:\PT860P13B_2403250500-retail\peopletools\src\pssys\qpm.cppSQL error. Stmt #: 8526 Error Position: 189 Return: 40 - ORA-00040: active time
limit exceeded - call abortedFailed SQL stmt: SELECT A.LEDGER, A.FISCAL_YEAR, A.BUSINESS_UNIT, …
10-FEB-25 04.41.47.421800 PM 2 50 380
Error in running query because of SQL Error, Code=40, Message=ORA-00040: active time limit exceeded - call aborted
…Public/ Private Ru Exec Msg Msg Msg P.I. DBNAME OPRID RUNCNTLID Query QRYNAME St Secs DTTM_STAMP_SEC Seq Set Nbr --------- ------- -------- ------------------------------ ------- ------------------------------ -- ------ ---------------------------- --- --- ---- MSG ---------------------------------------------------------------------------------------------------------------------------------------------------- 12319513 FSPROD USR002 Transactions Public GBR_JRNL_LINE_DTL_ACCT 10 25 13-FEB-25 11.13.35.746644 PM 1 65 30 File: C:\PT860P13B_2403250500-retail\peopletools\src\pssys\qpm.cppSQL error. Stmt #: 8526 Error Position: 2783 Return: -8581 - ORA-56955: quarant ined plan usedFailed SQL stmt: SELECT A.FISCAL_YEAR, A.ACCOUNTING_PERIOD, A.JRNL_HDR_STATUS, B.LED … 13-FEB-25 11.13.35.814112 PM 2 50 380 Error in running query because of SQL Error, Code=-8581, Message=ORA-56955: quarantined plan used
The final stage is to close the feedback loop and go back to the users who are producing the queries, find out what they are trying to do, and why the queries are running for such a long time.
SQL Profiles for Application Engine Steps that Reference Temporary Records via the %Table() Meta-SQL
Oracle generally advises using SQL Profiles/Patches/Baselines rather than introducing hints into application code. Using one of these forms of plan stability saves you from having to alter the code, and then having to test and verify that the change is functionally neutral and release it to production. It also saves repeating that whole process if you ever choose to remove or change the hint.
I generally use SQL profiles with PeopleSoft, because they are the only plan stability mechanism that can force match a statement. That is to say, a force-matching SQL profile will match other SQL statements that differ only in embedded literal values. PeopleSoft code makes extensive use of literal values. SQL patches and baselines only exactly match statements (so they match the SQL ID and not the force matching signature). Note that SQL Profiles require that the Tuning Pack is licenced, and that is only available on Enterprise Edition or Oracle.
I have written previously about using SQL Profiles to inject hints into dynamically generated code. If any part of the SQL (other than literal values) changes, then a different SQL profile is needed for each variation.
When generating SQL profiles for dynamically generated code, you have to be able to predict every possible variation in the code and generate a SQL profile for every combination of every variation. You end up writing code that mimics the dynamic code generation in the application
The same is true of Application Engine steps that reference temporary records via the %Table() meta-SQL but are otherwise static. This variation is a special case because it is a part of PeopleTools' behaviour. It is easy to determine which tables could be referenced by querying some of the PeopleTools tables. Although, the table name itself can be set dynamically!
Let's take an example statement. I identified it as performing poorly during an analysis of ASH data. I have a SQL_ID, a plan hash value, and ACTION indicates the name of the Application Engine step that generated it (because Application Engine instrumentation is enabled by setting EnableAEMonitoring - see https://blog.go-faster.co.uk/2015/03/undocumented-application-engine.html).
- SQL ID = bk98x60cspttj
- SQL Plan Hash Value = 113493817
- Action = XX_TREESEL.iDetVal.iDetVal.S
This is the statement and execution plan report generated by dbms_xplan.display_workload_repository (formatted and edited slightly). I have the statement, the execution plan and the outline of hints that describes the plan.
SELECT * FROM table(dbms_xplan.display_workload_repository('bk98x60cspttj',113493817,'ADVANCED +ADAPTIVE'));
SQL_ID
-------------
INSERT INTO PS_TSEL_P_TAO12 (CHARTFIELD, CHARTFIELD_VALUE,
PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO12 A, PS_GL_ACCOUNT_TBL DV
WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678
AND DV.SETID = '12345'
AND DV.EFFDT = (SELECT MAX(EFFDT)
FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT
AND EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD'))
AND DV.ACCOUNT >= A.RANGE_FROM_30
AND DV.ACCOUNT <= A.RANGE_TO_30
Plan hash value: 113493817
--------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------------------
| 0 | INSERT STATEMENT | | | | 38754 (100)| |
| 1 | LOAD TABLE CONVENTIONAL | PS_TSEL_P_TAO17 | | | | |
| 2 | SORT UNIQUE | | 1 | 161 | 38754 (1)| 00:00:02 |
|* 3 | FILTER | | | | | |
| 4 | MERGE JOIN | | 24219 | 3807K| 8 (50)| 00:00:01 |
| 5 | SORT JOIN | | 1138 | 151K| 3 (34)| 00:00:01 |
|* 6 | INDEX STORAGE FAST FULL SCAN | PSATSEL_R30_TAO17 | 1138 | 151K| 2 (0)| 00:00:01 |
|* 7 | FILTER | | | | | |
|* 8 | SORT JOIN | | 8513 | 207K| 3 (34)| 00:00:01 |
|* 9 | INDEX STORAGE FAST FULL SCAN| PS_GL_ACCOUNT_TBL | 8513 | 207K| 2 (0)| 00:00:01 |
| 10 | SORT AGGREGATE | | 1 | 25 | | |
| 11 | FIRST ROW | | 1 | 25 | 2 (0)| 00:00:01 |
|* 12 | INDEX RANGE SCAN (MIN/MAX) | PS_GL_ACCOUNT_TBL | 1 | 25 | 2 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------------
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
1 - SEL$1
6 - SEL$1 / A@SEL$1
9 - SEL$1 / DV@SEL$1
10 - SEL$2
12 - SEL$2 / B@SEL$2
Outline Data
-------------
/*+
BEGIN_OUTLINE_DATA
IGNORE_OPTIM_EMBEDDED_HINTS
OPTIMIZER_FEATURES_ENABLE('19.1.0')
DB_VERSION('19.1.0')
ALL_ROWS
OUTLINE_LEAF(@"SEL$2")
OUTLINE_LEAF(@"SEL$1")
OUTLINE_LEAF(@"INS$1")
FULL(@"INS$1" "PS_TSEL_P_TAO17"@"INS$1")
INDEX_FFS(@"SEL$1" "A"@"SEL$1" ("PS_TSEL_R30_TAO17"."PROCESS_INSTANCE"
"PS_TSEL_R30_TAO17"."CHARTFIELD" "PS_TSEL_R30_TAO17"."RANGE_FROM_30"
"PS_TSEL_R30_TAO17"."RANGE_TO_30"))
INDEX_FFS(@"SEL$1" "DV"@"SEL$1" ("PS_GL_ACCOUNT_TBL"."SETID" "PS_GL_ACCOUNT_TBL"."ACCOUNT"
"PS_GL_ACCOUNT_TBL"."EFFDT"))
LEADING(@"SEL$1" "A"@"SEL$1" "DV"@"SEL$1")
USE_MERGE(@"SEL$1" "DV"@"SEL$1")
PQ_FILTER(@"SEL$1" SERIAL)
INDEX(@"SEL$2" "B"@"SEL$2" ("PS_GL_ACCOUNT_TBL"."SETID" "PS_GL_ACCOUNT_TBL"."ACCOUNT"
"PS_GL_ACCOUNT_TBL"."EFFDT"))
END_OUTLINE_DATA
*/
…Below is the underlying Application Engine step that generated the SQL. There is other dynamic code in this step that is driven by configuration data as well as the %Table() meta-SQLs. Other reasons not to introduce hints into the statement include:
- different dynamic variations of the code might require different hints, and then I would have to write more code to generate the hint dynamically. However, in this case, I am only going to deal with a single variation, and I am only going to produce one set of SQL Profiles,
- this step and the code generation are delivered by PeopleSoft, any change in these areas would be considered as a customisation.
%InsertSelect(DISTINCT
, %Bind(FT_TSEL_AET.RECNAME_SEL_TBL, NoQuotes)
, %Bind(FT_TSEL_STR_AET.DTL_RECNAME, NoQuotes
) DV
, %Bind(FT_TSEL_AET.FIELDNAME_CF, NoQuotes) = %Bind(FT_TSEL_STR_AET.DTL_FIELDNAME)
, %Bind(FT_TSEL_AET.FIELDNAME_VALUE, NoQuotes) = DV.%Bind(FT_TSEL_STR_AET.DTL_FIELDNAME, NoQuotes)
, PROCESS_INSTANCE = %Bind(PROCESS_INSTANCE)
, TREE_NAME = %Bind(FT_TSEL_AET.TREE_NAME)
, TREE_NODE = A.TREE_NODE
, TREE_NODE_NUM = A.TREE_NODE_NUM
, TREE_LEVEL_NUM = A.TREE_LEVEL_NUM, SETCNTRLVALUE =%Bind(FT_TSEL_AET.SETCNTRLVALUE)
, %Bind(FT_TSEL_GEN_AET.FIELDNAME_FROM, NoQuotes) = A.%Bind(FT_TSEL_GEN_AET.FIELDNAME_FROM, NoQuotes)
, %Bind(FT_TSEL_GEN_AET.FIELDNAME_TO, NoQuotes) = A.%Bind(FT_TSEL_GEN_AET.FIELDNAME_TO, NoQuotes)
, SETID_TREE = %Bind(FT_TSEL_GEN_AET.SETID_TREE)
, EFFDT = %Bind(FT_TSEL_STR_AET.EFFDT)
, CFV_SET = %Bind(FT_TSEL_AET.CFV_SET))
FROM %Table(%Bind(FT_TSEL_GEN_AET.RECNAME_SEL_TBL, NoQuotes)) A
, %Table(%Bind(FT_TSEL_STR_AET.DTL_RECNAME, NoQuotes)) DV
WHERE A.CHARTFIELD = %Bind(FT_TSEL_AET.FIELDNAME)
AND A.PROCESS_INSTANCE = %Bind(PROCESS_INSTANCE)
%Bind(FT_TSEL_WRK_AET.WHERE_TXT_LONG, NoQuotes)WHENEVER SQLERROR EXIT SQL.SQLCODE;
VAR signature NUMBER;
DECLARE
sql_txt CLOB;
h SYS.SQLPROF_ATTR;
BEGIN
sql_txt := q'[
INSERT INTO PS_TSEL_P_TAO17 (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO17 A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678 AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
]';
h := SYS.SQLPROF_ATTR(
q'[BEGIN_OUTLINE_DATA]',
q'[IGNORE_OPTIM_EMBEDDED_HINTS]',
q'[OPTIMIZER_FEATURES_ENABLE('19.1.0')]',
q'[DB_VERSION('19.1.0')]',
q'[ALL_ROWS]',
q'[OUTLINE_LEAF(@"SEL$683B0107")]',
q'[OUTLINE_LEAF(@"SEL$C772B8D1")]',
q'[UNNEST(@"SEL$2")]',
q'[OUTLINE_LEAF(@"INS$1")]',
q'[OUTLINE(@"SEL$2")]',
q'[OUTLINE(@"SEL$7511BFD2")]',
q'[OUTLINE(@"SEL$1")]',
q'[FULL(@"INS$1" "PS_TSEL_P_TAO17"@"INS$1")]',
q'[NO_ACCESS(@"SEL$C772B8D1" "VW_SQ_1"@"SEL$7511BFD2")]',
q'[INDEX(@"SEL$C772B8D1" "DV"@"SEL$1" ("PS_GL_ACCOUNT_TBL"."SETID" "PS_GL_ACCOUNT_TBL"."ACCOUNT" "PS_GL_ACCOUNT_TBL"."EFFDT"))]',
q'[FULL(@"SEL$C772B8D1" "A"@"SEL$1")]',
q'[LEADING(@"SEL$C772B8D1" "VW_SQ_1"@"SEL$7511BFD2" "DV"@"SEL$1" "A"@"SEL$1")]',
q'[USE_NL(@"SEL$C772B8D1" "DV"@"SEL$1")]',
q'[USE_MERGE(@"SEL$C772B8D1" "A"@"SEL$1")]',
q'[PARTIAL_JOIN(@"SEL$C772B8D1" "A"@"SEL$1")]',
q'[INDEX_FFS(@"SEL$683B0107" "B"@"SEL$2" ("PS_GL_ACCOUNT_TBL"."SETID" "PS_GL_ACCOUNT_TBL"."ACCOUNT" "PS_GL_ACCOUNT_TBL"."EFFDT"))]',
q'[END_OUTLINE_DATA]');
:signature := DBMS_SQLTUNE.SQLTEXT_TO_SIGNATURE(sql_txt);
DBMS_SQLTUNE.IMPORT_SQL_PROFILE (
sql_text => sql_txt,
profile => h,
name => 'coe_g7wz6ctquwjcy_2476903986',
description => 'coe g7wz6ctquwjcy 2476903986 '||:signature||'',
category => 'DEFAULT',
validate => TRUE,
replace => TRUE,
force_match => TRUE /* TRUE:FORCE (match even when different literals in SQL). FALSE:EXACT (similar to CURSOR_SHARING) */ );
END;
/
WHENEVER SQLERROR CONTINUE
SET ECHO OFF;
PRINT signature
PRO
PRO ... manual custom SQL Profile has been created
PRO
SET TERM ON ECHO OFF LIN 80 TRIMS OFF NUMF "";
SPO OFF;
PRO
PRO COE_XFR_SQL_PROFILE_g7wz6ctquwjcy_2476903986 completedREM coe_xfr_sql_profile_XX_TREESEL.iDetVal.iDetVal.sql
SPO coe_xfr_sql_profile_XX_TREESEL.iDetVal.iDetVal.log;
WHENEVER SQLERROR CONTINUE
SET serveroutput on ECHO ON TERM ON LIN 2000 TRIMS ON NUMF 99999999999999999999;
clear screen
ALTER SESSION SET CURRENT_SCHEMA=SYSADM;1
…
WHENEVER SQLERROR EXIT SQL.SQLCODE;
VAR signature NUMBER;
DECLARE
l_recname1 VARCHAR2(15) := 'TSEL_R30_TAO';2
l_table1 VARCHAR2(18) := 'PS_TSEL_R30_TAO17';
l_recname2 VARCHAR2(15) := 'TSEL_P_TAO';
l_table2 VARCHAR2(18) := 'PS_TSEL_P_TAO17';
l_name VARCHAR2(30);
sql_txt CLOB;
h SYS.SQLPROF_ATTR;
e_no_sql_profile EXCEPTION;
PRAGMA EXCEPTION_INIT(e_no_sql_profile, -13833);
BEGIN
FOR i IN (3
WITH v4 AS (SELECT rownum-1 row_number FROM dual CONNECT BY LEVEL <= 100)
SELECT DISTINCT
v1.row_number id1, r1.recname recname1, t1.table_name table_name1
, v2.row_number id2, r2.recname recname2, t2.table_name table_name2
, o.TEMPTBLINSTANCES
FROM psrecdefn r1
, psrecdefn r2
, pstemptblcntvw5 i1
, pstemptblcntvw i2
, all_tables t1
, all_tables t2
, psoptions o
, ps.psdbowner p
, v v1
, v v2
WHERE r1.rectype = 7 AND r1.recname = i1.recname AND r1.recname = l_recname1
AND r2.rectype = 7 AND r2.recname = i2.recname AND r2.recname = l_recname2
AND v1.row_number <= i1.temptblinstances + o.temptblinstances6 --up to total number of instances
AND v2.row_number <= i2.temptblinstances + o.temptblinstances --up to total number of instances
AND (v1.row_number = 0 OR v1.row_number > o.temptblinstances)7 --omit online temp tables
AND (v2.row_number = 0 OR v2.row_number > o.temptblinstances) --omit online temp tables
and t1.owner = p.ownerid AND t1.table_name
= DECODE(r1.sqltablename,' ','PS_'||r1.recname,r1.sqltablename)
||DECODE(v1.row_number,0,'',LTRIM(TO_NUMBER(v1.row_number)))8 --derive table table
and t2.owner = p.ownerid AND t2.table_name
= DECODE(r2.sqltablename,' ','PS_'||r2.recname,r2.sqltablename)
||DECODE(v2.row_number,0,'',LTRIM(TO_NUMBER(v2.row_number))) --derive table table
AND ABS(v1.row_number-v2.row_number)<=19 --max variation in instance numbers
ORDER BY id1, id2
) LOOP
-----------123456789012345678901234567890
l_name := 'XX_TREESEL.iDetVal.iDetVal'||i.id1||i.id2;10
sql_txt := q'[
INSERT INTO PS_TSEL_P_TAO17 (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO17 A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678 AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
]';
h := SYS.SQLPROF_ATTR(
q'[BEGIN_OUTLINE_DATA]',
q'[IGNORE_OPTIM_EMBEDDED_HINTS]',
q'[ALL_ROWS]',
q'[LEADING(@"SEL$1" "A"@"SEL$1")]',11
q'[END_OUTLINE_DATA]');
sql_txt := REPLACE(sql_txt, l_table1,i.table_name1);12
sql_txt := REPLACE(sql_txt, l_table2,i.table_name2);
:signature := DBMS_SQLTUNE.SQLTEXT_TO_SIGNATURE(sql_txt);
dbms_output.put_line(l_name||':'||:signature||':'||sql_txt);
for n in 1 .. h.count LOOP13
if h(n) LIKE '%'||l_table1||'%' THEN
h(n) := REPLACE(h(n), l_table1,i.table_name1);
dbms_output.put_line(n||':'||h(n));
end if;
if h(n) LIKE '%'||l_table2||'%' THEN
h(n) := REPLACE(h(n), l_table2,i.table_name2);
dbms_output.put_line(n||':'||h(n));
end if;
end loop;
BEGIN --drop profile if already exists
DBMS_SQLTUNE.drop_SQL_PROFILE(name => l_name);
EXCEPTION WHEN e_no_sql_profile THEN NULL;
END;
DBMS_SQLTUNE.IMPORT_SQL_PROFILE ( --create new profile
sql_text => sql_txt,
profile => h,
name => l_name,
description => 'coe XX_TREESEL.iDetVal.iDetVal '||l_name||' '||:signature||'',
category => 'DEFAULT',
validate => TRUE,
replace => TRUE,
force_match => TRUE /* TRUE:FORCE (match even when different literals in SQL). FALSE:EXACT (similar to CURSOR_SHARING) */ );
END LOOP;
END;
/
WHENEVER SQLERROR CONTINUE
…
SPO OFF;
PRO
PRO coe_xfr_sql_profile_XX_TREESEL.iDetVal.iDetVal completed- Set CURRENT_SCHEMA to specify PeopleSoft owning schema, SYSADM.
- For each temporary record in the original SQL statement, add pairs of variables to specify the name of PeopleSoft record and Oracle temporary table instance referenced in the statement. I could derive the record name from the table name, but it is easier just to hard-code it.
- The SQL statement produces all combinations of temporary records that could appear in the SQL statement. I will put it in an implicit cursor loop, and then for each row returned, the script will create a SQL profile.
- Common table expression V returns 100 rows, numbered 0 to 99. Irrespective of the number of temporary table instances specified in each Application Engine program, there can only be non-shared 99 table instances for each PeopleTools record, plus the shared instance (that doesn't have a suffix number).
- PSTEMPTBLCNTVW returns the number of non-shared batch (i.e. not online) instances of each temporary record that needs to be built. This is in addition to the number of online temporary table instances.
- The query will return a row for each instance of each temporary table up to the number of instances required by the application engines plus the number of online table instances, but not exceeding the 99 rows returned by CTE V.
- Most Application Engines do not run online in the component processor, therefore there is no need to build SQL profiles on these instances. There are exceptions, such as some as the journal and voucher edit and post processes in Financials, in which case these criteria should be removed.
- This expression joins the record and instance number to the table in the database. Instance 0 will be used to refer to the shared instance.
- The instance numbers of the temporary records is permitted to vary by up to one in either direction. Thus the script generates profiles with instance 12 of one table, and instances 11 to 13 of the other. It is rare, but possible, for there to be any difference in instance numbers between tables. It is possible, but rarer for the difference to be greater than one.
- SQL Profile names are limited to 30 characters. I have specified a meaningful name based on the Application Engine step, up to 26 characters, and then the two IDs (which can be up to 2 digits each).
- In this example, I am not going to use the full set of hints in the captured profile. I just want to introduce a single leading hint.
- I will substitute each table name in the SQL text with the specific table instance name.
- Tables are generally referenced in hints via the row source alias. However, sometimes the table name appears in the hints, and must also be replaced with the specific table name. So I also work through all the hints in array h and substitute any table names that may be there. Indexes are not referenced by their names but by a list of indexed columns.
XX_TREESEL.iDetVal.iDetVal00:11943215885789639839:
INSERT INTO PS_TSEL_P_TAO (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
XX_TREESEL.iDetVal.iDetVal1717:15747497907378648788:
INSERT INTO PS_TSEL_P_TAO17 (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO17 A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678 AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
XX_TREESEL.iDetVal.iDetVal1718:12015611546030583918:
INSERT INTO PS_TSEL_P_TAO18 (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO17 A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678 AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
XX_TREESEL.iDetVal.iDetVal1817:14883898515022367531:
INSERT INTO PS_TSEL_P_TAO17 (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO18 A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678 AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
…
Note- If the number of non-shared instances of a table in an Application Engine is increased, you need to build add table instances with Application Designer.
- If the number of online temporary instances is increased, you need to build additional instances for every temporary record in the database. If the number is reduced some tables will cease to be used, and they ought to be dropped.
- If either the number of application engine table instances or online temporary table instances are changed, then you will need to rerun the script to create additional SQL profiles.
SQL Profiles for Application Engine Steps that Reference Temporary Records via the %Table() Meta-SQL
Oracle generally advises using SQL Profiles/Patches/Baselines rather than introducing hints into application code. Using one of these forms of plan stability saves you from having to alter the code, and then having to test and verify that the change is functionally neutral and release it to production. It also saves repeating that whole process if you ever choose to remove or change the hint.
I generally use SQL profiles with PeopleSoft, because they are the only plan stability mechanism that can force match a statement. That is to say, a force-matching SQL profile will match other SQL statements that differ only in embedded literal values. PeopleSoft code makes extensive use of literal values. SQL patches and baselines only exactly match statements (so they match the SQL ID and not the force matching signature). Note that SQL Profiles require that the Tuning Pack is licenced, and that is only available on Enterprise Edition or Oracle.
I have written previously about using SQL Profiles to inject hints into dynamically generated code. If any part of the SQL (other than literal values) changes, then a different SQL profile is needed for each variation.
When generating SQL profiles for dynamically generated code, you have to be able to predict every possible variation in the code and generate a SQL profile for every combination of every variation. You end up writing code that mimics the dynamic code generation in the application
The same is true of Application Engine steps that reference temporary records via the %Table() meta-SQL but are otherwise static. This variation is a special case because it is a part of PeopleTools' behaviour. It is easy to determine which tables could be referenced by querying some of the PeopleTools tables. Although, the table name itself can be set dynamically!
Let's take an example statement. I identified it as performing poorly during an analysis of ASH data. I have a SQL_ID, a plan hash value, and ACTION indicates the name of the Application Engine step that generated it (because Application Engine instrumentation is enabled by setting EnableAEMonitoring - see https://blog.psftdba.com/2015/03/undocumented-application-engine.html).
- SQL ID = bk98x60cspttj
- SQL Plan Hash Value = 113493817
- Action = XX_TREESEL.iDetVal.iDetVal.S
This is the statement and execution plan report generated by dbms_xplan.display_workload_repository (formatted and edited slightly). I have the statement, the execution plan and the outline of hints that describes the plan.
SELECT * FROM table(dbms_xplan.display_workload_repository('bk98x60cspttj',113493817,'ADVANCED +ADAPTIVE'));
SQL_ID
-------------
INSERT INTO PS_TSEL_P_TAO12 (CHARTFIELD, CHARTFIELD_VALUE,
PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO12 A, PS_GL_ACCOUNT_TBL DV
WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678
AND DV.SETID = '12345'
AND DV.EFFDT = (SELECT MAX(EFFDT)
FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT
AND EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD'))
AND DV.ACCOUNT >= A.RANGE_FROM_30
AND DV.ACCOUNT <= A.RANGE_TO_30
Plan hash value: 113493817
--------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------------------
| 0 | INSERT STATEMENT | | | | 38754 (100)| |
| 1 | LOAD TABLE CONVENTIONAL | PS_TSEL_P_TAO17 | | | | |
| 2 | SORT UNIQUE | | 1 | 161 | 38754 (1)| 00:00:02 |
|* 3 | FILTER | | | | | |
| 4 | MERGE JOIN | | 24219 | 3807K| 8 (50)| 00:00:01 |
| 5 | SORT JOIN | | 1138 | 151K| 3 (34)| 00:00:01 |
|* 6 | INDEX STORAGE FAST FULL SCAN | PSATSEL_R30_TAO17 | 1138 | 151K| 2 (0)| 00:00:01 |
|* 7 | FILTER | | | | | |
|* 8 | SORT JOIN | | 8513 | 207K| 3 (34)| 00:00:01 |
|* 9 | INDEX STORAGE FAST FULL SCAN| PS_GL_ACCOUNT_TBL | 8513 | 207K| 2 (0)| 00:00:01 |
| 10 | SORT AGGREGATE | | 1 | 25 | | |
| 11 | FIRST ROW | | 1 | 25 | 2 (0)| 00:00:01 |
|* 12 | INDEX RANGE SCAN (MIN/MAX) | PS_GL_ACCOUNT_TBL | 1 | 25 | 2 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------------
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
1 - SEL$1
6 - SEL$1 / A@SEL$1
9 - SEL$1 / DV@SEL$1
10 - SEL$2
12 - SEL$2 / B@SEL$2
Outline Data
-------------
/*+
BEGIN_OUTLINE_DATA
IGNORE_OPTIM_EMBEDDED_HINTS
OPTIMIZER_FEATURES_ENABLE('19.1.0')
DB_VERSION('19.1.0')
ALL_ROWS
OUTLINE_LEAF(@"SEL$2")
OUTLINE_LEAF(@"SEL$1")
OUTLINE_LEAF(@"INS$1")
FULL(@"INS$1" "PS_TSEL_P_TAO17"@"INS$1")
INDEX_FFS(@"SEL$1" "A"@"SEL$1" ("PS_TSEL_R30_TAO17"."PROCESS_INSTANCE"
"PS_TSEL_R30_TAO17"."CHARTFIELD" "PS_TSEL_R30_TAO17"."RANGE_FROM_30"
"PS_TSEL_R30_TAO17"."RANGE_TO_30"))
INDEX_FFS(@"SEL$1" "DV"@"SEL$1" ("PS_GL_ACCOUNT_TBL"."SETID" "PS_GL_ACCOUNT_TBL"."ACCOUNT"
"PS_GL_ACCOUNT_TBL"."EFFDT"))
LEADING(@"SEL$1" "A"@"SEL$1" "DV"@"SEL$1")
USE_MERGE(@"SEL$1" "DV"@"SEL$1")
PQ_FILTER(@"SEL$1" SERIAL)
INDEX(@"SEL$2" "B"@"SEL$2" ("PS_GL_ACCOUNT_TBL"."SETID" "PS_GL_ACCOUNT_TBL"."ACCOUNT"
"PS_GL_ACCOUNT_TBL"."EFFDT"))
END_OUTLINE_DATA
*/
…Below is the underlying Application Engine step that generated the SQL. There is other dynamic code in this step that is driven by configuration data as well as the %Table meta-SQLs. Other reasons not to introduce hints into the statement include:
- different dynamic variations of the code might require different hints, and then I would have to write more code to generate the hint dynamically. However, in this case, I am only going to deal with a single variation, and I am only going to produce one set of SQL Profiles,
- this step and the code generation are delivered by PeopleSoft, any change in these areas would be considered as a customisation.
%InsertSelect(DISTINCT
, %Bind(FT_TSEL_AET.RECNAME_SEL_TBL, NoQuotes)
, %Bind(FT_TSEL_STR_AET.DTL_RECNAME, NoQuotes
) DV
, %Bind(FT_TSEL_AET.FIELDNAME_CF, NoQuotes) = %Bind(FT_TSEL_STR_AET.DTL_FIELDNAME)
, %Bind(FT_TSEL_AET.FIELDNAME_VALUE, NoQuotes) = DV.%Bind(FT_TSEL_STR_AET.DTL_FIELDNAME, NoQuotes)
, PROCESS_INSTANCE = %Bind(PROCESS_INSTANCE)
, TREE_NAME = %Bind(FT_TSEL_AET.TREE_NAME)
, TREE_NODE = A.TREE_NODE
, TREE_NODE_NUM = A.TREE_NODE_NUM
, TREE_LEVEL_NUM = A.TREE_LEVEL_NUM, SETCNTRLVALUE =%Bind(FT_TSEL_AET.SETCNTRLVALUE)
, %Bind(FT_TSEL_GEN_AET.FIELDNAME_FROM, NoQuotes) = A.%Bind(FT_TSEL_GEN_AET.FIELDNAME_FROM, NoQuotes)
, %Bind(FT_TSEL_GEN_AET.FIELDNAME_TO, NoQuotes) = A.%Bind(FT_TSEL_GEN_AET.FIELDNAME_TO, NoQuotes)
, SETID_TREE = %Bind(FT_TSEL_GEN_AET.SETID_TREE)
, EFFDT = %Bind(FT_TSEL_STR_AET.EFFDT)
, CFV_SET = %Bind(FT_TSEL_AET.CFV_SET))
FROM %Table(%Bind(FT_TSEL_GEN_AET.RECNAME_SEL_TBL, NoQuotes)) A
, %Table(%Bind(FT_TSEL_STR_AET.DTL_RECNAME, NoQuotes)) DV
WHERE A.CHARTFIELD = %Bind(FT_TSEL_AET.FIELDNAME)
AND A.PROCESS_INSTANCE = %Bind(PROCESS_INSTANCE)
%Bind(FT_TSEL_WRK_AET.WHERE_TXT_LONG, NoQuotes)WHENEVER SQLERROR EXIT SQL.SQLCODE;
VAR signature NUMBER;
DECLARE
sql_txt CLOB;
h SYS.SQLPROF_ATTR;
BEGIN
sql_txt := q'[
INSERT INTO PS_TSEL_P_TAO17 (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO17 A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678 AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
]';
h := SYS.SQLPROF_ATTR(
q'[BEGIN_OUTLINE_DATA]',
q'[IGNORE_OPTIM_EMBEDDED_HINTS]',
q'[OPTIMIZER_FEATURES_ENABLE('19.1.0')]',
q'[DB_VERSION('19.1.0')]',
q'[ALL_ROWS]',
q'[OUTLINE_LEAF(@"SEL$683B0107")]',
q'[OUTLINE_LEAF(@"SEL$C772B8D1")]',
q'[UNNEST(@"SEL$2")]',
q'[OUTLINE_LEAF(@"INS$1")]',
q'[OUTLINE(@"SEL$2")]',
q'[OUTLINE(@"SEL$7511BFD2")]',
q'[OUTLINE(@"SEL$1")]',
q'[FULL(@"INS$1" "PS_TSEL_P_TAO17"@"INS$1")]',
q'[NO_ACCESS(@"SEL$C772B8D1" "VW_SQ_1"@"SEL$7511BFD2")]',
q'[INDEX(@"SEL$C772B8D1" "DV"@"SEL$1" ("PS_GL_ACCOUNT_TBL"."SETID" "PS_GL_ACCOUNT_TBL"."ACCOUNT" "PS_GL_ACCOUNT_TBL"."EFFDT"))]',
q'[FULL(@"SEL$C772B8D1" "A"@"SEL$1")]',
q'[LEADING(@"SEL$C772B8D1" "VW_SQ_1"@"SEL$7511BFD2" "DV"@"SEL$1" "A"@"SEL$1")]',
q'[USE_NL(@"SEL$C772B8D1" "DV"@"SEL$1")]',
q'[USE_MERGE(@"SEL$C772B8D1" "A"@"SEL$1")]',
q'[PARTIAL_JOIN(@"SEL$C772B8D1" "A"@"SEL$1")]',
q'[INDEX_FFS(@"SEL$683B0107" "B"@"SEL$2" ("PS_GL_ACCOUNT_TBL"."SETID" "PS_GL_ACCOUNT_TBL"."ACCOUNT" "PS_GL_ACCOUNT_TBL"."EFFDT"))]',
q'[END_OUTLINE_DATA]');
:signature := DBMS_SQLTUNE.SQLTEXT_TO_SIGNATURE(sql_txt);
DBMS_SQLTUNE.IMPORT_SQL_PROFILE (
sql_text => sql_txt,
profile => h,
name => 'coe_g7wz6ctquwjcy_2476903986',
description => 'coe g7wz6ctquwjcy 2476903986 '||:signature||'',
category => 'DEFAULT',
validate => TRUE,
replace => TRUE,
force_match => TRUE /* TRUE:FORCE (match even when different literals in SQL). FALSE:EXACT (similar to CURSOR_SHARING) */ );
END;
/
WHENEVER SQLERROR CONTINUE
SET ECHO OFF;
PRINT signature
PRO
PRO ... manual custom SQL Profile has been created
PRO
SET TERM ON ECHO OFF LIN 80 TRIMS OFF NUMF "";
SPO OFF;
PRO
PRO COE_XFR_SQL_PROFILE_g7wz6ctquwjcy_2476903986 completedREM coe_xfr_sql_profile_XX_TREESEL.iDetVal.iDetVal.sql
SPO coe_xfr_sql_profile_XX_TREESEL.iDetVal.iDetVal.log;
WHENEVER SQLERROR CONTINUE
SET serveroutput on ECHO ON TERM ON LIN 2000 TRIMS ON NUMF 99999999999999999999;
clear screen
ALTER SESSION SET CURRENT_SCHEMA=SYSADM;1
…
WHENEVER SQLERROR EXIT SQL.SQLCODE;
VAR signature NUMBER;
DECLARE
l_recname1 VARCHAR2(15) := 'TSEL_R30_TAO';2
l_table1 VARCHAR2(18) := 'PS_TSEL_R30_TAO17';
l_recname2 VARCHAR2(15) := 'TSEL_P_TAO';
l_table2 VARCHAR2(18) := 'PS_TSEL_P_TAO17';
l_name VARCHAR2(30);
sql_txt CLOB;
h SYS.SQLPROF_ATTR;
e_no_sql_profile EXCEPTION;
PRAGMA EXCEPTION_INIT(e_no_sql_profile, -13833);
BEGIN
FOR i IN (3
WITH v4 AS (SELECT rownum-1 row_number FROM dual CONNECT BY LEVEL <= 100)
SELECT DISTINCT
v1.row_number id1, r1.recname recname1, t1.table_name table_name1
, v2.row_number id2, r2.recname recname2, t2.table_name table_name2
, o.TEMPTBLINSTANCES
FROM psrecdefn r1
, psrecdefn r2
, pstemptblcntvw5 i1
, pstemptblcntvw i2
, all_tables t1
, all_tables t2
, psoptions o
, ps.psdbowner p
, v v1
, v v2
WHERE r1.rectype = 7 AND r1.recname = i1.recname AND r1.recname = l_recname1
AND r2.rectype = 7 AND r2.recname = i2.recname AND r2.recname = l_recname2
AND v1.row_number <= i1.temptblinstances + o.temptblinstances6 --up to total number of instances
AND v2.row_number <= i2.temptblinstances + o.temptblinstances --up to total number of instances
AND (v1.row_number = 0 OR v1.row_number > o.temptblinstances)7 --omit online temp tables
AND (v2.row_number = 0 OR v2.row_number > o.temptblinstances) --omit online temp tables
and t1.owner = p.ownerid AND t1.table_name
= DECODE(r1.sqltablename,' ','PS_'||r1.recname,r1.sqltablename)
||DECODE(v1.row_number,0,'',LTRIM(TO_NUMBER(v1.row_number)))8 --derive table table
and t2.owner = p.ownerid AND t2.table_name
= DECODE(r2.sqltablename,' ','PS_'||r2.recname,r2.sqltablename)
||DECODE(v2.row_number,0,'',LTRIM(TO_NUMBER(v2.row_number))) --derive table table
AND ABS(v1.row_number-v2.row_number)<=19 --max variation in instance numbers
ORDER BY id1, id2
) LOOP
-----------123456789012345678901234567890
l_name := 'XX_TREESEL.iDetVal.iDetVal'||i.id1||i.id2;10
sql_txt := q'[
INSERT INTO PS_TSEL_P_TAO17 (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO17 A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678 AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
]';
h := SYS.SQLPROF_ATTR(
q'[BEGIN_OUTLINE_DATA]',
q'[IGNORE_OPTIM_EMBEDDED_HINTS]',
q'[ALL_ROWS]',
q'[LEADING(@"SEL$1" "A"@"SEL$1")]',11
q'[END_OUTLINE_DATA]');
sql_txt := REPLACE(sql_txt, l_table1,i.table_name1);12
sql_txt := REPLACE(sql_txt, l_table2,i.table_name2);
:signature := DBMS_SQLTUNE.SQLTEXT_TO_SIGNATURE(sql_txt);
dbms_output.put_line(l_name||':'||:signature||':'||sql_txt);
for n in 1 .. h.count LOOP13
if h(n) LIKE '%'||l_table1||'%' THEN
h(n) := REPLACE(h(n), l_table1,i.table_name1);
dbms_output.put_line(n||':'||h(n));
end if;
if h(n) LIKE '%'||l_table2||'%' THEN
h(n) := REPLACE(h(n), l_table2,i.table_name2);
dbms_output.put_line(n||':'||h(n));
end if;
end loop;
BEGIN --drop profile if already exists
DBMS_SQLTUNE.drop_SQL_PROFILE(name => l_name);
EXCEPTION WHEN e_no_sql_profile THEN NULL;
END;
DBMS_SQLTUNE.IMPORT_SQL_PROFILE ( --create new profile
sql_text => sql_txt,
profile => h,
name => l_name,
description => 'coe XX_TREESEL.iDetVal.iDetVal '||l_name||' '||:signature||'',
category => 'DEFAULT',
validate => TRUE,
replace => TRUE,
force_match => TRUE /* TRUE:FORCE (match even when different literals in SQL). FALSE:EXACT (similar to CURSOR_SHARING) */ );
END LOOP;
END;
/
WHENEVER SQLERROR CONTINUE
…
SPO OFF;
PRO
PRO coe_xfr_sql_profile_XX_TREESEL.iDetVal.iDetVal completed- I set CURRENT_SCHEMA to specify PeopleSoft owning schema - SYSADM.
- For each temporary record in the original SQL statement, add pairs of variables to specify the name of PeopleSoft record and Oracle temporary table instance referenced in the statement. I could derive the record name from the table name, but it is easier just to hard-code it.
- The SQL statement produces all combinations of temporary records that could appear in the SQL statement. I will put it in an implicit cursor loop, and then for each row returned, the script will create a SQL profile.
- Common table expression V returns 100 rows, numbered 0 to 99. Irrespective of the number of temporary table instances specified in each Application Engine program, there can only be non-shared 99 table instances for each PeopleTools record, plus the shared instance (that doesn't have a suffix number).
- PSTEMPTBLCNTVW returns the number of non-shared batch (i.e. not online) instances of each temporary record that needs to be built. This is in addition to the number of online temporary table instances.
- The query will return a row for each instance of each temporary table up to the number of instances required by the application engines plus the number of online table instances, but not exceeding the 99 rows returned by CTE V.
- Most Application Engines do not run online in the component processor, therefore there is no need to build SQL profiles on these instances. There are exceptions, such as some as the journal and voucher edit and post processes in Financials, in which case these criteria should be removed.
- This expression joins the record and instance number to the table in the database. Instance 0 will be used to refer to the shared instance.
- I allow the instance number to vary by up to one in either direction between each table. So I will profiles with instance 12 of one table, and instances 11 to 13 of the other. It is rare, but possible, for there to be any difference in instance numbers between tables. It is possible, but rarer for the difference to be greater than one.
- SQL Profile names are limited to 30 characters. I will specify a meaningful name based on the Application Engine step, up to 26 characters, and then the two IDs (which can be up to 2 digits each).
- In this example, I am not going to use the full set of hints in the captured profile. I just want to introduce a single leading hint.
- I will substitute each table name in the SQL text with the specific table instance name.
- Tables are generally referenced in hints via the row source alias. However, sometimes the table name appears in the hints, and must also be replaced with the specific table name. So I also work through all the hints in array h and substitute any table names that may be there. Indexes are not referenced by their names but by a list of indexed columns.
XX_TREESEL.iDetVal.iDetVal00:11943215885789639839:
INSERT INTO PS_TSEL_P_TAO (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
XX_TREESEL.iDetVal.iDetVal1717:15747497907378648788:
INSERT INTO PS_TSEL_P_TAO17 (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO17 A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678 AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
XX_TREESEL.iDetVal.iDetVal1718:12015611546030583918:
INSERT INTO PS_TSEL_P_TAO18 (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO17 A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678 AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
XX_TREESEL.iDetVal.iDetVal1817:14883898515022367531:
INSERT INTO PS_TSEL_P_TAO17 (CHARTFIELD, CHARTFIELD_VALUE, PROCESS_INSTANCE) SELECT DISTINCT 'ACCOUNT', DV.ACCOUNT, 12345678
FROM PS_TSEL_R30_TAO18 A, PS_GL_ACCOUNT_TBL DV WHERE A.CHARTFIELD = 'ACCOUNT' AND A.PROCESS_INSTANCE = 12345678 AND DV.SETID
= 'GLOBE' AND DV.EFFDT = (SELECT MAX(EFFDT) FROM PS_GL_ACCOUNT_TBL B WHERE SETID = DV.SETID AND ACCOUNT = DV.ACCOUNT AND
EFFDT <= TO_DATE('2025-01-31','YYYY-MM-DD')) AND DV.ACCOUNT >= A.RANGE_FROM_30 AND DV.ACCOUNT <= A.RANGE_TO_30
…- If the number of non-shared instances of a table in an Application Engine is increased, you need to build add table instances with Application Designer.
- If the number of online temporary instances is increased, you need to build additional instances for every temporary record in the database. If the number is reduced some tables will cease to be used, and they ought to be dropped.
- If either the number of application engine table instances or online temporary table instances are changed, then you will need to rerun the script to create additional SQL profiles.
In the Cloud Performance is Instrumented as Cost - A Resource Plan for PeopleSoft
In the cloud, either you are spending too much money on too much CPU, or your system is constrained by CPU at peak times. You can have as much performance as you are willing to pay for.
This presentation (from the UKOUG 2024 conference) is the story of how one PeopleSoft customer improved performance and reduced cloud subscription costs, by clearly stating their performance goals, and creating a matching resource manager plan.
Effective use of machine resources has always been a challenge for PeopleSoft systems. As systems move to the cloud that is in ever sharper focus. In the cloud, you mostly pay for CPU. You can generally have as much performance as you are willing to pay for, but every architectural decision you make has an immediate cost consequence. That drives out different behaviours.
In the cloud, you rent hardware as an operational expense, rather than purchasing it as a capital expense. If you are not short of CPU, you are probably spending too much. If you are short of CPU, then you need to the Oracle database's Resource Manager to manage what happens.
This presentation looks at how that played out at one PeopleSoft customer, who moved their GL reporting batch on Financials onto Exadata Cloud-at-Customer. The single most important thing they did was to clearly state their goals. That set the ground rules for sizing and configuring both their database and their application, implementing various database features, including defining a resource manager plan, as well as using partitioning, materialized views, compression, and in-memory.
They have continued to improve performance and save money on their cloud costs. They were recently able to switch off another CPU.
The session also describes a generic resource plan that can be used as a starting point for any PeopleSoft system to which individual requirements can be added.
- PeopleSoft DBA Blog: PSFT_PLAN: A Sample Oracle Database Resource Manager Plan for PeopleSoft
- GitHub: psft_resource_plan_simple.sql
Finally, there are some ideas for prioritising Tuxedo server processes on Linux.
PeopleSoft PS/Query: Finding Users' Cartesian Joins
Many PeopleSoft users like its ad hoc query tool because they can write their own queries directly on the system, without having to learn to write structured query language (SQL), or getting a developer to write it for them.
What is the Problem?It is easy for users to create poor queries, that either don't work as intended or can run for long periods, even indefinitely, consuming resources without ever producing results. This can consume significant amounts of CPU, and in the cloud, that is mostly what you pay for! The effect can be mitigated with the database's resource manager, but it is better not to do it in the first place.
One cause of long-running queries that I come across is missing join criteria leading the database to perform Cartesian Merge Joins. I should stress that not all Cartesian joins are evil. For example, in some data warehouse queries (e.g. GL nVision reporting), it can be a very effective strategy to Cartesian join dimension tables before visiting the fact table, especially if you can use Bloom filter a full scan on the fact table. It works well with parallel query, and on engineered systems this can also be pushed down to the storage cells.
Finding Execution Plans with Cartesian JoinsThe following query profiles database time by execution plan from ASH for SQL statements from PS/Queries run via the PSQUERY application engine program on a process scheduler. It returns the longest-running statement for each execution plan.
The data is generated and processed through several common table expressions.
- R returns the PSQUERY processes that ran in the time window of interest
- P returns the execution plans captured by AWR that generate Cartesian products for which the SQL text is also captured.
- X returns the ASH data for Cartesian join executions. When P is joined with the ASH data, then we just get the queries that performed Cartesian joins.
- Y sums and groups the ASH data by statement and process
- Z sums the data by execution plan and identifies the longest-running SQL statement for that plan.
REM qry_cartesianplans.sql
WITH r as ( /*processes of interest*/
SELECT /*+MATERIALIZE*/ r.oprid, r.prcsinstance, r.prcsname, r.begindttm, r.enddttm
, DECODE(c.private_query_flag,'Y','Private','N','Public') private_query_flag, c.qryname
FROM psprcsrqst r
LEFT OUTER JOIN ps_query_run_cntrl c ON c.oprid = r.oprid AND c.run_cntl_id = r.runcntlid
WHERE prcsname = 'PSQUERY'
AND r.begindttm >= trunc(SYSDATE)-0+8/24
AND r.begindttm <= trunc(SYSDATE)-0+19/24
), p as ( /*known Cartesian plans with SQL text*/
SELECT /*+MATERIALIZE*/ p.plan_hash_value, MAX(p.options) options
FROM dbA_hist_sql_plan p
, dba_hist_sqltext t
WHERE t.sql_id = p.sql_id
AND (p.id = 0 OR p.options = 'CARTESIAN')
GROUP BY p.plan_hash_Value
), x AS ( /*ASH for processes*/
SELECT /*+materialize leading(r x)*/ r.prcsinstance, r.oprid, r.private_query_flag, r.qryname
, h.event, x.dbid, h.sample_id, h.sample_time, h.instance_number
, CASE WHEN h.module IS NULL THEN REGEXP_SUBSTR(h.program, '[^@]+',1,1)
WHEN h.module LIKE 'PSAE.%' THEN REGEXP_SUBSTR(h.module, '[^.]+',1,2)
ELSE REGEXP_SUBSTR(h.module, '[^.@]+',1,1)
END AS module
, h.action
, NULLIF(h.top_level_sql_id, h.sql_id) top_level_sql_id
, h.sql_id, h.sql_plan_hash_value, h.force_matching_signature, h.sql_exec_id
, h.session_id, h.session_serial#, h.qc_instance_id, h.qc_Session_id, h.qc_Session_serial#
, f.name, p.options
, NVL(usecs_per_row,1e7) usecs_per_row
, CASE WHEN p.plan_hash_value IS NOT NULL THEN NVL(usecs_per_row,1e7) ELSE 0 END usecs_per_row2
FROM dba_hist_snapshot x
, dba_hist_active_sess_history h
LEFT OUTER JOIN p ON p.plan_hash_value = h.sql_plan_hash_value
LEFT OUTER JOIN dba_sql_profiles f ON h.force_matching_signature = f.signature
, r
, sysadm.psprcsque q
WHERE h.SNAP_id = X.SNAP_id
AND h.dbid = x.dbid
AND h.instance_number = x.instance_number
AND x.end_interval_time >= r.begindttm
AND x.begin_interval_time <= NVL(r.enddttm,SYSDATE)
AND h.sample_time BETWEEN r.begindttm AND NVL(r.enddttm,SYSDATE)
AND q.prcsinstance = r.prcsinstance
AND ( (h.module = r.prcsname AND h.action like 'PI='||r.prcsinstance||':Processing')
OR h.module like 'PSAE.'||r.prcsname||'.'||q.sessionidnum)
), y as( /*profile time by statement/process*/
SELECT prcsinstance, oprid, private_query_flag, qryname, sql_plan_hash_value, sql_id, force_matching_signature, name
, dbid, module, action, top_level_sql_id
, count(distinct qc_session_id||qc_session_serial#||sql_id||sql_exec_id) execs
, sum(usecs_per_row)/1e6 ash_Secs
, sum(usecs_per_Row2)/1e6 awr_secs
, avg(usecs_per_row)/1e6*count(distinct sample_time) elapsed_secs
, count(distinct instance_number||session_id||session_serial#) num_procs
, max(options) options
FROM x
GROUP BY prcsinstance, oprid, private_query_flag, qryname, sql_plan_hash_value, sql_id, force_matching_signature, name
, dbid, module, action, top_level_sql_id, qc_instance_id, qc_session_id, qc_session_serial#
), z as ( /*find top statement per plan and sum across all executions*/
SELECT row_number() over (partition by force_matching_signature, sql_plan_hash_value order by awr_secs desc) plan_seq
, prcsinstance, oprid, name, private_query_flag, NVL(qryname,action) qryname, options
, sql_id, sql_plan_hash_Value, force_matching_signature
, count(distinct sql_id) over (partition by force_matching_signature, sql_plan_hash_value) sql_ids
, sum(execs) over (partition by force_matching_signature, sql_plan_hash_value) plan_execs
, sum(ash_Secs) over (partition by force_matching_signature, sql_plan_hash_value) plan_ash_secs
, sum(awr_Secs) over (partition by force_matching_signature, sql_plan_hash_value) plan_awr_secs
, sum(elapsed_Secs) over (partition by force_matching_signature, sql_plan_hash_value) elap_secs
, sum(num_procs) over (partition by force_matching_signature, sql_plan_hash_value) max_procs
FROM y
)
Select z.*, z.plan_ash_secs/z.elap_secs eff_para
from z
where plan_seq = 1
and sql_id is not null
and plan_ash_secs >= 300
ORDER BY plan_ash_secs DESC
FETCH FIRST 50 ROWS ONLY
/Plan Plan
Process Private SQL Plan Force Matching SQL Plan ASH AWR Elapsed Max Eff.
# Instance OPRID NAME Query QRYNAME OPTIONS SQL_ID Hash Value Signature IDs Execs Secs Secs Seconds Prc Para
-- --------- --------- ------------------------------ ------- ------------------------------ --------- ------------- ----------- --------------------- ---- ------ ------- ------- ------- ---- -----
1 12344342 NXXXXXX Public XXX_TRIAL_BALANCE_BY_BU_XXX_V2 c4zfcub2bnju8 2128864041 4468535744829993986 4 4 103473 103473 103473 4 1.0
1 12344471 FXXXXXX Public XXXAM_FIN_GL_AP d8jnxzmgx20mq 4189069557 16033793374717384734 1 1 32599 32599 32599 1 1.0
1 12344448 VXXXXXX Private XXX1_LEDGERBAL1_UPRDAC_XXXX1 ftn7nz1xafh5z 0 15193759933860031914 2 2 20615 20615 20615 2 1.0
1 12345574 BXXXXXX Private XXX_GL_BJU CARTESIAN ab2v91h9zj3hv 603930234 4189289347608449750 1 1 16862 16862 16862 1 1.0
1 12345681 BXXXXXX Private XXX_GL_BJU CARTESIAN 05tphb379fu8j 603930234 6203431496815450503 1 1 15452 15452 15452 1 1.0
1 12345852 WXXXXXX Public XXXINSOLVENTS_JRNL_DETAIL 51aw4ahxba0gq 3918624993 11145663850623390044 1 1 13435 13435 13435 1 1.0
1 12345863 CXXXXXX Public XXX_COMMUTATIONS_JRNL_DTL 7q9kt75bh35dg 0 11985643849566057390 1 1 13283 13283 13283 1 1.0
1 12344773 WXXXXXX Private XXX_COMMUTATION_JRNL_DETAIL_2 361gck3w3mak7 0 18367721225324700858 1 2 12883 12883 12883 2 1.0
1 12344682 DXXXXXX Private COMBINED_JE_DETAIL_DV 2gchgaf465ku5 0 5375582220398622005 1 1 9279 9279 9279 1 1.0
1 12345618 DXXXXXX Private COMBINED_JE_DETAIL_DV_NO_AFF 2q2faj9c6003u 0 15355473744647942117 1 1 5079 5079 5079 1 1.0
…SELECT * FROM table(dbms_xplan.display_workload_repository('ab2v91h9zj3hv',603930234,'ADVANCED +ADAPTIVE'));
In this example, there are two similar SQL statements, with different force matching signatures, that produce the same execution plan. The difference is that one has an IN list of 3 accounts, and the other has an equi-join to just one account. This is enough to produce a different force matching signature. This is why I often group ASH data by execution plan hash value. Even if the SQL statement is different, if the execution plan is the same, then the issues and solutions tend to be the same.
The statements have been reformated to make them easier to read. Both are just joins between two objects. There are criteria on PS_JRNL_DRILL_VW (a view on PS_JRNL_LN), but there are no join criteria between it and its parent table JRNL_HEADER, thus a meaningless Cartesian product that joins every journal line to every journal header was created and sorted.
SQL_ID ab2v91h9zj3hv
--------------------
SELECT A.BUSINESS_UNIT, A.JOURNAL_ID,
TO_CHAR(A.JOURNAL_DATE,'YYYY-MM-DD'), B.DESCR254, A.ACCOUNT,
A.LINE_DESCR, SUM( A.MONETARY_AMOUNT), A.LEDGER, B.ACCOUNTING_PERIOD,
B.SOURCE, B.OPRID, A.PRODUCT, A.CLASS_FLD, A.PROGRAM_CODE,
A.CHARTFIELD1, A.CHARTFIELD3, A.CURRENCY_CD, A.FOREIGN_CURRENCY
FROM PS_JRNL_DRILL_VW A, PS_JRNL_HEADER B
WHERE ( A.BUSINESS_UNIT IN('12341','12347')
AND A.LEDGER IN ('CORE','LOCAL_ADJ','LOCAL_ADJ2')
AND A.ACCOUNT IN ('1234510040','1234510000','1234510060')
AND A.ACCOUNTING_PERIOD BETWEEN 1 AND 12 AND A.FISCAL_YEAR = 2023)
GROUP BY A.BUSINESS_UNIT, A.JOURNAL_ID, A.JOURNAL_DATE, B.DESCR254, A.ACCOUNT,
A.LINE_DESCR, A.LEDGER, B.ACCOUNTING_PERIOD, B.SOURCE, B.OPRID,
A.PRODUCT, A.CLASS_FLD, A.PROGRAM_CODE, A.CHARTFIELD1, A.CHARTFIELD3,
A.CURRENCY_CD, A.FOREIGN_CURRENCY ORDER BY 11SQL_ID 05tphb379fu8j
--------------------
SELECT A.BUSINESS_UNIT, A.JOURNAL_ID,
TO_CHAR(A.JOURNAL_DATE,'YYYY-MM-DD'), B.DESCR254, A.ACCOUNT,
A.LINE_DESCR, SUM( A.MONETARY_AMOUNT), A.LEDGER, B.ACCOUNTING_PERIOD,
B.SOURCE, B.OPRID, A.PRODUCT, A.CLASS_FLD, A.PROGRAM_CODE,
A.CHARTFIELD1, A.CHARTFIELD3, A.CURRENCY_CD, A.FOREIGN_CURRENCY
FROM PS_JRNL_DRILL_VW A, PS_JRNL_HEADER B
WHERE ( A.BUSINESS_UNIT IN('12341','12347')
AND A.LEDGER IN ('CORE','LOCAL_ADJ','LOCAL_ADJ2')
AND A.ACCOUNT = '1234510000'
AND A.ACCOUNTING_PERIOD BETWEEN 1 AND 12
AND A.FISCAL_YEAR = 2023)
GROUP BY A.BUSINESS_UNIT, A.JOURNAL_ID, A.JOURNAL_DATE, B.DESCR254, A.ACCOUNT
, A.LINE_DESCR, A.LEDGER, B.ACCOUNTING_PERIOD, B.SOURCE, B.OPRID
, A.PRODUCT, A.CLASS_FLD,A.PROGRAM_CODE, A.CHARTFIELD1, A.CHARTFIELD3
, A.CURRENCY_CD, A.FOREIGN_CURRENCY ORDER BY 11
Plan hash value: 603930234
---------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | Inst |IN-OUT|
---------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 84648 (100)| | | | | |
| 1 | SORT GROUP BY | | 57 | 9063 | 84648 (1)| 00:00:04 | | | | |
| 2 | MERGE JOIN CARTESIAN | | 57 | 9063 | 84647 (1)| 00:00:04 | | | | |
| 3 | NESTED LOOPS | | 1 | 145 | 1636 (0)| 00:00:01 | | | | |
| 4 | VIEW | PS_JRNL_HEADER | 112 | 4032 | 1188 (0)| 00:00:01 | | | | |
| 5 | UNION-ALL | | | | | | | | | |
| 6 | REMOTE | PS_JRNL_HEADER | 76 | 5624 | 18 (0)| 00:00:01 | | | FSARC~ | R->S |
| 7 | INLIST ITERATOR | | | | | | | | | |
| 8 | TABLE ACCESS BY INDEX ROWID BATCHED | PS_JRNL_HEADER | 16679 | 586K| 11634 (1)| 00:00:01 | | | | |
|* 9 | INDEX RANGE SCAN | PSEJRNL_HEADER | 16679 | | 347 (0)| 00:00:01 | | | | |
| 10 | VIEW | PS_JRNL_LN | 1 | 109 | 4 (0)| 00:00:01 | | | | |
| 11 | UNION-ALL PARTITION | | | | | | | | | |
|* 12 | FILTER | | | | | | | | | |
| 13 | REMOTE | PS_JRNL_LN | 1 | 217 | 5 (0)| 00:00:01 | | | FSARC~ | R->S |
|* 14 | FILTER | | | | | | | | | |
| 15 | PARTITION RANGE SINGLE | | 1 | 109 | 5 (0)| 00:00:01 | KEY | KEY | | |
|* 16 | TABLE ACCESS BY LOCAL INDEX ROWID BATCHED| PS_JRNL_LN | 1 | 109 | 5 (0)| 00:00:01 | KEY | KEY | | |
|* 17 | INDEX RANGE SCAN | PS_JRNL_LN | 1 | | 4 (0)| 00:00:01 | KEY | KEY | | |
| 18 | BUFFER SORT | | 7749K| 103M| 84644 (1)| 00:00:04 | | | | |
| 19 | VIEW | PS_JRNL_HEADER | 7749K| 103M| 83011 (1)| 00:00:04 | | | | |
| 20 | UNION-ALL | | | | | | | | | |
| 21 | REMOTE | PS_JRNL_HEADER | 5698K| 1880M| 50467 (1)| 00:00:02 | | | FSARC~ | R->S |
| 22 | TABLE ACCESS STORAGE FULL | PS_JRNL_HEADER | 2050K| 86M| 32544 (1)| 00:00:02 | | | | |
--------------------------------------------------------------------------------------------------------------------------------------------------- Stmt Stmt
SQL Plan SQL Plan H E I ASH ASH Elap
Hash Value Line ID EVENT P P x M Secs ELAP_SECS Secs Secs
----------- -------- ---------------------------------------------------------------- - - - - ------- ---------- ------- -------
603930234 1 CPU+CPU Wait N N Y N 217091 23405.3608 299088 32314
18 direct path read temp N N Y N 64395 7034.44748 299088 32314
18 CPU+CPU Wait N N Y N 16998 1812.39445 299088 32314
1 ASM IO for non-blocking poll N N Y N 195 20.4802032 299088 32314
21 CPU+CPU Wait N N Y N 195 20.47995 299088 32314
16 CPU+CPU Wait N N Y N 113 10.24021 299088 32314
CPU+CPU Wait N N Y N 103 10.25244 299088 32314The answer in this particular case is to fix the code. We have to go back to the user, explain why it is necessary to join parent and child tables and get them to correct their PS/Query.
Finding PS/Queries Without Joins on Related RecordsIn PeopleSoft, the parent of a child record is recorded on PSRECDEFN in the column PARENTRECNAME. However, this does not translate into a foreign key relationship in any database supported by PeopleSoft. This is part of PeopleSoft's original platform-agnosticism. Not all databases previously supported by PeopleSoft supported database enforced referential integrity. Therefore it never became part of the implementation, and there is no guarantee that the applications were written in such a way to honour foreign-key constraints (i.e. insert parents before children, delete children before parents etc.).
The below query looks at pairs of parent-child records in each select block of each PS/Query and counts the number of key columns for which there are criteria on the child record that are joined to the parent record. It is restricted to just the journal header/line tables and views.
It returns rows where no joined key columns are found. These queries are therefore suspected of being faulty. However, there may be false positives where child records are joined to grandparents rather than immediate parents. Such an approach in SQL is perfectly valid, and can even result in better performance.
WITH x as (
SELECT r1.oprid, r1.qryname, r1.selnum
, r1.rcdnum rcdnum1, r1.recname recname1, r1.corrname corrname1
, r2.rcdnum rcdnum2, r2.recname recname2, r2.corrname corrname2
, (SELECT count(*)
FROM psqryfield qf1 --INNER JOIN psrecfielddb f1 ON f1.recname = r1.recname AND f1.fieldname = qf1.fieldname
, psqryfield qf2 INNER JOIN psrecfielddb f2 ON f2.recname = r2.recname AND f2.fieldname = qf2.fieldname
AND MOD(f2.useedit,2)=1 /*key fields only*/
, psqrycriteria c
WHERE qf1.oprid = r1.oprid AND qf1.qryname = r1.qryname AND qf1.selnum = r1.selnum AND qf1.recname = r1.recname AND qf1.fldrcdnum = r1.rcdnum
AND qf2.oprid = r2.oprid AND qf2.qryname = r2.qryname AND qf2.selnum = r2.selnum AND qf2.recname = r2.recname AND qf2.fldrcdnum = r2.rcdnum
AND c.oprid = r1.oprid AND c.qryname = r1.qryname AND c.selnum = r1.selnum
AND ( (c.lcrtselnum = r1.selnum AND c.lcrtfldnum = qf1.fldnum AND c.r1crtselnum = r2.selnum AND c.r1crtfldnum = qf2.fldnum)
OR (c.lcrtselnum = r2.selnum AND c.lcrtfldnum = qf2.fldnum AND c.r1crtselnum = r1.selnum AND c.r1crtfldnum = qf1.fldnum))
-- AND rownum = 1
) num_key_fields
FROM psrecdefn r
, psqryrecord r1
INNER JOIN psqryrecord r2 ON r1.oprid = r2.oprid AND r1.qryname = r2.qryname AND r1.selnum = r2.selnum AND r1.rcdnum != r2.rcdnum --AND r1.corrname < r2.corrname
WHERE r.recname = r2.recname AND r.parentrecname = r1.recname
)
SELECT x.* FROM x
WHERE num_key_fields = 0
AND recname1 IN('JRNL_HEADER')
AND recname2 IN('JRNL_LN','JRNL_DRILL_VW')
ORDER BY 1,2,3
/However, these queries may not have been run recently. Users tend to write queries, save a modification as a new version, and then abandon the old version.
Sel Rec1 Cor Rec2 Cor #Key
OPRID QRYNAME # # Record 1 #1 # Record #2 Flds
--------- ------------------------------ ---- ---- ------------------ --- ---- ------------------ --- ----
2_XX_CHI_JOURNAL_MES2_RE 1 1 JRNL_HEADER A 2 JRNL_LN B 0
12300_GL_ACCOUNT_DETAIL 1 1 JRNL_HEADER A 2 JRNL_LN B 0
123_DK 1 1 JRNL_HEADER A 2 JRNL_LN B 0
123_NEW 1 1 JRNL_HEADER A 2 JRNL_LN B 0
12345_ACCRUAL_JE_DETAILS 1 1 JRNL_HEADER A 2 JRNL_LN C 0
12345_ACCRUAL_JE_DETAILS_V2 1 1 JRNL_HEADER A 2 JRNL_LN C 0
12345_ACCRUAL_JE_DETAILS_V3 1 1 JRNL_HEADER A 2 JRNL_LN C 0
12345_HARDSOFT_JE_DETAILS_V3 1 1 JRNL_HEADER A 2 JRNL_LN C 0
12345_BM_CURR_ACTIVITY2 1 1 JRNL_HEADER A 2 JRNL_LN B 0
AAIC_CBP_POOLS 1 1 JRNL_HEADER A 2 JRNL_LN B 0
…
I demonstrated how to identify long-running PS/Queries on the process schedulers in an earlier blog post. The following query merges in that query, so that it only considers queries that have run on a process scheduler within the purge period, for which join criteria may be missing. They are sorted by descending execution time.
REM qry_missingjoins.sql
WITH q1 as (
SELECT r.prcsinstance
, r.oprid runoprid, r.runcntlid
, DECODE(c.private_query_flag,'Y','Private','N','Public') private_query_flag
, DECODE(c.private_query_flag,'Y',r.oprid,' ') oprid
, c.qryname
, CAST(begindttm AS DATE) begindttm
, CAST(enddttm AS DATE) enddttm
, runstatus
, (CAST(NVL(enddttm,SYSDATE) AS DATE)-CAST(begindttm AS DATE))*86400 exec_Secs
FROM psprcsrqst r
LEFT OUTER JOIN ps_query_run_cntrl c ON c.oprid = r.oprid AND c.run_cntl_id = r.runcntlid
WHERE prcsname = 'PSQUERY'
AND dbname IN(select DISTINCT dbname from ps.psdbowner)
--AND r.begindttm >= trunc(SYSDATE)-2+8/24
--AND r.begindttm <= trunc(SYSDATE)-2+19/24
), q as (
Select /*+MATERIALIZE*/ oprid, qryname
, SUM(exec_secs) exec_secs
, COUNT(*) num_execs
, COUNT(DECODE(runstatus,'9',1,NULL)) complete_execs
, COUNT(DISTINCT runoprid) runoprids
FROM q1
GROUP BY oprid, qryname
), x as (
SELECT r1.oprid, r1.qryname, r1.selnum
, r1.rcdnum rcdnum1, r1.recname recname1, r1.corrname corrname1
, r2.rcdnum rcdnum2, r2.recname recname2, r2.corrname corrname2
, (SELECT count(*)
FROM psqryfield qf1 --INNER JOIN psrecfielddb f1 ON f1.recname = r1.recname AND f1.fieldname = qf1.fieldname
, psqryfield qf2 INNER JOIN psrecfielddb f2 ON f2.recname = r2.recname AND f2.fieldname = qf2.fieldname AND MOD(f2.useedit,2)=1
, psqrycriteria c
WHERE qf1.oprid = r1.oprid AND qf1.qryname = r1.qryname AND qf1.selnum = r1.selnum AND qf1.recname = r1.recname AND qf1.fldrcdnum = r1.rcdnum
AND qf2.oprid = r2.oprid AND qf2.qryname = r2.qryname AND qf2.selnum = r2.selnum AND qf2.recname = r2.recname AND qf2.fldrcdnum = r2.rcdnum
AND c.oprid = r1.oprid AND c.qryname = r1.qryname AND c.selnum = r1.selnum
AND ( (c.lcrtselnum = r1.selnum AND c.lcrtfldnum = qf1.fldnum AND c.r1crtselnum = r2.selnum AND c.r1crtfldnum = qf2.fldnum)
OR (c.lcrtselnum = r2.selnum AND c.lcrtfldnum = qf2.fldnum AND c.r1crtselnum = r1.selnum AND c.r1crtfldnum = qf1.fldnum))
AND rownum = 1
) num_key_fields
FROM psrecdefn r
, psqryrecord r1
INNER JOIN psqryrecord r2 ON r1.oprid = r2.oprid AND r1.qryname = r2.qryname AND r1.selnum = r2.selnum AND r1.rcdnum != r2.rcdnum --AND r1.corrname < r2.corrname
WHERE r.recname = r2.recname AND r.parentrecname = r1.recname
)
SELECT /*+LEADING(Q)*/ q.*, x.selnum
, x.rcdnum1, x.recname1, x.corrname1
, x.rcdnum2, x.recname2, x.corrname2, x.num_key_fields
FROM x
INNER JOIN q ON q.oprid = x.oprid AND q.qryname = x.qryname
WHERE num_key_fields = 0
AND exec_secs >= 600
ORDER BY exec_secs desc
/Now I have a list of candidate queries that have been used recently and may be missing joins that I investigate further.
Sel Rec1 Cor Rec2 Cor #Key
OPRID QRYNAME EXEC_SECS NUM_EXECS COMPLETE_EXECS RUNOPRIDS # # Record 1 #1 # Record 2 #2 Flds
--------- ------------------------------ ---------- ---------- -------------- ---------- ---- ---- ------------------ --- ---- ------------------ --- ----
UKXXXXXXX AR_VENDOR_LOCATION_DETAILB 264317 361 360 1 1 1 VENDOR A 8 VNDR_LOC_SCROL H 0
XX_COL_MOV_ALT_ACCT2_PERIO 193692 2096 2051 14 1 1 JRNL_HEADER A 3 OPEN_ITEM_GL C 0
APC_123_LEDGER_ACTIVITY_BY_BU 151438 2959 2938 73 2 1 JRNL_HEADER B 2 JRNL_LN C 0
MXXXXXX MT_AUSTRALIA_TAX_PMTS 137471 36 28 1 1 1 JRNL_HEADER A 2 JRNL_LN B 0
XX_PAN_ASIA_JOURNALS_REF 135825 48 47 4 1 1 JRNL_HEADER A 5 JRNL_OPENITM_VW E 0
XXX_STKCOMP_LIFE 120537 526 523 1 1 1 JRNL_HEADER A 2 JRNL_LN B 0
XXX_123_TB_LEDGER_BAL_BU 100848 2093 2044 17 3 1 JRNL_HEADER B 2 JRNL_LN C 0
KXXXXXX XXX_JRNL_LIST_AUDIT_KL 99843 489 482 1 1 2 JRNL_HEADER B 1 JRNL_DRILL_VW A 0
XXX_JE_ID_QUERY 86106 156 151 1 1 1 JRNL_HEADER A 2 JRNL_LN C 0
XXX_ACTIVITY_DETAILS_2 85356 336 302 5 1 1 JRNL_HEADER A 2 JRNL_LN B 0
…Anyone can inspect any public queries, but you must be logged in as the owner of a private query to be able to see it.
The scripts in this article can be downloaded from GitHub davidkurtz/psscripts.
PeopleSoft PS/Query: Identify Long Running Queries (on Process Schedulers)
- There is no limit to the number of queries that users can initiate concurrently.
- The number that can actually execute concurrently is limited by the number of PSQRYSRV processes in each application server domain. Any additional requests will simply queue up in Tuxedo.
- It is possible to set maximum execution times in the PeopleSoft configuration, on the ICQuery service on the PSQRYSRV server in the application server.
- A system-wide maximum number of concurrently executing instances of the application engine program can be set on the process definition.
- A maximum number of concurrently executing instances of the application engine program per process scheduler can be set (by using a process class).
- The application engine, or its process class, can be given a lower priority so that other queued processes are run in preference.
PS/Queries run either in the PIA or on the process scheduler can be mapped to low-priority consumer groups in an Oracle database resource manager plan so that they do not starve the rest of the system of CPU (see PeopleSoft DBA Blog: PSFT_PLAN: A Sample Oracle Database Resource Manager Plan for PeopleSoft).
A maximum run time, or maximum estimated run time, can be defined for a consumer group. If the limit is breached an Oracle error is raised: ORA-00040: active time limit exceeded - call aborted. In the PIA, the error message is simply presented to the user. The scheduled PSQUERY application engine process will terminate and the error will be logged. In both cases, the user has to recognise the error message and understand what it means. Otherwise, they will raise the issue with support.
The various methods of setting maximum execution time limits are quite blunt instruments. They are essentially one-size-fits-all approaches. Typically, some queries are expected to run for a long time, and then the limits must be set to accommodate them.
I can query who has run which queries, and how long they ran for. Simply outer join the run control record for the PSQUERY application engine (PS_QUERY_RUN_CNTL) to the process scheduler request table (PSPRCSRQST).
REM qry_missingjoins.sql
WITH x as (
SELECT r.prcsinstance, r.oprid, r.runcntlid
, DECODE(c.private_query_flag,'Y','Private','N','Public') private_query_flag, c.qryname
, CAST(begindttm AS DATE) begindttm
, CAST(enddttm AS DATE) enddttm
, runstatus
, (CAST(NVL(enddttm,SYSDATE) AS DATE)-CAST(begindttm AS DATE))*86400 exec_Secs
FROM psprcsrqst r
LEFT OUTER JOIN ps_query_run_cntrl c ON c.oprid = r.oprid AND c.run_cntl_id = r.runcntlid
WHERE prcsname = 'PSQUERY'
AND r.begindttm >= TRUNC(SYSDATE)-0+8/24 /*from 8am*/
AND r.begindttm <= TRUNC(SYSDATE)-0+19/24 /*to 7pm*/
)
SELECT x.* FROM x
WHERE exec_Secs >= 300 /*Over 5 minutes*/
ORDER BY exec_secs desc /*descending order of elapsed time*/
FETCH FIRST 50 ROWS ONLY /*top 50 ROWS ONLY*/
/
Process Private Run Exec
Instance OPRID RUNCNTLID Query QRYNAME BEGINDTTM ENDDTTM Stat Secs
--------- ---------- ------------------------------ ------- ------------------------------ ----------------- ----------------- ---- -------
12344471 F****** ***AM_FIN_GL_AP Public ***AM_FIN_GL_AP 10:06:21 19.**.** 19:08:52 19.**.** 8 32551
12344342 N****** ownxxxxxxxxxxxx Public ***_TRIAL_BALANCE_BY_BU_***_V2 09:41:58 19.**.** 18:20:09 19.**.** 10 31091
12344336 N****** ojnxxxxxxxxxx Public ***_TRIAL_BALANCE_BY_BU_*** 09:40:27 19.**.** 16:51:11 19.**.** 10 25844
12345209 N****** eowxxxxxxxxxxxxx Public ***_TRIAL_BALANCE_BY_BU_*** 12:41:17 19.**.** 19:08:30 19.**.** 8 23233
12345213 N****** iwoxxxxxxxxxxxxx Public ***_TRIAL_BALANCE_BY_BU_***_V2 12:41:53 19.**.** 19:08:56 19.**.** 8 23223
12345574 B****** gl Private ***_GL_BJU 14:27:32 19.**.** 19:08:59 19.**.** 8 16887
12345681 B****** gl Private ***_GL_BJU 14:51:06 19.**.** 19:09:02 19.**.** 8 15476
12345852 W****** insolvents Public ***INSOLVENTS_JRNL_DETAIL 15:24:41 19.**.** 19:09:04 19.**.** 8 13463
…
-------
sum 268112- Some details have been redacted from this real-world example.
- The result is not guaranteed to be completely accurate. A user might have reused a run control record and can only get the current value.
- This and other scripts can be downloaded from GitHub davidkurtz/psscripts.


