SQL进阶(五):With 函数 vs 视图函数

目录

    • [1. 课前小问答 🔎](#1. 课前小问答 🔎)
    • [2. 开始之前的准备](#2. 开始之前的准备)
    • [3. VIEW 函数基本](#3. VIEW 函数基本)
    • [4. WITH 函数基本](#4. WITH 函数基本)
    • [5. 一些 WITH 函数的使用经验](#5. 一些 WITH 函数的使用经验)
    • [6. 小结 🥡](#6. 小结 🥡)
    • [7. 闯关题](#7. 闯关题)
        • [Q1:下面哪个行为是 WITH 函数创建的 CTE 不支持的](#Q1:下面哪个行为是 WITH 函数创建的 CTE 不支持的)
        • [Q2:下面哪种场景最适合使用 VIEW (相对于使用 CTE )?](#Q2:下面哪种场景最适合使用 VIEW (相对于使用 CTE )?)

本文是在原本sql闯关的基础上总结得来,加入了自己的理解以及疑问解答(by GPT4)

原活动链接

数据准备

提取码:grtn

1. 课前小问答 🔎

1. 为什么需要使用视图函数 或是 with 函数

A :视图函数(VIEW)或是 with 函数所代表的 CTE (Common Table Expression,公用表表达式)的主要目的是提高部分查询的可复用性 ,对于相当多的日常工作而言,很多的查询是高度重复的,如果为了每一个查询都单独写一份 SQL 代码将会花费大量的时间,这也是为什么 VIEW 和 WITH 可以说是对于 SQL 用户最有帮助的语法。

同样的,VIEW 和 WITH 能够帮助我们更好的去梳理 SQL 查询语句的结构,相较于使用复杂的子查询嵌套, 两者都可以帮助我们提升代码的可阅读性,方便代码报错的时候提供更好的纠错调整能力

2. VIEW 和 WITH 的区别在于哪里

A : 简单而言,两者的差别在于作用域 (如果你熟悉一些编程的基础的话)和 持久性,下面的用表格列出他们的区别:

| | VIEW | WITH |
| 作用域 | 可以跨查询复用 | 只能在单次查询内复用 |

持久性 存入数据库,持续可用 单次使用,在数据库不存储

3. 我看到 VIEW 函数的结果是一张虚拟表,这种表和在数据库的数据表有什么关系?

A : 个人而言,我不是很愿意将 VIEW 函数所产出结果的结果称为"虚拟表"。尽管 VIEW 函数的结果确实 "可以像普通表格一样被访问 ",但是 VIEW 储存的本质上是一组"当前可用的查询语句",可以当做一个以某些数据表为参数输入,一张具体的表格作为输出的函数,当其所依赖的数据表出现改变,这些"查询语句"就有可能变得不再可用。
🔖小贴士 1
学习好 VIEW 和 WITH 并不能帮你写出更加"正确"的 SQL 语句,但是却能够帮助你更有效率地 维护和编写更多的语句。
在实际的工作中,维护好一套既定的 SQL 语句带来的工作效率/能力提升远大于你的想象

2. 开始之前的准备

python 复制代码
# 导入库
from IPython.display import Markdown as md
from matplotlib import pyplot as plt
import sqlite3
import pandas as pd
import numpy as np
import json
from copy import deepcopy
from datetime import datetime

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
python 复制代码
# 我们同样使用【天猫订单成交】的数据集
# 该数据集记录了 2w+ 条目成交订单数据

df = pd.read_csv('./data/tmall_order_report.csv')

# 将表格中 NaN 值替换为 NULL
df = df.where(pd.notnull(df), None)

# 为了简化下面的教程,我们新增一个【订单创建日期】的字段
df['订单创建日期'] = df['订单创建时间'].apply(lambda x:pd.Timestamp(x).date()).astype(str)

# 将数据写入一张名为 example_table 的表
with sqlite3.connect('example.db') as conn:
    df.to_sql('example_table', con=conn, if_exists='replace', index=False)

connection = sqlite3.connect('example.db')
python 复制代码
# 查看该数据集的字段名称
df.columns
Index(['订单编号', '总金额', '买家实际支付金额', '收货地址 ', '订单创建时间', '订单付款时间 ', '退款金额',
       '订单创建日期'],
      dtype='object')
python 复制代码
df.head()

| | 订单编号 | 总金额 | 买家实际支付金额 | 收货地址 | 订单创建时间 | 订单付款时间 | 退款金额 | 订单创建日期 |
| 0 | 1 | 178.8 | 0.0 | 上海 | 2020-02-21 00:00:00 | None | 0.0 | 2020-02-21 |
| 1 | 2 | 21.0 | 21.0 | 内蒙古自治区 | 2020-02-20 23:59:54 | 2020-02-21 00:00:02 | 0.0 | 2020-02-20 |
| 2 | 3 | 37.0 | 0.0 | 安徽省 | 2020-02-20 23:59:35 | None | 0.0 | 2020-02-20 |
| 3 | 4 | 157.0 | 157.0 | 湖南省 | 2020-02-20 23:58:34 | 2020-02-20 23:58:44 | 0.0 | 2020-02-20 |

4 5 64.8 0.0 江苏省 2020-02-20 23:57:04 2020-02-20 23:57:11 64.8 2020-02-20

3. VIEW 函数基本

创建

SQLite

VIEW 函数的使用相当的简单,其基本的语句就是通过 SELECT 语句来创建

python 复制代码
query = '''
CREATE VIEW     -- 创建 VIEW 
IF NOT EXISTS   -- 如果 VIEW 存在,则不创建
view_1          -- VIEW 的名字
AS              -- 关键词
SELECT              
    "订单创建时间" AS create_time 
    ,"收货地址 " AS address	
FROM example_table
                -- 常规的查询语句
'''
# result = connection.execute(query).fetchone()

在 MySQL 和 PostgreSQL 中创建视图(VIEW)的基本语法和 SQLite 中相似,但有一些关键的区别。以下是根据您提供的 SQLite 代码在 MySQL 和 PostgreSQL 中实现相同功能的方法:

MySQL

MySQL 支持 CREATE VIEW 语句,但它没有 IF NOT EXISTS 选项。因此,如果视图已经存在,尝试创建同名视图会导致错误。一种解决方法是先检查视图是否存在,如果存在,则删除它,然后创建新视图。但这通常不是推荐的做法,因为它会破坏现有的视图定义和可能的依赖关系。

在 MySQL 中创建视图的语法如下:

sql 复制代码
CREATE VIEW view_1 AS
SELECT
    `订单创建时间` AS create_time,
    `收货地址` AS address
FROM example_table;
PostgreSQL

PostgreSQL 支持 CREATE OR REPLACE VIEW 语句,这可以用来更新现有视图的定义或创建一个新视图,这和 SQLite 的 IF NOT EXISTS 功能相似。如果视图不存在,它会被创建;如果已经存在,它的定义会被替换。

在 PostgreSQL 中创建或替换视图的语法如下:

sql 复制代码
CREATE OR REPLACE VIEW view_1 AS
SELECT
    "订单创建时间" AS create_time,
    "收货地址" AS address
FROM example_table;

注意,列名如果包含特殊字符或空格,在 MySQL 中应该使用反引号 ` 来引用它们,在 PostgreSQL 中则应该使用双引号 "。

至于执行这个查询并获取结果的部分,这通常是在应用程序代码中完成的,使用相应的数据库驱动程序。例如,在 Python 中,您可以使用 pymysql(对于 MySQL)或 psycopg2(对于 PostgreSQL)来执行查询并获取结果。在实际应用中,创建视图通常不会返回结果集,因此使用 fetchone() 或类似的方法可能不适用。

删除

可以通过 DROP VIEW 的语句来删除已经有的 VIEW

在 SQLite、MySQL 和 PostgreSQL 中实现这一功能的方法是相同的

python 复制代码
query = '''
DROP VIEW       -- 创建 VIEW 
IF EXISTS       -- 如果 VIEW 存在,则删除
view_1          -- VIEW 的名字
'''
# result = connection.execute(query).fetchone()

下面我们通过上个教程当中的一个例子来看看 VIEW 的用法

例1:我们以订单数据为例

假设我们需要按照 订单创建日期 计算累积订单量

这是原来的 query:

python 复制代码
query = '''
SELECT
    t1.order_date,
    SUM(orders_num) OVER(ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM
    (SELECT "订单创建日期" AS order_date,
            COUNT("订单编号") AS orders_num
    FROM example_table
    GROUP BY "订单创建日期"
    )AS t1
'''
result = connection.execute(query).fetchall()
result
[('2020-02-01', 176),
 ('2020-02-02', 398),
 ('2020-02-03', 665),
 ('2020-02-04', 1134),
 ('2020-02-05', 1503),
 ('2020-02-06', 1647),
 ('2020-02-07', 1824),
 ('2020-02-09', 2228),
 ('2020-02-10', 2255),
 ('2020-02-11', 2270),
 ('2020-02-12', 2271),
 ('2020-02-13', 2276),
 ('2020-02-14', 2283),
 ('2020-02-15', 2288),
 ('2020-02-17', 2678),
 ('2020-02-18', 3693),
 ('2020-02-19', 4718),
 ('2020-02-20', 6063),
 ('2020-02-21', 8131),
 ('2020-02-22', 10158),
 ('2020-02-23', 12358),
 ('2020-02-24', 14356),
 ('2020-02-25', 17772),
 ('2020-02-26', 20621),
 ('2020-02-27', 23207),
 ('2020-02-28', 25898),
 ('2020-02-29', 28010)]
加入 VIEW
python 复制代码
# 加入 VIEW
query_view = '''
CREATE VIEW IF NOT EXISTS orders_by_date AS
SELECT 
    "订单创建日期" AS order_date
    ,COUNT("订单编号") AS orders_num
FROM example_table
GROUP BY "订单创建日期";

SELECT
      order_date
      ,SUM(orders_num)                  -- 聚合函数 SUM()
       OVER(                            -- OVER 关键字,()内定 frame
             ORDER BY order_date        -- 按照 order_date 排序 
             ROWS                       -- 按照表当中的相对位置
             BETWEEN                    -- 界定上下界
                  UNBOUNDED PRECEDING   -- 上界,不设限制
             AND 
                  CURRENT ROW           -- 下界,当前行
             ) AS running_total

FROM orders_by_date;
'''
### 【!注意】这里将 execute 换为 executescript
### 这样我们可以在一个 query 当中执行多条 SQL 语句
result = connection.executescript(query_view).fetchall() 
result
[]

🔖小贴士 2
你可能很快注意到了,上面的 SQL 语句返回的是一个空的数组,不要担心,这是因为 executescript 在执行多条语句之后并不知道返回哪一条语句的结果

例2:查看视图

我们可以像查看正常表格一样去"查看"视图

python 复制代码
### 例2:查看视图
### 我们可以像查看正常表格一样去"查看"视图
query_view = '''
SELECT order_date,orders_num
FROM orders_by_date;
'''
### 【!注意】这里将 execute 换为 executescript
### 这样我们可以在一个 query 当中执行多条 SQL 语句
result = connection.execute(query_view).fetchone() 
result
('2020-02-01', 176)

4. WITH 函数基本

通过 WITH 函数构建 CTE (公用表表达式)更加的简单和灵活:

例3 使用 WITH 函数构建 CTE

python 复制代码
query_cte = '''
WITH                             -- 关键字
orders_by_date_cte               -- CTE 的名字
AS                               -- 关键字
(                                -- 需要使用() 分割 CTE
    SELECT 
    "订单创建日期" AS order_date
    ,COUNT("订单编号") AS orders_num
FROM example_table
GROUP BY "订单创建日期"
)
SELECT
      order_date
      ,SUM(orders_num)                  -- 聚合函数 SUM()
       OVER(                            -- OVER 关键字,()内定 frame
             ORDER BY order_date        -- 按照 order_date 排序 
             ROWS                       -- 按照表当中的相对位置
             BETWEEN                    -- 界定上下界
                  UNBOUNDED PRECEDING   -- 上界,不设限制
             AND 
                  CURRENT ROW           -- 下界,当前行
             ) AS running_total
FROM orders_by_date_cte;
'''
result = connection.execute(query_cte).fetchall() 
result
[('2020-02-01', 176),
 ('2020-02-02', 398),
 ('2020-02-03', 665),
 ('2020-02-04', 1134),
 ('2020-02-05', 1503),
 ('2020-02-06', 1647),
 ('2020-02-07', 1824),
 ('2020-02-09', 2228),
 ('2020-02-10', 2255),
 ('2020-02-11', 2270),
 ('2020-02-12', 2271),
 ('2020-02-13', 2276),
 ('2020-02-14', 2283),
 ('2020-02-15', 2288),
 ('2020-02-17', 2678),
 ('2020-02-18', 3693),
 ('2020-02-19', 4718),
 ('2020-02-20', 6063),
 ('2020-02-21', 8131),
 ('2020-02-22', 10158),
 ('2020-02-23', 12358),
 ('2020-02-24', 14356),
 ('2020-02-25', 17772),
 ('2020-02-26', 20621),
 ('2020-02-27', 23207),
 ('2020-02-28', 25898),
 ('2020-02-29', 28010)]
python 复制代码
## 例4:
### CTE 不会储存在数据库当中
### 下面的语句会报错
query_cte = '''
SELECT
      order_date
      ,SUM(orders_num)                  -- 聚合函数 SUM()
       OVER(                            -- OVER 关键字,()内定 frame
             ORDER BY order_date        -- 按照 order_date 排序 
             ROWS                       -- 按照表当中的相对位置
             BETWEEN                    -- 界定上下界
                  UNBOUNDED PRECEDING   -- 上界,不设限制
             AND 
                  CURRENT ROW           -- 下界,当前行
             ) AS running_total
FROM orders_by_date_cte;
'''
try:
    result = connection.execute(query_cte).fetchall() 
except Exception as e:
    print(e)
no such table: orders_by_date_cte


🔖小贴士 3
WITH 函数创建的 CTE 是依赖于 后续的 SELECT 语句的,他们构成一条完整的语句,因此不需要使用 executescript。

WITH 函数的一个特色是,它可以在一条语句当中创建多个 CTE,并且在每个 CTE中可以使用已经预先定义好的 CTE

例5:with的连续使用

python 复制代码
### 假设我们需要按照 订单创建日期 计算累积订单量
### 同时我们希望按照,收获地址展现累计值
### 只返回 北京 和 上海 的结果
query = '''
WITH orders_by_date AS
(
SELECT "订单创建日期" AS order_date
               ,"收货地址 " AS address
               ,COUNT("订单编号") AS orders_num
      FROM example_table
      GROUP BY "订单创建日期","收货地址 "
)
,filter_address AS            -- 新的 CTE
(
SELECT 
    order_date
    ,orders_num 
    ,address
FROM orders_by_date           -- 引用上一个 CTE
WHERE address IN ('北京','上海')
)
SELECT
      t1.order_date
      ,t1.address
      ,SUM(orders_num)                  -- 聚合函数 SUM()
       OVER(                            -- OVER 关键字,()内定 frame
             PARTITION BY address       -- 每个 frame 内只有 address 相同的数据
             ORDER BY order_date        -- 按照 order_date 排序 
             ROWS                       -- 按照表当中的相对位置
             BETWEEN                    -- 界定上下界
                  UNBOUNDED PRECEDING   -- 上界,不设限制
             AND 
                  CURRENT ROW           -- 下界,当前行
             ) AS running_total

FROM filter_address AS t1
'''
result = connection.execute(query).fetchall()
result[:20]
[('2020-02-01', '上海', 48),
 ('2020-02-02', '上海', 114),
 ('2020-02-03', '上海', 197),
 ('2020-02-04', '上海', 330),
 ('2020-02-05', '上海', 414),
 ('2020-02-06', '上海', 459),
 ('2020-02-07', '上海', 499),
 ('2020-02-09', '上海', 596),
 ('2020-02-10', '上海', 603),
 ('2020-02-11', '上海', 608),
 ('2020-02-13', '上海', 609),
 ('2020-02-14', '上海', 612),
 ('2020-02-17', '上海', 685),
 ('2020-02-18', '上海', 834),
 ('2020-02-19', '上海', 963),
 ('2020-02-20', '上海', 1090),
 ('2020-02-21', '上海', 1291),
 ('2020-02-22', '上海', 1508),
 ('2020-02-23', '上海', 1717),
 ('2020-02-24', '上海', 1957)]

5. 一些 WITH 函数的使用经验

  1. 充分利用 WITH 函数创建的 CTE 可以互相引用的特色,在 SQL 脚本当中将使用率最高的一部分语句率先定义成 CTE

  2. 控制 WITH 函数产生的 CTE 的数量:如果 CTE 数量过多的情况可能反而会导致代码维护起来较为吃力

  3. 对于有着复杂 JOIN 的场景,可以充分利用 CTE 在前期进行数据过滤 / 索引利用

  4. 如果一个 CTE 需要被多个查询语句使用,推荐将这个 CTE 变成一个 VIEW

6. 小结 🥡

作为通关题之前的最后一个小关卡,本教程用相当轻量级的方式介绍了两个可以简单优化我们 SQL 语句的方式。这两个函数的应用并不是直接帮助我们解决问题,但是却能够让我们在工作效率上事半功倍。

正如我在第三关的结尾说的:

因此提供相较于纯粹的知识输入和技能培养,我更希望你在本关结束后能够 形成更加良好的 SQL 书写习惯,而这也是这个训练营的核心公式:

良好的习惯 + 知识 / 技能的储备 + 大量的练习 = S Q L 的进一步提升 良好的习惯 + 知识/技能的储备 + 大量的练习 = SQL 的进一步提升 良好的习惯+知识/技能的储备+大量的练习=SQL的进一步提升

无论是善于使用 WITH 函数还是积极去构建 VIEW,本质上都是我们在培养编写 SQL 时的习惯,在面对最终的通关题之前,希望这些建议能够帮助到你~

7. 闯关题

Q1:下面哪个行为是 WITH 函数创建的 CTE 不支持的

A . 创建多个 CTE
B . 在 CTE 中使用 JOIN
C . 引用未定义的 CTE
D . 在 CTE 中使用 WINDOW 函数

python 复制代码
# ...your code...
a1 = 'C'  # 在 = 后面填入你的结果,如 a1 = "A"
Q2:下面哪种场景最适合使用 VIEW (相对于使用 CTE )?

A . 统计某张表内的分组平均数,最后按照平均数大小排序输出
B . 统计某张表内的分组平均数,计算整体的方差
C . 统计某张表内的分组平均数,计算组内每个成员和平均数的之间的差距
D . 统计某张表内的分组平均数,并且在该表上列出和平均数差距最大的 3 个成员的id

在决定使用视图(VIEW)或公用表表达式(CTE,Common Table Expression)时,需要考虑的主要因素是查询的复杂性、重用性和性能。视图更适合于经常需要重用的查询,尤其是当这些查询需要在多个地方使用时。CTE更适合于复杂查询的一部分,它们通常只在一个更大的查询中使用一次。

让我们来看看您给出的各个场景:

A. 统计某张表内的分组平均数,最后按照平均数大小排序输出

  • 这个场景涉及一个相对简单的查询,可能不需要重用。使用CTE或视图都可以,但CTE在一次性查询中可能更方便。

B. 统计某张表内的分组平均数,计算整体的方差

  • 这可能需要两个步骤:首先计算分组平均数,然后计算方差。如果这两个步骤经常被一起使用,可以考虑创建一个视图来存储分组平均数,但如果仅在一个复杂查询中使用,CTE可能更合适。

C. 统计某张表内的分组平均数,计算组内每个成员和平均数的之间的差距

  • 这个场景涉及到对每个组成员进行操作,这是一个比较复杂的查询。CTE在这里可以提供更好的可读性和组织结构。

D. 统计某张表内的分组平均数,并且在该表上列出和平均数差距最大的 3 个成员的id

  • 这个查询相当复杂,需要多步操作。首先是计算分组平均数,然后找到差距最大的成员。这种一次性且复杂的查询更适合用CTE实现。

综合考虑,如果你的需求是经常重用某个特定的统计结果,且这些统计结果不需要在更复杂的查询中作为子查询使用,那么使用视图是个好选择。如果你的统计需求是一次性的,或者是更大查询的一部分,那么使用CTE更加合适。根据你给出的选项,大部分场景似乎都更倾向于使用CTE,尤其是在涉及更复杂的数据处理时。

python 复制代码
# ...your code...
a2 = 'D'  # 在 '' 中填入你的结果,如 a2 = "A"
python 复制代码
# 生成 csv 作业答案文件
def save_csv(answers):
    import pandas as pd
    
    if len(answers)!=2:
        raise Exception("请检查你的答案数列中是否包含了 2 道题的答案。\n")
    df = pd.DataFrame({"id": ["q1","q2"], "answer": answers})
    df.to_csv("answer_workflow_1_5.csv", index=None, encoding='utf_8_sig')
save_csv([a1,a2])  # 该csv文件在左侧文件树project工作区下,你可以自行右击下载或者读取查看
相关推荐
明月清风徐徐8 分钟前
Scrapy爬取豆瓣电影Top250排行榜
python·selenium·scrapy
theLuckyLong9 分钟前
SpringBoot后端解决跨域问题
spring boot·后端·python
Yongqiang Cheng12 分钟前
Python operator.itemgetter(item) and operator.itemgetter(*items)
python·operator·itemgetter
MavenTalk15 分钟前
Move开发语言在区块链的开发与应用
开发语言·python·rust·区块链·solidity·move
FksLiao27 分钟前
Superset安装
python
L Jiawen34 分钟前
【Python · PyTorch】卷积神经网络(基础概念)
pytorch·python·cnn
goomind40 分钟前
深度学习模型评价指标介绍
人工智能·python·深度学习·计算机视觉
日里安1 小时前
8. 基于 Redis 实现限流
数据库·redis·缓存
->yjy1 小时前
wordcloud库基本介绍
python
2401_840192271 小时前
python基础大杂烩
linux·开发语言·python