Slow BI Publisher reports are one of the most common Oracle Fusion frustrations. Reports that take minutes to render, time out during peak hours, or become unusable as data volumes grow are usually caused by inefficient SQL - not Oracle Fusion itself.
In Oracle Fusion Cloud, BI Publisher performance depends heavily on how the SQL data model is written. Missing date filters, inefficient joins, unnecessary columns, and poorly structured queries can dramatically increase execution time and memory usage.
This guide covers 12 practical SQL optimization techniques Oracle Fusion teams use to improve BI Publisher report performance in real production environments - including a real-world case study, before/after execution plans, and a consolidated example of what a bad query actually looks like.
Real-World Case Study: 6-Minute Report Reduced to 18 Seconds
The Original SQL (Slow)
SELECT * FROM ap_invoices_all ai JOIN ap_checks_all ac ON ac.vendor_id = ai.vendor_id JOIN per_all_people_f p ON p.person_id = ai.created_by_id JOIN per_person_names_f pn ON pn.person_id = p.person_id WHERE TRUNC(ai.invoice_date) >= TRUNC(ADD_MONTHS(SYSDATE, -12)) ORDER BY ai.invoice_date, ai.vendor_id
Problems Identified
Before / After Execution Plan
Operation Rows (est.)
---------------------------------------------------------
SELECT STATEMENT
SORT ORDER BY
HASH JOIN
TABLE ACCESS FULL AP_INVOICES_ALL 4,287,612
TABLE ACCESS FULL AP_CHECKS_ALL 2,103,891
TABLE ACCESS FULL PER_ALL_PEOPLE_F 892,441 <-- all historical rows
TABLE ACCESS FULL PER_PERSON_NAMES_F 1,104,882 <-- all historical rows
Operation Rows (est.)
-------------------------------------------------------------------
SELECT STATEMENT
SORT ORDER BY
NESTED LOOPS
TABLE ACCESS BY INDEX ROWID AP_INVOICES_ALL
INDEX RANGE SCAN AP_INVOICES_N3 (ledger_id, invoice_date) 14,872
INDEX RANGE SCAN AP_CHECKS_N2 (vendor_id) 3,241
TABLE ACCESS BY INDEX ROWID PER_ALL_PEOPLE_F
INDEX RANGE SCAN PER_ALL_PEOPLE_F_N1 (person_id, eff) 14,872
TABLE ACCESS BY INDEX ROWID PER_PERSON_NAMES_F
INDEX RANGE SCAN PER_PERSON_NAMES_F_N1 (person_id, eff) 14,872
The Fixed SQL
SELECT ai.invoice_num,
ai.invoice_date,
ai.invoice_amount,
ai.payment_status_flag,
ai.vendor_id,
ac.check_number,
ac.check_date,
ac.amount AS payment_amount,
n.display_name AS created_by_name
FROM ap_invoices_all ai
JOIN ap_checks_all ac
ON ac.vendor_id = ai.vendor_id
JOIN per_all_people_f p
ON p.person_id = ai.created_by_id
AND TRUNC(SYSDATE) BETWEEN p.effective_start_date AND p.effective_end_date
JOIN per_person_names_f n
ON n.person_id = p.person_id
AND n.name_type = 'GLOBAL'
AND TRUNC(SYSDATE) BETWEEN n.effective_start_date AND n.effective_end_date
WHERE ai.ledger_id = :p_ledger_id
AND ai.invoice_date >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -12)
AND ai.invoice_date < TRUNC(SYSDATE, 'MM')
ORDER BY ai.invoice_date, ai.vendor_id
- Replaced SELECT * with 9 specific columns needed by the template
- Added effective-date filters on both PER tables - reduced from 1M+ to 14K rows each
- Added
ledger_id = :p_ledger_idand range-based date filter - enabled INDEX RANGE SCAN
Why Oracle Fusion BI Publisher Reports Become Slow
Most BI Publisher performance issues come from a small set of recurring SQL problems:
- Missing effective-date filters on
_Fand_Mtables - Selecting unnecessary columns from wide Oracle Fusion tables
- Applying filters too late in the query
- Using functions on indexed columns
- Correlated subqueries that execute repeatedly
- Overusing translation joins (
_TL/_VL) - Large unoptimized joins across HCM, ERP, and Procurement modules
- Sorting massive result sets unnecessarily
These problems are common because Oracle Fusion schemas are highly normalized, heavily date-effective, and often contain very large transactional tables. The good news: most performance problems are fixable with better SQL structure.
12 SQL Optimization Techniques
Filter Date-Effective Tables Correctly
Oracle Fusion tables ending with _F or _M store historical versions of records. Without proper effective-date filtering, Oracle returns all historical rows instead of the current version. This is the single most common cause of duplicate rows and exploding result sets.
SELECT p.person_id,
n.display_name,
a.assignment_number
FROM per_all_people_f p
JOIN per_person_names_f n ON n.person_id = p.person_id
JOIN per_all_assignments_m a ON a.person_id = p.person_id
SELECT p.person_id,
n.display_name,
a.assignment_number
FROM per_all_people_f p
JOIN per_person_names_f n
ON n.person_id = p.person_id
AND n.name_type = 'GLOBAL'
AND TRUNC(SYSDATE) BETWEEN n.effective_start_date AND n.effective_end_date
JOIN per_all_assignments_m a
ON a.person_id = p.person_id
AND TRUNC(SYSDATE) BETWEEN a.effective_start_date AND a.effective_end_date
WHERE TRUNC(SYSDATE) BETWEEN p.effective_start_date AND p.effective_end_date
Every effective-dated table in the query should have its own effective-date filter.
Avoid SELECT *
Oracle Fusion tables are wide. PER_ALL_ASSIGNMENTS_M contains dozens of columns; AP_INVOICES_ALL contains well over 100. Selecting all columns increases I/O, memory usage, network transfer, and XML generation time in BI Publisher.
SELECT * FROM per_all_assignments_m
SELECT assignment_number,
assignment_status_type,
job_id,
organization_id
FROM per_all_assignments_m
Only return fields the report template actually uses.
Push Filters as Early as Possible
The earlier Oracle can reduce row counts, the faster the query executes. Reduce rows before joins expand the dataset.
SELECT *
FROM (
SELECT ai.invoice_num,
ai.invoice_amount,
s.vendor_name
FROM ap_invoices_all ai
JOIN ap_suppliers s ON s.vendor_id = ai.vendor_id
)
WHERE invoice_amount > 10000
SELECT ai.invoice_num,
ai.invoice_amount,
s.vendor_name
FROM ap_invoices_all ai
JOIN ap_suppliers s ON s.vendor_id = ai.vendor_id
WHERE ai.invoice_amount > 10000
AND ai.cancelled_date IS NULL
Avoid Functions on Indexed Columns
Using functions like TRUNC() on indexed columns prevents Oracle from using indexes efficiently. Range conditions preserve index usage and improve performance significantly on large tables.
WHERE TRUNC(ai.invoice_date) = TRUNC(SYSDATE)
WHERE ai.invoice_date >= TRUNC(SYSDATE) AND ai.invoice_date < TRUNC(SYSDATE) + 1
Replace Correlated Subqueries with JOINs
Correlated subqueries execute once for every row returned by the outer query. On large reports, this becomes extremely expensive. JOINs scale much better.
SELECT ph.segment1,
(SELECT s.vendor_name
FROM ap_suppliers s
WHERE s.vendor_id = ph.vendor_id) AS vendor_name
FROM po_headers_all ph
SELECT ph.segment1,
s.vendor_name
FROM po_headers_all ph
JOIN ap_suppliers s ON s.vendor_id = ph.vendor_id
Use EXISTS Instead of DISTINCT
Using DISTINCT to remove duplicates after joins often forces expensive sorting operations. If you only need to confirm existence, use EXISTS. It short-circuits on the first match and is typically more efficient.
SELECT DISTINCT p.person_id FROM per_all_people_f p JOIN per_all_assignments_m a ON a.person_id = p.person_id
SELECT p.person_id
FROM per_all_people_f p
WHERE EXISTS (
SELECT 1
FROM per_all_assignments_m a
WHERE a.person_id = p.person_id
)
Minimize Translation Table Joins (_TL / _VL)
Oracle Fusion uses translation layers for multilingual display values. These joins add overhead. Only join translation tables if the report actually displays translated values.
-- Only return IDs; join to _TL only when the display name is required
SELECT assignment_number,
job_id,
organization_id
FROM per_all_assignments_m
Avoid unnecessary joins to _TL or _VL tables unless the translated name is rendered in the report output.
Use Correct Parameter Data Types
Implicit type conversions can block index usage. Always use explicit date formats for BI Publisher parameters.
WHERE invoice_date = :p_date
WHERE invoice_date >= TO_DATE(:p_from_date, 'YYYY-MM-DD') AND invoice_date < TO_DATE(:p_to_date, 'YYYY-MM-DD') + 1
Split Very Large Reports into Multiple Data Sets
A single massive SQL query is not always the best approach. BI Publisher supports multiple linked data sets. Instead of one huge query joining GL headers to GL lines to code combinations to AP invoices to suppliers, consider splitting into a header query, a line query, and a summary query - then link them in the BI Publisher data model. This often simplifies optimization and reduces execution time.
Validate Joins Incrementally
Missing join conditions create accidental Cartesian joins - one of the worst performance problems in Oracle Fusion SQL. Build queries incrementally and validate row counts after each join. If counts suddenly double or explode, the join condition is likely incorrect.
-- Step 1: baseline row count
SELECT COUNT(*)
FROM per_all_people_f
WHERE TRUNC(SYSDATE) BETWEEN effective_start_date AND effective_end_date
-- Step 2: add first JOIN, verify count is unchanged
SELECT COUNT(*)
FROM per_all_people_f p
JOIN per_person_names_f n
ON n.person_id = p.person_id
AND n.name_type = 'GLOBAL'
AND TRUNC(SYSDATE) BETWEEN n.effective_start_date AND n.effective_end_date
WHERE TRUNC(SYSDATE) BETWEEN p.effective_start_date AND p.effective_end_date
Remove Unnecessary ORDER BY
Sorting very large result sets consumes memory and CPU. If BI Publisher templates already define sorting, SQL-level sorting is redundant. Remove ORDER BY unless it is required for window functions, running totals, or deterministic grouping. Let the template handle presentation-layer sorting whenever possible.
SELECT ... FROM ... WHERE ... ORDER BY n.display_name
Regularly Audit Unused Columns
Over time, BI Publisher reports evolve. Columns added years ago often remain in SQL even after templates stop using them. Regularly audit data models and remove unused fields. Reducing unused columns improves XML generation, network transfer, memory usage, and parsing time - especially in large scheduled reports.
Oracle Fusion-Specific Performance Considerations
Oracle Fusion introduces several unique SQL performance challenges beyond standard Oracle database tuning.
PER_ALL_PEOPLE_F, PER_ALL_ASSIGNMENTS_M, and PER_PERSON_NAMES_F. Missing even one effective-date filter on any of these tables can multiply row counts dramatically - producing incorrect totals and very slow report runtimes.AP_INVOICES_ALL, GL_JE_LINES, and PO_DISTRIBUTIONS_ALL in particular. Poor filtering strategies become expensive very quickly. Always apply ledger, org, and date filters as early as possible in the query._ALL often contain data across all business units. Always filter by business_unit_id, org_id, ledger, or legal entity when applicable to avoid cross-org data leakage and unnecessary scan scope.Anti-Pattern Example: Five Mistakes in One Query
The following query combines the most common Oracle Fusion SQL performance mistakes. It resembles queries that arrive in production BI Publisher reports from developers new to Oracle Fusion schema conventions.
-- 1: SELECT * on wide tables
-- 2: No effective-date filters on _F / _M tables (returns all historical rows)
-- 3: TRUNC() on indexed column blocks index usage
-- 4: Correlated subquery executes once per row
-- 5: No ledger / org filter on _ALL table
SELECT *
FROM per_all_people_f p
JOIN per_all_assignments_m a ON a.person_id = p.person_id
JOIN per_person_names_f pn ON pn.person_id = p.person_id
JOIN ap_invoices_all ai ON ai.created_by = p.person_id
WHERE TRUNC(ai.invoice_date) >= TRUNC(SYSDATE) - 365
AND (SELECT COUNT(*)
FROM ap_invoice_distributions_all aid
WHERE aid.invoice_id = ai.invoice_id) > 0
ORDER BY pn.display_name, ai.invoice_date
SELECT p.person_number,
n.display_name,
a.assignment_number,
a.assignment_status_type,
a.organization_id,
ai.invoice_num,
ai.invoice_date,
ai.invoice_amount,
ai.payment_status_flag
FROM per_all_people_f p
JOIN per_all_assignments_m a
ON a.person_id = p.person_id
AND TRUNC(SYSDATE) BETWEEN a.effective_start_date AND a.effective_end_date
AND a.assignment_type = 'E'
AND a.primary_flag = 'Y'
JOIN per_person_names_f n
ON n.person_id = p.person_id
AND n.name_type = 'GLOBAL'
AND TRUNC(SYSDATE) BETWEEN n.effective_start_date AND n.effective_end_date
JOIN ap_invoices_all ai
ON ai.created_by = p.person_id
AND ai.ledger_id = :p_ledger_id
AND ai.invoice_date >= TRUNC(SYSDATE) - 365
AND ai.invoice_date < TRUNC(SYSDATE)
AND ai.cancelled_date IS NULL
WHERE TRUNC(SYSDATE) BETWEEN p.effective_start_date AND p.effective_end_date
AND EXISTS (
SELECT 1
FROM ap_invoice_distributions_all aid
WHERE aid.invoice_id = ai.invoice_id
)
ORDER BY n.display_name, ai.invoice_date
The correlated COUNT(*) becomes EXISTS (short-circuits on first match), effective-date filters are added to all three PER tables, TRUNC() is moved off the indexed column, SELECT * is replaced with 9 specific columns, and ledger_id scopes the invoice scan to the correct legal entity.
Validate and Optimize SQL Before BI Publisher
The best optimization strategy is preventing inefficient SQL from reaching production BI Publisher reports in the first place. A typical optimization workflow looks like this:
FusionLens SQL connects directly to your Oracle Fusion Cloud connection. Explore tables and columns, discover join keys, and test SQL before moving it into BI Publisher.
Final Thoughts
Most slow Oracle Fusion BI Publisher reports are caused by SQL design problems - not Oracle infrastructure limitations. As the case study above shows, three targeted SQL changes reduced a 6-minute report to 18 seconds. Proper date-effective filtering, better joins, fewer columns, earlier filtering, and smarter parameter handling compound into dramatic performance gains.
The most effective Oracle Fusion teams treat SQL optimization as part of the report development lifecycle - not as a production firefighting exercise after reports become slow. Well-structured SQL makes BI Publisher faster, more scalable, easier to maintain, and significantly more reliable as Oracle Fusion data volumes grow.
Try FusionLens SQL to explore Oracle Fusion schemas, discover join paths, and build optimized SQL for your BI Publisher data models.