You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Rails中结合SQL GROUP子句与FOR UPDATE SKIP LOCKED?

Rails中结合GROUP BY与FOR UPDATE SKIP LOCKED的ActiveRecord实现问题

问题描述

我希望在Rails中使用ActiveRecord而非原生SQL,实现结合SQL GROUP子句与FOR UPDATE SKIP LOCKED的功能。现有通用模型Job,包含client_class属性,需按该属性分组,同时排除超过类变量CLIENT_LIMITS设定上限的客户端。代码实现如下:

CLIENT_LIMITS = { CLIENT_1: 2, CLIENT_2: 3, ... }.freeze

unavailable_clients = Job.running
  .group(:client_class)
  .count
  .select { |k, v| v >= CLIENT_LIMITS[k] }
  .keys

job = Job.lock("FOR UPDATE SKIP LOCKED")
  .queued
  .where.not(client_class: unavailable_clients)
  .sample

错误信息

运行后出现如下错误:

.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/activerecord-6.1.4.1/lib/active_record/connection_adapters/postgresql_adapter.rb:672:in `exec_params': PG::FeatureNotSupported: ERROR:  FOR UPDATE is not allowed with GROUP BY clause (ActiveRecord::StatementInvalid)

.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/activerecord-6.1.4.1/lib/active_record/connection_adapters/postgresql_adapter.rb:672:in `exec_params': ERROR:  FOR UPDATE is not allowed with GROUP BY clause (PG::FeatureNotSupported)

解决方案

核心原因

PostgreSQL不允许同一个查询中同时包含GROUP BY和FOR UPDATE子句。你的代码逻辑上是分两次查询,但可能因为作用域包含GROUP BY、延迟加载特性或sample方法的数据库层面执行,导致两个查询逻辑被意外合并,触发了该错误。

具体修复步骤

  1. 在SQL层面完成超量客户端筛选,隔离统计查询
    把超量客户端的判断逻辑放到SQL的HAVING子句中,直接从数据库获取结果,确保统计查询独立执行,不会和后续锁查询合并:

    # 动态生成匹配各客户端上限的HAVING条件
    having_conditions = CLIENT_LIMITS.map do |client, limit|
      "client_class = '#{client}' AND COUNT(*) >= #{limit}"
    end.join(" OR ")
    
    unavailable_clients = Job.running
      .group(:client_class)
      .having(having_conditions)
      .pluck(:client_class)
      .map(&:to_sym) # 保证和CLIENT_LIMITS的key类型一致
    
  2. 替换sample方法,避免数据库层面的查询冲突
    sample在数据量大时会自动切换为数据库层面的随机查询,可能和锁语句产生冲突。改用显式的数据库随机排序+取第一条,逻辑更可控:

    job = Job.lock("FOR UPDATE SKIP LOCKED")
      .queued
      .where.not(client_class: unavailable_clients)
      .order(Arel.sql('RANDOM()')) # 用Arel规避SQL注入风险
      .first
    
  3. 检查并清理作用域中的GROUP BY
    确认running和queued作用域中没有包含GROUP BY语句,如果有,需要调整作用域逻辑,或者在查询时通过unscope(:group)移除分组:

    # 若running作用域自带GROUP BY,查询时手动移除
    unavailable_clients = Job.running.unscope(:group)
      .group(:client_class)
      .having(having_conditions)
      .pluck(:client_class)
    

内容的提问来源于stack exchange,提问作者kskickislk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 03:20:14