diff -r b3394ccce096 django/contrib/admin/media/css/base.css
--- a/django/contrib/admin/media/css/base.css	Tue May 31 21:29:35 2011 +0000
+++ b/django/contrib/admin/media/css/base.css	Wed Jun 01 17:31:26 2011 +0100
@@ -326,6 +326,36 @@
     background: url(../img/admin/arrow-down.gif) right .4em no-repeat;
 }
 
+table thead th.sorted a span.text {
+   display: block;
+   float: left;
+}
+
+table thead th.sorted a span.sortpos {
+   display: block;
+   float: right;
+   font-size: .6em;
+}
+
+table thead th.sorted a img {
+   vertical-align: bottom;
+   /* Make it look like a link */
+   border-bottom: 1px solid #5b80b2;
+}
+
+table thead th.sorted a span.clear {
+   display: block;
+   clear: both;
+}
+
+#sorting-popup-div {
+    position: absolute;
+    background-color: white;
+    border: 1px solid #ddd;
+    z-index: 2000; /* more than filters on right */
+    padding-right: 10px;
+}
+
 /* ORDERABLE TABLES */
 
 table.orderable tbody tr td:hover {
diff -r b3394ccce096 django/contrib/admin/templates/admin/change_list_results.html
--- a/django/contrib/admin/templates/admin/change_list_results.html	Tue May 31 21:29:35 2011 +0000
+++ b/django/contrib/admin/templates/admin/change_list_results.html	Wed Jun 01 17:31:26 2011 +0100
@@ -1,3 +1,4 @@
+{% load adminmedia %}
 {% if result_hidden_fields %}
 <div class="hiddenfields">{# DIV for HTML validation #}
 {% for item in result_hidden_fields %}{{ item }}{% endfor %}
@@ -8,10 +9,18 @@
 <table id="result_list">
 <thead>
 <tr>
-{% for header in result_headers %}<th scope="col"{{ header.class_attrib }}>
-{% if header.sortable %}<a href="{{ header.url }}">{% endif %}
-{{ header.text|capfirst }}
-{% if header.sortable %}</a>{% endif %}</th>{% endfor %}
+{% for header in result_headers %}
+<th scope="col" {{ header.class_attrib }}>
+  {% if header.sortable %}<a href="{{ header.url }}">{% endif %}
+  <span class="text">{{ header.text|capfirst }}</span>
+  {% if header.sortable %}
+    {% if header.sort_pos > 0 %}<span class="sortpos">
+      {% if header.sort_pos == 1 %}<img id="primary-sort-icon" src="{% admin_media_prefix %}img/admin/icon_primary_sort.png" alt="Primary sort field" />{% endif %}
+      {{ header.sort_pos }}</span>
+    {% endif %}
+    <span class="clear"></span></a>
+  {% endif %}
+</th>{% endfor %}
 </tr>
 </thead>
 <tbody>
@@ -24,4 +33,49 @@
 </tbody>
 </table>
 </div>
+
+{# Sorting popup: #}
+<div style="display: none;" id="sorting-popup-div">
+<p>Sorting by:</p>
+<ol>
+{% for header in result_headers|dictsort:"sort_pos" %}
+  {% if header.sort_pos > 0 %}
+    <li>{{ header.text|capfirst }}</li>
+  {% endif %}
+{% endfor %}
+</ol>
+<p><a href="{{ reset_order_url }}">Reset sorting</a></p>
+</div>
+<script type="text/javascript">
+<!--
+(function($) {
+    $(document).ready(function() {
+        var popup = $('#sorting-popup-div');
+        /* These next lines seems necessary to prime the popup: */
+        popup.offset({left:-1000, top:0});
+        popup.show();
+        var popupWidth = popup.width()
+        popup.hide();
+
+        $('#primary-sort-icon').toggle(function(ev) {
+                                          ev.preventDefault();
+                                          var img = $(this);
+                                          var pos = img.offset();
+                                          pos.top += img.height();
+                                          if (pos.left + popupWidth >
+                                              $(window).width()) {
+                                              pos.left -= popupWidth;
+                                          }
+                                          popup.show();
+                                          popup.offset(pos);
+                                      },
+                                      function(ev) {
+                                          ev.preventDefault();
+                                          popup.hide();
+                                      });
+    });
+})(django.jQuery);
+//-->
+</script>
+
 {% endif %}
diff -r b3394ccce096 django/contrib/admin/templatetags/admin_list.py
--- a/django/contrib/admin/templatetags/admin_list.py	Tue May 31 21:29:35 2011 +0000
+++ b/django/contrib/admin/templatetags/admin_list.py	Wed Jun 01 17:31:26 2011 +0100
@@ -93,6 +93,7 @@
             if field_name == 'action_checkbox':
                 yield {
                     "text": header,
+                    "sort_pos": 0,
                     "class_attrib": mark_safe(' class="action-checkbox-column"')
                 }
                 continue
@@ -100,7 +101,7 @@
             # It is a non-field, but perhaps one that is sortable
             admin_order_field = getattr(attr, "admin_order_field", None)
             if not admin_order_field:
-                yield {"text": header}
+                yield {"text": header, "sort_pos": 0 }
                 continue
 
             # So this _is_ a sortable non-field.  Go to the yield
@@ -110,14 +111,44 @@
 
         th_classes = []
         new_order_type = 'asc'
-        if field_name == cl.order_field or admin_order_field == cl.order_field:
-            th_classes.append('sorted %sending' % cl.order_type.lower())
-            new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
+        ordering_fields = cl.get_ordering_fields()
+        sort_pos = 0
+        if field_name in ordering_fields or admin_order_field in ordering_fields:
+            if not field_name in ordering_fields:
+                field_name = admin_order_field
+            order_type = ordering_fields.get(field_name).lower()
+            sort_pos = ordering_fields.keys().index(field_name) + 1
+            th_classes.append('sorted %sending' % order_type)
+            new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type]
+
+        # build new ordering param
+        o_list = []
+        make_qs_param = lambda t, n: ('-' if t == 'desc' else '') + str(n)
+
+        for f in ordering_fields.keys():
+            try:
+                n = cl.list_display.index(f)
+            except ValueError:
+                continue
+
+            if f == field_name:
+                # We want clicking on this header to bring the ordering to the
+                # front
+                o_list.insert(0, make_qs_param(new_order_type, n))
+            else:
+                o_list.append(make_qs_param(ordering_fields.get(f), n))
+
+        if field_name not in ordering_fields:
+            n = cl.list_display.index(field_name)
+            o_list.insert(0, make_qs_param(new_order_type, n))
+
+        o_list = '.'.join(o_list)
 
         yield {
             "text": header,
             "sortable": True,
-            "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
+            "sort_pos": sort_pos,
+            "url": cl.get_query_string({ORDER_VAR: o_list}),
             "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')
         }
 
@@ -231,6 +262,7 @@
     return {'cl': cl,
             'result_hidden_fields': list(result_hidden_fields(cl)),
             'result_headers': list(result_headers(cl)),
+            'reset_order_url': cl.get_query_string(remove=[ORDER_VAR]),
             'results': list(results(cl))}
 
 @register.inclusion_tag('admin/date_hierarchy.html')
diff -r b3394ccce096 django/contrib/admin/views/main.py
--- a/django/contrib/admin/views/main.py	Tue May 31 21:29:35 2011 +0000
+++ b/django/contrib/admin/views/main.py	Wed Jun 01 17:31:26 2011 +0100
@@ -3,6 +3,7 @@
 from django.core.exceptions import SuspiciousOperation
 from django.core.paginator import InvalidPage
 from django.db import models
+from django.utils.datastructures import SortedDict
 from django.utils.encoding import force_unicode, smart_str
 from django.utils.translation import ugettext, ugettext_lazy
 from django.utils.http import urlencode
@@ -75,7 +76,7 @@
             self.list_editable = ()
         else:
             self.list_editable = list_editable
-        self.order_field, self.order_type = self.get_ordering()
+        self.ordering = self.get_ordering()
         self.query = request.GET.get(SEARCH_VAR, '')
         self.query_set = self.get_query_set(request)
         self.get_results(request)
@@ -171,35 +172,46 @@
         # manually-specified ordering from the query string.
         ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
 
-        if ordering[0].startswith('-'):
-            order_field, order_type = ordering[0][1:], 'desc'
-        else:
-            order_field, order_type = ordering[0], 'asc'
         if ORDER_VAR in params:
-            try:
-                field_name = self.list_display[int(params[ORDER_VAR])]
+            # Clear ordering and used params
+            ordering = []
+            order_params = params[ORDER_VAR].split('.')
+            for p in order_params:
                 try:
-                    f = lookup_opts.get_field(field_name)
-                except models.FieldDoesNotExist:
-                    # See whether field_name is a name of a non-field
-                    # that allows sorting.
+                    none, pfx, idx = p.rpartition('-')
+                    field_name = self.list_display[int(idx)]
                     try:
-                        if callable(field_name):
-                            attr = field_name
-                        elif hasattr(self.model_admin, field_name):
-                            attr = getattr(self.model_admin, field_name)
-                        else:
-                            attr = getattr(self.model, field_name)
-                        order_field = attr.admin_order_field
-                    except AttributeError:
-                        pass
-                else:
-                    order_field = f.name
-            except (IndexError, ValueError):
-                pass # Invalid ordering specified. Just use the default.
-        if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
-            order_type = params[ORDER_TYPE_VAR]
-        return order_field, order_type
+                        f = lookup_opts.get_field(field_name)
+                    except models.FieldDoesNotExist:
+                        # See whether field_name is a name of a non-field
+                        # that allows sorting.
+                        try:
+                            if callable(field_name):
+                                attr = field_name
+                            elif hasattr(self.model_admin, field_name):
+                                attr = getattr(self.model_admin, field_name)
+                            else:
+                                attr = getattr(self.model, field_name)
+                            field_name = attr.admin_order_field
+                        except AttributeError:
+                            pass
+                    else:
+                        field_name = f.name
+
+                    ordering.append(pfx + field_name)
+
+                except (IndexError, ValueError):
+                    pass # Invalid ordering specified. Skip it.
+
+        return ordering
+
+    def get_ordering_fields(self):
+        # Returns a SortedDict of ordering fields and asc/desc
+        ordering_fields = SortedDict()
+        for o in self.ordering:
+            none, t, f = o.rpartition('-')
+            ordering_fields[f] = 'desc' if t == '-' else 'asc'
+        return ordering_fields
 
     def get_lookup_params(self, use_distinct=False):
         lookup_params = self.params.copy() # a dictionary of the query string
@@ -290,8 +302,8 @@
                             break
 
         # Set ordering.
-        if self.order_field:
-            qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
+        if self.ordering:
+            qs = qs.order_by(*self.ordering)
 
         # Apply keyword searches.
         def construct_search(field_name):
