Query Service lets you run SQL directly on AEP datasets. You use it to validate ingestion, check identity and consent coverage, find duplicates, and build derived datasets for audiences and reporting. Open it in AEP: Data management > Queries > Create query.
0 of 0 done
Modules
Module 1
SELECT basics
Concept
SELECT picks columns, FROM picks the dataset (table), LIMIT caps rows. Each AEP dataset is queryable as a table.
Where it's used
First look at newly ingested data in the Dev sandbox.
SELECT *
FROM crm_profiles
LIMIT 10;
Practice: Run this on your own CRM or web events dataset and note 3 fields you recognise from the client JSON.
Screenshot Query editor with resultsAdd screenshots/sql/module-01.pngModule 2
Filtering & sorting
Concept
WHERE filters rows; AND/OR combine conditions; IS NULL finds missing values; ORDER BY sorts.
Where it's used
Find records missing required fields before enabling Profile.
SELECT _tenantId.crmId, personalEmail.address
FROM crm_profiles
WHERE personalEmail.address IS NULL
ORDER BY _tenantId.crmId;
Practice: List every profile with no email. How many are there?
Screenshot Result showing records with null emailAdd screenshots/sql/module-02.pngModule 3
Nested XDM fields
Concept
XDM data is nested. Use dot notation to reach inner fields, e.g. person.name.firstName or your custom _tenantId fields.
Where it's used
Reading the same structure you designed in the schema editor.
SELECT _tenantId.crmId AS crm_id,
person.name.firstName AS first_name,
person.name.lastName AS last_name
FROM crm_profiles;
Practice: Write a query returning crmId, first name, last name and email from your sample data.
Screenshot Query with aliased nested fieldsAdd screenshots/sql/module-03.pngModule 4
Aggregates & GROUP BY
Concept
COUNT, SUM, AVG, MIN, MAX summarise rows; GROUP BY makes one row per group; HAVING filters groups.
Where it's used
Ingestion QC: event counts per type per day vs. the source system.
SELECT eventType, COUNT(*) AS events
FROM web_events
WHERE timestamp >= current_date - INTERVAL 7 DAY
GROUP BY eventType
ORDER BY events DESC;
Practice: Count events per day for the last 7 days and compare with the source total.
Screenshot Grouped counts per event typeAdd screenshots/sql/module-04.pngModule 5
Data-quality checks
Concept
Use CASE inside SUM to count conditions; GROUP BY + HAVING COUNT(*) > 1 finds duplicates.
Where it's used
QC checklist: null rates, duplicate IDs, consent coverage before activation.
SELECT COUNT(*) AS total,
SUM(CASE WHEN personalEmail.address IS NULL
THEN 1 ELSE 0 END) AS missing_email
FROM crm_profiles;
SELECT _tenantId.crmId, COUNT(*)
FROM crm_profiles
GROUP BY _tenantId.crmId
HAVING COUNT(*) > 1;
Practice: Calculate the % of profiles missing email and list any duplicate crmIds.
Screenshot Null-rate and duplicate resultsAdd screenshots/sql/module-05.pngModule 6
Joins
Concept
INNER JOIN keeps matches in both tables; LEFT JOIN keeps all rows from the left table even with no match.
Where it's used
Link purchases to profiles to check identity keys line up across datasets.
SELECT p._tenantId.crmId,
COUNT(e._id) AS purchases
FROM crm_profiles p
LEFT JOIN purchase_events e
ON p._tenantId.crmId = e._tenantId.crmId
GROUP BY p._tenantId.crmId;
Practice: Find customers with zero purchases. Which join type do you need, and why?
Screenshot Join result with purchase countsAdd screenshots/sql/module-06.pngModule 7
Window functions
Concept
ROW_NUMBER() OVER (PARTITION BY … ORDER BY …) ranks rows within a group without collapsing them.
Where it's used
Get each customer’s latest event, or first purchase, for segmentation.
SELECT * FROM (
SELECT _tenantId.crmId, eventType, timestamp,
ROW_NUMBER() OVER (PARTITION BY _tenantId.crmId
ORDER BY timestamp DESC) AS rn
FROM web_events) t
WHERE rn = 1;
Practice: Return each customer’s most recent event type and time.
CREATE TABLE … AS SELECT saves query output as a new dataset; scheduled queries refresh it.
Where it's used
Build derived attributes (e.g. lifetime orders) for audiences and dashboards.
CREATE TABLE customer_order_summary AS
SELECT _tenantId.crmId AS crm_id,
COUNT(*) AS lifetime_orders
FROM purchase_events
GROUP BY _tenantId.crmId;
Practice: Create a summary dataset and find it under Data management > Datasets.
Screenshot New dataset created from queryAdd screenshots/sql/module-08.png
Must know
Query Service reads AEP data lake datasets; each dataset is queried like a table.
XDM is nested, so most queries use dot notation (person.name.firstName, _tenantId.crmId).
Validation queries (counts, nulls, duplicates, timestamps) are part of every ingestion QC.
CTAS (CREATE TABLE AS SELECT) writes results to a new dataset for audiences and reporting.
Filter on timestamp in large event datasets; scanning everything is slow.
Interview questions
From your board. Answer out loud before you reveal.
What is the difference between WHERE and HAVING?
WHERE filters rows before grouping; HAVING filters groups after aggregation, e.g. HAVING COUNT(*) > 1 for duplicates.
INNER JOIN vs LEFT JOIN?
INNER returns only matching rows; LEFT returns all left-table rows with NULLs where no match exists — useful for finding profiles with no events.
How would you validate an AEP ingestion with SQL?
Compare row counts with the source, check null rates on required and identity fields, look for duplicate IDs, and check timestamps fall in the expected range.
How do you query nested XDM fields?
With dot notation along the schema path, e.g. person.name.firstName or _tenantId.crmId for custom fields.
What is a window function and when would you use one?
It calculates across related rows without collapsing them — e.g. ROW_NUMBER to get each customer’s latest event.
What is CTAS used for in Query Service?
CREATE TABLE AS SELECT writes results to a new dataset, often scheduled, to build derived attributes for segmentation or reporting.