|
| 1 | +--- |
| 2 | +title: GROUPING SETS, ROLLUP, and CUBE |
| 3 | +sidebar_label: GROUPING SETS |
| 4 | +description: GROUPING SETS, ROLLUP, and CUBE SQL keyword reference for computing multiple levels of aggregation in a single query. |
| 5 | +--- |
| 6 | + |
| 7 | +`GROUPING SETS`, `ROLLUP`, and `CUBE` perform aggregation over multiple |
| 8 | +dimensions within a single query. This can be used, for example, to compute |
| 9 | +subtotals and grand totals alongside detail-level results, without multiple |
| 10 | +passes over the data. |
| 11 | + |
| 12 | +## Syntax |
| 13 | + |
| 14 | +Grouping sets can be used with both `GROUP BY` and `SAMPLE BY`. |
| 15 | + |
| 16 | +With `GROUP BY`: |
| 17 | + |
| 18 | +```questdb-sql |
| 19 | +SELECT column [, ...], aggregate(column) [, ...] |
| 20 | +FROM table |
| 21 | +[WHERE condition] |
| 22 | +GROUP BY |
| 23 | + column [, ...], |
| 24 | + ROLLUP(column [, ...]) |
| 25 | + | CUBE(column [, ...]) |
| 26 | + | GROUPING SETS ((column [, ...]) [, ...]) |
| 27 | +``` |
| 28 | + |
| 29 | +With `SAMPLE BY`: |
| 30 | + |
| 31 | +```questdb-sql |
| 32 | +SELECT [column [, ...],] aggregate(column) [, ...] |
| 33 | +FROM table |
| 34 | +[WHERE condition] |
| 35 | +SAMPLE BY n{units} |
| 36 | + [ROLLUP(column [, ...]) | CUBE(column [, ...]) | GROUPING SETS (...)] |
| 37 | + [FILL(...)] |
| 38 | + [ALIGN TO ...] |
| 39 | +``` |
| 40 | + |
| 41 | +## GROUPING SETS |
| 42 | + |
| 43 | +`GROUPING SETS` gives explicit control over which grouping combinations to |
| 44 | +compute. Each set in the list produces its own group of aggregated rows. |
| 45 | + |
| 46 | +```questdb-sql title="Explicit grouping sets" demo |
| 47 | +SELECT symbol, side, SUM(amount) AS total_amount, COUNT(*) AS trade_count |
| 48 | +FROM trades |
| 49 | +WHERE timestamp IN '$now-1m..$now' |
| 50 | + AND symbol IN ('BTC-USDT', 'ETH-USDT') |
| 51 | +GROUP BY GROUPING SETS ( |
| 52 | + (symbol, side), |
| 53 | + (symbol), |
| 54 | + () |
| 55 | +); |
| 56 | +``` |
| 57 | + |
| 58 | +- `(symbol, side)` groups by both columns (detail rows) |
| 59 | +- `(symbol)` groups by symbol only (subtotals per symbol, `side` is `NULL`) |
| 60 | +- `()` is the empty set, producing a single grand total row (both columns `NULL`) |
| 61 | + |
| 62 | +You can specify any combination of column subsets. `ROLLUP` and `CUBE` are |
| 63 | +shorthand for common `GROUPING SETS` patterns. |
| 64 | + |
| 65 | +## ROLLUP |
| 66 | + |
| 67 | +`ROLLUP` generates hierarchical subtotals, progressively dropping columns from |
| 68 | +right to left. With N columns, `ROLLUP` produces N+1 grouping sets. |
| 69 | + |
| 70 | +```questdb-sql title="Trade volume breakdown with ROLLUP" demo |
| 71 | +SELECT symbol, side, |
| 72 | + SUM(price * amount) AS volume, |
| 73 | + COUNT(*) AS trades |
| 74 | +FROM trades |
| 75 | +WHERE timestamp IN '$now-1m..$now' |
| 76 | + AND symbol IN ('BTC-USDT', 'ETH-USDT') |
| 77 | +GROUP BY ROLLUP(symbol, side) |
| 78 | +ORDER BY symbol, side; |
| 79 | +``` |
| 80 | + |
| 81 | +This produces: |
| 82 | + |
| 83 | +- Per-symbol, per-side detail rows |
| 84 | +- Per-symbol subtotals (`side` is `NULL`) |
| 85 | +- A single grand total row (both `NULL`) |
| 86 | + |
| 87 | +`ROLLUP(symbol, side)` is equivalent to: |
| 88 | + |
| 89 | +```questdb-sql |
| 90 | +GROUP BY GROUPING SETS ( |
| 91 | + (symbol, side), |
| 92 | + (symbol), |
| 93 | + () |
| 94 | +) |
| 95 | +``` |
| 96 | + |
| 97 | +With three columns, `ROLLUP(a, b, c)` produces four grouping sets: |
| 98 | + |
| 99 | +```questdb-sql |
| 100 | +GROUP BY GROUPING SETS ( |
| 101 | + (a, b, c), |
| 102 | + (a, b), |
| 103 | + (a), |
| 104 | + () |
| 105 | +) |
| 106 | +``` |
| 107 | + |
| 108 | +## CUBE |
| 109 | + |
| 110 | +`CUBE` generates all possible combinations of the specified columns. With N |
| 111 | +columns, `CUBE` produces 2^N grouping sets. |
| 112 | + |
| 113 | +```questdb-sql title="Cross-tabulation with CUBE" demo |
| 114 | +SELECT symbol, side, |
| 115 | + SUM(amount) AS total_amount, |
| 116 | + GROUPING_ID(symbol, side) AS grp |
| 117 | +FROM trades |
| 118 | +WHERE timestamp IN '$now-1m..$now' |
| 119 | + AND symbol IN ('BTC-USDT', 'ETH-USDT') |
| 120 | +GROUP BY CUBE(symbol, side) |
| 121 | +ORDER BY grp, symbol, side; |
| 122 | +``` |
| 123 | + |
| 124 | +`CUBE(symbol, side)` is equivalent to: |
| 125 | + |
| 126 | +```questdb-sql |
| 127 | +GROUP BY GROUPING SETS ( |
| 128 | + (symbol, side), -- both grouped |
| 129 | + (symbol), -- symbol only |
| 130 | + (side), -- side only |
| 131 | + () -- grand total |
| 132 | +) |
| 133 | +``` |
| 134 | + |
| 135 | +Ordering by `GROUPING_ID` groups the output by aggregation level: |
| 136 | + |
| 137 | +- `grp=0`: all detail combinations |
| 138 | +- `grp=1`: per-symbol totals (side rolled up) |
| 139 | +- `grp=2`: per-side totals (symbol rolled up) |
| 140 | +- `grp=3`: grand total |
| 141 | + |
| 142 | +`CUBE` is limited to 15 columns maximum (2^15 = 32,768 grouping sets). |
| 143 | + |
| 144 | +## Composite syntax |
| 145 | + |
| 146 | +Plain `GROUP BY` columns can be combined with `ROLLUP` or `CUBE`. The plain |
| 147 | +columns are always included in every grouping set. |
| 148 | + |
| 149 | +```questdb-sql title="symbol always grouped, side rolled up" demo |
| 150 | +SELECT symbol, side, SUM(amount) AS total_amount |
| 151 | +FROM trades |
| 152 | +WHERE timestamp IN '$now-1m..$now' |
| 153 | + AND symbol IN ('BTC-USDT', 'ETH-USDT') |
| 154 | +GROUP BY symbol, ROLLUP(side); |
| 155 | +``` |
| 156 | + |
| 157 | +This is equivalent to: |
| 158 | + |
| 159 | +```questdb-sql |
| 160 | +GROUP BY GROUPING SETS ( |
| 161 | + (symbol, side), |
| 162 | + (symbol) |
| 163 | +) |
| 164 | +``` |
| 165 | + |
| 166 | +There is no empty set `()` here because `symbol` is always present. |
| 167 | + |
| 168 | +## GROUPING() and GROUPING_ID() functions |
| 169 | + |
| 170 | +When columns are rolled up, they appear as `NULL` in the result. The data might |
| 171 | +also contain genuine `NULL` values. `GROUPING()` and `GROUPING_ID()` distinguish |
| 172 | +between the two. |
| 173 | + |
| 174 | +### GROUPING(column) |
| 175 | + |
| 176 | +Accepts a single column. Returns: |
| 177 | + |
| 178 | +- `0` if the column is actively grouped (a `NULL` is a real data value) |
| 179 | +- `1` if the column is rolled up (the `NULL` is a placeholder) |
| 180 | + |
| 181 | +```questdb-sql title="Identify rolled-up rows" demo |
| 182 | +SELECT symbol, side, SUM(amount) AS total_amount, |
| 183 | + GROUPING(symbol) AS gs, |
| 184 | + GROUPING(side) AS gsd |
| 185 | +FROM trades |
| 186 | +WHERE timestamp IN '$now-1m..$now' |
| 187 | + AND symbol IN ('BTC-USDT', 'ETH-USDT') |
| 188 | +GROUP BY ROLLUP(symbol, side) |
| 189 | +ORDER BY gs, gsd, symbol, side; |
| 190 | +``` |
| 191 | + |
| 192 | +In the results: |
| 193 | + |
| 194 | +| gs | gsd | Meaning | |
| 195 | +| -- | --- | ------- | |
| 196 | +| 0 | 0 | Detail row: both columns actively grouped | |
| 197 | +| 0 | 1 | Subtotal: grouped by symbol, side rolled up | |
| 198 | +| 1 | 1 | Grand total: both columns rolled up | |
| 199 | + |
| 200 | +### GROUPING_ID(column1, column2, ...) |
| 201 | + |
| 202 | +Accepts one or more columns. Returns an integer bitmask combining the |
| 203 | +`GROUPING()` values of all specified columns. Bit positions are assigned |
| 204 | +right-to-left: the rightmost argument occupies bit 0 (least significant bit). |
| 205 | + |
| 206 | +```questdb-sql title="Bitmask for aggregation levels" demo |
| 207 | +SELECT symbol, side, SUM(amount) AS total_amount, |
| 208 | + GROUPING_ID(symbol, side) AS grp |
| 209 | +FROM trades |
| 210 | +WHERE timestamp IN '$now-1m..$now' |
| 211 | + AND symbol IN ('BTC-USDT', 'ETH-USDT') |
| 212 | +GROUP BY CUBE(symbol, side) |
| 213 | +ORDER BY grp, symbol, side; |
| 214 | +``` |
| 215 | + |
| 216 | +For `GROUPING_ID(symbol, side)`, bit 1 is assigned to `symbol` and bit 0 to |
| 217 | +`side`: |
| 218 | + |
| 219 | +| grp | Binary | Meaning | |
| 220 | +| --- | ------ | ------- | |
| 221 | +| 0 | 0b00 | Both columns grouped | |
| 222 | +| 1 | 0b01 | `side` rolled up | |
| 223 | +| 2 | 0b10 | `symbol` rolled up | |
| 224 | +| 3 | 0b11 | Both rolled up (grand total) | |
| 225 | + |
| 226 | +Writing `GROUPING_ID(side, symbol)` would reverse the bit assignments. |
| 227 | + |
| 228 | +## SAMPLE BY integration |
| 229 | + |
| 230 | +Grouping sets work with QuestDB's `SAMPLE BY` clause for time-bucketed |
| 231 | +aggregation with multiple rollup levels. |
| 232 | + |
| 233 | +```questdb-sql title="Hourly breakdown with ROLLUP" demo |
| 234 | +SELECT timestamp, symbol, SUM(amount) AS total_amount, AVG(price) AS avg_price |
| 235 | +FROM trades |
| 236 | +WHERE timestamp IN '$now-1d..$now' |
| 237 | + AND symbol IN ('BTC-USDT', 'ETH-USDT') |
| 238 | +SAMPLE BY 1h ROLLUP(symbol) |
| 239 | +ORDER BY timestamp, symbol; |
| 240 | +``` |
| 241 | + |
| 242 | +Each time bucket contains one row per symbol plus one grand total row (where |
| 243 | +`symbol` is `NULL`). The timestamp column is never rolled up - it is always |
| 244 | +present as the time bucket key. |
| 245 | + |
| 246 | +### FILL support |
| 247 | + |
| 248 | +`FILL` works with grouping sets. Missing time buckets are filled per key |
| 249 | +combination - each distinct (symbol, grouping level) pair gets its own fill row. |
| 250 | + |
| 251 | +```questdb-sql title="SAMPLE BY with FILL and ROLLUP" demo |
| 252 | +SELECT timestamp, symbol, SUM(amount) AS total_amount, AVG(price) AS avg_price |
| 253 | +FROM trades |
| 254 | +WHERE timestamp IN '$now-1d..$now' |
| 255 | + AND symbol IN ('BTC-USDT', 'ETH-USDT') |
| 256 | +SAMPLE BY 1h ROLLUP(symbol) FILL(0) |
| 257 | +ORDER BY timestamp, symbol; |
| 258 | +``` |
| 259 | + |
| 260 | +Supported FILL modes: |
| 261 | + |
| 262 | +| FILL mode | Supported | |
| 263 | +| ------------ | --------- | |
| 264 | +| `FILL(NONE)` | Yes | |
| 265 | +| `FILL(NULL)` | Yes | |
| 266 | +| `FILL(value)` | Yes | |
| 267 | +| `FILL(PREV)` | No | |
| 268 | +| `FILL(LINEAR)` | No | |
| 269 | + |
| 270 | +`GROUPING()` and `GROUPING_ID()` values are preserved in fill rows. They are not |
| 271 | +replaced by the fill value. |
| 272 | + |
| 273 | +```questdb-sql title="GROUPING values preserved in fill rows" demo |
| 274 | +SELECT GROUPING(symbol) AS gs, timestamp, symbol, SUM(amount) AS total_amount |
| 275 | +FROM trades |
| 276 | +WHERE timestamp IN '$now-1d..$now' |
| 277 | + AND symbol IN ('BTC-USDT', 'ETH-USDT') |
| 278 | +SAMPLE BY 1h ROLLUP(symbol) FILL(NULL) |
| 279 | +ORDER BY timestamp, gs, symbol; |
| 280 | +``` |
| 281 | + |
| 282 | +A fill row for a missing hour shows `gs=0` for detail-level fills and `gs=1` for |
| 283 | +grand-total-level fills, just like real data rows. Only aggregate columns get the |
| 284 | +fill value. |
| 285 | + |
| 286 | +## Limitations |
| 287 | + |
| 288 | +- **Expressions not allowed** in `ROLLUP`, `CUBE`, or `GROUPING SETS` - only |
| 289 | + column references are accepted. `ROLLUP(a + b)` is rejected; use a subquery or |
| 290 | + alias. Plain columns in composite syntax (`GROUP BY expr, ROLLUP(col)`) are not |
| 291 | + restricted. |
| 292 | + |
| 293 | +- **No mixed qualified/unqualified references** to the same column - |
| 294 | + `ROLLUP(a, t.a)` is rejected. Use one form consistently. |
| 295 | + |
| 296 | +- **Not supported with `LATEST ON`** - rejected with an error. |
| 297 | + |
| 298 | +- **`FILL(PREV)` and `FILL(LINEAR)` not supported** with grouping sets. |
| 299 | + |
| 300 | +- **`CUBE` limited to 15 columns** (2^15 = 32,768 grouping sets). |
| 301 | + |
| 302 | +- **`GROUPING()` / `GROUPING_ID()` limited to 31 `GROUP BY` key columns** - the |
| 303 | + bitmask is int-based. |
| 304 | + |
| 305 | +- **No multiple `ROLLUP`/`CUBE` in the same `GROUP BY`** - |
| 306 | + `GROUP BY ROLLUP(a), CUBE(b)` is not supported. |
| 307 | + |
| 308 | +- **Maximum grouping sets per query** - controlled by the |
| 309 | + `cairo.sql.max.grouping.sets` |
| 310 | + [configuration property](/docs/configuration/overview/) (default 4096). |
| 311 | + `ROLLUP` produces N+1 sets, `CUBE` produces 2^N sets, and explicit |
| 312 | + `GROUPING SETS` produces one set per listed group. Queries exceeding this limit |
| 313 | + are rejected at parse time. |
| 314 | + |
| 315 | +## See also |
| 316 | + |
| 317 | +- [GROUP BY](/docs/query/sql/group-by/) - Standard grouping |
| 318 | +- [SAMPLE BY](/docs/query/sql/sample-by/) - Time-series aggregation |
| 319 | +- [PIVOT](/docs/query/sql/pivot/) - Transform GROUP BY results from rows to columns |
| 320 | +- [Aggregation functions](/docs/query/functions/aggregation/) - Available aggregate functions |
0 commit comments