Django: 使用 order_by 对数值进行排序

31

我面临这样的情况:必须通过一个用于存储街道地址的CharField输出一份非常大的对象列表。

我的问题是,由于它是一个Charfield,显然数据按ASCII码排序,结果可预测。比如,数字会像这样排序;

1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 20, 21....

显然,下一步应该将CharField更改为正确的字段类型(比如IntegerField),但由于一些地址可能有公寓号,例如“128A”,因此这种方法无法起作用。

我真的不知道该如何正确排序这个问题……


想知道你是否找到了解决方案。非常感谢。 - Nathan Keller
这个解决方案在我的电脑上运行良好:https://dev59.com/hLHma4cB1Zd3GeqPT_6Z#54797177 - cem
11个回答

28

如果你确定该字段中只有整数,你可以通过使用 extra 方法将其转换为整数类型并按照该字段进行排序:

MyModel.objects.extra(
    select={'myinteger': 'CAST(mycharfield AS INTEGER)'}
).order_by('myinteger')

1
不是所有的地址都以数字开头。这种方法适用于“我的字符字段中有数字”的特殊情况,但是无法对混合数据进行排序。 - Dave W. Smith
非常有趣的额外使用方法,我很少尝试那种方法。但不幸的是,在我的情况下似乎无法工作。 - h3.
3
如果你的MYSQL版本不支持INTEGER,使用'SIGNED'或'UNSIGNED'代替它。 - Adriaan Tijsseling
5
由于django正在弃用extra()方法,下面介绍如何使用annotate()方法实现相同的功能:MyModel.objects.annotate(myinteger=RawSQL('CAST(mycharfield AS UNSIGNED)', params=[])).order_by('myinteger')。在这个方法中,通过引入annotate()RawSQL()方法,并将无符号字符字段转换为整数来实现所需的查询效果。最后按照新的整数字段进行排序。 - coredumperror

23

Django正在废弃extra()方法,但在v1.10中引入了Cast()。至少在sqlite中,CAST可以接受诸如10a这样的值,并将其转换为整数10,因此您可以执行以下操作:

from django.db.models import IntegerField
from django.db.models.functions import Cast

MyModel.objects.annotate(
    my_integer_field=Cast('my_char_field', IntegerField())
).order_by('my_integer_field', 'my_char_field')

这将返回按照街道号码首先按数字顺序,然后按字母顺序排序的对象,例如:...14, 15a, 15b, 16, 16a, 17...

2
Cast doesn't support values such as 10a. If it receives one, it throws DataError: invalid input syntax for integer. I found a workaround by removing all characters in the value that are not numbers (PostgreSQL):from django.db.models.expressions import F, Value, Func`queryset.annotate(my_integer_field=Cast(Func(F('my_char_field'), Value('[^\d]'), Value(''), Value('g'), function='regexp_replace'), IntegerField()))` - Aylen
1
@Filly,你能提供导致错误的完整示例吗? - practual
@practual 您的解决方案在 Sqlite3 上可以工作,但在 PostgreSQL 上会出现错误,例如 Filly 的评论中所述。 - ishak O.
当所有值都是数字时,它可以正常工作,但当结果中有一个字符串值时:类型整数的输入语法无效:"x13079" - cem

19

如果您正在使用PostgreSQL(不确定MySQL),则可以在char / text字段上安全使用以下代码,避免类型转换错误:

MyModel.objects.extra(
    select={'myinteger': "CAST(substring(charfield FROM '^[0-9]+') AS INTEGER)"}
).order_by('myinteger')

如果您需要按具有数字的字符串进行排序,并且模式为'.[0-9]+',则此方法是最佳选择。 - Ajoy
MariaDB的语法应该是:"CAST(REGEXP_SUBSTR(name, '^[0-9]+') AS INTEGER)"。谢谢! - Igor Sobreira
太棒了,当我查找该字段的外键时,我该如何做? - Alex Stewart
这对我不起作用,我得到的错误是:ProgrammingError: syntax error at or near "[" LINE 1: ...ECT DISTINCT ('SELECT CAST(substring(value FROM '^[0-9]+') A...。我使用的是PostgreSQL 11、Python 2.7和Django 1.11。我认为这可能是因为我使用的版本过旧。 - andramos

4
我知道我来晚了,但由于这与问题有密切关系,并且我花了很长时间才找到这个:
您必须知道可以直接将Cast放入模型的ordering选项中。
from django.db import models
from django.db.models.functions import Cast


class Address(models.Model):

    street_number = models.CharField()

    class Meta:
        ordering = [
            Cast("street_number", output_field=models.IntegerField()),
        ]

在有关排序的文档中,你也可以使用查询表达式。

而从数据库函数的文档中得知,函数也是表达式,因此它们可以与聚合函数等其他表达式一起使用和组合。


3

很好的提示!对我很有用!:) 这是我的代码:

revisioned_objects = revisioned_objects.extra(select={'casted_object_id': 'CAST(object_id AS INTEGER)'}).extra(order_by = ['casted_object_id'])

2
您面临的问题与按文件名排序时文件名的排序方式非常相似。在那里,您希望“2 Foo.mp3”出现在“12 Foo.mp3”之前。
一种常见的方法是将数字“标准化”为扩展到固定位数,然后基于标准化形式进行排序。也就是说,为了排序,“2 Foo.mp3”可能会扩展为“0000000002 Foo.mp3”。
Django 不能直接帮助您解决这个问题。您可以添加一个字段来存储“标准化”的地址,并让数据库使用 order_by 进行排序,或者在将记录列表传递给模板之前,在视图中(或在视图使用的帮助程序中)对地址记录进行自定义排序。

2

在我的情况下,我有一个名为“name”的CharField,它具有混合(int+string)值,例如。“a1”,“f65”,“P”,“55”等。

通过使用sql cast解决了这个问题(已在postgres和mysql中测试),首先,我尝试按转换后的整数值进行排序,然后再按名称字段的原始值进行排序。

parking_slots = ParkingSlot.objects.all().extra(
        select={'num_from_name': 'CAST(name AS INTEGER)'}
    ).order_by('num_from_name', 'name')

无论如何,这种方式对我来说都能正确排序。

2

如果您需要对由点分隔的多个数字组成的版本号进行排序(如1.9.0,1.10.0),这里有一个仅适用于Postgres的解决方案:

class VersionRecordManager(models.Manager):

    def get_queryset(self):
        return super().get_queryset().extra(
            select={
                'natural_version': "string_to_array(version, '.')::int[]",
            },
        )

    def available_versions(self):
        return self.filter(available=True).order_by('-natural_version')

    def last_stable(self):
        return self.available_versions().filter(stable=True).first()

class VersionRecord(models.Model):
    objects = VersionRecordManager()
    version = models.CharField(max_length=64, db_index=True)
    available = models.BooleanField(default=False, db_index=True)
    stable = models.BooleanField(default=False, db_index=True)

如果您想允许非数字字符(例如0.9.0 beta2.0.0 stable):
def get_queryset(self):
    return super().get_queryset().extra(
        select={
            'natural_version':
                "string_to_array(                     "  
                "   regexp_replace(                   "  # Remove everything except digits
                "       version, '[^\d\.]+', '', 'g'  "  # and dots, then split string into
                "   ), '.'                            "  # an array of integers.
                ")::int[]                             "
        }
    )

1
我正在寻找一种方法来对CharField中的数字字符进行排序,我的搜索引导我到了这里。我的对象中的name字段是CC许可证,例如'CC BY-NC 4.0'。
由于extra()将被弃用,所以我能够用这种方式完成:
MyObject.objects.all()
    .annotate(sorting_int=Cast(Func(F('name'), Value('\D'), Value(''), Value('g'), function='regexp_replace'), IntegerField()))
    .order_by('-sorting_int')

因此,具有name='CC BY-NC 4.0'MyObject现在具有sorting_int=40

有没有关于如何处理空字符串的想法? - kei nagae

0

这个线程中的所有答案对我来说都不起作用,因为它们假定是数字文本。我找到了一个适用于一部分情况的解决方案。考虑这个模型

Class Block(models.Model):
      title = models.CharField()

假设我有一些字段,有时会有前导字符和尾数数字。如果我尝试正常排序

 >>> Block.objects.all().order_by('title')
<QuerySet [<Block: 1>, <Block: 10>, <Block: 15>, <Block: 2>, <Block: N1>, <Block: N12>, <Block: N4>]>

正如所预期的那样,按字母顺序排列是正确的,但对我们人类来说毫无意义。我为这个特定的用例使用的技巧是将找到的任何文本替换为数字9999,然后将该值强制转换为整数并按其排序。
对于大多数具有前导字符的情况,这将获得所需的结果。请参见下面的内容。
from django.db.models.expressions import RawSQL

>>> Block.objects.all()\
.annotate(my_faux_integer=RawSQL("CAST(regexp_replace(title, '[A-Z]+', '9999', 'g') AS INTEGER)", ''))\    
.order_by('my_faux_integer', 'title')
    
<QuerySet [<Block: 1>, <Block: 2>, <Block: 10>, <Block: 15>, <Block: N1>, <Block: N4>, <Block: N12>]>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接