MariaDB window functions (introduced in MariaDB 10.2) compute results across a set of rows related to the current row using the OVER() clause with PARTITION BY and ORDER BY. MariaDB window function syntax follows the SQL standard and is compatible with MySQL 8.0+ and PostgreSQL window function queries. Key window functions include ROW_NUMBER() for unique row numbering, RANK() and DENSE_RANK() for ranking with and without gaps, and LAG()/LEAD() for accessing previous and next row values.
ROW_NUMBER
MariaDB 10.2+→ BIGINT
Assigns a unique sequential integer to each row within the window partition, starting at 1.
Signature
ROW_NUMBER() OVER ([PARTITION BY col] ORDER BY col)
Examples
Basic row numbering by salary descending
sql
SELECTname, salary, ROW_NUMBER() OVER (ORDERBYsalaryDESC) ASrnFROMemployees;
→-- 1, 2, 3, ... unique for every row
Partitioned row numbering per department
sql
SELECTdept, name, salary, ROW_NUMBER() OVER (PARTITIONBYdeptORDERBYsalaryDESC) ASrnFROMemployees;
→-- numbering restarts at 1 for each department
With explicit frame clause (frame is noted but ROW_NUMBER ignores it)
sql
SELECTname, salary, ROW_NUMBER() OVER (PARTITIONBYdeptORDERBYhire_dateROWSBETWEENUNBOUNDEDPRECEDINGANDCURRENTROW) ASrnFROMemployees;
→-- sequential number within running frame
Simple ascending order numbering
sql
SELECTROW_NUMBER() OVER (ORDERBYsalary) ASrn, salaryFROMemployees;
→-- 1, 2, 3 ...
In CTE to fetch top row per partition
sql
WITHrankedAS (SELECT*, ROW_NUMBER() OVER (PARTITIONBYdeptORDERBYsalaryDESC) ASrnFROMemployees) SELECT*FROMrankedWHERErn=1;
→-- top earner per department
⚠Anti-Pattern— Using ROW_NUMBER without ORDER BY
Omitting ORDER BY inside OVER() produces an arbitrary, non-deterministic numbering.
✓ Instead: Always specify ORDER BY inside the OVER() clause to get a predictable sequence.
◆MariaDB Note
Available since MariaDB 10.2.
⇄ vs MySQL: MySQL added ROW_NUMBER() in 8.0; MariaDB had it since 10.2.
⇄ vs PostgreSQL: PostgreSQL syntax is identical; behavior is the same.
Returns the value of an expression from the first row of the window frame.
Signature
FIRST_VALUE(expr) OVER ([PARTITION BY col] ORDER BY col [frame_clause])
Parameters
Parameter
Type
Description
expr
ANY
Expression to evaluate from the first row of the window frame
Examples
First salary in hire-date order for all rows
sql
SELECTname, salary, FIRST_VALUE(salary) OVER (ORDERBYhire_date) ASfirst_salaryFROMemployees;
→-- salary of the first employee hired
Partitioned: highest salary per department
sql
SELECTdept, name, salary, FIRST_VALUE(salary) OVER (PARTITIONBYdeptORDERBYsalaryDESC) AStop_salaryFROMemployees;
→-- highest salary in the department
With explicit ROWS frame for running first value
sql
SELECTname, salary, FIRST_VALUE(salary) OVER (PARTITIONBYdeptORDERBYhire_dateROWSBETWEENUNBOUNDEDPRECEDINGANDCURRENTROW) ASfirst_salFROMemployees;
→-- first salary seen so far within partition
Running first value in a time series
sql
SELECTdate, close, FIRST_VALUE(close) OVER (PARTITIONBYtickerORDERBYdateROWSBETWEENUNBOUNDEDPRECEDINGANDCURRENTROW) ASperiod_start_closeFROMstock_prices;
→-- opening price of the period for each ticker
In CTE to get top earner per department
sql
WITHfvAS (SELECT*, FIRST_VALUE(name) OVER (PARTITIONBYdeptORDERBYsalaryDESC) AStop_earnerFROMemployees) SELECTDISTINCTdept, top_earnerFROMfv;
→-- top earner name per department
⚠Anti-Pattern— Expecting the first row of the partition regardless of frame
FIRST_VALUE returns the first row of the frame, not necessarily the partition; if the frame starts at CURRENT ROW, FIRST_VALUE equals the current row's value.
✓ Instead: Explicitly specify ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING to always get the partition-wide first value.
◆MariaDB Note
Available since MariaDB 10.2.
⇄ vs MySQL: MySQL added FIRST_VALUE() in 8.0; MariaDB had it since 10.2.
⇄ vs PostgreSQL: PostgreSQL syntax is identical; PostgreSQL also supports GROUPS frame mode.
Returns the value of an expression from the last row of the window frame.
Signature
LAST_VALUE(expr) OVER ([PARTITION BY col] ORDER BY col [frame_clause])
Parameters
Parameter
Type
Description
expr
ANY
Expression to evaluate from the last row of the window frame
Examples
Last salary in hire-date order — needs UNBOUNDED FOLLOWING frame
sql
SELECTname, salary, LAST_VALUE(salary) OVER (ORDERBYhire_dateROWSBETWEENUNBOUNDEDPRECEDINGANDUNBOUNDEDFOLLOWING) ASlast_salaryFROMemployees;
→-- salary of the most recently hired employee
Partitioned last value per department
sql
SELECTdept, name, salary, LAST_VALUE(name) OVER (PARTITIONBYdeptORDERBYsalaryROWSBETWEENUNBOUNDEDPRECEDINGANDUNBOUNDEDFOLLOWING) ASlowest_earnerFROMemployees;
→-- name of highest earner (last in ASC order) per dept
Running last value — current row is always the last of the running frame
sql
SELECTname, salary, LAST_VALUE(salary) OVER (PARTITIONBYdeptORDERBYhire_dateROWSBETWEENUNBOUNDEDPRECEDINGANDCURRENTROW) ASlatest_salFROMemployees;
→-- most recent salary seen so far in partition
Last close price for the whole period
sql
SELECTdate, close, LAST_VALUE(close) OVER (PARTITIONBYtickerORDERBYdateROWSBETWEENUNBOUNDEDPRECEDINGANDUNBOUNDEDFOLLOWING) ASperiod_end_closeFROMstock_prices;
→-- closing price of the period for each ticker
In CTE with DISTINCT to collapse partition result
sql
WITHlvAS (SELECT*, LAST_VALUE(name) OVER (PARTITIONBYdeptORDERBYsalaryROWSBETWEENUNBOUNDEDPRECEDINGANDUNBOUNDEDFOLLOWING) ASbest_earnerFROMemployees) SELECTDISTINCTdept, best_earnerFROMlv;
→-- best earner per department
⚠Anti-Pattern— Using LAST_VALUE with the default frame
With the default RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW frame, LAST_VALUE simply returns the current row's value, not the last row of the partition.
✓ Instead: Explicitly write ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING to get the partition-level last value.
◆MariaDB Note
Available since MariaDB 10.2.
⇄ vs MySQL: MySQL added LAST_VALUE() in 8.0; MariaDB had it since 10.2.
⇄ vs PostgreSQL: PostgreSQL syntax is identical; PostgreSQL also supports GROUPS frame mode.
Computes a discrete percentile value by returning the first actual value in the sorted set whose cumulative distribution meets or exceeds the given fraction.
Signature
PERCENTILE_DISC(fraction) WITHIN GROUP (ORDER BY expr) OVER ([PARTITION BY col])
Parameters
Parameter
Type
Description
fraction
DOUBLE
Percentile fraction between 0 and 1 (e.g., 0.5 for median)
expr
ANY
Expression defining the sort order for the percentile calculation
Examples
Company-wide discrete median — returns a real observed salary
sql
SELECTPERCENTILE_DISC(0.5) WITHINGROUP (ORDERBYsalary) OVER () ASmedian_salaryFROMemployeesLIMIT1;
→-- actual median value that exists in the data
Partitioned discrete median per department
sql
SELECTDISTINCTdept, PERCENTILE_DISC(0.5) WITHINGROUP (ORDERBYsalary) OVER (PARTITIONBYdept) ASdept_medianFROMemployees;
→-- discrete median salary per department
75th percentile discrete value
sql
SELECTname, dept, PERCENTILE_DISC(0.75) WITHINGROUP (ORDERBYsalary) OVER (PARTITIONBYdept) ASp75_salaryFROMemployees;
→-- 75th percentile actual salary value per department
Discrete median latency per API endpoint
sql
SELECTendpoint, PERCENTILE_DISC(0.5) WITHINGROUP (ORDERBYresponse_ms) OVER (PARTITIONBYendpoint) ASmedian_latencyFROMapi_logs;
→-- actual observed median response time per endpoint
In CTE to report discrete median per department
sql
WITHpctAS (SELECTDISTINCTdept, PERCENTILE_DISC(0.5) WITHINGROUP (ORDERBYsalary) OVER (PARTITIONBYdept) ASp50FROMemployees) SELECT*FROMpctORDERBYdept;
→-- median salary per department as actual observed value
⚠Anti-Pattern— Using PERCENTILE_DISC when a smooth interpolation is needed
PERCENTILE_DISC returns an existing value; on small datasets the result can be far from the true percentile position.
✓ Instead: Use PERCENTILE_CONT for a smoothly interpolated percentile on continuous data.
◆MariaDB Note
Available since MariaDB 10.3.3 as an ordered-set aggregate window function.
⇄ vs MySQL: MySQL does not support PERCENTILE_DISC; this is a MariaDB-specific advantage.
⇄ vs PostgreSQL: PostgreSQL syntax is identical; PERCENTILE_DISC is available since PostgreSQL 9.4.
Returns the median (50th percentile) of values in a group using continuous interpolation; a convenient shorthand for PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY expr).
Signature
MEDIAN(expr) OVER ([PARTITION BY col])
Parameters
Parameter
Type
Description
expr
NUMERIC
Numeric expression whose median is calculated over the window
Examples
Company-wide median without collapsing rows
sql
SELECTname, salary, MEDIAN(salary) OVER () AScompany_medianFROMemployees;
→-- interpolated median salary on every row
Partitioned median per department
sql
SELECTdept, name, salary, MEDIAN(salary) OVER (PARTITIONBYdept) ASdept_medianFROMemployees;
→-- median salary per department on every row
With frame clause for running median
sql
SELECTname, salary, MEDIAN(salary) OVER (PARTITIONBYdeptORDERBYhire_dateROWSBETWEENUNBOUNDEDPRECEDINGANDCURRENTROW) ASrunning_medianFROMemployees;
→-- running median within department
Median sale amount per product
sql
SELECTproduct_id, sale_date, amount, MEDIAN(amount) OVER (PARTITIONBYproduct_id) ASmedian_saleFROMsales;
→-- median sale amount per product on every row
In CTE to compare individual salary to department median