Problem
I want the top N by some measure, the next page of results, or a list sorted by one key ascending and another descending. The usual pagination and ranking questions.
Setup
val orders = [
{ id = 1, quantity = 12, customer = "Bramble Cafe", region = "north" },
{ id = 2, quantity = 30, customer = "Granary Foods", region = "south" },
{ id = 3, quantity = 8, customer = "Bramble Cafe", region = "north" },
{ id = 4, quantity = 50, customer = "Ironbridge Hotel", region = "west" },
{ id = 5, quantity = 6, customer = "Kiln & Crumb", region = "east" },
{ id = 6, quantity = 20, customer = "Granary Foods", region = "south" }
];
Example
Top three orders by quantity:
from ord in orders
order DESC ord.quantity
take 3
yield { ord.id, ord.customer, ord.quantity };
val it =
[{customer="Ironbridge Hotel",id=4,quantity=50},
{customer="Granary Foods",id=2,quantity=30},
{customer="Granary Foods",id=6,quantity=20}]
: {customer:string, id:int, quantity:int} list
What's happening
order, take, and skip are query steps, the same flavour as
where and yield. They compose: each one takes a collection and
returns a collection, so the order you write them is the order they
run. order DESC ord.quantity take 3 is "sort descending, then keep
the first three" — a top-N.
DESC is a type constructor that flips the ordering of whatever it
wraps. Morel's order step compares values structurally, so DESC x
comes before DESC y when the original x would have come after
y. That sounds backwards until you realise it's exactly what you
want for a sort direction.
take and skip together do pagination. skip n take m is the Morel
equivalent of SQL's OFFSET n LIMIT m. They're separate steps, which
means you can put a yield or a where between them if your pipeline
calls for it.
One thing SQL makes awkward but Morel makes cheap: the "top 3 per
region" question. Sort by (region, DESC quantity), then use a
where that compares ordinal positions. The two-key form of order —
a tuple — is where the composable pipeline starts paying off.
Variations
Skip two, take two — page 2 of a page-size-2 index:
from ord in orders
order ord.id
skip 2
take 2
yield ord.id;
Multi-key sort: region ascending, then quantity descending within
each region. The tuple on the order step sorts lexicographically,
so the first key dominates and later keys break ties.
from ord in orders
order (ord.region, DESC ord.quantity)
yield { ord.region, ord.quantity };
See also
- Recipe 07 — Filter rows — filter before you sort; cheaper on the database inside.
- Recipe 08 — Select and rename columns — project after you've picked the rows you care about.
- Recipe 10 — Group and aggregate — when "top 3 per group" wants a
groupstep rather than a self-join.