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

SQL存储函数getRunPointValue触发1365除零错误排查

跑酷积分模块SQL除零错误排查

问题描述

开发游戏跑酷记录积分核算、排名更新模块时,编写了2个自定义SQL函数与1个触发器,功能调用入口示例:

SELECT insertRun(76561198117520501, 99, 0, 757.91015625, 0, TRUE) as run_id

运行过程中getRunPointValue函数会抛出除零错误,已在两处显式除法运算前添加防护逻辑:检测到除数为0时强制将其赋值为1,理论上所有参与显式除法运算的数值都不应为0。若取消insertRun函数中被注释的UPDATE语句,该问题会消失,但该语句是新记录参与排名计算的必要逻辑,移除后新提交的跑酷记录无法完成排名。
经实际观测,函数在99%的场景下可正常运行,仅在提交记录排名为第1位或末位等边缘场景下触发除零错误,返回的错误信息为:

1365, Division by zero

问题根因

现有防护只覆盖了代码中显式写的/除法运算,漏掉了两类隐式除零场景,恰好对应排名首尾的边缘case:

  1. 对数运算隐式除零:MySQL中LOG(底数, 真数)的底层计算逻辑为LN(真数)/LN(底数),当底数bracket_max为1时,LN(1)=0,直接触发除零。当地图总完成数小于20时,前5%排名的人数不足1人,计算出的bracket_max = total_completions * 0.05结果小于等于1,就会触发这个问题,对应排名第1的报错场景。
  2. 百分位分支未覆盖导致bracket_max为初始值0:现有逻辑只处理了排名在前25%的记录,当记录排名在25%之后(即末位场景),四个百分位判断分支都不会命中,bracket_max会保持声明时的默认值0,传入LOG函数后直接触发除零。

现有完整代码

CREATE FUNCTION insertRun(userid BIGINT UNSIGNED, mid INT, styleid INT, run_time FLOAT(12,4), type INT, best BOOLEAN)
RETURNS INT
BEGIN
    DECLARE runid INT;
    DECLARE totalPoints INT DEFAULT 0;
    IF best = TRUE THEN
        UPDATE surf_run SET best_run = FALSE WHERE user_id = userid AND map_id = mid AND style = styleid AND run_type = type;
    END IF;
    INSERT INTO surf_run (user_id, map_id, style, time, run_type, best_run) VALUES (userid, mid, styleid, run_time, type, best);
    -- if best_run is true then set all other runs by user_id and map_id to false
    SELECT run_id INTO runid FROM surf_run WHERE user_id = userid AND map_id = mid AND style = styleid AND run_type = type AND best_run = TRUE ORDER BY run_id DESC LIMIT 1;
    --UPDATE surf_run SET points = getRunPointValue(runid) WHERE run_id = runid;
    SELECT SUM(points) INTO totalPoints FROM surf_run WHERE user_id = userid AND best_run=TRUE;
    UPDATE surf_user SET points = totalPoints WHERE user_id = userid;
    RETURN runid;
END;


CREATE FUNCTION getRunPointValue(id INT)
RETURNS INT
exit_getrunpointvalue:BEGIN
    DECLARE points INT DEFAULT 10;
    DECLARE total_completions INT DEFAULT 1;
    DECLARE place INT DEFAULT 1;
    DECLARE run_time FLOAT(12,4);
    DECLARE run_style INT;
    DECLARE type INT;
    DECLARE mapid INT;
    DECLARE maptier INT;
    DECLARE tierMulti FLOAT(3,2) DEFAULT 1.0;

    DECLARE percentile FLOAT(12,4);
    DECLARE total_points INT DEFAULT 3000;
    DECLARE percentile_potential INT;
    DECLARE completionsbonus INT DEFAULT 0;

    DECLARE temp FLOAT;

    DECLARE bracket_min INT DEFAULT 0;
    DECLARE bracket_max INT DEFAULT 0;

    SELECT time INTO run_time FROM surf_run WHERE run_id = id LIMIT 1;
    SELECT style INTO run_style FROM surf_run WHERE run_id = id;
    SELECT run_type INTO type FROM surf_run WHERE run_id = id;
    SELECT map_id INTO mapid FROM surf_run WHERE run_id = id;
    SELECT tier INTO maptier FROM surf_map WHERE map_id = mapid;

    IF type < 0 THEN
        LEAVE exit_getrunpointvalue;
    END IF;

    SELECT COUNT(*) INTO place FROM surf_run WHERE map_id = mapid AND time <= run_time AND style = run_style AND run_type = type AND best_run = TRUE;
    SELECT COUNT(*) INTO total_completions FROM surf_run WHERE map_id = mapid AND style = run_style AND run_type = type AND best_run = TRUE;

    IF total_completions <= 0 THEN
        SET total_completions = 1;
    END IF;
    SET completionsbonus = FLOOR((total_completions / 2) * 0.6);

    IF maptier = 1 THEN
        SET tierMulti = 1.0;
    ELSEIF maptier = 2 THEN
        SET tierMulti = 1.53;
    ELSEIF maptier = 3 THEN
        SET tierMulti = 2.5;
    ELSEIF maptier = 4 THEN
        SET tierMulti = 3.8;
    ELSEIF maptier = 5 THEN
        SET tierMulti = 6.0;
    ELSEIF maptier = 6 THEN
        SET tierMulti = 8.5;
    END IF;

    IF place = 0 THEN
        SET place = 1;
    END IF;

    IF place = 1 THEN
        SET points = 1500 + completionsbonus;
    ELSEIF place = 2 THEN
        SET points = 1250 + completionsbonus;
    ELSEIF place = 3 THEN
        SET points = 1100 + completionsbonus;
    ELSEIF place = 4 THEN
        SET points = 1000 + completionsbonus;
    ELSEIF place = 5 THEN
        SET points = 900 + completionsbonus;
    ELSE

        SET percentile = (place / total_completions);

        IF percentile <= 0.05 THEN
            SET percentile_potential = 800;
            SET points = 600;
            SET bracket_min = 0;
            SET bracket_max = total_completions * 0.05;
        ELSEIF percentile <= 0.10 THEN
            SET bracket_max = total_completions * 0.05 + 1;
            SET bracket_max = total_completions * 0.1;
            SET percentile_potential = 600;
            SET points = 450;
        ELSEIF percentile <= 0.15 THEN
            SET bracket_max = total_completions * 0.1 + 1;
            SET bracket_max = total_completions * 0.15;
            SET percentile_potential = 450;
            SET points = 200;
        ELSEIF percentile <= 0.25 THEN
            SET bracket_max = total_completions * 0.15 + 1;
            SET bracket_max = total_completions * 0.25;
            SET percentile_potential = 200;
            SET points = 10;
        END IF;
        SET points = points + GREATEST(0, ROUND(percentile_potential - percentile_potential * LOG(place, bracket_max))) + completionsbonus;
    END IF;
    RETURN FLOOR(points * tierMulti);
END;


CREATE TRIGGER updateRunPointValue AFTER INSERT ON surf_run FOR EACH ROW
exit_updaterunpoints_trugger:BEGIN

    DECLARE place INT DEFAULT 0;
    DECLARE total INT DEFAULT 1;
    DECLARE runoffset INT DEFAULT 0;

    DECLARE worst_time FLOAT(12,4);

    IF NEW.run_type < 0 THEN
        LEAVE exit_updaterunpoints_trugger;
    END IF;

    SELECT COUNT(*) INTO place FROM surf_run WHERE map_id = NEW.map_id AND time <= NEW.time AND style = NEW.style AND run_type = NEW.run_type AND best_run = TRUE;
    SELECT COUNT(*) INTO total FROM surf_run WHERE map_id = NEW.map_id AND style = NEW.style AND run_type = NEW.run_type AND best_run = TRUE;

    -- if total < 250 then
    --  LEAVE exit_updaterunpoints_trugger;
    -- END IF;

    SET runoffset = (total * 0.25) + 1;
    
    SELECT time INTO worst_time FROM surf_run WHERE map_id = NEW.map_id AND style = NEW.style AND run_type = NEW.run_type AND best_run = TRUE ORDER BY time DESC LIMIT 1 OFFSET runoffset;
    call rerunPointValue(NEW.map_id, worst_time, NEW.style, NEW.run_type);

END;

修复方法

修改getRunPointValue函数中place>5的分支逻辑:

  1. 补全percentile>0.25的兜底分支,给低排名记录设置对应的积分规则和bracket_max值
  2. 所有bracket_max赋值后增加最小值校验,强制保证bracket_max >= 2,避免对数底数为0或1触发隐式除零
    修复后的核心逻辑片段参考:
ELSE
    SET percentile = (place / total_completions);

    IF percentile <= 0.05 THEN
        SET percentile_potential = 800;
        SET points = 600;
        SET bracket_min = 0;
        SET bracket_max = GREATEST(2, total_completions * 0.05);
    ELSEIF percentile <= 0.10 THEN
        SET bracket_max = GREATEST(2, total_completions * 0.1);
        SET percentile_potential = 600;
        SET points = 450;
    ELSEIF percentile <= 0.15 THEN
        SET bracket_max = GREATEST(2, total_completions * 0.15);
        SET percentile_potential = 450;
        SET points = 200;
    ELSEIF percentile <= 0.25 THEN
        SET bracket_max = GREATEST(2, total_completions * 0.25);
        SET percentile_potential = 200;
        SET points = 10;
    ELSE
        -- 25%排名后的兜底逻辑,直接给基础分,不需要对数计算
        SET points = 10;
        SET bracket_max = total_completions;
    END IF;
    -- 仅当percentile_potential有值时才做对数加权计算,避免空值计算异常
    IF percentile_potential IS NOT NULL THEN
        SET points = points + GREATEST(0, ROUND(percentile_potential - percentile_potential * LOG(place, bracket_max))) + completionsbonus;
    ELSE
        SET points = points + completionsbonus;
    END IF;
END IF;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 15:45:39