Skip to content

Commit b0588b6

Browse files
authored
docs: add custom ordering for categorical values recipe (cube-js#10671)
Add a recipe showing how to sort categorical dimensions (e.g., pipeline stages) in a business-meaningful order using a CASE expression, both at query time in semantic SQL and as a permanent dimension in the data model. Made-with: Cursor
1 parent 74e6cfe commit b0588b6

3 files changed

Lines changed: 192 additions & 2 deletions

File tree

docs-mintlify/docs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,8 @@
583583
"recipes/data-modeling/funnels",
584584
"recipes/data-modeling/cohort-retention",
585585
"recipes/data-modeling/xirr",
586-
"recipes/data-modeling/dbt"
586+
"recipes/data-modeling/dbt",
587+
"recipes/data-modeling/custom-order"
587588
]
588589
}
589590
]
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
---
2+
title: Custom ordering for categorical values
3+
description: "This recipe shows how to define a custom sort order for dimension values that don't follow alphabetical or numeric ordering."
4+
---
5+
6+
## Use case
7+
8+
When working with categorical dimensions like pipeline stages, priority levels,
9+
or status values, you often need to sort them in a specific business-meaningful
10+
order rather than alphabetically. For example, a sales pipeline might have
11+
stages like *Pipeline*, *Best Case*, *Most Likely*, *Commit*, and *Closed*
12+
that should always appear in that funnel order.
13+
14+
Sometimes stages are prefixed with numbers (e.g., *1. Pipeline*, *2. Best
15+
Case*) which makes alphabetical sorting work. But when they don't have
16+
numbers, alphabetical order produces results that don't match the business
17+
logic.
18+
19+
There are two ways to solve this:
20+
21+
- **At query time** — write a `CASE` expression directly in a [semantic
22+
SQL][ref-sql-api] query. This is the fastest way to get results and works
23+
great when you're exploring data in a [workbook][ref-workbooks] or asking AI
24+
to build a query for you.
25+
- **In the data model** — add a permanent dimension with the ordering logic.
26+
This is the right choice when the same sort order is reused across many
27+
queries, dashboards, or consumers.
28+
29+
## Query-level approach
30+
31+
You can define a custom ordering dimension directly in a semantic SQL query
32+
without changing the data model. This is especially useful when working in
33+
workbooks — you can ask AI to sort results in a specific order and it will
34+
generate the appropriate `CASE` expression for you.
35+
36+
```sql
37+
SELECT
38+
deals.forecast_category,
39+
CASE
40+
WHEN deals.forecast_category = 'Pipeline' THEN 1
41+
WHEN deals.forecast_category = 'Best Case' THEN 2
42+
WHEN deals.forecast_category = 'Most Likely' THEN 3
43+
WHEN deals.forecast_category = 'Commit' THEN 4
44+
WHEN deals.forecast_category = 'Closed' THEN 5
45+
ELSE 6
46+
END AS funnel_order,
47+
MEASURE(total_amount) AS total_amount
48+
FROM
49+
deals
50+
GROUP BY
51+
1, 2
52+
ORDER BY
53+
2 ASC
54+
```
55+
56+
The `CASE` expression creates an inline `funnel_order` column that maps each
57+
category to its position. The query then sorts by that column instead of by
58+
the category name.
59+
60+
This approach requires no changes to the data model and is ideal for ad-hoc
61+
analysis. In a workbook, you can simply ask the AI assistant something like
62+
*"sort forecast categories in pipeline order: Pipeline, Best Case, Most
63+
Likely, Commit, Closed"* and it will generate a query like the one above.
64+
65+
## Data model approach
66+
67+
When the same custom order is needed across multiple queries, dashboards, or
68+
BI tools, it's better to encode it as a dimension in the data model. This
69+
way any consumer can sort by it without re-implementing the `CASE` logic.
70+
71+
Consider the following data model with a `forecast_category` dimension that
72+
has no inherent sort order:
73+
74+
<CodeGroup>
75+
76+
```yaml title="YAML"
77+
cubes:
78+
- name: deals
79+
sql_table: deals
80+
81+
dimensions:
82+
- name: forecast_category
83+
sql: forecast_category
84+
type: string
85+
86+
- name: forecast_category_order
87+
sql: |
88+
CASE
89+
WHEN {forecast_category} = 'Pipeline' THEN 1
90+
WHEN {forecast_category} = 'Best Case' THEN 2
91+
WHEN {forecast_category} = 'Most Likely' THEN 3
92+
WHEN {forecast_category} = 'Commit' THEN 4
93+
WHEN {forecast_category} = 'Closed' THEN 5
94+
ELSE 6
95+
END
96+
type: number
97+
98+
measures:
99+
- name: total_amount
100+
sql: amount
101+
type: sum
102+
```
103+
104+
```javascript title="JavaScript"
105+
cube(`deals`, {
106+
sql_table: `deals`,
107+
108+
dimensions: {
109+
forecast_category: {
110+
sql: `forecast_category`,
111+
type: `string`
112+
},
113+
114+
forecast_category_order: {
115+
sql: `
116+
CASE
117+
WHEN ${forecast_category} = 'Pipeline' THEN 1
118+
WHEN ${forecast_category} = 'Best Case' THEN 2
119+
WHEN ${forecast_category} = 'Most Likely' THEN 3
120+
WHEN ${forecast_category} = 'Commit' THEN 4
121+
WHEN ${forecast_category} = 'Closed' THEN 5
122+
ELSE 6
123+
END
124+
`,
125+
type: `number`
126+
}
127+
},
128+
129+
measures: {
130+
total_amount: {
131+
sql: `amount`,
132+
type: `sum`
133+
}
134+
}
135+
})
136+
```
137+
138+
</CodeGroup>
139+
140+
The `forecast_category_order` dimension uses a `CASE` expression to assign a
141+
numeric position to each category value. This dimension references the
142+
`forecast_category` dimension so that the mapping stays consistent.
143+
144+
The `ELSE 6` clause handles any unexpected values, placing them at the end
145+
of the sort order.
146+
147+
Once the dimension is in the data model, queries become straightforward:
148+
149+
```sql
150+
SELECT
151+
forecast_category,
152+
forecast_category_order,
153+
MEASURE(total_amount)
154+
FROM
155+
deals
156+
GROUP BY
157+
1, 2
158+
ORDER BY
159+
2 ASC
160+
```
161+
162+
## Result
163+
164+
Both approaches produce the same result — a business-meaningful funnel order
165+
instead of alphabetical sorting:
166+
167+
| Forecast Category | funnel_order | Total Amount |
168+
| ----------------- | -----------: | -------------: |
169+
| Pipeline | 1 | $17,830,500 |
170+
| Best Case | 2 | $6,786,250 |
171+
| Most Likely | 3 | $537,499.70 |
172+
| Commit | 4 | $688,000 |
173+
| Closed | 5 | $9,232,800.46 |
174+
175+
This pattern works for any set of categorical values that need a custom order:
176+
support ticket priorities, project phases, approval workflows, and so on.
177+
178+
Use the **query-level approach** when you need a quick, one-off sort order
179+
while exploring data. Use the **data model approach** when the ordering is a
180+
stable business rule that should be available to all consumers.
181+
182+
183+
[ref-data-apis]: /reference#data-apis
184+
[ref-sql-api]: /reference/sql-api
185+
[ref-custom-sorting]: /recipes/core-data-api/sorting
186+
[ref-workbooks]: /docs/workspace/workbooks

docs-mintlify/recipes/index.mdx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: Step-by-step tutorials and best practices for getting the most out
44
mode: wide
55
---
66

7-
Explore **38 recipes** across data modeling, calculations, analytics patterns,
7+
Explore **39 recipes** across data modeling, calculations, analytics patterns,
88
pre-aggregations, configuration, APIs, and AI.
99

1010
## Data Modeling
@@ -28,6 +28,9 @@ pre-aggregations, configuration, APIs, and AI.
2828
<Card title="Dynamic union tables" icon="layer-group" href="/recipes/data-modeling/dynamic-union-tables">
2929
Combine multiple database tables that relate to the same entity into a single cube.
3030
</Card>
31+
<Card title="Custom ordering" icon="arrow-down-1-9" href="/recipes/data-modeling/custom-order">
32+
Define a custom sort order for categorical values like pipeline stages that don't sort alphabetically.
33+
</Card>
3134
</CardGroup>
3235

3336
## Calculations & Metrics

0 commit comments

Comments
 (0)