Indu Learning Platform
Course/Unit 4 · Data engineering for master data
Lesson 4-4

SQL for master data

Join BP header, address and tax tables, find duplicates with window functions, write data-quality queries, and see how this maps to HANA SQL and CDS views.

Watch firstSQL Window Function | How to write SQL Query using RANK, DENSE RANK, LEAD/LAG | SQL Queries Tutorial · techTFQOpen on YouTube ↗

The BP data model as tables

Partner, addresses, tax numbers and roles are separate tables joined by keys. This is a simplified teaching subset; check real structures in SE11 or ADT before relying on a field.

Table (key)HoldsBUT000 (CLIENT, PARTNER)BUT020 (PARTNER, ADDRNUMBER)ADRC (ADDRNUMBER, DATE_FROM,NATION)DFKKBPTAXNUM (PARTNER, TAXTYPE)BUT100 (PARTNER, RLTYP)BP header: type, grouping,NAME_ORG1, BU_SORT1Which addresses belong to theBPStreet, CITY1, POST_CODE1,REGION, COUNTRYTax numbers, e.g. GSTIN underan IN tax typeBP roles, e.g. FLVN00 / FLVN01supplierPARTNER → ADDRNUMBERvia ADDRNUMBERvia PARTNERvia PARTNER
BUT020 is the bridge between the partner and the central address table ADRC.

Joins: keep the gaps visible

For data quality use LEFT JOIN: 'partner without address' is exactly the finding you want, and an inner join hides it. Two SAP traps: on a raw schema, join and filter on CLIENT; and ADRC holds history and international versions, so filter on current validity and nation = ' ' or rows multiply.

INNER JOINOnly BPs with an address and tax IDRows silently disappearGood for clean reportingBad for finding gapsLEFT JOINEvery BP is keptMissing parts show as NULLNULLs become DQ findingsDefault choice for profilingvs
For data quality, a missing match is information.
SQL
SELECT b.partner, b.name_org1, a.city1, a.post_code1, a.country,
       t.taxnum AS gstin
FROM but000 b
LEFT JOIN but020 l
       ON l.client = b.client AND l.partner = b.partner
LEFT JOIN adrc a
       ON a.client = l.client AND a.addrnumber = l.addrnumber
      AND a.nation = ' '                 -- default version
      AND a.date_to >= CURRENT_DATE      -- currently valid
LEFT JOIN dfkkbptaxnum t
       ON t.client = b.client AND t.partner = b.partner
      AND t.taxtype = 'IN3'              -- check the GST tax type in your system
WHERE b.client = '100'
  AND b.type = '2';                      -- organisations

On HANA, unquoted identifiers are upper-cased, so but000 resolves to BUT000.

Window functions: duplicates without losing detail

GROUP BY ... HAVING COUNT(*) > 1 collapses rows, so you lose which partners are involved. COUNT(*) OVER (PARTITION BY taxnum) writes the group size onto every row; ROW_NUMBER() OVER (PARTITION BY taxnum ORDER BY crdat) numbers rows within the group, and row 1 is the survivor candidate. HANA, PostgreSQL, SQL Server, Oracle and SQLite all support this.

GSTIN (fake)PartnerCreatedrnProposal27ABCDE1234F1Z510002312014-03-021SURVIVOR27ABCDE1234F1Z510044102018-11-202MERGE27ABCDE1234F1Z510077882022-06-143MERGE29PQRSX5678K1Z210020172016-01-091SURVIVOR29PQRSX5678K1Z210061202021-08-302MERGE
Every row kept, grouped by tax ID, with a survivor proposal per group.

Build the duplicate query step by step

Step through · 1 of 6
Base rowsBUT000 + taxNormalise keyUPPER / TRIMGroup sizeCOUNT OVEROrder in groupROW_NUMBERFilter groupsdup_count > 1ProposalCASE rn = 1
Start with a CTE returning one row per partner and tax number. Compare the row count with BUT000 to be sure the join does not multiply rows.
Click Next to build the query clause by clause.

Try it: write the duplicate query

Try it · Fill the blanks

Fill the blanks so the query flags duplicate GSTINs and keeps the oldest partner.

WITH flagged AS (
  SELECT partner, taxnum, crdat,
    COUNT(*)  (PARTITION BY taxnum) AS dup_count,
    ROW_NUMBER() OVER ( taxnum  crdat) AS rn
  FROM bp
)
SELECT * FROM flagged
 dup_count > 1;

Try it: pick the window function

Try it · Match

Match each window function to its master data use.

Pick an item on the left, then its match on the right.

A data-quality query pack

Keep one query per rule, each returning a rule ID and a count, combined with UNION ALL over a shared base CTE (the LEFT JOIN above). Run it after every load and you get a report-ready table, using the same rule IDs as your Python profile. SAP often stores empty fields as spaces or empty strings, so test for both, not just NULL.

DQ-10 no address214DQ-11 PIN not 6 digits1204DQ-12 no GSTIN3870DQ-13 GSTIN wrong length640DQ-14 no region955
Synthetic DQ pack result after mock 2. One row per rule is all a status report needs.

From HANA SQL to CDS views to OData

In S/4HANA, application logic does not query raw tables with native SQL. It is modelled in CDS view entities, which handle the client automatically and build on released views like I_BusinessPartner and I_Supplier. A CDS view can be exposed as OData, which is how the API in lesson 4-3 gets its data. Prototype in SQL, productise in CDS.

OData service / Fiori / AI agentconsumesConsumption CDS viewDQ flags, parametersReleased CDS viewsI_BusinessPartner, I_SupplierHANA SQL enginejoins, window functionsTablesBUT000, BUT020, ADRC, DFKKBPTAXNUM
Same logic, different homes.

Try it: where should the logic live?

Try it · What would you do?

Your duplicate-GSTIN query works in the sandbox. Now a Fiori app and an AI agent in S/4HANA both need it. Where do you put it?

Remember

  • BUT000 → BUT020 → ADRC is the BP-to-address path; use LEFT JOINs, include the client on raw schemas, and filter ADRC to the current default version.
  • COUNT OVER and ROW_NUMBER find duplicate groups and propose a survivor while keeping every row.
  • Prototype in SQL, model in CDS view entities, expose via OData, and never change SAP tables directly.

Check yourself

You want to list every organisation BP, including those with no address. Which join from BUT000 to BUT020/ADRC?
What does COUNT(*) OVER (PARTITION BY taxnum) add to each row?
Why filter ADRC on NATION = ' ' and current validity?
In S/4HANA, where should reusable data-quality logic for consumers like Fiori apps and APIs normally live?