写给新员工的十点SQL开发建议

前言:最近带了一波毕业生。无论是不是计算机背景出身,研究生还是本科学历,Java上手都很快,但是到了写SQL,就普遍存在各种个样的问题。当年我们上学的时候也一样,在学校里能把增删改查的SQL语句写通了,没有语法错误,基本上数据库部分就OK了,从来没有见过大表,也根本想不到“数据清洗“是门单独的活计。这里总结了一些SQL代码的建议,供新员工参考。本文出现的代码以SQLSERVER为例

1、建立自己的知识体系

  • 摘抄一句话你所拥有的知识并不取决于你记得多少,而在于它们能否在恰当的时候被回忆起来

    • 做笔记;
    • 把笔记放在可以随时被找到的地方。个人的笔记可以放在印象笔记之类工具上,单位上的笔记,本地保存一份后,建议同步到码云
  • 少数核心的内容要被熟练记忆,数据仓库常见表,常见表的常见字段,常见表的关联条件;

2、知己知彼

2.1、使用前查看数据,做数据探索


 use db
    go
    --查看一下内容;
    select top 10 * from XXXXX
    select * from XXXXX limit 10;
    xxxx.head(10);
    --查看B表的数据字典
    select XXXXX.* from sys.talbes a,sys.columns b where a.object_id=b.object_id and a.name='XXXXX'
    describe xxxx
    --大小
    select count(1),max(日期),min(日期),len(日期) from XXXXX
    summary()
    --看索引
    sp_helpindex XXXXX
    --看存储过程
    sp_helptext proc1 

2.2、注意数据的类型

日期:YYYYMMDD
交易表:YYYY-MM-DD

3、高危操作

无论何时,用你自己的用户登陆数据仓库

3.1、drop

    --使用合理权限的用户,避免做以下的事
    drop dbname
    drop tablename
    truncate table tablename
    rm -rf xxxxx
  
    --还有一种非常危险的操作,你不能保证表1里有没有数据,数据重不重要
    
    if exists(1) drop table1 create1
    如果要求改表,最好这样:alter table1 

3.2、Delete

    ---如果你对tablea有权限,需要执行以下的操作;
    delete from tablea where 日期>='20180101' and 状态='d'
    --直接做是非常危险的,一定记住,所有的delete 之前,先select出来看看是不是可以删除!!!
    --select * from tablea where 日期>='20180101' and 状态='d'
    delete from tablea where 日期>='20180101' and 状态='d'

3.3、用落后的版本更新存储过程

直接打开本地目录一个存储过程的代码
–开始修改这段代码
–alter proc xxx 提交
–发现前天张三修改过了,服务器上的代码实际并不是你之前本地保存的,现在无法回退。呵呵。。。

  --受到目前生产环境的网络等影响,生产环境上并没有代码管理工具。一个存储过程,通常由多个人在生产上维护,本地不是最新的版本。不管你有多么确定,修改存储过程前都找到服务器端最新的代码,再修改。修改完后记得在本地更新保存一次
      use db
      go
      --查看存储过程实际版本
      sp_helptext procedurename
      --复制出来在新窗口修改

4、好的结构

标准的存储过程结构:

1、头部说明

2、备注

3、出错处理

4、返回值

下面举例三种常见类型的存储过程说明

4.1、一个查询的存储过程

    use mytemp
    go
    --========================================
    --功能:新员工培训,查询示例代码
    --作者:kk
    --时间:20181024
    --说明:返回行部信息
    /*
    exec mytemp.dbo.PRpTblRpt '125506','20180930'
    */
    --========================================
    create proc dbo.PRpTblRpt(
    @BrnNbr varchar(20), --行部
    @RptDat varchar(10)  --统计月份
    )as
    begin
     
     select brnbrnnbr 行部代码,brnbrnnam 行部名称
     from cmbnjis..pbtempinf,cmbnjis..pbtbrnnew
     where empbrnnew=brnbrnnbr  --关联条件
           and (brnbrnnbr=@brnnbr or @brnnbr='全部'and (brnbrndat=@RptDat or @RptDat='全部'end

4.2、一个增删改的存储过程

    use mytemp
    go
    --========================================
    --功能:新员工培训,数据增删改示例代码,
    --作者:kk
    --时间:20181024
    --说明:1、修改表1的序列为1的行的电话号码字段
    --     2、表1里的客户经理字段记录了可以维护这条记录的用户工号,如果更新的用户和工号不匹配,则抛出错误
    --     3、能够捕获系统错误
    /*
    declare @rc int
    exec @rc =mytemp.dbo.PRpTblMng '1','18915990062','01029488'
    select @rc
    */
    --========================================
    create proc dbo.PRpTblMng(
    @IddNbr varchar(10),  --序号
    @MblNbr varchar(20),   --号码
    @UsrNbr varchar(20)   --维护人工号
    )as
    begin
    declare @rc     int,
    		@erro   int
    select @rc=0,@erro=0
    
    --判断传入的维护人工号是否可以维护该条记录
    --把所有的错误判断放在正确的判断前面
    --尽量不要在前面reutrn 
    if not exists(select * from1 where 序列=@IddNbr and 工号=@UsrNbr)
       select @rc=-120
    
    --
    if @rc=0
    begin
      update1
         set 手机号=@MblNbr,
             维护人=@UsrNbr,     --后面这两个,把维护的非功能性信息记录下来
             维护日期=getdate()
      where 序列=@IddNbr and 工号=@UsrNbr
        select @erro=@@error   --捕获系统错误
           if @erro!=0
        select @rc=-121  
       
    end
    
    return @rc  --返回值
    /*
    返回值:增:-101~-109
           删:-110~-119
           改:-120~-129
    */
    
    end

4.3、一个数据清洗的存储过程
编写每日定时调度的数据清洗之前,一定要记得,千万不要相信你所依赖的表都正常生成了!

    use mytemp
    go
    --========================================
    --功能:新员工培训,数据清洗示例代码
    --作者:kk
    --时间:20181024
    --说明:1、把交易表里日期大于当前表1最大日期的数据插入到表1中
    --     2、在做数据清洗的时候,要充分考虑到你所依赖的数据不一定生成了
    /*
    declare @rc int
    exec @rc =mytemp.dbo.PRpTblRfh ''
    select @rc
    */
    --========================================
    create proc dbo.PRpTblRfh(
    @Dat varchar(20) --分行所有的清洗调度都需要加入一个日期参数,不用到没有关系
    )as
    begin
    declare @rc          int,
    		@erro        int,
    		@maxRptdat   varchar(10)
    		@maxTrxDat   varchar(10)
    		
    select @rc=0,@erro=0
    select @maxRptdat=max(日期) from1
    select @maxTrxDat=max(trx_Dte) from 交易表
    
    
    --如果交易表日期不是最新的,则不执行清洗脚本
    if @maxRptdat>=@maxTrxDat
    begin
      select @rc=999001
    end
    
    if @rc=0
    begin
      insert1
      select * from 交易表 where 日期>=@maxRptda
       select @erro=@@error   --捕获系统错误
           if @erro!=0
        select @rc=-101 
    
    end
    
    return @rc  --返回值
    
    end

5、保留所有的数据

先看一下这张表的定义

    create table dbo.1(   
    序号     int idenlity,
    工号     varchar(10) default '',
    姓名     varchar(50) default '',
    电话     varchar(20) default '',
    状态     char(1) default 'A',
    添加日期  datetime    default getdate(),
    添加用户  varchar(10) default '',
    更新日期  datetime    default getdate(),
    更新用户  varchar(10) default '',
    备注1    varchar(100) default '',
    备注2    varchar(100) default ''

5.1 应用系统delete?

看看下面的代码

 delete from1 where 序号=1

这并没有什么问题,你所看到的很多应用系统都是这样写的。mybatis连过去直接删除一条记录就OK了。然而,覆水难收,真正安全的系统,所有的数据都是可以回朔的。不建议在前台的应用系统里直接编写delete ,除非你又设计了一个日志表记录这一切。建议你这样写

update1 
set 状态='C',更新日期=getdate(),更新用户='111111' 
where 序号=1

这需要你在查询的时候,select脚本里加上一条where 状态=‘A’ .过程数据都被保存下来了。

5.2 非功能性的字段

注意上表从添加日期开始的字段,其实都不是这张表必须存在的字段。但是建议你在所有的表设计里都把它们加上。这样什么时候数据被第一次加入,什么时候被维护的,都很清楚

6、君子不立危墙之下(NULL值处理)

运算列中有NULL值,结果变成NULL。关联列中有NULL,记录会神秘消失。查询条件有NULL,有些记录会被查不到。所有的NULL值都需要转换

  • 在建立表的时候,字段类型not null defaul ‘’
  • 查询的时候,select isnull(a,’’)
  • 在实际的处理过程中,一定要时时注意有没有空,举一个实际的案例:
    --#temp 这张表有业务人员提供的卡号和姓名
    -- vbscus.dbo.cseasdtaprv 这张表为短户口表,有系统内正确的卡号和姓名和客户号
    --现在我在#temp里判断下卡号姓名是否一致,并把客户号加进去
    select a.*,b.客户号,case when a.姓名=b.姓名 then 'ok' else 'no' end as tag 
    from #temp a 
    left join vbscus.dbo.cseasdtaprv b  
    on a.卡号=b.卡号

这段话看起来没有问题,但是,实际上#temp表里有些卡号是完全错误,根本不在vbscus.dbo.cseasdtaprv 这张表里的.这一步关联,如果#temp表有100条记录,只有90条卡号是存在的,剩下10条不存在,左关联后100条记录,tag字段除了OK,和NO之外,还有一个你没有意料出来的NULL…后续在使用的时候,就会出现问题,有些问题会导致严重的后果。应该这样写

   select a.*,b.客户号,isnull((case when a.姓名=b.姓名 then 'ok' else 'no' end),'not exists') as tag 
   from #temp a 
   left join vbscus.dbo.cseasdtaprv b  
   on a.卡号=b.卡号
                           
                           
   --你也可以单独找出这些卡号不为空的字段
   select a.*,b.客户号,'not exists' as tag 
   from #temp a 
   left join vbscus.dbo.cseasdtaprv b  
   on a.卡号=b.卡号  
   where b.姓名 is null                        
   

7、消失和多出的数据

  • 7.1表关联后数据变多了
    假设表a和表b 关联。关联条件是客户号 where a.客户号=b.客户号

    • 1:1关联

       Insert a(客户号,姓名,电话) values1,zhangsan,'139139139'Insert b(客户号,资产类型,金额) values1,"存款",100Insert b(客户号,资产类型,金额) values2,"存款",200select  a.客户号,姓名,资产类型,金额 from a,b where a.客户号=b.客户号
       
       --结果:
       1,zhangsang,"存款",100
      
    • 1:N关联

    •   Insert a(客户号,姓名,电话) values1,zhangsan,'139139139'Insert b(客户号,资产类型,金额) values1,"存款",100Insert b(客户号,资产类型,金额) values1,"基金",200Insert b(客户号,资产类型,金额) values2,"存款",200select  a.客户号,姓名,资产类型,金额 from a,b where a.客户号=b.客户号
        
        --结果:
        1,zhangsang,"存款",100
        1,zhangsang,"基金",200
      
    • M:N关联
      特别注意这种情况的数据!

    •    --注意此时a表客户1有两条记录
         Insert a(客户号,姓名,电话) values1,zhangsan,'139139139'Insert a(客户号,姓名,电话) values1,zhangsan,'189189189'Insert b(客户号,资产类型,金额) values1,"存款",100Insert b(客户号,资产类型,金额) values1,"基金",200Insert b(客户号,资产类型,金额) values2,"存款",200select  a.客户号,姓名,资产类型,金额 from a,b where a.客户号=b.客户号
         
         --结果,此时数据出现了翻倍现象!
         1,zhangsang,"存款",100
         1,zhangsang,"基金",200
         1,zhangsang,"存款",100
         1,zhangsang,"基金",200
         
         --如果你在写代码之前没有判断两个表的关联条件是否会有重复,然后写出了下面一段代码;
         select a.客户号,姓名,sum(金额) as 总资产
         from a,b 
         where a.客户号=b.客户号
      

      客户1实际的总资产只有300块,此时在你的协助下,翻了一倍变成600快。所以,当关联的条件是1:1,1:N时,关联的结果不会出现什么问题,但如果关联的条件时多对多,如上例M:N的情况时,结果集会翻M倍。这是严重的数据错误。所以,在关联前,检查数据的重复是很重要的,检查的方法如下:

       --如果有值,说明a表有重复的数据
       select 客户号,count(1) cnt
       from a
       group by 客户号
       having count(1)>1
       
       --假设上面查出来客户号为11111的客户出现了重复,去原始表里看下这个客户是什么情况
       select *
       from a
       where 客户号='1'
      
  • 7.2表关联后数据变少了

    看一下关联的条件,是否出现了NULL!!!还有关联的表字段内容是否一致,比如,拿日期关联,一张表的日期是YYYYMMDD 格式,另外一张表的格式是YYYY-MM-DD等等。

8、索引和效率

  • 1、小表不建议用索引(几十万行且列不多)

  • 2、大表尽量用索引 用sp_helpindex 表名可以查看索引,然后想办法去用它。下面举一个例子,先看两张表:

  •      --有一张10个亿的交易表a,结构如下,索引是 卡号+日期
         create table a(
         客户号
         卡号
         日期
         交易类型
         金额
         )
         
         --现在有一张表 b ,有一万个客户号和姓名,结构如下
         create table b(
         客户号
         姓名
         )
         
    
  • 现在想查询表b客户在10月交易类型为"取款"的交易金额汇总。一般你能想到的写法是

  •    select b.客户号,b.姓名,sum(金额) 金额
       from a,b
       where a.客户号=b.客户号 and a.日期 between '' and '' and a. 交易类型='取款'
       group by 金额
    
  • 然而这个写法没有用到索引,速度非常慢。推荐一个做法是:

  •    --先找到B表客户的所有卡号,因为一个客户可能有多张卡号,此时可能一万条记录变成2万条
       select b.客户号,卡号
       into #x
       from b,vbscus..cseasdtaprv c
       where b.客户号=c.客户号
       
       --再把临时表和交易表做关联,此时使用到了索引,虽然从关联1万条到关联2万条记录,但是效率翻了NNN倍
       select a.客户号,sum(金额) 金额
       into #y
       from #x,a
       where #x.卡号=a.卡号 and a.日期 between '' and '' and a. 交易类型='取款'
       group by a.客户号
       
       --番外话,B表中的有些客户可能在10月没有发生取款交易,需要再添加一步使返回的数据完整.这一步1万条记录OK
       select b.客户号,b.姓名,isnull(金额,0) 金额  
       from b
       left join #y
       on b.客户号=#y.客户号
    

-4、 联合索引如果用不到第一个字段,索引没有用 。假设表的索引为卡号,日期,交易类型,使用卡号,卡号+日期,卡号+交易类型 都是用到了索引,而日期+交易类型 就然并卵

  • 5、没有区分度的索引不要建立,比如一张一千万的表,按照性别建立索引

9、简洁

  • 1、减少扫描大表的次数

    • 看以下的表,有一亿条记录,索引是客户号+日期

    •     --有一张1个亿的交易表c
          create table c(
          客户号
          日期
          交易类型
          金额
          )
          --b表有一万个客户,统计它们在10月份交易类型为取款、转账、三方存管、代发等的交易金额和笔数,
          create table b(
          客户号
          姓名
          )
      
    • 你会怎么设计呢?推荐一种实现方案

    •     --这样写的好处是,一个亿的大表,只扫描了一遍,就把该取到的数据全部取走了,生成了一张小表
          select b.客户号,
          sum(case when 交易类型='取款' then 金额 else 0 end) 取款金额, 
          sum(case when 交易类型='取款' then 1 else 0 end) 取款次数,
          sum(case when 交易类型='转账' then 金额 else 0 end) 转账金额, 
          sum(case when 交易类型='转账' then 1 else 0 end) 转账次数,
          sum(case when 交易类型='三方存管' then 金额 else 0 end) 三方存管金额, 
          sum(case when 交易类型='三方存管' then 1 else 0 end) 三方存管次数,
          sum(case when 交易类型='代发' then 金额 else 0 end) 代发金额, 
          sum(case when 交易类型='代发' then 1 else 0 end) 代发次数
          into #x
          from c,b
          where c.客户号=b.客户号 and c.交易类型 in ('','','','','') and 日期 between '' and ''group by b.客户号
          
          --防止b表里有些客户在统计日期一笔交易都没有,这里左关联处理下
          select b.客户号,b.姓名,
          isnull(取款金额,0) 取款金额,isnull(取款次数,0) 取款次数,
          isnull(转账金额,0) 转账金额,isnull(转账次数,0) 转账次数,
          isnull(三方存管金额,0) 三方存管金额,isnull(三方存管次数,0) 三方存管次数,
          isnull(代发金额,0) 代发金额,isnull(代发次数,0) 代发次数
          from b
          left join #x
          on b.客户号=#x.客户号
      
  • 一定要用OR吗?
    看下需求:统计下客户号为"1111111",和"22222"的客户10月交易笔数。把条件写成 :

    where 客户号 ="1111111" or 客户号 "22222"  and  日期 between '' and ''
    

    这种写法是错误的,一旦条件中出现OR,OR一定要包起来,否则会数据会翻上去,下面这个写法是正确的:

    where (客户号 ="1111111" or 客户号 "22222" ) and  日期 between '' and ''
    

    但是这是不好的,第一OR会影响查询效率,有可能扫描多次表,第二为什么你们会这样考虑问题呢?建议这样写

    where 客户号 in("1111111", "22222" ) and  日期 between '' and ''
    

10、其他好习惯

  • 指定模式名 create table dbo.xxxx,create proc dbo.yyy, select from dbo.xxx ,update dbo.xxx
  • where条件少用函数
  • 不要相信业务人员给你的数据数据质量
  • Insert 时指定列(insert table1 (a,b,c) select ‘a’,‘b’,‘c’ from table2)
  • 随手保存,建立你的文件保存体系
  • 遵循命名规范

猜你喜欢

转载自blog.csdn.net/bluewhalelove/article/details/83413762