Rails应用AJAX响应缓慢 多表关联查询优化方案咨询
RoR 三表关联慢查询与AJAX响应优化方案
一、优先补充数据库索引(性能提升最明显)
慢查询80%以上的成因是缺失匹配的索引,针对该场景需要补充核心索引:
patients表的disease_code_id字段加普通索引,这是where筛选的核心条件,无索引时会触发全表扫描- 中间关联表
patient_phases加(patient_id, phase_id)联合唯一索引,覆盖两表关联的外键字段,避免join时回表查询 recruitment_phases表的phase_id如果未设为主键,需要加唯一索引保证关联查询效率
索引添加的迁移代码示例:
class AddIndexForPhaseQuery < ActiveRecord::Migration[6.1] def change add_index :patients, :disease_code_id add_index :patient_phases, [:patient_id, :phase_id], unique: true end end
二、重构低效SQL查询逻辑
原查询存在两个明显的性能问题:一是手写硬编码join语句容易出现逻辑冗余,二是用全量join后distinct去重的方式,会产生大量无效中间结果集,优化方案如下:
- 先配置标准ActiveRecord关联,替代手写SQL join
# app/models/patient.rb class Patient < ApplicationRecord has_many :patient_phases has_many :recruitment_phases, through: :patient_phases end # app/models/patient_phase.rb class PatientPhase < ApplicationRecord belongs_to :patient belongs_to :recruitment_phase end
- 用EXISTS子查询替代三表全量inner join,数据库匹配到符合条件的记录就会终止扫描,不需要拉取全量关联数据后再去重;同时去掉冗余的
distinct(patients.disease_code_id)——where条件已经固定disease_code_id为传入参数,不需要额外查询和去重该字段
# 优化后的控制器查询代码 def phasenames @disease_code_id = params[:disease_code_id] @phase_names = RecruitmentPhase.where( exists: PatientPhase.joins(:patient) .where(patients: { disease_code_id: @disease_code_id }) .where("recruitment_phases.phase_id = patient_phases.phase_id") ).select(:phase_id, :phase_name) respond_to do |format| format.html format.js format.json { render json: @phase_names } end end
*注意:原路由配置中phasenames路径携带了未被控制器使用的:id参数,建议简化路由减少匹配开销:
# routes.rb 对应配置修改 get 'transfer_requests/phasenames/:disease_code_id' => "transfer_requests#phasenames"
三、优化前端请求逻辑,减少无效请求
原前端逻辑存在串行两次AJAX请求的问题:选中cohort后先请求fallas接口拿到disease_code_id,再发第二次请求拿阶段列表,用户等待时间直接翻倍。优化方案是在页面渲染时直接把cohort和disease_code_id的映射关系存在DOM属性上,省去第一次请求:
- 表单渲染对应的action(new/edit)中提前查询映射关系:
# 对应表单页的控制器方法中添加 @cohort_disease_map = Cohort.pluck(:name, :disease_code_id).to_h
- 视图中把映射关系绑定到cohort选择框的data属性上:
= f.association :cohort, collection: Cohort.all, prompt: 'Select Cohort', id: 'cohort', :input_html => { :onchange => "popup()", :class => "stext", data: {disease_map: @cohort_disease_map.to_json} }
*补充:原代码用onclick触发下拉事件不符合交互逻辑,改成onchange在选项变化时触发即可
3. 简化JS逻辑,只发一次AJAX请求,同时修复原代码option值错误的bug(原代码把value设为disease_code_id,提交表单时会传错recruitment_phase_id):
function popup() { const $phaseSelect = $("#transfer_request_recruitment_phase_id"); const defaultOption = '<option value="-1">Select Recruitment Phase</option>'; $phaseSelect.html(defaultOption); const cohortSelect = document.getElementById('transfer_request_cohort_id'); const selectedIndex = cohortSelect.selectedIndex; if (selectedIndex === 0) return; // 选中提示项时不发请求 const selectedCohortName = cohortSelect.options[selectedIndex].text.trim(); const diseaseMap = JSON.parse(cohortSelect.dataset.diseaseMap); const targetDiseaseId = diseaseMap[selectedCohortName]; if (!targetDiseaseId) return; $.ajax({ url: `/transfer_requests/phasenames/${targetDiseaseId}`, dataType:'json', type:'GET', success: function (data) { let options = defaultOption; data.forEach(item => { options += `<option value="${item.phase_id}">${item.phase_name}</option>`; }) $phaseSelect.html(options); } }); }
四、可选缓存优化
如果疾病编码和招募阶段的对应关系不是高频变动的数据,可以给接口结果加低层缓存,避免每次请求都查库:
def phasenames @disease_code_id = params[:disease_code_id] @phase_names = Rails.cache.fetch("disease_#{@disease_code_id}_phase_list", expires_in: 2.hours) do RecruitmentPhase.where( exists: PatientPhase.joins(:patient) .where(patients: { disease_code_id: @disease_code_id }) .where("recruitment_phases.phase_id = patient_phases.phase_id") ).select(:phase_id, :phase_name).to_a end respond_to do |format| format.html format.js format.json { render json: @phase_names } end end
内容的提问来源于stack exchange,提问作者raghu palakodety
相关产品推荐
相关产品推荐

