0

I have a table columns that is something like this:

id     |   date
1      |   January 1 2014 12:00 AM
2      |   February 1 2014 12:00 AM
3      |   March 1 2014 12:00 AM
4      |   April 1 2014 12:00 AM
5      |   May 1 2014 12:00 AM
6      |   June 1 2014 12:00 AM
7      |   July 15 2014 12:00 AM
8      |   July 15 2014 1:00 PM
9      |   July 15 2014 2:00 PM
10     |   July 15 2014 3:00 PM

Since ID 1-6 is already passed, I want them to be sorted from the bottom, and the latest one that is upcoming should be sorted at the top.

Desired output:

id     |   date
7      |   July 15 2014 12:00 AM
8      |   July 15 2014 1:00 PM
9      |   July 15 2014 2:00 PM
10     |   July 15 2014 3:00 PM
6      |   June 1 2014 12:00 AM
5      |   May 1 2014 12:00 AM
4      |   April 1 2014 12:00 AM
3      |   March 1 2014 12:00 AM
2      |   February 1 2014 12:00 AM
1      |   January 1 2014 12:00 AM
double-beep
  • 5,031
  • 17
  • 33
  • 41

5 Answers5

1

Best way I can think of is to first sort on future/historic dates, then sort the historic dates reversed and the future dates normally.

Simply reversing the date by -1 doesn't work, because MySQL will not be able to handle date type in combination with number type to sort on. Therefore, in both case it needs to be converted to a number.

So, like this:

ORDER BY
    (CASE WEN date1 < NOW() THEN 1 ELSE 0 END),
    (CASE WHEN date1 < NOW() THEN -1 * date1 ELSE 1 * date1 END)

Fiddle, based on fiddle from VBlades: http://sqlfiddle.com/#!2/eb83b/17

wvdz
  • 16,251
  • 4
  • 53
  • 90
  • Hi, I fiddled your solution here (http://sqlfiddle.com/#!2/eb83b/1). Didn't seem to work Wasn't quite producing the results OP was intending. I put an answer up, but interested in a better way. Did I set up the fiddle wrong or can you tweak your answer? Thanks. – VBlades Jun 14 '14 at 22:10
  • Thanks, I found the problem after 'fiddling' around a bit. See my revised answer. – wvdz Jun 15 '14 at 09:18
  • This is cool. I can't wrap my head around what's going on in (-1 * date1) - can you please explain what's going on here a bit? This is an illegal operation in SQL Server, I believe. Is this a MySQL thing? Thanks, just trying to understand a bit more. – VBlades Jun 15 '14 at 11:34
  • I assumed this question was about MySQL because of the PHP tag, which is usually combined with MySQL. Doing `1 * date` does an implicit conversion from date to number. In this fiddle you can see exactly what's going on: http://sqlfiddle.com/#!2/eb83b/18. – wvdz Jun 15 '14 at 12:37
  • Ok, I get it, thanks for the fiddle. I like this solution, very simple. Thanks. – VBlades Jun 15 '14 at 18:43
0

Depending on your DBMS the date arithmetic is a bit differerent. The trick however is to use a case expression to map historical dates into the future. Something like:

order by date_col + case when date_col < now() then 100 else 0 end year
Lennart - Slava Ukraini
  • 6,936
  • 1
  • 20
  • 32
  • Hi, this will not handle the two sorts OP is looking for. With DESC at the end of your statement, the order of the ids returned will be 7, 8, 9, 10, 1, 2 3, 4, 5, 6. – VBlades Jun 14 '14 at 21:40
  • Not only will it ignore indices, it might give wrong results flat out (ie, if it effectively adds 100 days, and there are future event 100 days or more in the future...). – Clockwork-Muse Jun 14 '14 at 23:40
0

Here's a way to do it in SQL Server, but should be easily portable (http://sqlfiddle.com/#!3/89d35/24):

select
          id,
          date
from
  (select
            (select count(*) FROM table1 where id <= t1.id and date >= getdate()) as row,
            *,
            1 AS TableSort
  from
            table1 t1
  where
            t1.date >= getdate()

  union all

  select
            (select count(*) FROM table1 where id >= t2.id and date < getdate()) as row,
            *,
            2 AS TableSort
  from
            table1 t2
  where
            date < getdate()) t3
order by
          tablesort,
          row
VBlades
  • 2,241
  • 1
  • 12
  • 8
  • Yeah, I know what you mean and was using ROW_NUMBER() first but didn't want a db-specific solution. I thought about it, but I am not sure how window functions are handled in other systems (I am mostly a SQL Server guy), so I wanted to stay away from them. Even if you UNION ALL both sets, how could you uniquely identify each row in the result set in sorted order without a window function? – VBlades Jun 14 '14 at 23:56
  • That's the first time I've heard that about UNION, actually. I don't feel confident relying on that, sounds risky. But nice solution. Not sure if ID will need to be ordered in case of duplicate dates, but OP can figure it out. – VBlades Jun 15 '14 at 00:47
  • Hmm... actually, [I think you may be right](http://stackoverflow.com/questions/4329598/sql-union-order/4329764#4329764). Especially because there seems to be some indication PostgreSQL will combine rows in whatever order it pleases (probably due to query parallelism). I think I need to ask a new question... – Clockwork-Muse Jun 15 '14 at 03:43
  • ...whups, completely right - the MySQL documentation states that an inner `ORDER BY` without a `LIMIT` in this context may be optimized away. Goodbye expected results! Thanks for making me look into this more. – Clockwork-Muse Jun 15 '14 at 03:58
0

Your problem is that you have essentially two queries here, with different ordering. If you're not able to actually run them as two queries (that is, in your application layer), you need to get fancy.

However:

  • The problem with ORDER BY CASE... solutions is that they are (almost) guaranteed to never use an index - the optimizer can't page up/down through the index because the order is dependent on the row value.
  • The same limitation as above applies to doing various types of "math" on the desired columns. Plus the additional overhead of whatever math you're doing.

If your result set is small, the above is a non-issue. On dbs of any real size, though, this is important. So...

When you have two (semi-related) queries to combine, you use UNION ALL (or UNION if there are duplicates to remove). The problem is that the resulting sets are stitched together in an "undefined" order (this is not always explicitly spelled out in documentation). Reasons for this vary, but will include if the system is using data parallelism, which side may currently be waiting for a lock, or just because it's Tuesday in Sunnydale. So we'll need an ORDER BY...

Except the result in your query essentially requires two (different) ORDER BYs. And we can only supply one for use with a UNION (ALL) operator (because the sort will be performed on the combined set). Fortunately, different ordering can be given for each column for the clause! And UNION would allow us to supply a "dummy" null column - so where does that sort results?

SELECT id, COALESCE(date, other) AS sortedDate
FROM (SELECT id, date, null AS other
      FROM <sourceTable>
      WHERE date >= CURRENT_TIMESTAMP
      UNION ALL
      SELECT id, null, date
      FROM <sourceTable>
      WHERE date < CURRENT_TIMESTAMP) Derived
ORDER BY date ASC, other DESC

PostgreSQL Fiddle Demo

Why does this work? In RDBMSs, nulls sort to one end of the range. For PostgreSQL, Oracle, and DB2, they are sorted as the "greatest" value. That is, an intermediate table that looks like this is generated:

id     |   date                       |  other
7      |   July 15 2014 12:00 AM      |  null
8      |   July 15 2014 1:00 PM       |  null
9      |   July 15 2014 2:00 PM       |  null
10     |   July 15 2014 3:00 PM       |  null
6      |   null                       |  June 1 2014 12:00 AM
5      |   null                       |  May 1 2014 12:00 AM
4      |   null                       |  April 1 2014 12:00 AM
3      |   null                       |  March 1 2014 12:00 AM
2      |   null                       |  February 1 2014 12:00 AM
1      |   null                       |  January 1 2014 12:00 AM

... you can see how this compares with the ordering expressed in the ORDER BY, right?

Unfortunately, for MySQL and SQL Server, null is the lowest value, so we have to change our statement!

SELECT id, COALESCE(date, other) AS sortedDate
FROM (SELECT id, 1 AS orderGroup, date, null AS other
      FROM <sourceTable>
      WHERE date >= CURRENT_DATE
      UNION ALL
      SELECT id, 2, null, date
      FROM <sourceTable>
      WHERE date < CURRENT_DATE) Derived
ORDER BY orderGroup ASC, date ASC, other DESC

SQL Server Fiddle Demo

... here, the intermediate table looks like this:

id   |  og  |  date                       |  other
7    |  1   |  July 15 2014 12:00 AM      |  null
8    |  1   |  July 15 2014 1:00 PM       |  null
9    |  1   |  July 15 2014 2:00 PM       |  null
10   |  1   |  July 15 2014 3:00 PM       |  null
6    |  2   |  null                       |  June 1 2014 12:00 AM
5    |  2   |  null                       |  May 1 2014 12:00 AM
4    |  2   |  null                       |  April 1 2014 12:00 AM
3    |  2   |  null                       |  March 1 2014 12:00 AM
2    |  2   |  null                       |  February 1 2014 12:00 AM
1    |  2   |  null                       |  January 1 2014 12:00 AM

(incidently, this version will work even when null sorts as "greatest")

So what about the ability for the db to use indices?

I don't know. (I don't have a good box/sample set to test on anyways...)

It's going to depend on how smart the optimizer is, and if it can extract the constants from the SELECT lists. If so, I expect that it's going to scan the relevant index twice - once ascending and once descending, from the start of the range.

Clockwork-Muse
  • 12,806
  • 6
  • 31
  • 45
0

Simple:

select  *
from test
order by case when date < now() then 1 else 0 end, date;
TommCatt
  • 5,498
  • 1
  • 13
  • 20