美文网首页
PostgreSQL除数为0解决方案

PostgreSQL除数为0解决方案

作者: 定金喜 | 来源:发表于2023-01-20 15:14 被阅读0次

1.问题背景

数学上除法除数为0是不允许的,所以在数据库设计时,有的数据库会对这种除数为0做特殊的处理,MySQL是当除数为0时结果为null


MySQL处理

但是在PostgreSQL中会报错


PostgreSQL处理

我们在做即席查询时,需要建计算字段,底层数据库是PostgreSQL,而计算字段的公式可以是任意的,可以是表中任意字段的组合,可以加减乘除或者函数等,而且建立这个计算字段是客户设置的,客户不一定能考虑到除数不能为有可能是0的字段,所以遇到这种情况可能出现报错。
例如表A中有字段fd1,fd2,建立计算字段fd3,公式为fd1/fd2,那么整个的查询sql为:
select fd1,fd2,(fd1/fd2) as fd3 from A;
在正常情况下该语句是没有问题的,但是当fd2出现为0的值时,整个sql执行会报错,而且这个公式不是固定的,客户可以随时改动。

但是不管是MySQL还是PostgreSQL,当除数为null时,都做了特殊处理,返回都是null


MySQL除数为null处理 PostgreSQL除数为null处理

所以我们的解决思路就是当除数为0时将其替换成null即可。

2.解决思路

方案1:POSTSQL 自定义函数

CREATE OR REPLACE FUNCTION DIV(NUMERIC,NUMERIC)
 RETURNS NUMERIC AS $BODY$
 SELECT CASE WHEN $2 = 0 THEN null ELSE $1/$2 END;
 $BODY$
 LANGUAGE SQL;

前端交互不用改变,客户还是配置公式fd1/fd2,在后端进行处理,后端在解析/运算符时,替换成DIV函数
fd1/fd2 -> DIV(fd1,fd2)
难点:fd1和fd2如果是嵌套这种复杂的,解析这个公式并且识别到除数和被除数,并且进行替换,整个过程就不是一个简单的事情,所以这种方案不合适。

方案2:配置分母时需要增加函数

NULLIF函数
NULLIF(arg1,arg2)
函数解析:如果两个参数相等返回null,否则返回第一个参数,这个函数可以把0替换成null

1.前端交互需要修改,客户在配置计算字段时,如果该字段配置成了除数并且可能存在为0的情况,那么分母必须是要加NULLIF

[fd1]/NULLIF([fd2],0)
缺点:需要客户去控制,但是客户不一定知道这个限制,出错的可能性很大。

2.前端不用改变,后端解析出分母后进行替换

难点:解析出分母比较复杂,要考虑的场景较多,可能会有各种隐藏的bug

方案3:自定义操作符

结合方案1,因为PostgreSQL是可以自定义操作符的
create operator // (procedure=DIV, leftarg=numeric, rightarg=numeric);

前端交互不用改变,后端在解析/运算符时,替换成自定义的符号(这个符号可以任意定义,但是不要和其他符号冲突),这样在sql语句中,解析到这样的符号就可以按照自定义的操作进行
问题:自定义的操作符是不满足加减乘除等运算规律的(先乘除后加减),所以这种方案是不可行的。

3.解决方案

其实解决思路不外部从以下几点出发
1.需要有框架能解析公式,能解析出是除法运算operator和左右表达式;
2.通过对右边表达式NULLIF包装,NULLIF(fd2,0),所以包装后的表达式应该为:fd1/NULLIF(fd2,0),整个sql变为了:
select fd1,fd2,(fd1/NULLIF(fd2,0)) as fd3 from A;
3.一些其他细节的考虑

表达式解析框架使用druid

<dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.22</version>
 </dependency>

具体代码直接贴出来,仅供参考:

 public static void main(String[] args) {
        PGExprParser parser = new PGExprParser("sum(cast(a/b as varchar))");
        SQLExpr sqlExpr = parser.expr();
        System.out.println(resolveExpr(sqlExpr));
    }

    private static String resolveExpr(SQLExpr sqlExpr) {
        // 复合表达式,需要解析子节点
        if (sqlExpr instanceof SQLBinaryOpExpr) {
            SQLBinaryOpExpr opExpr = (SQLBinaryOpExpr) sqlExpr;
            String resolvedSql = resolveComposition(opExpr);
            return resolvedSql;
        }
        // sql表达式存在方法调用
        else if (sqlExpr instanceof SQLMethodInvokeExpr) {
            SQLMethodInvokeExpr expr = (SQLMethodInvokeExpr) sqlExpr;
            StringBuffer buf = new StringBuffer();
            if (expr.getOwner() != null) {
                expr.getOwner().output(buf);
                buf.append(".");
            }

            buf.append(expr.getMethodName());
            buf.append("(");
            for (int i = 0, size = expr.getArguments().size(); i < size; ++i) {
                if (i != 0) {
                    buf.append(",");
                }

                buf.append(resolveExpr(expr.getArguments().get(i)));
            }
            buf.append(")");
            return buf.toString();
        }
        // 存在cast函数的方法调用
        else if (sqlExpr instanceof SQLCastExpr) {
            SQLCastExpr sqlCastExpr = (SQLCastExpr) sqlExpr;
            SQLExpr childExpr = sqlCastExpr.getExpr();
            StringBuffer buf = new StringBuffer();
            buf.append("cast (");
            buf.append(resolveExpr(childExpr));
            buf.append(" as ").append(sqlCastExpr.getDataType().getName()).append(")");
            return buf.toString();
        }
        else {
            return processOutput(sqlExpr);
        }
    }

    private static String resolveComposition(SQLBinaryOpExpr opExpr) {
        SQLBinaryOperator operator = opExpr.getOperator();
        SQLExpr left = opExpr.getLeft();
        SQLExpr right = opExpr.getRight();
        if (SQLBinaryOperator.Divide == operator) {
            return surroundByBrackets(resolveExpr(left) + operator.name + surroundByNULLIfZero(resolveExpr(right)));
        } else {
            return surroundByBrackets(resolveExpr(left) + operator.name + resolveExpr(right));
        }
    }

    /**
     * @param sql 必须是数值类型
     */
    public static String surroundByNULLIfZero(String sql) {
        return "NULLIF(" + sql + ",0)";
    }

    /**
     * @param sql 必须是字符串类型
     */
    public static String surroundByNULLIfBlank(String sql) {
        return "NULLIF(" + sql + ",'')";
    }

    /**
     * 把sql用小括号包围并返回
     */
    public static String surroundByBrackets(String sql) {
        return "(" + sql + ")";
    }

    private static String processOutput(SQLExpr sqlExpr) {
        StringBuffer buf = new StringBuffer();
        sqlExpr.output(buf);
        return buf.toString();
    }

运行结果:
sum(cast ((a/NULLIF(b,0)) as varchar))

相关文章

  • PostgreSQL除数为0解决方案

    1.问题背景 数学上除法除数为0是不允许的,所以在数据库设计时,有的数据库会对这种除数为0做特殊的处理,MySQL...

  • 案例1:除法计算器

    题目:除法计算器1-输入除数和被除数,计算结果。2-如果除数为0,提示“0是不能做除数的!”3-当除数和被除数无法...

  • NaN (not a number)

    这是被除数为0造成的

  • 异常

    常用异常:①:ZeroDivisionError:除数为0②:FileNotFoundError:文件找不到 tr...

  • C#进行取整(/)运算时,被除数为0有哪些情况

    无论是整数还是浮点数进行除法运算时,被除数为0必然是不合理的,在C#中,如果被除数为0,到底会发生什么? 1. 整...

  • iOS开发小技巧

    1.取摸除数为0,存在内存泄露NSLog(@"%d",6%0); //结果:8409152 2.UIButton ...

  • MySQL 中的运算符

    算术运算符 除法运算和模运算中,如果除数为 0,将是非法除数,返回结果为 NULL 对于模运算,还有另外一种表达方...

  • 【学生作品】《除数是两位数的除法》知识梳理

    1、去0法:被除数和除数的末尾同时去掉相同个数的0,商不变。 2、除数是两位数的除法的计算方法: �从被除数的高位...

  • python中各种错误的意思

    NameError: 尝试访问一个没有声明的变量; ZeroDivisionError: 除数为0 SyntaxE...

  • Swift基础

    变量 常量 除数为0的时候 在swift 中 typealias 关键字(取别名的) 数组

网友评论

      本文标题:PostgreSQL除数为0解决方案

      本文链接:https://www.haomeiwen.com/subject/ezzqhdtx.html