If you've come from standard SQL and tried to write a join in OBIEE Logical SQL, you've probably hit this immediately:
-- This is valid SQL โ but it WILL NOT work in OBIEE Logical SQL SELECT w."Worker Name", a."Grade Name" FROM "Workforce Management"."Workers" w LEFT JOIN "Workforce Management"."Assignments" a ON w."Person Number" = a."Person Number"
The BI Server rejects this with an error similar to:
nQSError: [nQSError: 26012] โ Unrecognized token 'LEFT' at position X. OR: Syntax error near 'JOIN'.
This is not a bug. OBIEE Logical SQL is a semantic query language, not standard SQL. It was designed to query the Oracle BI metadata layer (the RPD / semantic model), not a physical database. The join conditions are defined in the RPD โ not in your query. When you write Logical SQL, you are telling the BI Server what you want, and it decides how to join.
Understanding this is the key to writing correct Logical SQL. The Oracle BI Server architecture has three layers:
| Layer | Purpose | Where Joins Live |
|---|---|---|
| Physical Layer | Actual database tables and connections | Physical join definitions with ON conditions |
| Business Model (BMM) | Logical tables, measures, and dimensions | Logical join paths and cardinality |
| Presentation Layer | Subject areas and folders users query | No joins โ exposes columns from BMM |
When you write a Logical SQL query, the BI Server:
JOIN ... ON clausesYou never see step 4. The join happens automatically. This is why LEFT JOIN ... ON in your Logical SQL is rejected โ you're trying to define the join, but that's the BI Server's job.
Practical implication: If two columns from different presentation tables don't return meaningful results together, the issue is in the RPD join path โ not in your query syntax. You can't fix it by rewriting the query.
In OBIEE Logical SQL, you join tables by listing them in the FROM clause separated by commas. That's it. The BI Server applies the join path from the RPD.
SELECT w."Worker Name", a."Grade Name", a."Assignment Status" FROM "Workers" w LEFT JOIN "Assignments" a ON w."Person Number" = a."Person Number"
SELECT "Worker"."Worker Name", "Grade"."Grade Name", "Assignment"."Assignment Status" FROM "Workforce Management" ORDER BY "Worker"."Worker Name"
Notice: in Logical SQL you reference the subject area in the FROM clause (e.g., "Workforce Management"), not individual tables. The column references use the folder and column name path. The BI Server resolves the physical join between Worker, Grade, and Assignment through the RPD.
Comma join gotcha: If you list multiple subject area folders in FROM without a valid join path between them, you get a cartesian product โ thousands of duplicated rows. See our guide on the OTBI comma join problem for the full diagnosis.
This is the question that bites every Oracle HCM consultant: "I need all workers, even those without a grade or position assigned โ how do I LEFT JOIN in OBIEE?"
The short answer: you don't control the join type in Logical SQL. Whether the join is inner or outer is determined by the BMM layer join definition. But there are three practical approaches:
If you need outer join behavior, the join in the Business Model layer should be defined as an outer join. In OBIEE Administration Tool (or OAC Semantic Modeler):
1..N on the fact side, 0..1 on the dimension sideLEFT OUTER JOIN in the physical SQL automaticallyThis is the correct architectural fix. If you can't modify the RPD, use the workarounds below.
For measure columns, the FILTER() function is the Logical SQL equivalent of a conditional aggregate. It handles cases where you want to count or sum only when a dimension value exists:
SELECT "Worker"."Department Name", COUNT("Worker"."Worker ID") AS "Total Workers", FILTER(COUNT("Worker"."Worker ID") USING "Grade"."Grade Name" IS NOT NULL) AS "Workers with Grade", FILTER(COUNT("Worker"."Worker ID") USING "Grade"."Grade Name" IS NULL) AS "Workers Without Grade" FROM "Workforce Management"
When you need a true outer join across subject areas or with data that doesn't join cleanly in the RPD, use BI Publisher with two separate OTBI queries as datasets. Join them in the data model on PERSON_NUMBER or ASSIGNMENT_ID. This gives you full SQL join control in the BIP layer.
The FILTER() function is the most powerful Logical SQL tool that most OTBI users miss. It wraps an aggregate function with a USING condition:
FILTER(aggregate_expression USING filter_condition)
The USING clause is a Logical SQL filter expression โ same syntax as a WHERE clause predicate but applied to a single measure. This lets you compute multiple conditional counts in a single query pass without subqueries:
SELECT "Worker"."Business Unit Name", COUNT("Worker"."Worker ID") AS "Total HC", FILTER(COUNT("Worker"."Worker ID") USING "Worker"."Employment Category" = 'FULL_TIME') AS "FT Headcount", FILTER(COUNT("Worker"."Worker ID") USING "Worker"."Employment Category" = 'PART_TIME') AS "PT Headcount", FILTER(COUNT("Worker"."Worker ID") USING "Worker"."Assignment Status Type" IN ('ACTIVE_PROCESS', 'ACTIVE_NO_PROCESS')) AS "Active HC" FROM "Workforce Management" ORDER BY "Worker"."Business Unit Name"
Performance note: A single query with multiple FILTER() expressions is more efficient than running three separate OTBI analyses and combining in a dashboard. The BI Server makes one database pass and computes all conditions simultaneously.
SELECT "Worker"."Person Number", "Worker"."Worker Name", "Assignment"."Assignment Number", "Grade"."Grade Name", "Job"."Job Name" FROM "Workforce Management" WHERE "Assignment"."Primary Assignment Flag" = 'Y' AND "Assignment"."Assignment Status Type" = 'ACTIVE_PROCESS' ORDER BY "Worker"."Worker Name"
The join between Worker, Assignment, Grade, and Job is resolved by the RPD. You never write it.
SELECT "Worker"."Department Name", COUNT(DISTINCT "Worker"."Person Number") AS "Headcount", FILTER( COUNT(DISTINCT "Worker"."Person Number") USING "Absence Entry"."Absence Status" = 'APPROVED' ) AS "Workers on Approved Absence" FROM "Absence Management" WHERE "Absence Entry"."Absence Start Date" BETWEEN TIMESTAMPADD(SQL_TSI_MONTH, -1, CURRENT_DATE) AND CURRENT_DATE ORDER BY "Worker"."Department Name"
SELECT "Worker"."Worker Name", "Assignment"."Grade Name", "Salary"."Annual Salary", "Grade Rate"."Grade Midpoint", CASE WHEN "Salary"."Annual Salary" < "Grade Rate"."Grade Minimum" THEN 'Below Range' WHEN "Salary"."Annual Salary" > "Grade Rate"."Grade Maximum" THEN 'Above Range' ELSE 'In Range' END AS "Compa Status" FROM "Compensation" WHERE "Assignment"."Primary Assignment Flag" = 'Y' ORDER BY "Compa Status", "Worker"."Worker Name"
SELECT "Worker"."Department Name", FILTER( COUNT(DISTINCT "Worker"."Person Number") USING "Worker"."Action Name" = 'Hire' AND "Worker"."Action Date" >= TIMESTAMPADD(SQL_TSI_YEAR, -1, CURRENT_DATE) ) AS "YTD Hires", FILTER( COUNT(DISTINCT "Worker"."Person Number") USING "Worker"."Action Name" = 'Termination' AND "Worker"."Action Date" >= TIMESTAMPADD(SQL_TSI_YEAR, -1, CURRENT_DATE) ) AS "YTD Terminations" FROM "Workforce Management" ORDER BY "Worker"."Department Name"
SELECT "Payroll"."Cost Center", "Payroll"."Payroll Name", SUM("Payroll Results"."Result Value") AS "Total Payroll", FILTER( SUM("Payroll Results"."Result Value") USING "Element"."Classification Name" = 'Standard Earnings' ) AS "Regular Pay", FILTER( SUM("Payroll Results"."Result Value") USING "Element"."Classification Name" = 'Supplemental Earnings' ) AS "Supplemental Pay" FROM "Payroll" WHERE "Payroll Run"."Period Status" = 'COMPLETE' ORDER BY "Payroll"."Cost Center"
| Error | Cause | Fix |
|---|---|---|
nQSError: 26012 โ Unrecognized token 'LEFT' |
Using standard SQL JOIN syntax in Logical SQL | Remove JOIN...ON. List columns from the subject area; the BI Server resolves joins via RPD. |
nQSError: 14025 โ No fact table at requested level |
Columns from two unrelated subject areas with no join path in RPD | Use a single subject area that spans both, or use BIP with two datasets. |
| Cartesian product โ row count explodes | Comma-separated subject area folders with no shared grain | Remove one folder reference; use FILTER() instead of separate columns. |
nQSError: 27002 โ Syntax error near comma |
Comma-join attempt between incompatible presentation tables | Use a single subject area FROM clause. Joins between subject areas must be via BIP. |
| Results missing workers with no assignment | RPD join defined as inner join, excluding workers with no matching assignment row | Change the BMM join to outer join in RPD, or filter using FILTER(... USING column IS NULL). |
OBIEE Logical SQL is powerful, but it has a ceiling. Use BI Publisher (BIP) when:
| Scenario | Use OTBI? | Use BIP? |
|---|---|---|
| Standard headcount with grade and job | โ Yes | Not needed |
| FILTER() conditional aggregation | โ Yes | Not needed |
| Left outer join across two subject areas | โ No | โ Two datasets + link |
| ROW_NUMBER() or RANK() window functions | โ Not supported | โ Full physical SQL |
| Payroll actuals with element-type split | โ With FILTER() | Either |
| Cross-module compliance report (HCM + Payroll + Benefits) | Limited | โ Multiple datasets |
See also: OTBI vs BI Publisher โ When to Use Which for a full decision guide with examples.
No. OBIEE Logical SQL FROM takes a subject area name in quotes, not table aliases. Column references use "Folder"."Column" path notation. You cannot assign table aliases with AS in the FROM clause.
OBIEE Logical SQL supports a limited JOIN syntax for multi-fact queries (where you explicitly join two fact sources), but it is rarely used and has significant restrictions. For most HCM reports, the implicit join through the subject area is correct and preferred.
If the two presentation folders resolve to logical tables with a one-to-many join, adding the "many" side column fans out the results. This is the comma-join issue โ the BI Server is correctly applying the join, but the grain of your query changed. Add COUNT(DISTINCT ...) or aggregate, or remove the low-grain column.
Yes โ inline views and scalar subqueries are supported with restrictions. See our guide on OBIEE and OTBI Subquery Joins for syntax, nQSError fixes, and production examples.
Production-ready patterns for headcount, compensation, absence, recruiting, and payroll โ each with the correct subject area, FILTER() usage, and date-effective join notes. Used by Oracle HCM consultants billing $150โ$350/hr.
Get the OTBI Template Pack ($97) → EEO-1, ACA & FLSA SQL Pack ($499) →