是否应弃用Django ORM改用SQL以降低带宽与查询耗时?
问题背景
出于兴趣首次使用Django ORM开发以太坊浏览器,定义了以下数据模型:
class AddressModel(models.Model): id = models.BigIntegerField(primary_key=True) first_seen = models.DateTimeField(db_index=True) addr = models.CharField(max_length=42, db_index=True, on_delete=models.PROTECT) is_contract = models.BooleanField() is_token = models.BooleanField() is_wallet = models.BooleanField() class BlockModel(models.Model): id = models.BigIntegerField(primary_key=True) number = models.BigIntegerField() status = models.CharField(max_length=20) timestamp = models.DateTimeField(db_index=True) epoch_proposal = models.IntegerField() slot_proposal = models.IntegerField() fee_recipient = models.ForeignKey(AddressModel, on_delete=models.PROTECT) block_reward = models.BigIntegerField() total_difficulty = models.CharField(max_length=100) size = models.IntegerField() gas_used = models.BigIntegerField() gas_limit = models.BigIntegerField() base_fee_per_gas = models.BigIntegerField() burnt_fee = models.BigIntegerField() extra_data = models.TextField() hash = models.CharField(max_length=66) parent_hash = models.CharField(max_length=66) state_root = models.CharField(max_length=66) withdrawal_root = models.CharField(max_length=66) Nonce = models.CharField(max_length=20) class TransactionModel(models.Model): id = models.BigIntegerField(primary_key=True) hash = models.CharField(max_length=66) block = models.ForeignKey(BlockModel, on_delete=models.PROTECT) from_addr = models.ForeignKey('AddressModel', related_name='from_addr', on_delete=models.PROTECT) to_addr = models.ForeignKey('AddressModel', related_name='to_addr', on_delete=models.PROTECT) input = models.TextField() is_valid = models.BooleanField()
担忧点:查询特定from_addr关联的所有交易时,担心ORM会自动获取关联的block完整数据;若该地址有1万条交易,就会返回1万条无需的block数据,造成带宽浪费并增加查询耗时。疑问:这是否属于不应使用ORM的场景?
解答
你的担忧其实是对Django ORM加载机制的误解——默认情况下,Django ORM采用懒加载策略:当你查询addr.from_addr.all()(或TransactionModel.objects.filter(from_addr=addr))时,ORM只会返回TransactionModel的数据,并不会自动加载关联的BlockModel完整数据。只有当你主动访问某个transaction对象的block属性(比如transaction.block.number)时,才会单独查询这条Block数据,此时若有1万条交易,会触发1万次额外查询(N+1问题),才会造成性能损耗。
但解决这个问题非常简单,Django ORM提供了多种方式精准控制查询范围:
- 只获取交易数据及block_id:如果仅需交易本身的字段和关联的block_id,直接查询时指定需要的字段即可,示例代码:
# 仅查询交易必要字段和block_id,不会加载BlockModel实例 target_addr = AddressModel.objects.get(addr="0x...") transactions = TransactionModel.objects.filter(from_addr=target_addr).only( 'id', 'hash', 'from_addr_id', 'to_addr_id', 'input', 'is_valid', 'block_id' )
此时返回的每个transaction对象中,block_id就是关联的Block主键,和原生SQL返回结果完全一致,不会额外查询Block数据。
- 按需加载Block的部分字段:如果确实需要Block的部分信息(比如区块号、时间戳),可以用
select_related结合only/defer做连表查询,只获取需要的Block字段,避免加载整个Block对象:
# 连表查询,但仅获取交易字段和Block的number、timestamp transactions = TransactionModel.objects.filter(from_addr=target_addr).select_related('block').only( 'id', 'hash', 'input', 'block__number', 'block__timestamp' )
这种方式只会执行一次SQL联表查询,返回结果包含需要的Block字段,不会加载不必要的数据(如extra_data、state_root等)。
- 避免N+1查询:若之前不小心触发了N+1问题,针对外键关联(Transaction到Block是多对一),使用
select_related可以一次性加载所有关联的Block数据,替代多次单独查询。
总结:这绝对不属于不该用ORM的场景。Django ORM的灵活性完全能满足这类性能需求,只要掌握好字段选择和关联加载的控制方法,既能享受ORM带来的开发效率,又能达到和原生SQL相近的性能表现。
内容的提问来源于stack exchange,提问作者sliders_alpha

