【TD迁移】【函数迁移之一】因字符集变化引起的函数适配

举报
SeqList 发表于 2021/04/23 16:36:42 2021/04/23
【摘要】 一般我们建议GaussDB(DWS)使用UTF8作为字符集。而TD(TERADATA)一般默认是使用latin1的字符集,存放中文的也是GBK字符集,与GaussDB(DWS)推荐使用的UTF8不一致。这样的不一致会导致一些字符串相关的函数,会因为字符集不一致导致结果不一致,本文主要针对这样的变化,给读者提供如何对这样的变化进行函数的开发适配

【前言】

      在国内环境通常使用中文的情况下,一般我们建议GaussDB(DWS)使用UTF8作为字符集。而TD(TERADATA)一般默认是使用latin1的字符集,存放中文的也是GBK字符集,与GaussDB(DWS)推荐使用的UTF8不一致。这样的不一致会导致一些字符串相关的函数,会因为字符集不一致导致结果不一致。例如substr,length这样的常用函数。length('TD迁移')在TD的latin1数据库下和GaussDB(DWS)的UTF8数据库下的结果分别是6和4

【转换相关的考虑】

1. 考虑重点是脚本迁移,调研存量的td脚本,若SQL中的字符函数较少且使用对象从业务角度无中文,则可以不考虑本文描述的情况;

2.使用新建的自定义函数来替代脚本中的字符函数,自定义函数使用统一命名规则,以便在脚本中替换。例如substr,新的自定义函数可以命名为td_substr;

3.目标是减少迁移的工作量,使用自定义函数替换系统自带函数,有一定的性能损失;目前项目经验看,性能损失在接受范围内(无测试结果,在大面积替换函数后,GaussDB(DWS)的SQL执行时间仍然在接受范围内)

4.适配的自定义函数应统一建在public下,用户使用时不需要在前面带上schema名称;

5.考虑会存在使用UTF8字符集结果需求,且以后迁移完成后能方便切换为UTF8,在自定义函数中增加可以指定字符集的参数,例如 td_length(column,'UTF8')实现的逻辑与utf8字符集的GaussDB(DWS)的length函数一致,而td_length(column,'GBK')则实现TD在latin1字符集下的length函数。

【函数实现】

在考虑好以后,可以参考以下函数的实现样例。td的字符相关函数有很多,可以大概分为以下几类:

  • 获取字符串长度,如length
  • 截取字符串,如substr
  • 获取字符位置,如instr

【函数命名基本原则】

  • GaussDB(DWS)有的函数名称或者关键字,需增加类似td_的前缀来区分已有的函数或关键字,例如char 是GaussDB(DWS)的关键字,但也是td的函数名称,因此该适配UDF名称需要增加前缀,td_char
  • GaussDB(DWS)没有的函数,可以沿用td的函数名称,例如chars

下面为具体函数的实现

【获取字符串长度】

获取字符串长度的函数在td中有很多同义函数,我们以char()这个函数作为原始函数,其他同义函数则通过引用该函数实现。获取字符串长度的函数会作为基础函数,被其他类型的函数使用。

CREATE OR REPLACE FUNCTION public.td_char(text)
RETURNS int
LANGUAGE plpgsql
IMMUTABLE NOT FENCED
AS $$
DECLARE CNT INTEGER;

BEGIN

    CNT = 0;
    FOR I IN 1..length($1) LOOP
        IF(ASCII(SUBSTR($1,I,1)) <= 128) THEN
             CNT = CNT + 1;
        ELSE
             CNT = CNT + 2;
        END IF;
    END LOOP;
RETURN CNT;
END;
$$
;

--还可以通过转换字符集的方式实现,该方式开始迁移时没想到,还没经过大量脚本验证,但初步验证能处理中文且结果准确,速度比上面LOOP快
CREATE OR REPLACE FUNCTION public.td_char(text)
RETURNS int
LANGUAGE sql
IMMUTABLE NOT FENCED
AS $$
SELECT octet_length(convert(replace($1,'\','\\')::bytea,'UTF8','GBK')); --'\'无法转换为bytea
$$
;


同义的获取字符串长度的函数,下面的函数有一些在原来GaussDB(DWS)就存在的,比如length,需要建一个与length不一样的名字,td_length。同时GaussDB(DWS)不存在的,需要建一个与td一样的名字,如char_length。

  • character
  • char_length
  • character_length
  • length
  • chars

下面以char_length函数举例,其他函数与例子一致。

CREATE OR REPLACE FUNCTION public.td_char_length(text,text)
RETURNS int
LANGUAGE plpgsql
IMMUTABLE NOT FENCED
AS $$
begin
   IF $2 ~* '^utf-?8\s*$' THEN return length($1);
ELSIF $2 ~* '^gbk\s*$' THEN
return public.td_char($1);
ELSE
RAISE EXCEPTION '% invalid,input GBK or UTF8',$2;
END IF;
END;
$$
;

/*区分现有char_length函数,增加td_前缀*/
CREATE OR REPLACE FUNCTION public.td_char_length(text)
RETURNS int
LANGUAGE sql
IMMUTABLE NOT FENCED
AS $$
select public.td_char($1);
$$
;

【截取字符串】

 TD常用的截取字符串函数有

  • substr
  • left
  • right
  • substring

对于substring(sn from 3 for 3) 这样的语法,我们无法新增含有关键字的函数,建议对substring函数统一都修改为substr

下面具体展示substr的SQL代码,具体其他的代码见附件。

CREATE OR REPLACE FUNCTION public.td_substr(text,int)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE NOT FENCED SHIPPABLE
AS $$
DECLARE
    CNT INTEGER;
    RST VARCHAR;
    TMP VARCHAR;
    
    BEGIN
        IF($2 <= -1 ) THEN
            RST = $1;
        ELSE
            CNT = 0;
            RST = '';
            IF (LENGTH($1) = 0 ) THEN
                RETURN $1;
            END IF;
            
            FOR I IN 1..LENGTH($1) LOOP
                TMP = SUBSTR($1, I, 1);
                IF CNT < $2 THEN
                    /*判断是否中文*/
                    IF ASCII(TMP) <= 128 THEN
                        CNT = CNT + 1;
                        IF CNT = $2 THEN
                            RST = TMP;
                        END IF;
                        
                    ELSE
                    /*中文跳一位字节*/
                        CNT = CNT + 2;
                        IF CNT > $2 THEN 
                             RST = TMP;
                             /*如果刚好截取是中文的中间,则返回固定0F字节*/
                        ELSIF CNT = $2 THEN
                            RST = e'\x0f';
                        END IF;
                    END IF;
                     
                ELSE
                    RST = RST || TMP;
                END IF;
            END LOOP;
         END IF;
         
       RETURN RST;
    END;
$$
;


【获取字符位置】

TD常用的获取字符位置的函数有

  • instr
  • index
  • position

其中position类似substring,函数中有IN语法,导致无法创建UDF。建议对含有position的语法都修改为instr。

下面简单举td_instr的实现代码。

CREATE OR REPLACE FUNCTION public.td_instr(text,text,int)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE NOT FENCED SHIPPABLE
AS $$
DECLARE
    POINT INTEGER;
    FLEN INTEGER;
    CLEN INTEGER;
    TMP VARCHAR;
    SCAN VARCHAR;
    ST VARCHAR;
    
    BEGIN
        POINT = 0;
        CLEN = LENGTH($1);
        FLEN = LENGTH($2);
        /*输入为整数为大部分场景*/
        IF $3 >= 0 THEN
            FOR I IN 1..CLEN LOOP
                /*TMP为比较字符*/
                TMP = SUBSTR($1,I,FLEN);
                /*SCAN用来判断是否中文*/
                SCAN = SUBSTR($1,I,1);
                
                POINT = POINT + 1;
                IF POINT >= $3 THEN
                    IF TMP = $2 THEN
                        RETURN POINT;
                    END IF;
                END IF;
                
                /*中文则+1*/
                IF ASCII(SCAN) > 128 THEN
                    POINT = POINT + 1;
                END IF;
                
            END LOOP;
         
         /*小于0的情况,反向搜索*/
         ELSE
             FOR I IN 1..CLEN LOOP
                 ST = CLEN - I + 1;
                /*TMP为比较字符*/
                TMP = SUBSTR($1,ST,FLEN);
                /*SCAN用来判断是否中文*/
                SCAN = SUBSTR($1,ST,1);
                
                POINT = POINT + 1;
                IF POINT >= $3 THEN
                    IF TMP = $2 THEN
                        RETURN TD_CHAR($1) - POINT + 1;
                    END IF;
                END IF;
                
                /*中文则+1*/
                IF ASCII(SCAN) > 128 THEN
                    POINT = POINT + 1;
                END IF;
                
            END LOOP;
         END IF;
         POINT = 0;
     RETURN POINT;
    END;
$$
;

【总结】

因篇幅有限,只能举一些常用例子,具体实现的SQL可以参考附件。在现网某局点中,这些自定义函数能大大减少对数的工作量,但会有一定的性能损耗,在充分测试后,读者可以看看这样的性能损耗是否能在接受范围内。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。