1. 程式人生 > >寫給新員工的十點SQL開發建議

寫給新員工的十點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)
  • 隨手儲存,建立你的檔案儲存體系
  • 遵循命名規範