← Real-time Analytics

Level 2 · Module 05

Analyzing Data in Apache Doris

Move from scalar transformations to aggregates and table functions.
90 minCourse availableLab availableQuiz available
Course informationValue
CourseReal-time Analytics with Apache Doris · Level 2
DorisApache Doris 4.1.3
EnvironmentThe course sandbox and events_modelled from Lab 4
Estimated timeAbout 90 minutes: reading 50, Lab 30, quiz 10

Course README · Open Lab 5 · Open Quiz 5

Module goal

Turn event detail into an analytical result whose meaning you can defend: define the result first, then choose functions by the work they do to rows, and build the query in stages whose grain stays visible.

An operations team wants one row per date and region, with event volume, active users, purchasing users, purchase revenue, the previous day's revenue, cumulative revenue, and the region's rank on that date. Lab 5 builds that report from events_modelled, the event table of Module 4.

Learning objectives

After completing this module, you will be able to:

  1. Choose a function category, such as scalar, aggregate, window, Table Function, or TVF, from its inputs, outputs, and row-count behavior.
  2. Define the population of each measure with conditional aggregates, and filter input rows with WHERE and groups with HAVING.
  3. Use ANY_VALUE only when any representative value satisfies the meaning of the group.
  4. Name each stage of an analysis with a CTE and state the grain of its result.
  5. Choose window partitions, ordering, frames, and ranking functions for previous-value, cumulative, and ranking questions.
  6. Choose between built-in functions, Lambda expressions, Alias Functions, and user-defined functions for missing reusable logic.

Module structure

SectionTimeOutcome
5.1 Define the Analytical Answer Before Choosing Functions6 minWrite the population, grain, measures, and ordering of a result
5.2 Choose a Function Category from the Required Work6 minClassify functions by input, output, row count, and dependency
5.3 Use Scalar Functions to Derive Explainable Dimensions5 minDerive dates, hours, and labels while keeping each event row
5.4 Use Aggregate Functions to Answer Defined Metric Questions9 minDefine each measure's population and filter at the right stage
5.5 Use a Table Function to Expand Values into Rows5 minTell a Table Function from a TVF, and reassess the grain after expansion
5.6 Use CTEs to Express the Stages of a Complex Analysis6 minGive each stage of a query one responsibility and one grain
5.7 Use Window Functions to Compare Rows Without Losing Their Grain8 minChoose partitions, ordering, frames, and tie behavior
5.8 Choose the Right Extension for Missing Reusable Logic5 minChoose built-in functions first, then Lambdas, Alias Functions, or UDFs
Lab 530 minBuild the daily-region report in stages and read each stage's grain
Quiz 510 minSix questions on these objectives

5.1 Define the Analytical Answer Before Choosing Functions

“Show regional performance” is not yet a query contract. It leaves several questions unresolved:

  • Which events belong to the report?
  • What does one output row represent?
  • Which events contribute to each metric?
  • Should dates or regions with no qualifying events appear?
  • What sequence defines “previous,” “cumulative,” and “rank”?
  • Is the result for interactive inspection or for a downstream file workflow?

Resolve these choices before selecting function names. For the scenario in this module, one possible contract is:

DecisionRequirement
Input populationEvents from March 1 inclusive to March 4 exclusive
Result grainOne row per event_date and region
Activity measuresAll events and distinct users in each daily-region group
Purchase measuresDistinct purchasing users and purchase revenue in the same group
Time comparisonPrevious observed daily revenue and cumulative revenue within each region
Cross-region comparisonRevenue rank among regions on the same date
Final orderingDate, rank, then region for a stable display

This contract prevents superficially plausible mistakes. Grouping only by date would remove the region grain. Filtering to purchases before calculating event_count would turn that column into a purchase-event count. Ranking all 24 rows together would answer a different question from ranking eight regions independently on each date.

Trace the report grain through the query

The query can be understood as a sequence of row shapes:

The report grain from event-detail rows through filtering, grouping, windows, and ordering

Trace the meaning and row shape of the result through every analytical stage.

Filtering can remove rows. Grouping usually reduces many detail rows to fewer grouped rows. Window calculation retains the rows it receives and adds a value to each one. Final ordering changes their presentation sequence, not their grain.

Separate result meaning from result delivery

A MySQL-compatible client and a Jupyter Notebook can display the same SQL result in different forms. In the MySQL command-line client, \G requests a vertical display; it is a client command terminator, not part of a query sent through the Notebook helper. Column aliases can improve readability, but they do not change which rows or values the query computes.

When another system needs files, Doris supports SELECT INTO OUTFILE with formats such as comma-separated values (CSV), Parquet, and Optimized Row Columnar (ORC). Export destination, format, permissions, and credentials form a delivery contract. They do not repair an incorrectly defined metric or grain. Lab 5 displays results and performs no export. See SELECT INTO OUTFILE.

5.2 Choose a Function Category from the Required Work

The Doris SQL function catalog is easier to navigate when you first ask what the operation consumes, what it returns, and what happens to the number of rows. The categories are not merely lists of names.

Function categoryMain input and output relationshipSuitable scenarioRepresentative form
Scalar FunctionArguments for one input row produce one valueDerive a date or normalize a labelTO_DATE(event_time)
Aggregate FunctionValues from multiple rows produce one value for a groupCalculate regional revenueSUM(revenue)
CombinatorAn aggregate is adapted with state, merge, or array-position behaviorMerge aggregate states or aggregate corresponding array elementsSUM_FOREACH(metric_array)
Analytic (Window) FunctionRelated result rows contribute to one value for every retained rowCompare a daily row with its predecessorLAG(revenue) OVER (...)
Table FunctionA value in each current input row produces zero to many rowsExpand the tags attached to an eventEXPLODE(tags) with LATERAL VIEW
Table-Valued Function (TVF)The function itself provides a relation to FROMGenerate numbers or query files as rows and columnsNUMBERS(...) or S3(...)
AI FunctionDoris invokes a model through an AI ResourceClassify, extract, translate, summarize, or analyze textAI_SENTIMENT(resource, text)

Follow the row shape through the analysis

A useful first approximation connects the core function shapes:

How scalar, aggregate, Table, and window functions change or retain rows

Function categories differ in what they consume, return, and do to row shape.

In shorthand, these are one-to-one, many-to-one, and one-to-many. They describe what the function contributes to the query, not a guarantee that the complete SQL statement preserves that exact row count. A WHERE clause can remove rows, a Join can multiply them, and GROUP BY defines how many aggregate groups remain.

The later sections follow that row-shape progression. Section 5.3 derives values while retaining event rows. Section 5.4 deliberately collapses many events into each daily-region row. Section 5.5 expands one row into several element rows. Section 5.6 then gives those transformations explicit stages, and Section 5.7 uses a window of related rows to calculate one value for every retained result row:

event rows
   |  Scalar: derive event_date
   v
enriched event rows
   |  Aggregate: group by event_date, region
   v
daily-region rows
   |  Window: compare related daily-region rows
   v
the same daily-region rows, now with previous values, totals, or ranks

Some categories describe different dimensions

Row shape and capability source can overlap. AI_SENTIMENT behaves like a per-row scalar calculation, but “AI Function” also tells you that it requires a configured AI Resource and an external model call. AI_AGG instead operates across grouped text. Seeing an AI_* name is therefore not enough to infer its row-count behavior.

Likewise, SUM(revenue) is an Aggregate Function when it summarizes a group. When an aggregate is written with OVER, it follows window semantics and produces a value for every retained input result row. SQL context determines how the operation participates in the result.

Table Functions and TVFs both produce rows, but their starting points differ. A Table Function expands something in a current row; a TVF acts as a table source. Section 5.5 develops that distinction with concrete row shapes.

Treat AI calls as an external dependency

An AI Function is appropriate only after the application has defined the model provider, AI Resource, credentials, network access, latency expectations, failure policy, data-governance rules, and acceptable cost. A sentiment label returned by an external model is not equivalent to a deterministic local string transformation.

The lab classifies AI_SENTIMENT but does not execute it. This keeps the lab self-contained and avoids sending course data to an external provider. Consult the AI Functions overview and the chosen function's current contract before adopting it.

5.3 Use Scalar Functions to Derive Explainable Dimensions

The event table already stores event_time, region, and event_type in typed columns. A report may still need an event date, hour boundary, hour number, or display label. Scalar Functions derive those values independently for each selected input row.

Distinguish time meaning from time presentation

Different time expressions answer different questions:

NeedExpression roleExample
Calendar date for groupingConvert the timestamp to a DATE valueTO_DATE(event_time)
Start of the event's hourNormalize to an hourly temporal boundaryDATE_TRUNC(event_time, 'hour')
Hour component for a distributionExtract one componentEXTRACT(HOUR FROM event_time)
Human-readable labelFormat for presentationDATE_FORMAT(event_time, ...)

A formatted string may look like a date, but its contract is still a string. Prefer a temporal value when later operations must compare dates, calculate intervals, or preserve calendar semantics. Format the value when the consumer actually needs a label.

The query's time zone and boundary convention are part of the metric contract. “Daily” is ambiguous if the source timestamp and the business reporting zone differ. A function cannot resolve that business decision by itself.

Separate normalization, labeling, and matching

String operations also serve distinct purposes:

  • LOWER, UPPER, and TRIM can normalize values for a stated comparison or presentation rule.
  • REPLACE can construct a display label when the transformation is truly mechanical.
  • LIKE and REGEXP are predicates that decide which rows qualify; they do not standardize the stored value.

Do not casually normalize an identifier that is case-sensitive by contract. If several spellings represent the same category, decide whether ingestion should establish one canonical value. Repeating cleanup logic across every analytical query can signal that the data boundary is too permissive.

Scalar expressions do not group rows by themselves. If eight event rows enter a projection containing TO_DATE(event_time) and UPPER(region), eight rows can still leave that projection. A later GROUP BY changes the grain.

5.4 Use Aggregate Functions to Answer Defined Metric Questions

An Aggregate Function combines observations. The correct function depends on both the mathematical operation and the population being observed.

Define the population for every measure

Consider four measures in one daily-region row:

MeasureContributing populationSuitable expression pattern
Event countAll selected events in the groupCOUNT(*)
Active usersDistinct non-NULL users among all selected eventsCOUNT(DISTINCT user_id)
Purchasing usersDistinct users among purchase events onlyCOUNT(DISTINCT CASE WHEN event_type = 'purchase' THEN user_id END)
Purchase revenueRevenue from purchase events onlySUM(CASE WHEN event_type = 'purchase' THEN revenue ELSE 0 END)

The conditional expressions allow measures with different contributing populations to share the same daily-region result grain. Filtering the entire query to purchases would also remove non-purchase events from event_count and active_users, changing their meaning.

Distinct counts are not generally additive. A user active in two regions or on two dates contributes to each relevant group but should appear once in a global distinct-user count. Summing daily-region distinct counts does not produce a globally distinct population.

Choose the statistic that matches the question

COUNT, SUM, AVG, MIN, and MAX answer different questions. An average needs a defined observation and denominator. “Average revenue” might mean average per purchase event, per purchasing user, or per day; these values are not interchangeable.

Variance describes spread in one numeric measure. Covariance describes how two measures vary together in their units. Correlation normalizes that relationship to a dimensionless scale. None establishes causation. Before using a statistical aggregate, define the observation grain, sample or population interpretation, missing-value policy, and whether extreme values should remain in scope.

Put filters at the stage they are meant to affect

WHERE and HAVING operate at different row shapes:

WHERE filtering event rows before grouping and HAVING filtering aggregate groups afterward

WHERE tests input rows; HAVING tests groups after their aggregates exist.

To find products whose purchase revenue reaches a threshold on one day, WHERE should choose that day's purchase events, while HAVING SUM(revenue) should choose qualifying product groups. A predicate such as WHERE revenue >= 120000 asks whether each individual event reaches the threshold. It does not test the product total.

Use ANY_VALUE only under a real constraint

A grouped query cannot project an ordinary column whose value varies within the group unless that column also defines the group. Doris rejects an ambiguous projection such as choosing event_time while grouping only by region.

ANY_VALUE(expression) explicitly requests any non-NULL representative value when one exists; it returns NULL when all candidates are NULL. This is meaningful when the exact selected value cannot change the business meaning. For example, after a predicate restricts every input row to one calendar date, any event date within each region is that same date. It is not meaningful when the group spans several dates and the result is supposed to identify one of them. In that case, add the date to the grain or state a rule such as MIN or MAX that has an interpretable meaning.

Never depend on which eligible row ANY_VALUE happens to select. Its purpose is to express indifference, not to provide a shortcut to a predictable first or latest value. See ANY_VALUE.

Introduce a Combinator only for its specialized behavior

A Combinator modifies how an Aggregate Function is represented or applied. State and merge forms support workflows that store or combine intermediate aggregate states. A FOREACH form applies an aggregate independently to corresponding positions of array values across rows.

These are useful when the data product genuinely contains mergeable aggregate states or positionally meaningful arrays. They are unnecessary for an ordinary revenue total. Prefer the direct and readable SUM(revenue) until the specialized requirement exists. See STATE, MERGE, and FOREACH for supported combinations and type constraints.

5.5 Use a Table Function to Expand Values into Rows

An event may store several tags, map entries, or delimited values in one row. When the analysis needs one of those elements per result row, use a Table Function to expand the value while retaining its relationship to the source row.

Expand a value belonging to each input row

Suppose an event has an array of tags:

event_idtags
41['mobile', 'promotion']
42['desktop']

Using EXPLODE(tags) with LATERAL VIEW associates every generated tag with its source event:

event_idtag
41mobile
41promotion
42desktop

The result grain is now one row per event-tag occurrence, not one row per event. Summing event revenue after this expansion would count event 41 twice unless the metric intentionally measures tag attribution or the query first returns to an appropriate grain. Empty and NULL collections also require an explicit retention decision; consult the chosen Table Function's contract.

Table Functions suit row-dependent parsing or expansion. Examples include array expansion and splitting a delimited value. Doris commonly uses them with LATERAL VIEW. See EXPLODE; the documentation lists the other Table Functions beside it.

Table Function and TVF are different categories. A Table Function such as EXPLODE(tags) runs against each current input row and produces zero to many rows associated with that row. A Table-Valued Function (TVF) such as NUMBERS(...) or S3(...) is itself a relation source in FROM; it does not need a row from events_modelled to generate its relation. Module 3 already used the S3 TVF to expose external files as a temporary relation. Lab 5 uses NUMBERS("number" = "4") only as a small, self-contained contrast and does not revisit S3 access or persistence. See NUMBERS and S3.

5.6 Use CTEs to Express the Stages of a Complex Analysis

A Common Table Expression gives a temporary result set a name within one SQL statement. It can make the grain and responsibility of each stage visible. It does not create a permanent table.

For the daily-region scenario, divide the work as follows:

StageResponsibilityResult grain
filteredChoose the three-day input population and derive event_dateOne row per selected event
daily_regionGroup by date and region; calculate activity and purchase measuresOne row per date and region
scoredAdd previous revenue, cumulative revenue, and daily rankOne row per date and region
Final SELECTChoose visible columns and presentation orderOne row per date and region

The structural outline is easier to review than one query layer that attempts every operation at once:

WITH filtered AS (
    -- select the input population and derive event_date
),
daily_region AS (
    -- establish the daily-region grain and calculate measures
),
scored AS (
    -- retain those rows and add window calculations
)
SELECT ...
FROM scored
ORDER BY ...;

This organization makes errors easier to locate. If purchase revenue is wrong, inspect the input predicate and conditional aggregate. If the row count is not three dates multiplied by eight regions, inspect the daily_region grouping. If “previous” crosses from one region to another, inspect the window partition.

A CTE is a semantic and readability boundary. Its presence does not guarantee that Doris materializes the result, executes each named block separately, or makes the query faster. Use EXPLAIN and a Query Profile when physical execution matters; do not infer a performance property from the syntax alone. See Common Table Expressions.

Preserve missing-period meaning

Grouping event detail produces rows only for groups represented by qualifying input. If a region has no selected event on a date, an event-only aggregation does not automatically invent a zero-valued row for that combination.

This matters to later windows. LAG reads the previous result row in its ordered window, which may not be the previous calendar day. A report that must show every calendar date needs a calendar relation and an outer Join or another explicit gap-filling design. Zero activity and an absent result row are different states.

5.7 Use Window Functions to Compare Rows Without Losing Their Grain

After aggregation, each row already represents a date and region. Window functions compare those rows while preserving them. Three clauses establish the comparison contract:

Region-over-time and date-across-regions window populations in one report

The same daily-region rows can participate in different window populations without changing their grain.

Window componentQuestion it answers
PARTITION BYWhich rows form an independent comparison population?
Window ORDER BYIn what sequence are previous, next, and cumulative positions defined?
Window frameWhich positions around the current row contribute to a frame-sensitive calculation?

Compare each region with its previous observed row

For regional revenue trends, the independent sequence is each region:

LAG(purchase_revenue, 1, 0) OVER (
    PARTITION BY region
    ORDER BY event_date
)

The offset 1 means one preceding result row, and 0 is the default when no such row exists. The first displayed date therefore receives zero in the lab. That value does not claim that revenue before the selected range was zero. It only makes the boundary behavior explicit.

If dates are missing, LAG still reads the preceding row. Describe it as “previous observed date” unless the query has guaranteed a consecutive calendar relation.

State the cumulative frame

A running total for each region can use:

SUM(purchase_revenue) OVER (
    PARTITION BY region
    ORDER BY event_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)

The ROWS frame starts with the first selected row in that region and ends at the current row. Filtering earlier dates out of the input also removes them from the cumulative total. “Cumulative” must always name its starting scope.

A frame such as ROWS BETWEEN 2 PRECEDING AND CURRENT ROW contains at most three row positions. It represents three calendar days only when the input has one row for each consecutive date.

Choose the intended tie behavior

ROW_NUMBER assigns distinct sequential positions. Provide a tie-breaker when the position must be deterministic. RANK gives equal ordering values the same rank and leaves a gap after a tie:

RegionRevenueROW_NUMBER with region as tie-breakerRANK by revenue only
East10011
West10021
North8033

Choose ROW_NUMBER when the output needs one deterministic position for each row. Choose RANK when equal metric values should share a business rank. For the module scenario, PARTITION BY event_date restarts the competition each day, while ORDER BY purchase_revenue DESC gives the largest value rank 1.

Window ordering and final result ordering serve different purposes. A window may define a revenue rank while the final ORDER BY displays date, rank, and region. The display order does not recalculate the window values. See Analytic (Window) Functions.

5.8 Choose the Right Extension for Missing Reusable Logic

Start with Doris built-in functions and clear SQL expressions. They already cover the date transformations, grouping, conditional metrics, expansion, and windows used in this module. Repeated expression syntax alone does not justify deploying user code.

Compare reuse and extension choices

RequirementSuitable starting point
Doris already provides the operationBuilt-in function
One query needs a readable combination of existing expressionsOrdinary SQL expression or CTE
A higher-order array operation needs a short element expressionLambda expression passed to a built-in function
Supported expressions need a reusable SQL signatureAlias Function
Required reusable behavior cannot be expressed with supported SQLAn appropriate user-defined function family

In ARRAY_MAP(x -> x * 2, [1, 2, 3, 4]), the Lambda expression defines what happens to each array element. ARRAY_FILTER uses a Lambda predicate to decide which elements remain in the returned array. Neither expression registers a new function for other queries, and filtering array elements does not filter the surrounding table rows.

An Alias Function gives a reusable name to an expression composed from supported functions. It remains expression reuse. It does not provide a place to hide an external proprietary algorithm. See Alias Function.

Match the user-defined family to its row behavior

When the missing logic requires an implementation artifact, choose the family that matches its input and output contract:

ExtensionInput and output behaviorSuitable example
User-Defined Function (UDF)One input row produces one valueApply a proprietary customer-risk score
User-Defined Aggregate Function (UDAF)Rows in a group maintain and merge state to produce one resultCalculate a company-specific grouped index
User-Defined Window Function (UDWF)Window state produces a value for each retained rowApply a specialized rolling state and reset rule
User-Defined Table Function (UDTF)One input row produces zero to many rowsParse and expand a proprietary encoded payload

A Java scalar UDF illustrates the deployment boundary:

implement the Java method and its type contract
        |
        v
package the implementation in a Java Archive (JAR)
        |
        v
make the artifact available to the required Doris nodes
        |
        v
register the SQL signature and implementation with CREATE FUNCTION
        |
        v
invoke the registered function in a query

CREATE FUNCTION registers metadata that points to the implementation; the statement does not contain the algorithm. The contract includes argument and return types, null behavior, implementation location, privileges, and function properties. A function declared immutable must return the same result for the same inputs. Logic that reads changing external state does not satisfy that promise.

The Backend (BE) executes the registered implementation as part of query execution. Deployment therefore introduces artifact availability, security, resource use, failure handling, compatibility, and upgrade responsibilities. Choose an extension because the behavior is both missing and reusable, not because the built-in catalog is unfamiliar.

Doris 4.1.3 documentation marks Python UDF, UDAF, and UDTF support as experimental. The lab uses the documented Java lifecycle as a non-executing template and does not build a JAR or register a function. Consult Java UDF, UDAF, UDWF, and UDTF, Python UDF family, and CREATE FUNCTION for the current implementation contracts.

Lab 5: Explore Doris Functions and Build Analytical Queries

Open Lab 5 — Explore Doris Functions and Build Analytical Queries after completing Lab 4, and plan about 30 minutes. It uses the persistent doris_course.events_modelled table containing 10,158,080 event rows. It reads that model without replacing it so Module 6 can enrich the same events.

The lab turns the choices in this module into observable results:

Lab stepWhat you doWhat the result establishes
1Match seven function categoriesInput, output, row behavior, and dependencies distinguish the categories
2Run EXPLODE and NUMBERSA Table Function expands a current row; a TVF supplies a relation
3Derive dates, hours, and labelsScalar Functions add values while retaining the event-detail rows
4Calculate daily-region measuresAggregation changes the grain from events to groups
5Apply WHERE and HAVINGInput-row and group filters answer different questions
6Compare an invalid projection with ANY_VALUEA representative value is valid only under a supporting business constraint
7Display a named daily CTEA CTE exposes an intermediate result and its grain
8Add LAG, cumulative SUM, and ROW_NUMBERWindow calculations retain the eight daily rows
9Run the complete daily-region analysisThree dates and eight regions produce 24 rows with activity, trend, and rank measures
10Apply array Lambda expressionsElement transformation and filtering return new array values
11Assemble a query from bounded choicesGrain, input filter, and metric selections map to visible SQL
12Compare UDF, UDAF, UDWF, and UDTFExtension type follows the missing reusable logic and its row behavior

In Step 9, follow region_02 across the three dates. Its March 2 row reads the March 1 revenue through LAG, adds March 2 revenue to its regional cumulative total, and receives rank 1 within the March 2 partition. These values are direct query results. The lab does not claim that its CTEs were materialized or that one query form is faster.

When you change the guided builder, state two things before reading its values: what one row represents and which events contribute to the chosen metric. A different date grain, event filter, or measure produces a different business answer even when every query runs successfully.

The Lab ends with a practice: find the region that grew most from one day to the next, in stages with LAG and RANK. Predict the result first, then write the code in the empty cell; the collapsed reference solution after it also runs with Run All and leaves the Lab's tables as they were.

Summary

  • Scalar functions derive one value per row, aggregate functions summarize a group, window functions add a value to every retained row, a Table Function expands each input row into zero or more rows, and a TVF supplies a relation to FROM; an AI Function also depends on an external model.
  • Each measure has its own population: conditional aggregates let all events and purchases share one daily-region row. WHERE removes input rows before grouping, and HAVING removes groups by their aggregate values.
  • ANY_VALUE returns any value of the group, so use it only when every candidate means the same thing, as the date does after a one-day filter; for "latest" or "earliest", use MAX or MIN.
  • A CTE names one stage of a query, such as the filtered events, the daily-region groups, and the scored rows; it does not promise materialization or speed, and an event-only aggregation creates no row for a date without events.
  • PARTITION BY sets each window's population, its ORDER BY defines "previous" and "cumulative", the frame decides which rows a running total includes, and ROW_NUMBER and RANK differ only when values tie.
  • Start with built-in functions and SQL expressions; a Lambda passes element logic to a higher-order function, an Alias Function names an existing expression, and a UDF, UDAF, UDWF, or UDTF brings in external code with its deployment cost.

Quiz 5: Analyzing Data in Apache Doris

Open Quiz 5: Analyzing Data in Apache Doris. It has six single-choice questions and needs no running environment.

Next module: Module 6: Joining Data in Apache Doris.

References

Need help? Contact us!