TL;DR
SQL is the foundation for both Oracle Database (Callidus Commissions) and SAP HANA Database (SAP SuccessFactors IM) — master the core before going platform-specific.
JOINs, GROUP BY, CTEs, and window functions are the four constructs you'll use constantly in ICM reporting and migration work.
Syntax differences between Oracle Database and SAP HANA Database are minor for standard SQL — the significant divergence is in procedural extensions (PL/SQL vs HANA SQL Script).

Before you write a single Oracle PL/SQL procedure for Callidus Commissions or a SAP HANA Database calculation view for SAP SuccessFactors Incentive Management, you need SQL. Not surface-level SQL — the kind where you can explain why a LEFT JOIN returns NULLs, when to use a CTE vs a subquery, and what a window function is actually doing to your result set. This article builds that foundation with examples drawn directly from the ICM world.

What SQL Is (and Isn't)

SQL — Structured Query Language — is the standard language for relational databases. It is not a programming language in the traditional sense. You describe what data you want, not how to retrieve it. The database engine figures out the "how" via its query optimizer.

Both Oracle Database (which underpins Callidus Commissions) and SAP HANA Database (which underpins SAP SuccessFactors Incentive Management) implement SQL with minor dialect differences. Most of what you learn here applies to both. Where they diverge, I'll call it out.

The SELECT Statement

Everything starts here. The anatomy of a SELECT:

SQL — works on both Oracle Database and SAP HANA Database
SELECT   p.NAME        AS PARTICIPANT_NAME,
         p.POSITION_ID,
         r.PERIOD_ID,
         r.RESULT_AMOUNT
FROM     CSC_PARTICIPANT p        -- table alias: p
JOIN     CSC_RESULTS    r         -- table alias: r
         ON r.PARTICIPANT_ID = p.ID
WHERE    r.PERIOD_ID = '2026-Q1'
  AND  r.RESULT_AMOUNT > 0
ORDER BY p.NAME ASC;

The execution order matters: FROM → JOIN → WHERE → SELECT → ORDER BY. The database processes your FROM and JOINs first, then filters with WHERE, then projects the columns you asked for in SELECT. This is why you cannot reference a SELECT alias in a WHERE clause — WHERE runs before SELECT.

JOINs: The Most Misunderstood Concept

JOINs combine rows from two tables based on a matching condition. Getting them wrong produces either missing rows (when you needed a LEFT JOIN) or inflated row counts (when you have a cartesian product).

SQL — INNER JOIN vs LEFT JOIN comparison
-- INNER JOIN: only participants with results returned
SELECT p.NAME, r.RESULT_AMOUNT
FROM   CSC_PARTICIPANT p
JOIN   CSC_RESULTS    r ON r.PARTICIPANT_ID = p.ID;

-- LEFT JOIN: ALL participants returned, NULL result for those with no results
SELECT p.NAME,
       COALESCE(r.RESULT_AMOUNT, 0) AS RESULT_AMOUNT
FROM   CSC_PARTICIPANT p
LEFT JOIN CSC_RESULTS r ON r.PARTICIPANT_ID = p.ID;
⚠️In ICM reporting, always ask: "Should a participant with zero results appear in this report?" If yes, use LEFT JOIN. If you use INNER JOIN, they silently disappear — which is how compensation disputes start.

GROUP BY and Aggregate Functions

GROUP BY collapses multiple rows into summary rows. Every column in your SELECT must either be in GROUP BY or wrapped in an aggregate function.

SQL — GROUP BY with aggregates
SELECT
  p.NAME,
  r.PERIOD_ID,
  SUM(r.RESULT_AMOUNT)   AS TOTAL_EARNINGS,
  COUNT(r.ID)            AS TRANSACTION_COUNT,
  AVG(r.RESULT_AMOUNT)   AS AVG_EARNING,
  MAX(r.RESULT_AMOUNT)   AS HIGHEST_SINGLE_EARNING
FROM   CSC_RESULTS r
JOIN   CSC_PARTICIPANT p ON p.ID = r.PARTICIPANT_ID
GROUP BY p.NAME, r.PERIOD_ID
HAVING SUM(r.RESULT_AMOUNT) > 10000  -- filter on aggregated result
ORDER BY TOTAL_EARNINGS DESC;

HAVING filters after aggregation. WHERE filters before. Use HAVING when your filter condition involves an aggregate function.

CTEs: Making Complex Queries Readable

A Common Table Expression (CTE) is a named temporary result set defined at the top of your query with WITH. They make complex queries readable and maintainable — critical when you're writing a 60-line ICM reporting query that three people need to understand and modify.

SQL — CTE example (works on Oracle Database and SAP HANA Database)
WITH
-- Step 1: get quota targets
quota_base AS (
  SELECT PARTICIPANT_ID, PERIOD_ID,
         SUM(QUOTA_AMOUNT) AS TOTAL_QUOTA
  FROM   CSC_QUOTA
  GROUP BY PARTICIPANT_ID, PERIOD_ID
),
-- Step 2: get actual results
results_base AS (
  SELECT PARTICIPANT_ID, PERIOD_ID,
         SUM(RESULT_AMOUNT) AS TOTAL_EARNED
  FROM   CSC_RESULTS
  GROUP BY PARTICIPANT_ID, PERIOD_ID
)
-- Step 3: join and calculate attainment
SELECT
  p.NAME,
  r.PERIOD_ID,
  r.TOTAL_EARNED,
  q.TOTAL_QUOTA,
  ROUND(r.TOTAL_EARNED / NULLIF(q.TOTAL_QUOTA,0) * 100, 2) AS ATTAINMENT_PCT
FROM   results_base r
JOIN   quota_base   q ON q.PARTICIPANT_ID = r.PARTICIPANT_ID
                      AND q.PERIOD_ID     = r.PERIOD_ID
JOIN   CSC_PARTICIPANT p ON p.ID = r.PARTICIPANT_ID
ORDER BY ATTAINMENT_PCT DESC;

Window Functions: Analytics Without Collapsing Rows

Window functions perform calculations across a set of rows related to the current row — without GROUP BY collapsing the result. This is your primary tool for ranking, running totals, and period-over-period comparisons in ICM reporting.

SQL — Window functions (works on both Oracle Database and SAP HANA Database)
SELECT
  p.NAME,
  r.PERIOD_ID,
  r.RESULT_AMOUNT,
  -- Rank within each period (1 = highest earner)
  RANK() OVER (
    PARTITION BY r.PERIOD_ID
    ORDER BY r.RESULT_AMOUNT DESC
  ) AS EARNINGS_RANK,
  -- Running total of earnings within participant across periods
  SUM(r.RESULT_AMOUNT) OVER (
    PARTITION BY p.ID
    ORDER BY r.PERIOD_ID
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS RUNNING_TOTAL
FROM   CSC_RESULTS r
JOIN   CSC_PARTICIPANT p ON p.ID = r.PARTICIPANT_ID
ORDER BY r.PERIOD_ID, EARNINGS_RANK;

Key Syntax Differences: Oracle Database vs SAP HANA Database

OperationOracle DatabaseSAP HANA Database
String concatenation|| operator|| or CONCAT()
Null handlingNVL(col, val)IFNULL(col, val) or COALESCE()
Date arithmetic (days)date1 - date2DAYS_BETWEEN(date1, date2)
Get current dateSYSDATECURRENT_DATE
Top N rowsWHERE ROWNUM <= NLIMIT N
Hierarchical queriesCONNECT BY PRIORRecursive CTE with WITH ... UNION ALL
Dual tableSELECT 1 FROM DUALSELECT 1 FROM DUMMY
💡For migration work: The CONNECT BY PRIOR → recursive CTE conversion is the most common SQL rewrite you'll encounter moving from Callidus Commissions (Oracle Database) to SAP SuccessFactors IM (SAP HANA Database). We cover this in detail in Article 04.

What Comes Next

This is your foundation. The next two articles go platform-specific: Oracle Database and PL/SQL for Callidus Commissions work (Article 02), and SAP HANA Database SQL with its columnar architecture advantages for SAP SuccessFactors IM (Article 03). Article 04 then puts both side-by-side for migration teams.