Blog / Performance
Performance

Oracle Fusion BI Publisher SQL Performance: 12 Ways to Make Reports Faster

May 27, 2026 16 min read
Back to Blog

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

Context
A vendor payment status report used by the Finance team ran across a 280,000-worker Oracle Fusion Cloud tenant. At month-end, the report consistently timed out at 6 minutes 12 seconds. The report joined AP invoice, payment, and HCM person tables to show who approved and processed each payment.

The Original SQL (Slow)

❌ Original - 6 min 12 sec
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

SELECT *
AP_INVOICES_ALL has 100+ columns. Pulling all of them increased XML generation and network transfer time by 8x.
No Effective-Date Filters
PER_ALL_PEOPLE_F and PER_PERSON_NAMES_F returned all historical rows - 892K and 1.1M rows respectively instead of ~14K current records.
TRUNC() on Indexed Column
TRUNC(ai.invoice_date) prevented the invoice_date index from being used, forcing a FULL TABLE SCAN across 4.2 million invoice rows.
No Ledger Filter
AP_INVOICES_ALL contained data across multiple ledgers. Without ledger_id, the query scanned all invoices across all legal entities.

Before / After Execution Plan

❌ Before - FULL TABLE SCAN on every table
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
✓ After - INDEX RANGE SCAN, 18 seconds
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

✓ Optimized - 18 seconds
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
Three changes, 20x improvement:
  • 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_id and 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 _F and _M tables
  • 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

1

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.

❌ Avoid
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
✓ Better
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.

2

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.

❌ Avoid
SELECT *
FROM   per_all_assignments_m
✓ Better
SELECT assignment_number,
       assignment_status_type,
       job_id,
       organization_id
FROM   per_all_assignments_m

Only return fields the report template actually uses.

3

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.

❌ Avoid
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
✓ Better
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
4

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.

❌ Avoid
WHERE TRUNC(ai.invoice_date) = TRUNC(SYSDATE)
✓ Better
WHERE ai.invoice_date >= TRUNC(SYSDATE)
AND   ai.invoice_date <  TRUNC(SYSDATE) + 1
5

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.

❌ Avoid
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
✓ Better
SELECT ph.segment1,
       s.vendor_name
FROM   po_headers_all ph
JOIN   ap_suppliers   s ON s.vendor_id = ph.vendor_id
6

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.

❌ Avoid
SELECT DISTINCT p.person_id
FROM   per_all_people_f      p
JOIN   per_all_assignments_m a ON a.person_id = p.person_id
✓ Better
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
)
7

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.

8

Use Correct Parameter Data Types

Implicit type conversions can block index usage. Always use explicit date formats for BI Publisher parameters.

❌ Avoid - if :p_date is passed as text
WHERE invoice_date = :p_date
✓ Better
WHERE invoice_date >= TO_DATE(:p_from_date, 'YYYY-MM-DD')
AND   invoice_date <  TO_DATE(:p_to_date,   'YYYY-MM-DD') + 1
9

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.

10

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
11

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.

❌ Avoid - when template already handles the sort
SELECT ... FROM ... WHERE ...
ORDER BY n.display_name
12

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.

Effective-Dated HCM Tables
HCM queries frequently join 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.
Large ERP Transaction Tables
Finance and Procurement tables can contain millions of rows in production environments - 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.
Multi-Org Data
Tables ending with _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.

❌ Bad Query - five anti-patterns combined
-- 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
1 - SELECT *
Pulls 100+ columns from AP_INVOICES_ALL and dozens from PER tables. Only a handful are used in the report template.
2 - No Effective Dates
Returns all historical rows from three PER tables - easily 10–50x more rows than current records alone.
3 - TRUNC() on Filter Column
TRUNC(ai.invoice_date) forces Oracle to compute TRUNC for every row - blocking the invoice_date index entirely.
4 - Correlated Subquery
The COUNT(*) subquery runs once per invoice row. On 50K invoices that is 50K additional index lookups inside the subquery.
5 - No Ledger / Org Filter
AP_INVOICES_ALL contains data across all legal entities. Without ledger_id the query scans invoices the user should never see.
✓ Optimized - all five issues addressed
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:

1
Explore Oracle Fusion metadata - understand table structures, column types, and naming conventions before writing queries
2
Identify correct join paths - confirm which ID columns link tables together across HCM, Finance, and Procurement
3
Validate row counts incrementally - add one JOIN at a time and check that counts do not unexpectedly multiply
4
Test filters and parameters - run the query with realistic bind parameter values before finalizing
5
Optimize query structure - apply the 12 techniques above and verify performance improvement
6
Move finalized SQL into BI Publisher - deploy only validated, optimized queries into the data model

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.

📋
BI Publisher Ready
Test and tune your BI Publisher SQL data models directly against Oracle Fusion — before deploying to production.
SQL Autocomplete
Autocomplete for Oracle Fusion tables and columns — so you can write and optimize SQL without leaving the editor.
📊
SQL History
Track SQL iterations as you tune BI Publisher data models - compare which changes reduced row counts and improved report speed.

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.