🔥【SQL】SELECT专题
🗓 2018年03月20日 📁 文章归类: 0x11_算法平台
版权声明:本文作者是郭飞。转载随意,标明原文链接即可。
原文链接:https://www.guofei.site/2018/03/20/sqlselect.html
SQL 系列文章
- 🔥【SQL】SELECT专题,主要内容是 SELECT 语句
- 【SQL】通用语法,主要内容是 CREATE、ALTER、INSERT 等通用的 SQL 语法
- 各种数据库方言,各种数据库的相关命令,如 HIVE、MySQL、SQL Server
- 【python】sqlAlchemy
SELECT
SELECT 列名称
FROM 表名
WHERE 条件1
GROUP BY 列名 HAVING 条件2
ORDER BY 列名 ASC/DESC;
执行顺序
- 先连接from后的数据源(若有join,则先执行on后条件,再连接数据源)。
- 执行where条件
- 执行group by
- 执行having
- 执行order by
- 最后select 输出结果。
(8)SELECT (9)DISTINCT (11)<Top Num><select list>
(1)FROM[left_table]
(3)<join_type>JOIN<right_table>
(2) ON<join_condition>
(4)WHERE<where_condition>
(5)GROUPBY<group_by_list>
(6)WITH<CUBE | RollUP>
(7)HAVING<having_condition>
(10)ORDERBY<order_by_list>
逻辑查询处理阶段简介
- FROM:对FROM子句中的前两个表执行笛卡尔积(Cartesian product)(交叉联接),生成虚拟表VT1
- ON:对VT1应用ON筛选器。只有那些使
为真的行才被插入VT2 - OUTER(JOIN):如 果指定了OUTER JOIN(相对于CROSS JOIN 或(INNERJOIN),保留表(preserved table:左外部联接把左表标记为保留表,右外部联接把右表标记为保留表,完全外部联接把两个表都标记为保留表)中未找到匹配的行将作为外部行添加到 VT2,生成VT3.如果FROM子句包含两个以上的表,则对上一个联接生成的结果表和下一个表重复执行步骤1到步骤3,直到处理完所有的表为止。
- WHERE:对VT3应用WHERE筛选器。只有使
为true的行才被插入VT4. - GROUPBY:按GROUP BY子句中的列列表对VT4中的行分组,生成VT5.
- CUBE/ROLLUP:把超组(Suppergroups)插入VT5,生成VT6.
- HAVING:对VT6应用HAVING筛选器。只有使
为true的组才会被插入VT7. - SELECT:处理SELECT列表,产生VT8
- DISTINCT:将重复的行从VT8中移除,产生VT9
- ORDERBY:将VT9中的行按ORDER BY 子句中的列列表排序,生成游标(VC10)
- TOP:从VC10的开始处选择指定数量或比例的行,生成表VT11,并返回调用者
GROUP BY
group by 相关的汇总函数
count([distinct|all] 字段1)
count(*),计算包含null行数
count(a),计算不包含a位null的行数
max, min, sum, avg, sum, avg
var_pop, var_samp 方差和样本方差
stddev_pop, stddev_samp 偏差和样本偏差
covar_pop(col1,col2), vovar_samp 协方差和样本协方差
corr(col1,col2) 相关系数
count(DISTINCT col1) - 合适的函数,都可以接受DISTINCT
HAVING 是 配合 GROUP BY 使用的,后接分组过滤条件,组勇士过滤掉不满足条件的分组。例子:
-- 只取出分组内的个数大于2的那些的分组
HAVING count(*)>2;
-- 平均工资高于 20000 的哪些分组
HAVING AVG(salary)>20000;
-- 总销售额大于 100 的
HAVING SUM(amount) > 100;
别名
SELECT 列名 as 别名
FROM table_name
WHERE ...
集合类运算
UNION:两个表并到一起,并且删掉重复内容
SELECT field1,field2,...,from tablename1
UNION
SELECT field1,field2,...,from tablename2
UNION
SELECT field1,field2,...,from tablenamen
...
- UNION ALL,把两个表并到一起,不删除重复内容
- EXCEPT 差集,也就是在table1中,但不在table2中的结果。
- EXCEPT ALL 不删除重复行
- 有的数据库还可以用 MINUS 和 MINUS ALL
- INTERSECT 交集,表示同时出现在table1和table2中的结果
- INTERSECT ALL 表示不删除重复行。
注:差集和交集也可以用in/join来实现
join
- 笛卡尔积(CARTESIAN PRODUCT)
- 内连接(INNER JOIN):在笛卡尔积中,保留匹配的,舍去不匹配的。
- 自然连接(natural join):对相同字段匹配
- 等值连接:对等值匹配
- 不等连接:对不等值匹配
- 外连接(outer join):不但保留匹配,还保留一部分不匹配
- 左外连接(left [outer] join):保留匹配,还保留左边表的不匹配
- 右外连接(right [outer] join):保留匹配,还保留右边表的不匹配
- 全外连接(full [outer] join):保留匹配,保留左表、右表的不匹配
- 交叉连接
- 额外:semi join, anti join
SELECT field1, field2, ... ,fieldn
from join_tablename1
inner join join_tablename2
on joincondition;
三重内连接+别名
SELECT a.id, a.name, b.salary, c.department
FROM table_name1 a
INNER JOIN table_name2 b
ON a.id=b.id
INNER JOIN table_name3 c
ON a.id=c.id;
以上可以用where的形式实现(更简单)
SELECT a.id, a.name, b.salary, c.department
FROM table_name1 a, table_name2 b, table_name3 c
WHERE a.id=b.id AND a.id=c.id;
子查询
SELECT * FROM t_employee
WHERE
-- 支持多行多列
(sal,job) IN (SELECT sal,job FROM t_employee)
-- NOT IN
AND (sal,job) NOT IN (SELECT sal,job FROM t_manager)
-- 等号
AND (dept_id,dept_name) = (SELECT dept_id,dept_name FROM t_engineer)
-- 可以是枚举值
AND job IN ("算法工程师", "会计师");
;
函数
输出bool的运算符
>
<
=
!=、<>
>=
<=
between 取值1 and 取值2
is null
in(1,2)
like 通配符匹配
-- %表示多个字符或0个,
-- _表示单个字符,
-- [charlist]字符列中的任意单一字符,
-- [^charlist]不在字符列中的任意单一字符
regexp 正则表达式
举例:‘cjgong’regexp 'g$' 返回1
^ 匹配开始部分
$ 匹配结束部分
. 匹配任意一个字符
[字符集合] 匹配集合中的任意一个字符
[^字符集合] 匹配集合中外的任意一个字符
str1|str2|str3 匹配任意一个字符串
*
+
字符串{N} 字符串出现N次
字符串(M,N) 字符串出现至少M次,最多N次
逻辑运算符
(与C语言很像)
条件1 and 条件2 and 条件3
条件1 or 条件2 or 条件3
and(&&)
or(||)
not(!)
xor(异或)
位运算符(与C语言完全相同)
&
|
~
^
<<
>>
CASE
简单Case
-- 公式:
case 列名
when 条件值1 then 选择项1
when 条件值2 then 选项2.......
else 默认值
end
-- 例子:
select
case job_level
when '1' then 'rich'
when '2' then 'middle'
when '3' then 'poor'
else 'not sure'
end
from table_employee
复杂Case
-- 公式:
case
when 条件值1 then 选择项1
when 条件值2 then 选择项2
...
else 选择项n
end
-- 例子:
case
when job_level = '1' then e_wage*1.97
when job_level = '2' then e_wage*1.07
when job_level = '3' then e_wage*1.06
else e_wage*1.05
end
函数1
distinct 字段1
类型转化函数
decimal, double, Integer, smallint,real
Hex(arg):转化为参数的16进制表示。
转化为字符串类型的:
char, varchar
Digits(arg):返回arg的字符串表示法,arg必须为decimal。
转化为日期时间的:
date, time,timestamp
时间
Mysql 中的时间
year, quarter, month, week, day, hour, minute, second
dayofyear(arg)
Dayofweek(arg)
days(arg):返回日期的整数表示法,从0001-01-01来的天数。
midnight_seconds(arg):午夜和arg之间的秒数。
Monthname(arg):返回arg的月份名。
Dayname(arg):返回arg的星期。
字符串时间
SELECT to_date('2008-12-29 16:25:46'); -- 返回日期
SELECT date_sub('2008-12-29',5); -- 日期减去一个天数
SELECT date_add('2008-12-29',5); -- 日期增加一个天数
SELECT datediff('2008-12-19','2008-12-10'); -- 两个日期之差
多列函数
coalesce(arg1,arg2….) -- 返回参数集中第一个非null参数。
greatest(col1,col2,...,coln) -- 返回多列中,最大的那个值
least()
字符串函数
length,lcase, ucase, ltrim , rtrim
CONCAT(arg1,arg2) -- 连接两个字符串arg1和arg2。
insert(arg1,pos,size,arg2) -- 返回一个,将arg1从pos处删除size个字符,将arg2插入该位置。
left(arg,length) -- 返回arg最左边的length个字符串。
right(arg,length) -- 返回一个有arg右边length个字节组成的字符串。
locate(arg1,arg2,<pos>;) -- 在arg2中查找arg1第一次出现的位置;如果指定pos,则从arg2的pos处开始查找
posstr(arg1,arg2) -- 返回arg2第一次在arg1中出现的位置。
repeat(arg1 ,num_times) -- 返回arg1被重复num_times次的字符串。
replace(arg1,arg2,arg3) -- 将在arg1中的所有arg2替换成arg3。
space(arg) -- 返回一个包含arg个空格的字符串。
substr(arg1,pos,<length>;):返回arg1中pos位置开始的length个字符,如果没指定length,则返回剩余的字符。
initcap(col) -- 每个单词的首字母大写(一个字符串中可以有多个单词)
contains:CONTAINS( 字段1, '"HEIBEI province" OR beijing' )
CONTAINS(*,'beijing')
数学函数
Abs
ln, log10, log2, log(base,arg)
exp(d), power(a,b)
Power(arg1,arg2) -- 返回arg1的arg2次方。
Mod(arg1,arg2) --返回arg1除以arg2的余数,符号与arg1相同。
Round(arg1,arg2) -- 四舍五入截断处理,arg2是位数,如果arg2为负,则对小数点前的数做四舍五入处理。
Ceil(arg) -- 返回大于或等于arg的最小整数。
Floor(arg) -- 返回小于或等于参数的最小整数。
Rand() -- 返回1到1之间的随机数。
Sign(arg) -- 返回arg的符号指示符。-1,0,1表示。
truncate(arg1,arg2) -- 截断arg1,arg2是位数,如果arg2是负数,则保留arg1小数点前的arg2位。
e(),pi() 常数e和常数pi
factorial(5) -- 阶乘
算术运算符
(与C语言很像)
+
-
*
/
% 余数
A & B 按位与
A|B 按位或
A^B 按位异或
~A 按位反
统计运算符
percentile_approx(col_name,0.25) # 求0.25分位数
窗口函数
一般语法是:
function() OVER([partition_clause] order_by_clause)
over部分
over(order by col1):对查询的整个数据范围内的数据,按照字段col1排序
over(partition by col2):按照字段col2分组,统计每个组内的数据
over(partition by col2 order by col1):按照字段col2分组,每个分组内按照字段col1排序
over(order by col3 range between 2 preceding and 2 following):窗口范围为当前行数据幅度减2加2后的范围内的 ---- 按列值控制窗口大小
over(order by col3 rows between 2 preceding and 2 following):窗口范围为当前行前后各移动2行 ---- 按行数控制窗口大小
关键字解释:
- ROWS:指定行
- RANGE:指定范围
- PRECEDING:往前
- FOLLOWING:往后
- CURRENT ROW:当前行
- UNBOUNDED:起点
- UNBOUNDED PRECEDING:表示从前面的起点开始
- UNBOUNDED FOLLOWING:表示到后面的终点结束
function部分
1.排名函数
ROW_NUMBER(): 会对所有数值输出不同的序号,序号唯一连续;
RANK(): 相同的值排名相同,并且排名数字靠前,(例如,排名可能是这样的:1,1,3,3,5,6,7)
DENSE_RANK:排名序号连续(例如:1,1,2,2,3,3,4)
NTILE(n): 全部数据n等分,序号就是等分数(1到n),如果不能平均分配,那么序号靠后的组的数量不多于序号靠前的(例如,ntile(3)分5条记录,就是2,2,1)
PERCENT_RANK():计算当前行的百分比排名 ——(当前行排名 - 1)/(窗口分区中的行数 - 1)
CUME_DIST:计算当前行的相对排名——(前面的行数[+相等值行数])/(分区中的总行数)
应用案例1: https://jingyan.baidu.com/article/9989c74604a644f648ecfef3.html
应用案例2:(选出每组最大的记录)
SELECT * from app.app_temp_test where row_number(id)<=1;
2.描述统计函数
根据有无 order/partition 分为4种,细心体会:
pd_df=pd.DataFrame(np.random.randint(low=0,high=3,size=(20,2)),columns=['a','b'])
df=spark.createDataFrame(pd_df).cache()
df.createOrReplaceTempView('df')
spark.sql('''
SELECT *,
count(*) over() as c1, -- 总行数
count(*) over(order by a asc) as c2, -- 递加行数,例如,a=[0,0,0,1,1], 那么结果就是[3,3,3,5,5]
count(*) over(partition by b) as c3, -- 分组行数,就是每个分组有多少行
count(*) over(partition by b order by a asc) as c4 --分组递加计数,先分组,对每组求递加行数
from
df
''').show()
COUNT(*) 4种都支持,但不支持 COUNT(distinct * )
SUM(col1)
AVG(col1)
MIN(col1), MAX(col1)
FIRST(col1), LAST(col1) 【貌似】又可以写成FIRST_VALUE(col1), LAST_VALUE(col1)
lag()???
stddev(col1), 样本标准差,只有一行数据时返回0
stddev_samp(col1), 样本标准差,只有一行数据时返回null
stddev_pop(col1), 总体标准差
variance(col1):计算样本方差,只有一行数据时返回0
var_samp(col1):计算样本方差,只有一行数据时返回null
var_pop(col1):计算总体方差
注:stddev()=sqrt( variance() ), stddev_samp()=sqrt( var_samp() ), stddec_pop=sqrt( var_pop() )
covar_samp(col1, col2):样本协方差
covar_pop(col1, col2): 总体协方差
corr(col1, col2): 相关系数
表生成函数
单行多列->单行单列(横向拼接)
-- 拼接行:
CONCAT(col1,col2) -- 拼接每一个字段,按行
CONCAT_WS(",",col1,col2) -- 拼接,并且中间用逗号分隔
多行->单行(纵向拼接)
-- 把列拼接到一个单元格:(要与group by 配合使用)
-- 注意:新字段是 array 对象
collect_list
collect_set
-- 综合用法:(列拼接成一个单元格,并转为string)
CONCAT_WS(',',COLLECT_LIST(name))
CONCAT_WS(',',COLLECT_LIST(cast (name as string))) -- 非 string 的必须要转格式
单行单列->多行单列(纵向展开)
SELECT explode(split('a,b,c,d,e',','))
(进阶)单行单列->多行多列
SELECT t1.id,t2.serialno,t2.arr
FROM (SELECT 'xxx' as id,array('a','b','c') as arr) t1
LATERAL VIEW posexplode(t1.arr) t2 AS serialno,arr;
输出:
| id | serialno | arr |
|---|---|---|
| xxx | 0 | a |
| xxx | 1 | b |
| xxx | 2 | c |
特殊数据结构
json
数据准备
CREATE TABLE tmp_example_json AS
SELECT id,json_str
FROM(VALUES
('0', '[{"name":"王二狗","sex":"男","age":"25"},{"name":"李狗嗨","sex":"男","age":"47"}]'),
('1', '[{"name":"王三狗","sex":"女","age":"21"}]')
) AS t(id, json_str)
-- 这种 json 是不带中括号的
CREATE TABLE tmp_example_json2 AS
SELECT id,json_str
FROM(VALUES
('0', '{"name":"王二狗","sex":"男","age":"25"}'),
('1', '{"name":"王三狗","sex":"女","age":"21"}')
) AS t(id, json_str)
提取指定的值
SELECT id
,GET_JSON_OBJECT(json_str,'$.[0]') -- 提取第0个
,GET_JSON_OBJECT(json_str,'$.[0].name') -- 提取第0个的name属性
FROM tmp_example_json;
-- 对于不带中括号的
SELECT GET_JSON_OBJECT(json_str,'$.name')
FROM tmp_example_json2;
json_tuple:可以同时提取多个字段,仅用于没有 []的情况,需要配合 LATERAL VIEW 中使用:
SELECT t1.id, t2.name, t2.age
FROM tmp_example_json2 t1
LATERAL VIEW JSON_TUPLE(json_str, 'name', 'age') t2 AS name, age
遍历提取。预先不知道 json array 长度,并且提取出所有name,需要手动分割,然后提取。(暂时没找到更优雅的纯 SQL 方法,建议还是用 udf 吧
SELECT id
,GET_JSON_OBJECT(json_str_split,'$.name') AS name
FROM (
SELECT t1.id
,t2.json_str_split
FROM (
SELECT id
,
REGEXP_REPLACE(
REGEXP_EXTRACT(json_str,'^\\[(.+)\\]$',1) --把前后中括号去掉
,'\}\,\{\"name"'
,'\}\|\|\{\"name\"' --把两个name之间的逗号换成||
)
AS json_str_tmp
FROM tmp_example_json
) t1
LATERAL VIEW EXPLODE(SPLIT(t1.json_str_tmp,'\\|\\|')) t2 AS json_str_split
)
;
生成JSON
-- 原始数据
CREATE TABLE tmp_example_json3 AS
SELECT name, sex, age FROM
(
VALUES
('王二狗','男',23),
('李狗嗨','男',29),
('王三狗','女',26),
('李三刀','女',31)
) AS t(name, sex, age);
-- 生成 JSON
SELECT TO_JSON(MAP("name", name,"sex", sex,"age", age))
FROM tmp_example_json3;
-- 另外,MAP 结构是这样取数的
SELECT MAP("name", name,"sex", sex,"age", age)['name']
FROM tmp_example_json3;
-- 或者生成列表嵌套的 JSON
SELECT
sex
,TO_JSON(COLLECT_LIST()(MAP('name', name, 'sex', sex, 'age', age))) AS json_arr
FROM tmp_example_json3
GROUP BY sex;
with语句
WITH
tmp_table_01 AS (SELECT 1 AS col1)
-- 前面定义的表,后面可以直接用
,tmp_table_02 AS (SELECT col1+9 AS col1 FROM tmp_table_01)
insert overwrite table table_name partition (dt='dt')
SELECT * FROM tmp_table_01 UNION ALL SELECT * FROM tmp_table_02;
查询案例
找出重复的记录
SELECT id,COUNT(id)
FROM table1
WHERE dt = '2018-02-16'
GROUP BY id HAVING COUNT(id) > 1;
每个班级选取前 5 个学生
select * from
(
select *
,row_number() over(partition by class order by grade_math,grade_english) as rownum
from table
) as temp
where temp.rownum <= 5;
注意,如果col1,col2仍有重复的话,可能每次返回的结果都不一样(一个小坑)
抽样
-- 20% 抽样
SELECT * FROM tablename
WHERE rand() < 0.2;
-- 每个类别随机抽 5 个
-- 类似前面,不过 ORDER BY 后面接 rand()
-- 随机抽样,但要求每次运行结果都一样
-- 思路:先做 md5,然后 md5_val % 10 = 1
性能优化
- 避免使用
select *,而是使用select col1, col2 - where 条件中尽量避免函数,比起
where col1 + 5 > 90,更推荐where col1 > 90 - 5 - 避免类型隐式转换,如果
col1是string类型,那么推荐where col1 = '1',而不是col1 = 1 - 如果确定知道只有1条记录,那么使用
limit- 原因:使用
limit 1之后,找到一个就不会再继续找了
- 原因:使用
- 尽量使用
union all而不是union,因为union还会尝试合并和排序
HIVE:
- 用
LEFT SEMI JOIN代替IN - 用
LEFT SEMI JOIN代替INTERSECT - 用
ANTI JOIN代替EXCEPT - 用
GROUP BY代替DISTINCT - JOIN 时,大表放左边,小表放右边(现代已经不太影响性能了)
关于 Presto 的
- 合理设置分区,以及查询时必须带上分区。跨分区查询往往很慢
- 使用 ORC 格式,它是一种文件格式
- 它按列存储。例如执行
SELECT * FROM t WHERE age > 30;时,会查看每个 Stripe 已经计算好的min(age), max(age),直接跳过不符合的 Stripe,从而极大提高效率 - 做了数据压缩,因为列的数据结构相似,因此压缩率很高。这减少了节点之间数据传输的IO带宽压力
- 它按列存储。例如执行
- 预先排序
- 排序后的 ORC 性能更高,更可能跳过不必要的数据处理
- 使用近似函数,例如 approx_distinct() 函数比Count(distinct x)有大概2.3%的误差,性能却快很多
- 用regexp_like代替多个like语句,Presto 没有对 多个 like 做优化
拉链表
介绍
- 全量表:每天的所有的最新状态的数据,
- 增量表:每天的新增数据,增量数据是上次导出之后的新数据。
- 流水表: 对于表的每一个修改都会记录,可以用于反映实际记录的变更。例如,存放每天的交易形成的历史。
- 拉链表:维护历史状态,以及最新状态数据的一种表,拉链表根据拉链粒度的不同,实际上相当于快照,只不过做了优化,去除了一部分不变的记录而已,通过拉链表可以很方便的还原出拉链时点的客户记录。
拉链表适用于以下情况
- 每天一小部分数据有变化,用全量表会浪费大量的存储空间
- 希望能较为方便的查询历史快照(流水表和增量表不满足需求)
这里介绍一种 HIVE 常见的拉链表构建方案
结构
- 会有这些字段:
ds, dt, end_date, start_date - 其中,分区字段:
ds, dt, end_date
分区示例(今天是2018年11月1日)
ds=ACTIVE/dt=8012-12-31/end_date=8012-12-31
ds=EXPIRED/dt=2018-10-29/end_date=2018-10-29
ds=EXPIRED/dt=2018-10-30/end_date=2018-10-30
ds=EXPIRED/dt=2018-10-31/end_date=2018-10-31
ds=HISTORY/dt=8012-12-31/end_date=8012-12-31
ds=INCREMENT/dt=0-2018-10-29/end_date=0-2018-10-29
ds=INCREMENT/dt=0-2018-10-30/end_date=0-2018-10-30
ds=INCREMENT/dt=0-2018-10-31/end_date=0-2018-10-31
ds=INCREMENT/dt=0-2018-11-01/end_date=0-2018-11-01
其中
- HISTORY 分区存放的是历史数据转结,非必须。
- INCREMENT 为实时导入的数据,非必须
- ds非必须,但为了查询语句易读,往往加上这个字段
- dt非必须,一般也不用到查询语句中,但这个dt可以用来表示产生此分区的日期,增强可读性
查询
- 查询昨日线上数据
ds='ACTIVE' - 查询某一天的数据(快照)
end_data>'2018-11-11' and start_date<='2018-11-11'
构建
| 2018-01-01 | ||
|---|---|---|
| key | col1 | col2 |
| 1 | A | A |
| 2 | B | B |
| 3 | C | C |
| 2018-01-02 | ||
|---|---|---|
| key | col1 | col2 |
| 1 | A | AA |
| 2 | B | B |
| 3 | C | C |
| 4 | D | D |
| key | col1 | col2 | start_date | end_date | ds | dt |
|---|---|---|---|---|---|---|
| 1 | A | A | 2018-01-01 | 2018-01-02 | EXPIRED | 2018-01-02 |
| 1 | A | AA | 2018-01-02 | 8012-12-31 | ACTIVE | 8012-12-31 |
| 2 | B | B | 2018-01-01 | 8012-12-31 | ACTIVE | 8012-12-31 |
| 3 | C | C | 2018-01-01 | 8012-12-31 | ACTIVE | 8012-12-31 |
| 4 | D | D | 2018-01-02 | 8012-12-31 | ACTIVE | 8012-12-31 |
参考文献
LATERAL VIEW
https://www.cnblogs.com/liuxuewen/archive/2012/03/12/2392644.html
w2school:SQL教程
SQL字符串函数
Hive字符串操作
Hive日期格式转换用法
http://lxw1234.com/archives/2015/04/20.htm
https://www.jianshu.com/p/799252156379
您的支持将鼓励我继续创作!