Postgres - Order by Date Descending Time Ascending

This week, I’ve got a request from our project manager to make an update to an existing feature. The update required the feature to have groups by date, which are shown descending, and then in those days, the times should be in ascending order.

Everything I’ve written here is executed on a Mac, iTerm 2 command line and using pgcli tool. It’s similar to psql, just with autocomplete and some other neat features.

To make a clear example, I’ll create a database, and populate it with some data. You can follow along.

CREATE DATABASE order_by_sample;

-- connect to that database

-- and add extension for generating id using UUID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE sample (
  id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
  date_created TIMESTAMP WITH TIME ZONE DEFAULT now(),
  note text
);

-- insert some data to work with
INSERT INTO sample (date_created, note) VALUES (now() - '1 hour'::INTERVAL,'1');
INSERT INTO sample (date_created, note) VALUES (now() - '2 hours'::INTERVAL,'2');
INSERT INTO sample (date_created, note) VALUES (now() - '3 hours'::INTERVAL,'3');
INSERT INTO sample (date_created, note) VALUES (now() - '4 hours'::INTERVAL,'4');
INSERT INTO sample (date_created, note) VALUES (now() - '5 hours'::INTERVAL,'5');
INSERT INTO sample (date_created, note) VALUES (now() - '1 day 1 hour'::INTERVAL,'6');
INSERT INTO sample (date_created, note) VALUES (now() - '1 day 2 hours'::INTERVAL,'7');
INSERT INTO sample (date_created, note) VALUES (now() - '1 day 3 hours'::INTERVAL,'8');
INSERT INTO sample (date_created, note) VALUES (now() - '1 day 4 hours'::INTERVAL,'9');
INSERT INTO sample (date_created, note) VALUES (now() - '1 day 5 hours'::INTERVAL,'10');
INSERT INTO sample (date_created, note) VALUES (now() - '2 days 1 hour'::INTERVAL,'11');
INSERT INTO sample (date_created, note) VALUES (now() - '2 days 2 hours'::INTERVAL,'12');
INSERT INTO sample (date_created, note) VALUES (now() - '2 days 3 hours'::INTERVAL,'13');
INSERT INTO sample (date_created, note) VALUES (now() - '2 days 4 hours'::INTERVAL,'14');
INSERT INTO sample (date_created, note) VALUES (now() - '2 days 5 hours'::INTERVAL,'15');

I’ve used interval for creating different times. It’s nice that it’s descriptive, so I can write day or days interchangeably. I’ve noticed that it works like that when I’ve seen that we have an enum in our code that are in plural, like HOURS, DAYS, WEEKS - and then I looked at Postgres manual to see that it uses singular. But I tried with plural anyway and it worked. You can write 1 days too, even if it’s a grammatical error, it’s not for postgres.

Back to the example, let’s see what’s in sample table:

SELECT * FROM sample;
+--------------------------------------+-------------------------------+--------+
| id                                   | date_created                   | note   |
|--------------------------------------+-------------------------------+--------|
| 34d20fb6-9d53-45d1-9a97-5a89995253d7 | 2019-08-18 06:04:09.050167+02 | 1      |
| 6d9b8206-7942-4a00-8f82-54882f1f6391 | 2019-08-18 05:04:09.051335+02 | 2      |
| 4469966c-ce64-4229-90d1-c88b0b226b1e | 2019-08-18 04:04:09.051906+02 | 3      |
| f3dbda1e-369e-48bd-8002-4fa9347c9870 | 2019-08-18 03:04:09.052571+02 | 4      |
| ae57e584-c3c2-41bd-a533-94f09f561d75 | 2019-08-18 02:04:09.053061+02 | 5      |
| c7995f72-026c-40a6-b57d-e137b5080df9 | 2019-08-17 06:04:09.05356+02  | 6      |
| 1172baeb-5137-4a99-887d-b8a99c00ec93 | 2019-08-17 05:04:09.054049+02 | 7      |
| a873fb06-4617-499b-99d6-a447f5090a94 | 2019-08-17 04:04:09.05453+02  | 8      |
| fd49820b-d2be-4509-be08-fd8039090daa | 2019-08-17 03:04:09.055001+02 | 9      |
| f4ae359b-7e65-40e4-b35c-33963b3432f9 | 2019-08-17 02:04:09.055475+02 | 10     |
| 39ed438d-9f74-493a-9db4-a793bcdd43dc | 2019-08-16 06:04:09.055901+02 | 11     |
| 2d579b1f-26f5-4490-a804-ef0be9eaf266 | 2019-08-16 05:04:09.056367+02 | 12     |
| e35fd6d9-9849-41bc-9df3-6e16880ffa3e | 2019-08-16 04:04:09.05683+02  | 13     |
| ebc98756-05b3-4242-8a58-cb4be4d23483 | 2019-08-16 03:04:09.057394+02 | 14     |
| 9564c34c-15cc-4069-a41b-8acb4f10241d | 2019-08-16 02:04:09.057979+02 | 15     |
+--------------------------------------+-------------------------------+--------+

Now we want to order by date_created, first just date descending, then by time ascending.

SELECT * FROM sample ORDER BY date_created::DATE DESC, date_created ASC;
+--------------------------------------+-------------------------------+--------+
| id                                   | date_created                   | note   |
|--------------------------------------+-------------------------------+--------|
| ae57e584-c3c2-41bd-a533-94f09f561d75 | 2019-08-18 02:04:09.053061+02 | 5      |
| f3dbda1e-369e-48bd-8002-4fa9347c9870 | 2019-08-18 03:04:09.052571+02 | 4      |
| 4469966c-ce64-4229-90d1-c88b0b226b1e | 2019-08-18 04:04:09.051906+02 | 3      |
| 6d9b8206-7942-4a00-8f82-54882f1f6391 | 2019-08-18 05:04:09.051335+02 | 2      |
| 34d20fb6-9d53-45d1-9a97-5a89995253d7 | 2019-08-18 06:04:09.050167+02 | 1      |
| f4ae359b-7e65-40e4-b35c-33963b3432f9 | 2019-08-17 02:04:09.055475+02 | 10     |
| fd49820b-d2be-4509-be08-fd8039090daa | 2019-08-17 03:04:09.055001+02 | 9      |
| a873fb06-4617-499b-99d6-a447f5090a94 | 2019-08-17 04:04:09.05453+02  | 8      |
| 1172baeb-5137-4a99-887d-b8a99c00ec93 | 2019-08-17 05:04:09.054049+02 | 7      |
| c7995f72-026c-40a6-b57d-e137b5080df9 | 2019-08-17 06:04:09.05356+02  | 6      |
| 9564c34c-15cc-4069-a41b-8acb4f10241d | 2019-08-16 02:04:09.057979+02 | 15     |
| ebc98756-05b3-4242-8a58-cb4be4d23483 | 2019-08-16 03:04:09.057394+02 | 14     |
| e35fd6d9-9849-41bc-9df3-6e16880ffa3e | 2019-08-16 04:04:09.05683+02  | 13     |
| 2d579b1f-26f5-4490-a804-ef0be9eaf266 | 2019-08-16 05:04:09.056367+02 | 12     |
| 39ed438d-9f74-493a-9db4-a793bcdd43dc | 2019-08-16 06:04:09.055901+02 | 11     |
+--------------------------------------+-------------------------------+--------+

The date_created::DATE casts date_created to date and results in (i.e.) 2019-08-18, so we have groups by date which are sorted, and now inside those groups we have date_created sorted ascending. Note that ASC part can be omitted, but I’ve preserved it just to make a clear distinction of orders.