LATERAL WITH ORDINALITY - numbering sets

One of the neat little features that arrived at PostgreSQL 9.4 is the WITH ORDINALITY ANSI-SQL construct. What this construct does is to tack an additional column called ordinality as an additional column when you use a set returning function in the FROM part of an SQL Statement.

Basic WITH ORDINALITY

Here is the simple example of its use:

SELECT *
FROM unnest('{my,dog, eats, dog food}'::text[] ) 
    WITH ordinality;
which outputs:
  unnest  | ordinality
----------+------------
 my       |          1
 dog      |          2
 eats     |          3
 dog food |          4

If you aren't happy with the default column names, you can change them by revising your query like this

 
SELECT f.*
FROM unnest('{my,dog, eats, dog food}'::text[] ) 
    WITH ordinality As f(phrase, sort_order);

Which would output this:

  phrase  | sort_order
----------+------------
 my       |          1
 dog      |          2
 eats     |          3
 dog food |          4

LATERAL WITH ORDINALITY

The greatest value of WITH ORDINALITY comes when you apply it to rows of data. How do you do that when you need to use WITH ORDINALITY. This is where one of our favorite constructs, the LATERAL construct comes to the rescue.

First let's construct our table with a text array column for demonstration. Note that the fish has no tags.

CREATE TABLE pets(pet varchar(100) PRIMARY KEY, tags text[]);
INSERT INTO pets(pet, tags)
    VALUES ('dog', '{big, furry, friendly, eats steak}'::text[]),
        ('cat', '{small, snob, eats greenbeans, plays with mouse}'::text[]),
        ('mouse', '{very small, fits in pocket, eat peanuts, watches cat}'::text[]),
        ('fish', NULL);

If you do a cross join, you'll leave out fish because he's got no tags

SELECT pet, sort_order, tag
FROM pets, unnest(tags) 
    WITH ORDINALITY As f(tag, sort_order) ;
 pet  | sort_order |       tag
------+------------+------------------
dog   |          1 | big
dog   |          2 | furry
dog   |          3 | friendly
dog   |          4 | eats steak
cat   |          1 | small
cat   |          2 | snob
cat   |          3 | eats greenbeans
cat   |          4 | plays with mouse
mouse |          1 | very small
mouse |          2 | fits in pocket
mouse |          3 | eat peanuts
mouse |          4 | watches cat

In order to include pets that have no tags, you need to do a LEFT JOIN like so

SELECT pet, sort_order, tag
FROM pets LEFT JOIN 
    LATERAL unnest(tags) 
        WITH ORDINALITY As f(tag, sort_order) ON true;

And voila, all our pets are accounted for:

 pet  | sort_order |       tag
------+------------+------------------
dog   |          1 | big
dog   |          2 | furry
dog   |          3 | friendly
dog   |          4 | eats steak
cat   |          1 | small
cat   |          2 | snob
cat   |          3 | eats greenbeans
cat   |          4 | plays with mouse
mouse |          1 | very small
mouse |          2 | fits in pocket
mouse |          3 | eat peanuts
mouse |          4 | watches cat
fish  |            |