PostgreSQL 高级特性
WITH查询
是PostgreSQL支持的高级SQL特性之一,这一特性常称CTE(Common Table Expressions),WITH查询在复杂查询中定义一个辅助语句(可理解成在一个查询中定义的临时表),这一特性常用于复杂查询或递归查询应用场景。
WITH t as(
SELECT generate_series(1,3)
)
SELECT * FROM t;
WITH regional_sales AS(
SELECT region,SUM(amount) AS total_sales
FROM orders
GROUP BY region
),top_regions AS(
SELECT region
FROM regional_sales
WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales)
)
SELECT region,product,
SUM (quantity) AS product_units,
SUM (amount) AS product_sales
FROM orders
WHERE region IN (SELECT region FROM top_regions)
GROUP BY region, product;
递归查询使用CTE
使用RECURSIVE属性可以引用自己的输出
# x从1开始,union加1后的值,循环直到x小于5结束,之后计算x值的总和
WITH recursive t (x) as(
SELECT 1
UNION
SELECT x+1
FROM t
WHERE x<5
)
SELECT sum(x) FROM t;
WITH recursive t (x) as(
SELECT 1
UNION
SELECT x+1
FROM t
WHERE x<5
)
SELECT * FROM t;
# 创建表
CREATE TABLE test_area(id int4,name varchar(32),fatherid int4);
# 插入数据
INSERT INTO test_area VALUES(1,'中国',0);
INSERT INTO test_area VALUES(2,'辽宁',1);
INSERT INTO test_area VALUES(3,'山东',1);
INSERT INTO test_area VALUES(4,'沈阳',2);
INSERT INTO test_area VALUES(5,'大连',2);
INSERT INTO test_area VALUES(6,'济南',3);
INSERT INTO test_area VALUES(7,'和平区',4);
INSERT INTO test_area VALUES(8,'沈河区',4);
# 查询ID为7以及以上的所有父节点
WITH RECURSIVE r AS(
SELECT * FROM test_area WHERE id = 7
UNION ALL
SELECT test_area.* FROM test_area,r WHERE test_area.id =r.fatherid
)
SELECT * FROM r ORDER BY id;
#
WITH RECURSIVE r AS(
SELECT * FROM test_area WHERE id = 7
UNION ALL
SELECT test_area.* FROM test_area,r WHERE test_area.id =r.fatherid
)
SELECT string_agg(name,'') FROM (SELECT name FROM r ORDER BY id) n;
# 查找当前节点以及其下的所有子节点
WITH RECURSIVE r AS(
SELECT * FROM test_area WHERE id = 4
UNION ALL
SELECT test_area.* FROM test_area,r WHERE test_area.fatherid =r.id
)
SELECT * FROM r ORDER BY id;
批量插入
INSERT INTO…SELECT…
通过表数据或函数批量插入
#创建表
CREATE TABLE tbl_batch1(user_id int8,user_name text);
#插入数据
INSERT INTO tbl_batch1(user_id,user_name)
SELECT user_id,user_name FROM user_ini;
# 创建表
CREATE TABLE tbl_batch2(id int4,info text);
# 插入数据
INSERT INTO tbl_batch2(id,info)
SELECT generate_series(1,5),'batch2';
INSERT INTO VALUES(),(),…()
CREATE TABLE tbl_batch3(id int4,info text);
INSERT INTO tbl_batch3(id,info) VALUES(1,'a'),(2,'b'),(3,'c');
SELECT * FROM tbl_batch3;
这种批量插入方式,相比一条SQL插入一条数据饿方式能减少数据库的交互,减少数据库WAL(Write-Ahead Logging)日志的生成,提升插入效率
COPY或\COPY元命令
# 创建表
CREATE TABLE tbl_batch4(
id int4,
info text,
create_time timestamp(6) with time zone default clock_timestamp()
);
# 插入数据
INSERT INTO tbl_batch4(id,info)SELECT n,n||'_batch4' FROM generate_series(1,10000000) n;
# 进入postgres的bin目录
cd /Library/PostgreSQL/13/bin
# psql连接数据库
./psql -h 192.168.1.41 -p 5432 gis postgres
# 开启时间
\timing
# 客户端导出数据
\copy public.tbl_batch4 TO '/Users/sungang/Documents/data/scripts/tbl_batch4.txt';
# 清空表
TRUNCATE TABLE public.tbl_batch4;
# 客户端导入数据
\copy public.tbl_batch4 FROM '/Users/sungang/Documents/data/scripts/tbl_batch4.txt';
# 查看表大小
\dt+ tbl_batch4
# 查看指定索引大小
\di+ tbl_batch4_pkey
RETURNING返回修改的数据
返回DML修改的数据
- INSERT语句后接RETURNING属性返回插入的数据
- UPDATE语句后接RETURNING属性返回更新后的新值
- DELETE语句后接RETURNING属性返回删除的数据
不需要额外的SQL获取这些值
RETURNING返回插入的数据
# 创建表
CREATE TABLE test_r1(id serial,flag char(1));
# 插入数据
INSERT INTO test_r1(flag) VALUES ('a') RETURNING *;
INSERT INTO test_r1(flag) VALUES ('b') RETURNING id;
RETURNING返回更新后数据
SELECT * FROM test_r1 WHERE id=1;
UPDATE test_r1 SET flag='p' WHERE id=1 RETURNING *;
RETURNING返回删除的数据
DELETE FROM test_r1 WHERE id=2 RETURNING *;
UPSERT
特性是指INSERT…ON CONFLICT UPDATE,用来解决在数据插入过程中数据冲突的情况
# 创建表
CREATE TABLE user_logins(user_name text primary key,
login_cnt int4,
last_login_time timestamp(0) without time zone
);
# 插入数据
INSERT INTO user_logins(user_name,login_cnt) VALUES('francs',1);
# 当产生冲突的时候解决问题
# 设置规则:当用户名数据冲突时将登陆次数login_cnt值加一,同时更新最近登陆时间last_login_time
INSERT INTO user_logins(user_name,login_cnt) VALUES ('matiler',1),('francs',1)
ON CONFLICT(user_name) DO UPDATE SET login_cnt=user_logins.login_cnt+EXCLUDED.login_cnt,last_login_time=now();
# 当冲突时啥也不做
INSERT INTO user_logins(user_name,login_cnt)
VALUES ('tutu',1),('francs',1) ON CONFLICT (user_name) DO NOTHING;
数据抽样
当表数量比较大时,随机查询表中一定数量纪录的操作很常见
ORDER BY random()
这种方式在功能上能满足随机返回指定行数据,但性能很低 ;这种方式进行了全表扫描和排序,效率非常低
SELECT * FROM user_ini ORDER BY random() LIMIT 1;
# 执行计划
EXPLAIN ANALYZE SELECT * FROM user_ini ORDER BY random() LIMIT 1;
SYSTEM抽样方式
SYSTEM抽样方式为随机抽取表上数据块上的数据,理论上被抽样表的每个数据块被检索的概率是一样的,SYSTEM抽样方式给予数据块级别,后接抽样参数,被选中的块上的所有数据将被检索
- 创建测试表
CREATE TABLE test_sample(
id int4,
message text,
create_time timestamp(6) without time zone default clock_timestamp()
);
# 插入150万条数据
INSERT INTO test_sample(id,message)
SELECT n,md5(random()::text) FROM generate_series(1,1500000) n;
# 抽样因子设置成0.01,返回 1500000*0.01%=150条
EXPLAIN ANALYZE SELECT * FROM test_sample TABLESAMPLE SYSTEM(0.01);
# 查看表占用的数据块数量
SELECT relname,relpages FROM pg_class WHERE relname='test_sample';
# 查看抽样数据的ctid
# citd 是表的隐藏列,括号的第一位表示逻辑数据块编号,第二位表示逻辑块上的数据的逻辑编号
SELECT ctid,* FROM test_sample TABLESAMPLE SYSTEM(0.01);
SYSTEM 抽样方式返回的数据以数据块为单位,被抽样的块上的所有数据被检索。
BERNOULLI抽样方式
BERNOULLI抽样方式随机抽取表的数据行,并返回指定百分比数据,BERNOULLI抽样方式基于数据行级别,理论上被抽样表的每行记录被检索的概率是一样的,因此BERNOULLI抽样方式抽取的数据比SYSTEM抽样方式具有更好的随机性,但性能上相比SYSTEM抽样方式低很多
EXPLAIN ANALYZE SELECT * FROM test_sample TABLESAMPLE BERNOULLI (0.01);
SELECT count(*) FROM test_sample TABLESAMPLE BERNOULLI(0.01);
BERNOULLI抽样对于数据行级别,数据位于不同的数据块上
SELECT ctid,* FROM test_sample TABLESAMPLE BERNOULLI(0.01);
SYSTEM抽样方式基于数据块级别,随机抽取表数据块上的记录,因此这种方式抽取的记录的随机性不是很好,但返回的数据以数据块为单位,抽样性能很高,适用于抽样效率优先的场景,例如抽样大小为上百GB的日志表;而BERNOULLI抽样方式基于数据行,相比SYSTEM抽样方式所抽样的数据随机性更好,但性能相比SYSTEM差很多,适用于抽样随机性优先的场景。读者可根据实际应用场景选择抽样方式。
聚合函数
常用聚合函数有avg()、sum()、min()、max()、count()
string_agg函数
简单地说string agg雨数能将结果集某个字段的所有行连接成字符串,并用指定delimiter分隔符分隔, expression表示要处理的字符类型数据;参数的类型为(text, text)或 (bytea, bytea), 函数返回的类型同输人参数类型一致, bytea 属于二进制类型,使用情况不 多。首先创建测试表并插人以下数据:
CREATE TABLE city (country character varying(64),city character varying(64)); INSERT INTO city VALUES ('中国','台北');
INSERT INTO city VALUES('中国','香港');
INSERT INTO city VALUES ('中国','上海');
INSERT INTO city VALUES ('日本', '东京');
INSERT INTO city VALUES (' 日本','大阪');
SELECT string_agg(city,',') FROM city ;
SELECT country,string_agg(city,',') FROM city GROUP BY country;
窗口函数
聚合函数将结果集进行计算并且通常返回一行。窗口函数也是基于结果集进行计算,与聚合丽数不同的是窗口函数不会将结果集进行分组计算并输出-行,而是将计算出的结果合并到输出的结果集上,并返回多行。使用窗口函数能大幅简化SQL代码。
PostgreSQL提供内置的窗口函数,例如row_ num()、 rank()、 lag() 等,除了内置的窗口函数外,聚合函数、自定义函数后接OVER属性也可作为窗口函数。
-
OVER表示窗口函数的关键字。
-
PARTITON BY属性对查询返回的结果集进行分组,之后窗口函数处理分组的数据。
-
ORDER BY属性设定结果集的分组数据排序。
avg() OVER()
聚合函数后接OVER属性的窗口函数表示在一个查询结果集上应用聚合函数,avg()聚合函数后接OVER属性的窗口函数,此窗口函数用来计算分组后数据的平均值。
创建一张成绩表并插人测试数据
CREATE TABLE score (
id serial primary key,
subject character varying(32),
stu_name character varying(32),
score numeric(3,0)
);
INSERT INTO score(subject,stu_name,score) VALUES ('Chinese','francs',70);
INSERT INTO score(subject,stu_name,score) VALUES ('Chinese','matiler',70);
INSERT INTO score(subject,stu_name,score) VALUES ('Chinese','tutu',80);
INSERT INTO score(subject,stu_name,score) VALUES ('English','francs',90);
INSERT INTO score(subject,stu_name,score) VALUES ('English','matiler',75);
INSERT INTO score(subject,stu_name,score) VALUES ('English','tutu',60);
INSERT INTO score(subject,stu_name,score) VALUES ('Math','francs',80);
INSERT INTO score(subject,stu_name,score) VALUES ('Math','matiler',99);
INSERT INTO score(subject,stu_name,score) VALUES ('Math','tutu',65);
- 查询每名学生学习成绩并且显示课程的平均分,通常是先计算出课程的平均分,然后score表与平均分表关联查询
SELECT s.subject,s.stu_name,s.score,tmp.avgscore FROM score s LEFT JOIN (SELECT subject,avg(score) avgscore FROM score GROUP BY subject)tmp ON s.subject =tmp.subject;
# 使用窗口函数
SELECT subject,stu_name,score,avg(score) OVER (PARTITION BY subject) FROM score;
row_number()
row_number()窗口函数对结果集分组后的数据标注行号,从1开始
SELECT row_number() OVER (partition by subject ORDER BY score desc),* FROM score;
如果不指定partition属性,row_number()窗口函数现实表所有记录的行号
SELECT row_number() OVER (ORDER BY id) AS rownum ,* FROM score;
rank()
rank()窗口函数和row_number()窗口函数相似,主要区别为当组内某行字段值相同时,行号重复并且行号产生间隙
SELECT rank() OVER(PARTITION BY subject ORDER BY score),* FROM score;
dense_rank()
dense_rank()窗口函数和row_number()窗口函数相似,主要区别为当组内某行字段值相同时,行号重复,但行号产生间隙
SELECT dense_rank() OVER(PARTITION BY subject ORDER BY score),* FROM score;
lag()
获取行偏移offset那行某个字段的数据
# 查询score表并获取向上偏移一行记录的id值
SELECT lag(id,1) OVER(),* FROM score;
# 查询score表并获取向上偏移两行记录的id值,并设置默认值
SELECT lag(id,2,1000) OVER(),* FROM score;
first_value()
取结果集每一个分组的第一行数据的字段值
# score表按课程分组后取分组的第一行的分数
SELECT frist_value(score) OVER(PARTITION BY subject),* FROM score;
# score表按课程分组同时取每门课的最高分
SELECT first_value(score) OVER(PARTITION BY subject ORDER BY score desc),* FROM score;
last_value()
取结果集每一个分组的最后一行数据的字段值
# score表按课程分组后取分组的最后一行的分数
SELECT last_value(score) OVER(PARTITION BY subject),* FROM score;
nth_value()
取结果集每一个分组的指定行数据的字段值
# score表按课程分组后取分组的第二行的分数
SELECT nth_value(score,2) OVER (PARTITON BY subject),* FROM score;
窗口函数别名的使用
SELECT avg(score) OVER(r),sum(score) OVER(r),* FROM score WINDOW r as(PARTITION BY subject);