数仓开发规范
# 简介
本文记录整理了本人在工作中的一些心得体会及个人见解。欢迎留言交流...
# 为什么写这篇文章?
相信每一个做数仓、ETL等开发的同学都遇到过这样的问题:
- 脚本写的乱,千人千面,每个人写法都不一样,各有各的风格。比如SQL里的逗号在前还是在后?
- 脚本放的到处都是,例如:很多目录都有
script
,tools
等这样的重复文件夹 - 脚本风格的问题:不缩进,不换行,没有统一大小写等
- 世纪难题之一:
命名
等等
以至于你看别人的代码,别人看你的代码都非常难受。 本文就以个人的观点从以下几个方面来说下数仓开发的一些规范。会根据情况不定时更新补充。
# 分层规范
这个没什么好说的,涉及到数仓架构范畴,什么功能的脚本放到什么地方。。
# 编码规范
# IDE统一
这个问题好像不是强制约定,但是为了同事之间方便交流等各种因素,还是建议大家使用统一的编辑器。 例如
- Python编辑器:Pycharm (opens new window)
- Shell编辑器:VS Code (opens new window)
- SQL客户端(可支持常见各种数据库):DataGrip (opens new window)
# 缩进
所有代码的缩进均不能使用tab
键,必须使用4个空格代替一个tab
键,主要是因为各个系统中tab
键显示的效果不一样,可能会串行等。
警告
所有代码必须保持缩进,以4个空格代替一次缩进tab
可以在IDE中装一些来显示tab
键的插件
# 大小写
- 所有SQL语句中的关键字必须大写
- 所有常量必须大写:包括Shell中的全局变量、Python中的全局变量等
# 注释规范
所有脚本必须有注释:Shell、Python、Java等各种代码脚本必须有头注释
# Shell脚本头部注释
参考
#!/bin/bash
# ========================================================================
# created info : zfang 2019-01-09
# contact email : zfang@hillinsight.com
# exec example : bash dw_payment_map.sh [param1 param2]
# describe : 为了迁移兼容,先从dw_payment中组装出来clinic_payment_book_all表结构,当做临时表,待数据稳定后可直接使用dw_payment,废弃本脚本!!
# change log :
# ========================================================================
2
3
4
5
6
7
8
9
# Python脚本头部注释
参考
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
-------------------------------------------------
@version : v1.0
@author : fangzheng
@contact : zfang@hillinsight.com
@software : PyCharm
@filename : monitor_db.py
@create time: 2018/7/31 下午5:06
@describe : 监控数据库状态,连接异常发送邮件
-------------------------------------------------
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
# Shell脚本规范
# 基本
- 文件名全部小写,单词间以下划线连接,不可用驼峰
- 定义在所有函数之外的变量为全局变量。必须大写,并用readonly修饰
# 注释
Shell脚本里也必须有注释
- 头部注释:参考上面Shell脚本注释
- 行注释:重要或复杂代码均必须写行级注释
# 变量
- 全局变量:必须大写,readonly
#!/bin/bash
# ========================================================================
# created info : zfang 2019-01-09
# contact email : zfang@hillinsight.com
# exec example : bash dw_payment_map.sh [param1 param2]
# describe : 为了迁移兼容,先从dw_payment中组装出来clinic_payment_book_all表结构,当做临时表,待数据稳定后可直接使用dw_payment,废弃本脚本!!
# change log :
# ========================================================================
# 全局变量:必须大写
FILENAME=""
2
3
4
5
6
7
8
9
10
11
12
13
- 局部变量:方法内的变量
必须加
local
关键字
function create_table
{
local create_sql=""
}
2
3
4
- 变量引用:
所有的变量引用必须使用双大括号的方式
${FILENAME}
# 缩进
Shell脚本里的缩进必须使用空格键,不可使用tab
键。可在vs Code中装一些插件来处理
# 函数
- 函数的大括号需要换行
function create_table
{
local create_sql=""
}
2
3
4
- 每个Shell脚本使用统一的模板(usage、main、耗时统计等)。特殊处理情况除外
参考脚本:
#!/bin/bash
# ========================================================================
# created info : zfang 2019-02-19
# contact email : zfang@hillinsight.com
# describe : 报表:中间表
# exec example : bash middle_med_record.sh [param1,param2]
# change log :
# ========================================================================
# 全局变量必须大写
DT="1970-01-01"
EXE_HIVE="hive"
DB="pet_medical"
TABLE="middle_med_record"
TABLE_NAME="${DB}.${TABLE}"
YESTERDAY=$(date -d "-1 day" +"%Y-%m-%d")
function usage
{
echo "Usage : bash $0 [yyyy-mm-dd]"
}
function check_param
{
# check your input param
if [ $# -eq 1 ]; then
DT=${1}
else
echo "======>The parameter is empty, using the default parameter!"
DT=$(date -d "-1 day" +"%Y-%m-%d")
fi
echo "======>etl_date is ${DT}"
}
function create_table
{
local create_sql=""
$EXE_HIVE -e "${create_sql}"
return $?
}
function load_data
{
local load_data_sql=""
$EXE_HIVE -e "${load_data_sql}"
return $?
}
function main
{
local start=$(date +%s -d "0 day ago")
check_param "$@"
create_table && load_data
local run_stat=$?
local end=$(date +%s -d "0 day ago")
echo "执行完成,状态:${run_stat}, 耗时:$(expr $end - $start ) 秒"
return ${run_stat}
}
main "$@"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# if
将 then 放在和 if 同一行。
if condition 1; then
...
elif condition 2; then
...
else
...
fi
2
3
4
5
6
7
8
# 循环
将 do 放在和 for 和 while 同一行。
for x in "foo" "bar" "quz"; do
...
done
while true; do
...
done
2
3
4
5
6
7
# Here document
需要写大段文本或模板时采用 here document。无需变量展开时结束符使用双引号。
function usage
{
cat << EOT
Usage: $0 [options]
--help Display this message and exit
--file FILE Specify target filename, default is /etc/myapp.conf
EOT
}
2
3
4
5
6
7
8
9
10
11
# SQL语句规范
# 关键词
关键字应该是大写。
/* Good */
SELECT COUNT(1) FROM tablename WHERE 1;
/* Bad */
select count(1) from tablename where 1;
2
3
4
5
# 名称
每个子句应该开始一个新的行。SELECT,JOIN,LEFT JOIN,OUTER JOIN,WHERE,UNION等是开始新子句的关键字。
/* Good */
SELECT COUNT(1)
FROM tablename
WHERE really_loooong_column = CONCAT(other_column, ' street');
/* Bad */
SELECT COUNT(1) FROM tablename WHERE really_loooong_column = CONCAT(other_column, ' street');
2
3
4
5
6
7
开始子句的关键字应该是右对齐的。我们的想法是在关键字和对象之间创建一个单一的字符列。
/* Good */
SELECT COUNT(1)
FROM tablename
WHERE 1;
SELECT key_column, COUNT(1)
FROM tablename
GROUP BY key_column;
/* Bad */
SELECT COUNT(1)
FROM tablename
WHERE 1;
2
3
4
5
6
7
8
9
10
11
12
13
子查询应该对齐,就像开括号是0列那样,它们应该作为一个单元缩进,以将它们标识为子查询。他们应该继续将开放关键字右对齐。
/* Good */
SELECT *
FROM ( SELECT candidates.name, count(1)
FROM candidates
JOIN votes ON candidates.id = votes.candidate_id
GROUP BY candidates.name) name_count
JOIN city c ON name_count.name = c.mayor;
/* Bad */
SELECT *
FROM (SELECT candidates.name, count(1)
FROM candidates
JOIN votes ON candidates.id = votes.candidate_id
GROUP BY candidates.name) name_count
JOIN city ON name_count.name = city.mayor;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 结构体
列别名应始终使用关键字AS当查询具有多个列别名的多个列时,这变得很重要。如果没有AS关键字,删除的逗号会使两列成为单个别名列。
/* Good */
SELECT ebe_ebs_sox_flag_set_for_all_crs AS sox_ok
FROM tablename;
/* Bad */
SELECT ebe_ebs_sox_flag_set_for_all_crs sox_ok
FROM tablename;
2
3
4
5
6
7
表别名和列别名应该是描述性的。与变量名称非常相似,“a”,“b”,“x”等在短示例之外通常不是有用的。
表别名的微小名称有时可以用作缩写。例如,如果频繁引用“版本”,则将其缩写为“r”可能是有意义的。但是,“rel”几乎一样短,而且更具描述性。有一个很好的理由“r”而不是“rel”。
子查询别名应该更具描述性。子查询有效地在内存中创建临时表。因此,如果你将它命名为“x”,那么绝对没有任何东西可以向后来的维护者提出表格背后的意图。
/* Good */
SELECT *
FROM (SELECT table1.id AS child,
table2.id AS parent
FROM table1
JOIN table2 ON (table2.parent_id = table1.id) ) parentage;
/* Bad */
SELECT *
FROM (SELECT table1.id AS child,
table2.id AS parent
FROM table1
JOIN table2 ON (table2.parent_id = table1.id) ) x;
/* Bad */
SELECT *
FROM (SELECT table1.id AS child,
table2.id AS parent
FROM table1
JOIN table2 ON (table2.parent_id = table1.id) ) link;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
缩写对子查询别名没有帮助
# Python编码规范
提示
参考
# 公共目录规范
在数仓的开发中,经常遇到这种情况,因为是集群,所以可能会有个公共用户(例如:hadoop)之类的来存放所有的脚本 但是这个用户是数据组的同学共享的,都需要使用到,在跑脚本时就会产生各种临时文件。所以可以按相应的规范来约定一些临时文件的存储路径
${脚本根目录}/tmp
:临时目录(文件以.tmp结尾)存放临时生成的文件,意味着可以删除
${脚本根目录}/log
:日志文件(文件以.log结尾)
存放所有脚本的一些输出日志,以模块分文件夹 例如:
${脚本根目录}/log/customer/
${脚本根目录}/log/app/
# 分享的理念
这里要说的是导数的部分,数据组做的很频繁的工作也有导数的工作。那么一个导数需求A分配给一个数据同学小马后,小马导完数,假如过段时间又需要导数,而小马在忙其他很重要的事,或者请假了,为了避免导数延时,其实应该把每个人导数的代码也分享出来。
规范:
${数仓脚本根目录}/导数记录/yyyyMMdd/姓名_需求名称/姓名_需求名称.sql
${数仓脚本根目录}/导数记录/yyyyMMdd/姓名_需求名称/姓名_需求名称.shell
例如:导出orgid=1234的2019年3月的所有消费客户数
${数仓脚本根目录}/导数记录/yyyyMMdd/小马_orgid=1234机构2019年3月所有消费客户数/1234机构2019年3月所有消费客户数.sql
这样的话,可以有以下好处:
- 代码复用:下次有类似的需求就可以直接参考该目录SQL,而不用再让别的同学做一遍这个工作
- 代码审核:导数的SQL可以上传到Git,这样大家(项目经理)也可以一起检查。
- 历史追溯:防止需求方后续再次问你一些本需求的问题(对数),
说导的数和需求不符合之类的