SQL进阶(二):复杂数据结构处理:让你的 SQL 更加高效

复杂数据结构处理:让你的 SQL 更加高效,以SQLite为例
本文是在原本sql闯关的基础上总结得来,加入了自己的理解以及疑问解答(by GPT4)

原活动链接

用到的数据:链接
提取码:l03e

目录

0.课前小问答 🔎

1. 什么是复杂的数据结构

A:复杂的数据结构,一般指在数据库的表存储过程中,除了常见的数值 (例如: 1,2,1.23,…),文本(例如:“张三”),布尔值(True, False),时间等基础数据类型外, 有一类较为较为复杂的数据类型,一般我们称之为数据结构。最为常见的两种数据结构分别是数组(Array,或者简单而言 Python 中的 List)或是 结构体(Struct,或者简单而言 Python 当中的 Dict)

🌰 : [1,2,3,4] 是一个 Array,{‘姓名’:‘张三’}

2. 为什么需要了解复杂数据结构?

A:简单而言,以下两个理由

  1. 数据并不总是准备完好的:实际的工作环境下,拿到干净且完善的数据的可能性总是不高的,无论是处于生产效率的考虑还是数据存储的效率,复杂的数据结构都是很有可能出现的

🌰 : 一个简单的例子,假设一张数据表需要记录在和鲸社区关注小鲸🐳 的人数,我们可以将表格设计成:

<tr>  
  <th style="padding: 8px; background-color: #499ed4; color: #fff; text-align: center;">被关注人</th>  
		<th style="padding: 8px; background-color: #499ed4; color: #fff; text-align: center;">粉丝名称</th>    
</tr>  
<tr>  
  <td style="padding: 8px;">小鲸🐳</td>  
  <td style="padding: 8px;">方小鲸</td>  
</tr>  
<tr>  
  <td style="padding: 8px;">小鲸🐳</td>  
  <td style="padding: 8px;">小小鲸</td>  
</tr>  
或是
被关注人粉丝名称
小鲸🐳[方小鲸,小小鲸]

前者看起来数据结构较为简单,但是如果小鲸🐳有很多粉丝的话,那么这个光是小鲸的数据就要占掉很多行,而第二种数据的储存方式可以更好的利用较为有限的储存空间。

  1. 一定的数据可以加快 SQL 查询的效率:对于同样的数据,有的时候高频度的需求往往是针对一组数据进行统计分析(例如,求和,求个数等),此时用数据结构来存储和计算会在一些时候帮助我们节约时间。还是以上面的数据结构为例子,假设我们有一个需求是统计小鲸🐳 上一周的新增粉丝数环比上升了多少,采用第一种数据存储的方式,我们需要将数据按照‘周’进行分组(不采用内置函数的话,这一步能够难倒绝大部分的 SQL 初学者了),在去计算每周新增的粉丝数

而采用第二种方式,我们只需要每周的结束统一将本周新增的粉丝数“打包”进一个数组当中,即

  <table style="border-collapse: collapse;">  
<tr>  
  <th style="padding: 8px; background-color: #499ed4; color: #fff; text-align: center;">被关注人</th>  
		<th style="padding: 8px; background-color: #499ed4; color: #fff; text-align: center;">粉丝名称</th>  
	<th style="padding: 8px; background-color: #499ed4; color: #fff; text-align: center;">周 ID</th>    
</tr>  
<tr>  
  <td style="padding: 8px;">小鲸🐳</td>  
  <td style="padding: 8px;">[方小鲸,小小鲸]</td>  
		<td style="padding: 8px;">1</td>  
</tr>  
<tr>  
  <td style="padding: 8px;">小鲸🐳</td>  
  <td style="padding: 8px;">[小皮鲸,小酒鲸]</td>  
		<td style="padding: 8px;">2</td>  
</tr>  
当我们需要计算的时候,只需要抽出特定周的数据后对比即可,而且这种存储方式可以对于极端灵活的需求(例如:对比今年第 10 周和 第 38-40 周平均值的数据 🤪 )也有着更好的应对能力。



3. 我们将如何学习复杂的数据结构?

坦白地说, SQLite 作为一款轻量级数据库,其对于复杂数据结构的支持较少,因此在本教程当中,我们将从其于 2022 年开始支持的 JSON 类型数据开始触及,通过 SQLite 对于 JSON 数据的处理,熟悉并了解可以一些常见的处理函数。在此过程中,你可以通过一些思考题和练习来提升对于复杂数据结构的处理能力

有关 SQLite 对于 JSON 的支持,可以查阅 SQLite 官方文档

🔖小贴士 1
在 Python 当中,一个 List 可以包含不同类型的数据(例如 [1,'张三',True] ),尽管在 SQL 也可以如此操作,但依然 强烈建议在同一个数据结构当中保持结构内有且只有一种数据类型
🔖小贴士 2
如果你有一定的计算机背景,你可能会在和 SQL 使用者沟通时,困惑于自己学习过的数据结构( data structure) 似乎和他们理解的不大一样,实际上包括本教程内,SQL 使用者以“数据结构"称呼 Array,Struct 以及下文提到的 JSON 数据的主要目的是为了将他们与 基础数据类型进行有效区分

1. 开始之前的准备

在我们正式开始前,我们需要为此次的教学准备一个案例数据库,因此请完成以下内容的检查


# 导入库
from IPython.display import Markdown as md
import sqlite3
import pandas as pd
import json
# 读取泰坦尼克号数据集
df = pd.read_csv('./data/train.csv')
# 将表格中 NaN 值替换为 NULL
df = df.where(pd.notnull(df), None) # 很奇怪,这句代码线上运行无误,但是线下运行似乎有问题,因为json文件还是会出现NaN的情况
origin = df.copy() # 注意原代码是 origin = df,并没有创建独立的副本,对origin任何的修改都会同样体现在df上,此处我按照自己的习惯做了修改

精彩代码解析(统一空值格式的代码让我获益匪浅,十分感谢原作者):

您提供的代码片段是一个用于处理Python pandas数据框(DataFrame)的操作。这个操作的目的是将数据框df中所有的NaN(Not a Number,非数字)或空值(null)替换为None。这通常在数据预处理步骤中进行,目的是在数据集中统一空值的表示。

代码解析如下:

  • df:这是您原始的数据框。
  • pd.notnull(df):这个函数调用会创建一个布尔掩码,其中每个元素如果对应于df中的元素不是NaN或空值,则为True;否则为False
  • df.where(condition, other):这是一个数据框方法,根据条件修改数据框。对于数据框中的每个元素,如果条件为True,则元素保持不变。如果条件为False,则元素被替换为other中的相应元素。在这个例子中,otherNone,因此df中所有的NaN或空值都被替换为None

执行这个操作后,您的数据框df中原来的NaN或空值位置将被None取代。这在准备数据给系统使用时非常有用,特别是当该系统将None和NaN视为不同处理,或者需要导出数据到更适合使用None而非NaN的格式时。

2. JSON 数据操作

作为最常见的一种数据类型,JSON 数据在 SQLite 较迟的版本当中才有了对应,其实际上是以“TEXT”数据类型对于 JSON 类型的数据进行存储的(和大部分支持 JSON 类型的数据库有所差别),我们简单改造一下 Titanic 数据集后,将其创建为数据库中的表:

2.1 创建数据表

# 制作两个新的列:passengerInfo 和 ticketInfo
# 前者记录乘客的基本信息,后者记录乘客所持船票的信息
# 
passengerInfo_cols = ['Name','Sex','Age','SibSp','Parch']
ticketInfo_cols = ['Pclass','Ticket','Fare','Cabin','Embarked']

# 两个列分别以一个 json 字符串的形式储存对应的数据
# df['passengerInfo'] = df[passengerInfo_cols].apply(lambda x: json.dumps(x.to_dict(),),axis=1)
# df['ticketInfo'] = df[ticketInfo_cols].apply(lambda x: json.dumps(x.to_dict()),axis=1)

# 和上述两句被注释掉的代码不同,额外增加了将NaN转换为None的措施
df['passengerInfo'] = df[passengerInfo_cols].apply(lambda x: json.dumps({k: (None if pd.isna(v) else v) for k, v in x.items()}), axis=1)
df['ticketInfo'] = df[ticketInfo_cols].apply(lambda x: json.dumps({k: (None if pd.isna(v) else v) for k, v in x.items()}), axis=1)

关键代码解析:

这两句代码的主要作用是创建两个新的列(passengerInfoticketInfo)在原始的 DataFrame df 中,并且这两个新列中的每个值都是一个 JSON 字符串。这些 JSON 字符串分别表示每个乘客的个人信息和船票信息。

  1. 第一句代码解释:

    df['passengerInfo'] = df[passengerInfo_cols].apply(lambda x: json.dumps({k: (None if pd.isna(v) else v) for k, v in x.items()}), axis=1)
    
    • df[passengerInfo_cols]: 这部分从原始的 DataFrame df 中选取 passengerInfo_cols 列表中指定的列(即乘客的个人信息相关列)。
    • .apply(lambda x: ..., axis=1): 这个 apply 函数对上面选取的每行数据应用一个函数。这里的 lambda x 是一个匿名函数,它作用于每一行数据。
    • json.dumps(...): 这个函数将 Python 对象转换成 JSON 字符串。在这里,它把每行数据(现在是 Python 字典)转换成 JSON 字符串。
    • {k: (None if pd.isna(v) else v) for k, v in x.items()}: 这是一个字典推导式,它遍历每行数据的键值对(kv)。如果值(v)是 NaN(pd.isna(v) 为真),它会把这个值替换为 None;否则,保留原始值。这确保了所有的 NaN 值在 JSON 中被正确地表示为 null
  2. 第二句代码解释:

    df['ticketInfo'] = df[ticketInfo_cols].apply(lambda x: json.dumps({k: (None if pd.isna(v) else v) for k, v in x.items()}), axis=1)
    
    • 这句代码的作用与第一句类似,只是它处理的是 ticketInfo_cols 列表中的列(即乘客的船票信息相关列)。
    • 其他部分(如 apply 函数、lambda xjson.dumps 和字典推导式)的作用与第一句代码相同,目的是将每行的船票信息转换成一个正确格式的 JSON 字符串。

总的来说,这两行代码的目的是将乘客的个人信息和船票信息从多个列转换成两个 JSON 格式的字符串列,以便于存储和查询。

# 滤掉原来的列
df = df.drop(passengerInfo_cols,axis=1)
df = df.drop(ticketInfo_cols,axis=1)

# 为了能够存入 SQLite,我们将两个字段改为 str 类型
# SQLite 仅支持将数据以 TEXT 形式进行存储
df['passengerInfo'] = df['passengerInfo'].astype(str)
df['ticketInfo'] = df['ticketInfo'].astype(str)
# 看下效果
df.head(5)
PassengerIdSurvivedpassengerInfoticketInfo
010{"Name": "Braund, Mr. Owen Harris", "Sex": "ma...{"Pclass": 3, "Ticket": "A/5 21171", "Fare": 7...
121{"Name": "Cumings, Mrs. John Bradley (Florence...{"Pclass": 1, "Ticket": "PC 17599", "Fare": 71...
231{"Name": "Heikkinen, Miss. Laina", "Sex": "fem...{"Pclass": 3, "Ticket": "STON/O2. 3101282", "F...
341{"Name": "Futrelle, Mrs. Jacques Heath (Lily M...{"Pclass": 1, "Ticket": "113803", "Fare": 53.1...
450{"Name": "Allen, Mr. William Henry", "Sex": "m...{"Pclass": 3, "Ticket": "373450", "Fare": 8.05...
# 将上面的数据写入一张名为 example_table_json 的表
with sqlite3.connect('example.db') as conn:
    df.to_sql('example_table_json', con=conn, if_exists='replace', index=False)
    origin.to_sql('example_table', con=conn, if_exists='replace', index=False)
# 链接到刚刚创建好的数据库
connection = sqlite3.connect('example.db')

# 执行查看存储的效果
connection.execute('''SELECT * FROM example_table_json;''').fetchone()
(1,
 0,
 '{"Name": "Braund, Mr. Owen Harris", "Sex": "male", "Age": 22.0, "SibSp": 1, "Parch": 0}',
 '{"Pclass": 3, "Ticket": "A/5 21171", "Fare": 7.25, "Cabin": null, "Embarked": "S"}')
# 判断json数据是否有效 可以看到结果里是否有无效的json数据
query = '''
SELECT *
FROM example_table_json 
WHERE json_valid(passengerInfo) = 0
LIMIT 10
'''
result = connection.execute(query).fetchall()
result
[]

关于上述代码中apply函数中的axis的补充:

简而言之:

axis=0:对列应用函数,每次处理一列。
axis=1:对行应用函数,每次处理一行。
例如,如果您想要计算DataFrame中每一列的平均值,您会使用.apply()函数并设置axis=0。如果您想要计算每一行的总和,您会设置axis=1。在您之前的代码中,使用axis=1是因为您想要对每一行中选定的列创建一个JSON字符串。

2.2 一个例子

让我们先从下面的一个简单的例子入手

# 第一个例子
# 从 example_table_json 中选取 PassengerId 和 Age
query = '''
SELECT 
    PassengerId
    ,json_extract(passengerInfo,'$.Age') -- 一个路径表达式
    
FROM example_table_json
'''
result = connection.execute(query).fetchone()
result
# 这里我们就返回了第一条数据的用户年龄和 Id
(1, 22.0)

上面的例子当中,我们使用了一个简单的函数 json_extract() ,通常用于处理JSON类型的数据。它从JSON字符串中提取出特定的值。在您的例子中,json_extract(passengerInfo,'$.Age')这部分从passengerInfo列中提取出Age字段。这里$.Age是一个JSON路径表达式,指定了在JSON对象中要查找的具体字段。

此外,json_extract 函数在 MySQL 和 PostgreSQL 中都存在,但它们的使用语法和细微的行为可能会有所不同,因为这两个数据库系统对 JSON 数据的支持各有特点。

MySQL 中的 json_extract

上述代码可以直接运行

PostgreSQL 中的 json_extract

在 PostgreSQL 中,处理 JSON 的函数稍有不同。针对 JSON 数据,您可以使用 ->->> 或其他 JSON 函数来提取数据。PostgreSQL 提供了两种 JSON 数据类型:jsonjsonb,后者是二进制格式,提供了更高的效率。

针对 JSON 数据的提取,您可以使用如下语法:

  • 使用 -> 提取 JSON 对象的字段(结果是 JSON):(注意单箭头一般用来访问嵌套类型,下边那句应该才是对的)

    SELECT PassengerId,passengerInfo->'Age' FROM example_table_json;
    
  • 使用 ->> 提取 JSON 对象的字段(结果是文本):

    SELECT PassengerId,passengerInfo->>'Age' FROM example_table_json;
    

所以,在 PostgreSQL 中,与 MySQL 的 json_extract 功能相似的操作可以通过这些运算符和函数来实现。

在使用时,您需要根据您所使用的数据库系统选择正确的函数或运算符。

2.3 路径参数/路径表达式

关于路径参数(下文用"路径表达式"代替),官方给出的要求和定义如下:

A well-formed PATH is a text value that begins with exactly one ‘$’ character followed by zero or more instances of “.objectlabel” or “[arrayindex]”

一个格式正确的路径表达式是一个文本类型的数据,这个表达式必须以一个 ‘$’ 字符开始,后面紧跟着零个或多个 “.objectlabel” 字段或 “[arrayindex]” 字段。

其中,“.objectlabel" 表示 JSON 对象的属性名,而 “[arrayindex]” 则表示数组的位置索引。

让我们先来关注使用 .objectlabel 表示 JSON 对象的属性名这个写法,在上面的例子当中,‘$.Age’里面 ‘.Age’ 代表我们希望查询这个 JSON 字符串当中名为 ‘Age’ 的字段值

类比一下,加入在 Python 当中的我们有这样的一个字典(Dictionary):

a = {'b':{'c':{'d':'e'}}}  

如果我们想要查询键 c 的值,我们可以写:

value = a['b']['c']  

其中 a[‘b’][‘c’] 的部分,在路径表达式中相当于

'$.b.c'  

下面是另一个使用 “.objectlabel” 表示的例子

query = '''
SELECT 
    PassengerId
    ,json_extract(passengerInfo,'$.Age') -- 获取用户的年龄
    ,json_extract(ticketInfo,'$.Cabin') -- 获取用户的船舱
    
FROM example_table_json
WHERE json_extract(ticketInfo,'$.Cabin') IS NOT NULL --#  JSON 数据当中的 NULL 值可以被正常的识别
                                                     --#  但是在通过 dataframe 制作表格的时候,需要将 NaN 替换成 None(见本教程导入数据的部分)


'''
result = connection.execute(query).fetchone()
result
(2, 38.0, 'C85')
提问:上述代码需要如何修改,以在mysql或者postgresql中正常执行

上述SQL代码的核心部分是使用 json_extract 函数从JSON类型的列中提取数据。不过,这个函数的具体实现和使用在 MySQL 和 PostgreSQL 中有所不同。让我们分别看看在这两种数据库系统中如何实现类似的功能。

在 MySQL 中

上述代码可以直接运行

在 PostgreSQL 中

PostgreSQL 处理 JSON 数据的方式与 MySQL 略有不同。它使用不同的操作符或函数来从 JSON 类型的列中提取数据。在 PostgreSQL 中,您可能需要使用 ->->> 运算符代替 json_extract。例如:

SELECT 
    PassengerId,
    passengerInfo->>'Age' AS Age,
    ticketInfo->>'Cabin' AS Cabin
FROM example_table_json
WHERE ticketInfo->>'Cabin' IS NOT NULL;

在这里,->> 运算符用于提取 JSON 对象中的字段并将其作为文本返回(如果字段是一个字符串或者数字)。如果字段是一个更复杂的JSON对象或数组,您可能需要使用 -> 运算符。

总结一下,您提供的原始SQL查询在 MySQL 中可以直接运行,但在 PostgreSQL 中需要进行适当的修改以符合其 JSON 处理方式。

✍️ 小练习 1:如果 a={‘b’:{‘c’:{‘d’:‘e’,‘f’:‘g’},‘h’:‘i’}},那么获取键 f 和 h 的值的路径表达式是什么呢?
sqlite & mysql
'$.b.c.f'
'$.b.h'
postgresql
column-> 'b' -> 'c' ->> 'f'
column->> 'h'

答案详解:

SQLite 和 MySQL

对于 SQLite(如果它支持JSON查询)和 MySQL,您的答案是正确的。这些系统使用标准的JSON路径表达式,以$作为根元素的开始:

  • 获取键f的值:'$.b.c.f'
  • 获取键h的值:'$.b.h'
PostgreSQL

PostgreSQL 使用不同的语法来处理JSON数据。对于 JSON 类型,您可以使用 ->->> 操作符来提取数据。在 PostgreSQL 中,路径表达式不是以$开始的,且通常不需要这种形式的字符串路径。相反,您会使用一系列的操作符来“深入”JSON结构:

  • 获取键f的值:您需要连续使用 -> 操作符来访问嵌套对象,然后使用 ->> 来获取最终的字符串值。表达式将是 column->'b'->'c'->>'f'
  • 获取键h的值:这里路径较短,可以直接使用 column->>'h' 来获取值。

所以,针对 PostgreSQL 的表达式将是:

  • 对于键fcolumn->'b'->'c'->>'f'
  • 对于键hcolumn->>'h'

请注意,column 是您的JSON列的名称,而不是a(a是python的变量,不一定是存储这个数据的列名)。在实际的SQL查询中,您需要将 column 替换为实际的列名。

2.4 一些常用的 JSON 相关函数

2.4.1 json_patch()

可以将两个 JSON 字段进行整合,类似于 Python 当中合并两个字典

# json_patch
query = '''
SELECT 
    PassengerId
    ,passengerInfo 
    ,ticketInfo 
    ,json_patch(passengerInfo,ticketInfo)   -- 等价于 passengerInfo + ticketInfo
FROM example_table_json
'''
result = connection.execute(query).fetchone()
result
(1,
 '{"Name": "Braund, Mr. Owen Harris", "Sex": "male", "Age": 22.0, "SibSp": 1, "Parch": 0}',
 '{"Pclass": 3, "Ticket": "A/5 21171", "Fare": 7.25, "Cabin": null, "Embarked": "S"}',
 '{"Name":"Braund, Mr. Owen Harris","Sex":"male","Age":22.0,"SibSp":1,"Parch":0,"Pclass":3,"Ticket":"A/5 21171","Fare":7.25,"Embarked":"S"}')
🔖小贴士
json_extract 中如果传入的路径表达式对应的值不存在,则返回 NULL

在您的示例中,代码解析:您正在使用 json_patch 函数来合并两个 JSON 对象。json_patch 函数通常用于修改或更新 JSON 文档。不过,需要注意的是,json_patch 函数并不是所有数据库系统都支持的标准函数。在不同的数据库系统中,合并 JSON 对象的方法可能会有所不同。

SQLite

在 SQLite 中,json_patch 函数可以用来合并两个 JSON 对象,就像您示例中所做的那样。这是 JSON1 扩展模块的一部分。如果您的 SQLite 版本支持 JSON1 扩展,您的代码应该可以正常工作。

MySQL

在 MySQL 中,合并两个 JSON 对象通常使用 JSON_MERGE_PRESERVEJSON_MERGE_PATCH 函数。JSON_MERGE_PRESERVE 会合并两个 JSON 对象,保留重复的键。JSON_MERGE_PATCH 则会使用第二个 JSON 对象的值覆盖第一个 JSON 对象中的相应键。

您的查询在 MySQL 中可以修改为:

SELECT 
    PassengerId,
    passengerInfo,
    ticketInfo,
    JSON_MERGE_PATCH(passengerInfo, ticketInfo) AS mergedInfo
FROM example_table_json;
PostgreSQL

PostgreSQL 不支持 json_patch 函数,但您可以使用其他方法来合并 JSON 对象。对于 jsonb 数据类型,您可以直接使用 || 运算符来合并两个 jsonb 对象。

例如,您的查询在 PostgreSQL 中可以写为:

SELECT 
    PassengerId,
    passengerInfo,
    ticketInfo,
    passengerInfo::jsonb || ticketInfo::jsonb AS mergedInfo
FROM example_table_json;

在这里,假设 passengerInfoticketInfojsonb 类型。如果它们是 json 类型,您需要先将它们转换为 jsonb

总之,合并 JSON 对象的具体方法取决于您使用的数据库系统及其版本。上述代码为不同数据库系统提供了相应的实现方法。

2.4.2 json_remove()

从指定的 JSON 字段中去掉某些特定的字段,可以用于反选

# json_remove
query = '''
SELECT 
    PassengerId
    ,passengerInfo 
    ,json_remove(passengerInfo,'$.Age','$.Sex')   -- 函数内可以传入多个想要反选的字段
FROM example_table_json
'''
result = connection.execute(query).fetchone()
result
(1,
 '{"Name": "Braund, Mr. Owen Harris", "Sex": "male", "Age": 22.0, "SibSp": 1, "Parch": 0}',
 '{"Name":"Braund, Mr. Owen Harris","SibSp":1,"Parch":0}')

在不同的数据库系统中,移除 JSON 对象中的特定字段的功能可能需要不同的函数或方法。您提供的示例使用了 SQLite 的 json_remove 函数,这个函数是用来从 JSON 对象中移除一个或多个指定的键。

SQLite

在 SQLite 中,如您所示,json_remove 函数正是用来实现这个功能的。您的查询在 SQLite 中应该可以正常工作,前提是您的 SQLite 版本支持 JSON1 扩展。

MySQL

在 MySQL 中,移除 JSON 对象中的字段可以通过 JSON_REMOVE 函数实现。这个函数的使用方式与 SQLite 的 json_remove 非常相似。您的查询在 MySQL 中可以这样写:

SELECT 
    PassengerId,
    passengerInfo,
    JSON_REMOVE(passengerInfo, '$.Age', '$.Sex') AS modifiedInfo
FROM example_table_json;
PostgreSQL

PostgreSQL 对 JSON 数据的处理略有不同。在 PostgreSQL 中,没有直接等价于 json_remove 的函数。但您可以通过一系列操作实现类似的效果,尤其是使用 jsonb 数据类型时。一个方法是使用 - 操作符来移除 jsonb 对象中的指定键。

例如,如果 passengerInfojsonb 类型,您的查询可以修改为:

SELECT 
    PassengerId,
    passengerInfo,
    passengerInfo - 'Age' - 'Sex' AS modifiedInfo
FROM example_table_json;

如果 passengerInfojson 类型而不是 jsonb,您需要先将其转换为 jsonb

请注意,这些查询都假设 example_table_json 表存在并且包含了所需的列。您需要根据您实际的数据库结构和版本来调整这些查询。

2.4.3 json_insert()

在 JSON 当中插入新的键值对

# json_insert
query = '''
SELECT 
    PassengerId
    ,passengerInfo 
    ,ticketInfo
    ,json_insert(ticketInfo,'$.Name',json_extract(passengerInfo,'$.Name'))   -- 将 passengerInfo 当中的 Name 字段插入
    ,json_insert(ticketInfo,'$.Ticket','长期饭票')   -- 已经存在的字段将不会被更新
FROM example_table_json
'''
result = connection.execute(query).fetchone()
result
(1,
 '{"Name": "Braund, Mr. Owen Harris", "Sex": "male", "Age": 22.0, "SibSp": 1, "Parch": 0}',
 '{"Pclass": 3, "Ticket": "A/5 21171", "Fare": 7.25, "Cabin": null, "Embarked": "S"}',
 '{"Pclass":3,"Ticket":"A/5 21171","Fare":7.25,"Cabin":null,"Embarked":"S","Name":"Braund, Mr. Owen Harris"}',
 '{"Pclass":3,"Ticket":"A/5 21171","Fare":7.25,"Cabin":null,"Embarked":"S"}')
🔖小贴士
再次提醒:json_insert 如果遇到了已有的字段名将 不会更新该字段

在不同的数据库系统中,向 JSON 对象中插入新字段的方法可能有所不同。您提供的示例使用的是 SQLite 的 json_insert 函数。这个函数在 JSON 对象中插入一个新的键值对,但如果键已经存在,则保持原值不变。下面是在 MySQL 和 PostgreSQL 中实现类似功能的方法。

MySQL

在 MySQL 中,您可以使用 JSON_INSERT 函数来实现类似的功能。这个函数的行为与 SQLite 的 json_insert 非常相似:它只在键不存在时插入新的键值对。您的查询在 MySQL 中可以写为:

SELECT 
    PassengerId,
    passengerInfo,
    ticketInfo,
    JSON_INSERT(ticketInfo, '$.Name', JSON_EXTRACT(passengerInfo, '$.Name')),
    JSON_INSERT(ticketInfo, '$.Ticket', '长期饭票') AS modifiedTicketInfo
FROM example_table_json;
PostgreSQL

PostgreSQL 处理 JSON 的方式与 MySQL 和 SQLite 有所不同。尤其是,PostgreSQL 没有直接等价于 json_insert 的函数。但您可以通过组合使用其他 JSON 函数和操作符来实现相似的功能。假设您正在使用 jsonb 数据类型,您可以使用 || 操作符来合并 JSON 对象,如果键不存在则添加,如果键存在则保留原值。例如:

SELECT 
    PassengerId,
    passengerInfo,
    ticketInfo,
    ticketInfo::jsonb || jsonb_build_object('Name', passengerInfo->'Name', 'Ticket', '长期饭票') AS modifiedTicketInfo
FROM example_table_json;

在这个查询中,jsonb_build_object 用于创建一个新的 JSON 对象,并与原始的 ticketInfo 对象合并。如果 ticketInfo 列是 json 类型而不是 jsonb,您需要先将其转换为 jsonb

请注意,您需要根据实际的数据库系统和表结构调整这些查询。

✍️ 小练习 2: 将 passengerInfo 中的 SibSp 和 Parch 字段移入 ticketInfo,并从 passengerInfo 当中移出
# ...你的代码...
query = '''
SELECT
    PassengerId,
    passengerInfo,
    ticketInfo,
    json_insert(ticketInfo,'$.SibSp',json_extract(passengerInfo,'$.SibSp'),
                '$.Parch',json_extract(passengerInfo,'$.Parch')) AS new_ticketInfo,
    json_remove(passengerInfo,'$.SibSp','$.Parch') AS new_passengerInfo
FROM  example_table_json
'''
result = connection.execute(query).fetchone()
result
(1,
 '{"Name": "Braund, Mr. Owen Harris", "Sex": "male", "Age": 22.0, "SibSp": 1, "Parch": 0}',
 '{"Pclass": 3, "Ticket": "A/5 21171", "Fare": 7.25, "Cabin": null, "Embarked": "S"}',
 '{"Pclass":3,"Ticket":"A/5 21171","Fare":7.25,"Cabin":null,"Embarked":"S","SibSp":1,"Parch":0}',
 '{"Name":"Braund, Mr. Owen Harris","Sex":"male","Age":22.0}')
在 MySQL 中

在 MySQL 中,您可以使用 JSON_INSERTJSON_EXTRACTJSON_REMOVE 函数来实现这个功能。首先使用 JSON_INSERTJSON_EXTRACT 将值复制到 ticketInfo,然后使用 JSON_REMOVEpassengerInfo 中移除它们。

SELECT
    PassengerId,
    JSON_REMOVE(passengerInfo, '$.SibSp', '$.Parch') AS new_passengerInfo,
    JSON_INSERT(
        ticketInfo, 
        '$.SibSp', JSON_EXTRACT(passengerInfo, '$.SibSp'),
        '$.Parch', JSON_EXTRACT(passengerInfo, '$.Parch')
    ) AS new_ticketInfo
FROM example_table_json;
在 PostgreSQL 中

PostgreSQL 处理 JSON 的方式不同,特别是当使用 jsonb 数据类型时。您可以使用 jsonb 数据类型的操作符和函数来实现这个功能。这包括使用 - 来移除字段,以及使用 || 来合并 jsonb 对象。

SELECT
    PassengerId,
    passengerInfo::jsonb - 'SibSp' - 'Parch' AS new_passengerInfo,
    ticketInfo::jsonb || jsonb_build_object(
        'SibSp', passengerInfo->'SibSp',
        'Parch', passengerInfo->'Parch'
    ) AS new_ticketInfo
FROM example_table_json;

在这个查询中,passengerInfoticketInfo 需要是 jsonb 类型。如果它们是 json 类型,则需要先转换为 jsonb

请注意,这些查询假设 example_table_json 表存在并包含所需的列,并且这些列包含适当的 JSON 或 JSONB 数据。您需要根据您实际的数据库系统和表结构来调整这些查询。

提问:因为是查询操作,insert和remove的执行顺序是否是不重要的(比如先json_remove(),然后执行json_insert())

回答:对的,您完全正确。在 SQL 中,SELECT 查询不会修改原始数据,它只是读取数据并可能对这些数据进行转换或计算。因此,即使您在同一个 SELECT 语句中使用了像 JSON_INSERTJSON_REMOVE 这样的函数,这些函数也只是在查询结果中临时创建新的值,而不会改变底层表中的原始数据。

在您的查询中,使用 JSON_INSERTJSON_REMOVE 的顺序确实不重要,因为每个函数都是独立于彼此,在原始数据的基础上进行操作的。您可以首先使用 JSON_INSERT 将数据从 passengerInfo 添加到 ticketInfo,然后使用 JSON_REMOVEpassengerInfo 中移除这些字段,或者反过来。无论顺序如何,最终的结果都是相同的,因为每个函数都是在原始的 passengerInfoticketInfo 数据上操作的。

所以,您在 SELECT 查询中对 JSON 数据的处理,只是用来生成查询结果中的新的 JSON 对象,并不会影响数据库表中存储的实际数据。

2.4.4 json_replace()

在 JSON 当中替换匹配的键所对应的值

# json_replace
query = '''
SELECT 
    PassengerId
    ,passengerInfo 
    ,ticketInfo
    ,json_replace(ticketInfo,'$.Name',json_extract(passengerInfo,'$.Name'))  -- 不匹配的值将不会被更新
    ,json_replace(ticketInfo,'$.Ticket','长期饭票')   -- 匹配的值会被更新
FROM example_table_json
'''
result = connection.execute(query).fetchone()
result
(1,
 '{"Name": "Braund, Mr. Owen Harris", "Sex": "male", "Age": 22.0, "SibSp": 1, "Parch": 0}',
 '{"Pclass": 3, "Ticket": "A/5 21171", "Fare": 7.25, "Cabin": null, "Embarked": "S"}',
 '{"Pclass":3,"Ticket":"A/5 21171","Fare":7.25,"Cabin":null,"Embarked":"S"}',
 '{"Pclass":3,"Ticket":"长期饭票","Fare":7.25,"Cabin":null,"Embarked":"S"}')

在您的查询中,您使用了 json_replace 函数,这是在处理 JSON 数据时用于更新现有键值的函数。不同的数据库系统提供了不同的方法来实现这种功能。

在 MySQL 中

MySQL 支持 json_replace 函数,它的行为与您的查询中所示相似:如果指定的键存在于 JSON 对象中,则 json_replace 函数会更新这个键的值;如果键不存在,则不会发生任何更改。因此,您的查询在 MySQL 中可以直接运行,不需要做任何修改。

在 PostgreSQL 中

PostgreSQL 处理 JSON 的方式与 MySQL 不同。尤其是,PostgreSQL 没有直接等价于 json_replace 的函数。但您可以通过组合使用其他 JSON 函数和操作符来实拟相似的行为。一个方法是使用 jsonb_set 函数(适用于 jsonb 数据类型)。

对于 jsonb 数据类型,您可以这样实现:

SELECT 
    PassengerId,
    passengerInfo,
    ticketInfo,
    jsonb_set(
        ticketInfo::jsonb, 
        '{Name}', 
        COALESCE((json_extract_path_text(passengerInfo::json, 'Name'))::jsonb, (ticketInfo->'Name')::jsonb)
    ) AS new_ticketInfo1,
    jsonb_set(
        ticketInfo::jsonb, 
        '{Ticket}', 
        '长期饭票'::jsonb
    ) AS new_ticketInfo2
FROM example_table_json;

在这个查询中:

  • jsonb_set 用于替换或添加指定键的值。
  • COALESCE 用于处理可能的 null 值。如果从 passengerInfo 中提取的 Name 值为 null,则保留 ticketInfo 中原有的 Name 值。

请注意,这里假设 ticketInfopassengerInfojsonb 类型。如果它们是 json 类型,则需要先转换为 jsonb。此外,您需要根据实际的数据库系统和表结构调整这些查询。

2.4.5 json_set()

json_set() = json_replace() + json_insert。即对于匹配的值进行替换,对于不匹配的值将进行插入。

# json_set
query = '''
SELECT 
    PassengerId
    ,passengerInfo 
    ,ticketInfo
    ,json_set(ticketInfo,'$.Name',json_extract(passengerInfo,'$.Name'))   -- 不匹配的值将会做插入
    ,json_set(ticketInfo,'$.Ticket','长期饭票')   -- 匹配的值会被更新
FROM example_table_json
'''
result = connection.execute(query).fetchone()
result
(1,
 '{"Name": "Braund, Mr. Owen Harris", "Sex": "male", "Age": 22.0, "SibSp": 1, "Parch": 0}',
 '{"Pclass": 3, "Ticket": "A/5 21171", "Fare": 7.25, "Cabin": null, "Embarked": "S"}',
 '{"Pclass":3,"Ticket":"A/5 21171","Fare":7.25,"Cabin":null,"Embarked":"S","Name":"Braund, Mr. Owen Harris"}',
 '{"Pclass":3,"Ticket":"长期饭票","Fare":7.25,"Cabin":null,"Embarked":"S"}')

您的查询使用了 json_set 函数,这是一个在 JSON 对象中设置键值的函数。不同的数据库系统对于类似操作的支持各不相同。

MySQL

您的查询在 MySQL 中可以修改为使用 JSON_SET

SELECT 
    PassengerId,
    passengerInfo,
    ticketInfo,
    JSON_SET(ticketInfo, '$.Name', JSON_EXTRACT(passengerInfo, '$.Name')),
    JSON_SET(ticketInfo, '$.Ticket', '长期饭票') AS new_ticketInfo
FROM example_table_json;
PostgreSQL

PostgreSQL 没有 json_set 函数。相应的功能可以通过使用 jsonb_set 函数(如果处理的是 jsonb 类型的数据)来实现。对于 jsonb 数据类型,可以直接使用 || 运算符来更新或插入键值对。

在 PostgreSQL 中,相应的查询可能如下所示:

SELECT 
    PassengerId,
    passengerInfo,
    ticketInfo,
    ticketInfo::jsonb || jsonb_build_object('Name', COALESCE(passengerInfo->>'Name', ticketInfo->>'Name')) || 
    jsonb_build_object('Ticket', '长期饭票') AS new_ticketInfo
FROM example_table_json;

在这个查询中,jsonb_build_object 用于创建新的 JSON 对象,|| 运算符用于合并 JSON 对象,从而实现更新或插入新的键值对。

请注意,这些查询假设 example_table_json 表存在并包含所需的列,并且这些列包含适当的 JSON 或 JSONB 数据。您需要根据您实际的数据库系统和表结构来调整这些查询。

✍️ 小练习 3:从数据表中往 ticketInfo 中加入 Name, PassengerId
方法一:sqlite & mysql
query = '''
SELECT
    PassengerId,
    passengerInfo,
    ticketInfo,
    json_set(ticketInfo,'$.Name',json_extract(passengerInfo,'$.Name'),
            '$.PassengerId',PassengerId) AS new_tickerInfo
FROM example_table_json
'''
result = connection.execute(query).fetchone()
result
(1,
 '{"Name": "Braund, Mr. Owen Harris", "Sex": "male", "Age": 22.0, "SibSp": 1, "Parch": 0}',
 '{"Pclass": 3, "Ticket": "A/5 21171", "Fare": 7.25, "Cabin": null, "Embarked": "S"}',
 '{"Pclass":3,"Ticket":"A/5 21171","Fare":7.25,"Cabin":null,"Embarked":"S","Name":"Braund, Mr. Owen Harris","PassengerId":1}')
方法一:PostgreSQL

PostgreSQL 没有 json_set 函数。对于 jsonb 数据类型,您可以使用 || 运算符来合并 JSON 对象,实现类似的功能。在 PostgreSQL 中,相应的查询可能如下所示:

SELECT
    PassengerId,
    passengerInfo,
    ticketInfo,
    ticketInfo::jsonb || jsonb_build_object(
        'Name', COALESCE(passengerInfo->>'Name', ticketInfo->>'Name'),
        'PassengerId', PassengerId::text
    ) AS new_ticketInfo
FROM example_table_json;

在这个查询中,jsonb_build_object 用于创建新的 JSON 对象,|| 运算符用于合并 JSON 对象,实现添加或更新键值对。

请注意,这些查询假设 example_table_json 表存在并包含所需的列,并且这些列包含适当的 JSON 或 JSONB 数据。您需要根据您实际的数据库系统和表结构来调整这些查询。

方法二:sqlite
# 由于要加入的项都是原本ticketInfo没有的,不涉及replace()操作,这里直接使用json_insert()也可以
query = '''
SELECT
    PassengerId,
    passengerInfo,
    ticketInfo,
    json_insert(ticketInfo,'$.Name',json_extract(passengerInfo,'$.Name'),
            '$.PassengerId',PassengerId) AS new_tickerInfo
FROM example_table_json
'''
result = connection.execute(query).fetchone()
result
(1,
 '{"Name": "Braund, Mr. Owen Harris", "Sex": "male", "Age": 22.0, "SibSp": 1, "Parch": 0}',
 '{"Pclass": 3, "Ticket": "A/5 21171", "Fare": 7.25, "Cabin": null, "Embarked": "S"}',
 '{"Pclass":3,"Ticket":"A/5 21171","Fare":7.25,"Cabin":null,"Embarked":"S","Name":"Braund, Mr. Owen Harris","PassengerId":1}')
方法二:MySQL

MySQL 提供了 JSON_INSERT 函数,其行为类似于您的查询中使用的 json_insert 函数。您的查询在 MySQL 中可以这样写:

SELECT
    PassengerId,
    passengerInfo,
    ticketInfo,
    JSON_INSERT(
        ticketInfo,
        '$.Name', JSON_EXTRACT(passengerInfo, '$.Name'),
        '$.PassengerId', CAST(PassengerId AS JSON)
    ) AS new_ticketInfo
FROM example_table_json;

这里,JSON_EXTRACT 用于从 passengerInfo 中提取 Name,而 CAST(PassengerId AS JSON) 用于将 PassengerId 转换为 JSON 格式(如果需要)。

方法二:PostgreSQL

PostgreSQL 中没有直接的 json_insert 函数。如果处理的是 jsonb 数据类型,可以使用 || 运算符来实现类似的功能。在 PostgreSQL 中,相应的查询可能如下所示:

SELECT
    PassengerId,
    passengerInfo,
    ticketInfo,
    ticketInfo::jsonb || jsonb_build_object(
        'Name', COALESCE(passengerInfo->>'Name', ticketInfo->>'Name'),
        'PassengerId', PassengerId::text
    ) AS new_ticketInfo
FROM example_table_json;

在这个查询中,jsonb_build_object 用于创建新的 JSON 对象,|| 运算符用于合并 JSON 对象,实现添加或更新键值对。这里 PassengerId::text 用于将 PassengerId 转换为文本格式,以便作为 JSON 值使用。

请注意,这些查询都假设 example_table_json 表存在并包含所需的列,并且这些列包含适当的 JSON 或 JSONB 数据。您需要根据您实际的数据库系统和表结构来调整这些查询。

2.5 数组

上面的例子当中,我们着重学习了如何通过 JSON 相关函数和路径表达式读取一个 JSON 数据结构当中的特定数据字段,但在我们介绍路径表达式的时候,我们也提到了其可以用来索引一个数组

A well-formed PATH is a text value that begins with exactly one ‘$’ character followed by zero or more instances of “.objectlabel” or “[arrayindex]”

一个格式正确的路径表达式是一个文本类型的数据,这个表达式必须以一个 ‘$’ 字符开始,后面紧跟着零个或多个 “.objectlabel” 字段或 “[arrayindex]” 字段

在 SQLite 当中,对于数组的支持也是可以通过 JSON 字符串的格式来实现的,但下面的教程中,我们将直接在创建好的数据表当中构造数组

🔖小贴士
Again,请注意 SQLite 在对于数组的支持上并非是数据库实现的标准案例
# 通过 GROUP BY 构造一个简单的数组
# 按照乘客的年龄分组,列出大于 40 岁且幸存乘客中的所有id
# 数组的展现形式和 Python 的List一致
query = '''
SELECT 
    json_extract(passengerInfo,'$.Age') AS Age
    ,json_group_array(PassengerId) -- json_group_array 将每个分组下的值制作成一个数组
FROM example_table_json
WHERE Survived=1 AND json_extract(passengerInfo,'$.Age') IS NOT NULL
      AND json_extract(passengerInfo,'$.Age') >40
GROUP BY json_extract(passengerInfo,'$.Age') -- json 相关函数在 GROUP BY 当中也能用哦!
'''
result = connection.execute(query).fetchall()
result
[(41.0, '[273,338]'),
 (42.0, '[289,381,433,622,708,866]'),
 (43.0, '[780]'),
 (44.0, '[195,415,524]'),
 (45.0, '[188,339,441,707,857]'),
 (47.0, '[872]'),
 (48.0, '[461,557,646,713,755,863]'),
 (49.0, '[53,454,600,797]'),
 (50.0, '[260,300,459,527,661]'),
 (51.0, '[766,858]'),
 (52.0, '[450,592,821]'),
 (53.0, '[572]'),
 (54.0, '[497,514,775]'),
 (55.0, '[16]'),
 (56.0, '[648,880]'),
 (58.0, '[12,196,269]'),
 (60.0, '[367,588]'),
 (62.0, '[571,830]'),
 (63.0, '[276,484]'),
 (80.0, '[631]')]

提问:关于where语句是否可以直接使用select中的别名问题:sqlite测试是可以使用的,但是最好使用完整的表达式

回答:如果在 SELECT 语句中已经定义了 Age 作为 json_extract(passengerInfo,'$.Age') 的别名,你仍然不能在同一查询的 WHERE 子句中直接使用这个别名。正如我之前提到的,这是因为 SQL 的执行顺序决定了 WHERE 子句在处理时,SELECT 子句中定义的别名还不可见。

在 SQL 查询中,执行顺序通常是这样的:

  1. FROM 子句:确定要从哪个表中检索数据。
  2. WHERE 子句:基于指定的条件过滤数据。
  3. GROUP BY 子句:将数据分组。
  4. HAVING 子句:过滤分组。
  5. SELECT 子句:选择最终的输出列。
  6. ORDER BY 子句:对结果进行排序。

因此,在 WHERE 子句中,你需要继续使用完整的 json_extract(passengerInfo,'$.Age') 表达式,而不能使用 Age 别名。但是在 GROUP BYORDER BY 子句中,你可以使用 Age 别名。

提问:group by也在select前,为什么group by子句中可以使用别名呢

回答:您指出的这个问题非常好。我的先前解释需要一点澄清。

在 SQL 中,GROUP BY 子句确实在 SELECT 子句之前执行,这意味着在处理 GROUP BY 时,原则上 SELECT 里定义的别名还不应该可用。然而,许多 SQL 数据库管理系统(DBMS)提供了一种特殊的功能,允许在 GROUP BY 子句中使用在 SELECT 子句中定义的别名。这是一种例外,主要是为了方便和简化查询。

但这个例外并不适用于 WHERE 子句。WHERE 子句严格按照逻辑顺序执行,必须在 SELECT 子句之前处理。因此,在 WHERE 子句中,不能使用 SELECT 子句定义的别名。

在实践中,这意味着即使 GROUP BY 子句中可以使用 SELECT 定义的别名,但在 WHEREJOIN ONHAVING 子句中,你仍然需要使用原始列名或表达式。

提问:实际测试发现sqlite中where是可以使用别名的

回答:非常感谢您的反馈和信息分享。在 SQL 的标准实践中,WHERE 子句通常不能使用 SELECT 中定义的别名,因为在查询执行计划中 WHERE 的计算发生在 SELECT 之前。然而,这种行为可能因不同的数据库管理系统(DBMS)而异。

您提到在 SQLite 中进行的实际测试表明 WHERE 子句可以使用 SELECT 中定义的别名。这表明 SQLite 在其查询解析器中实现了一些非标准的行为,允许在 WHERE 子句中引用 SELECT 子句中定义的别名。这是 SQLite 特有的灵活性,可能不适用于其他 SQL 数据库系统。

基于这一点,如果您在 SQLite 中工作,您可以在 WHERE 子句中使用 SELECT 中定义的别名。但请记住,这种做法可能不会在其他数据库系统(如 MySQL、PostgreSQL、SQL Server 等)中工作,因为它们可能遵循更严格的 SQL 标准。

因此,根据您的发现,您的查询可以简化为使用 Age 别名:

SELECT 
    json_extract(passengerInfo,'$.Age') AS Age,
    json_group_array(PassengerId)
FROM example_table_json
WHERE Survived = 1 
      AND Age IS NOT NULL
      AND Age > 40
GROUP BY Age;

在 SQLite 中,这应该能够正常工作。

要将这段 SQLite 代码转换为 MySQL 和 PostgreSQL 兼容的形式,我们首先需要了解原始代码的功能,然后找到这两个数据库中相应的函数和语法。

SQLite 代码的功能概述:

  1. example_table_json 表中选择 passengerInfo 字段的 '$.Age' JSON 路径。
  2. 对于年龄大于 40 且幸存的乘客,使用 json_group_array 函数将每个分组下的 PassengerId 制作成一个数组。
  3. 使用 GROUP BY 对每个不同的年龄进行分组。
MySQL 转换

MySQL 中,我们可以使用 JSON_EXTRACTJSON_UNQUOTE(JSON_EXTRACT()) 函数来提取 JSON 对象中的值。对于数组聚合,可以使用 JSON_ARRAYAGG

MySQL 查询可能如下:

SELECT 
    JSON_UNQUOTE(JSON_EXTRACT(passengerInfo, '$.Age')) AS Age,
    JSON_ARRAYAGG(PassengerId)
FROM example_table_json
WHERE Survived=1 
      AND JSON_EXTRACT(passengerInfo, '$.Age') IS NOT NULL
      AND JSON_EXTRACT(passengerInfo, '$.Age') > 40
GROUP BY JSON_EXTRACT(passengerInfo, '$.Age');
PostgreSQL 转换

在 PostgreSQL 中,可以使用 jsonb 数据类型(如果 passengerInfojsonb 类型)和 ->> 运算符来提取 JSON 对象中的值。对于数组聚合,可以使用 array_agg 函数。

PostgreSQL 查询可能如下:

SELECT 
    passengerInfo ->> 'Age' AS Age,
    array_agg(PassengerId)
FROM example_table_json
WHERE Survived=1 
      AND passengerInfo ->> 'Age' IS NOT NULL
      AND (passengerInfo ->> 'Age')::int > 40
GROUP BY passengerInfo ->> 'Age';

注意:

  • 在 PostgreSQL 查询中,我们将 JSON 字符串转换为整数,以进行比较((passengerInfo ->> 'Age')::int > 40)。
  • 确保数据类型和函数与你的数据库设置相匹配。如果 passengerInfojson 而非 jsonb,则需要相应地调整代码。
2.5.1 数组当中的路径表达式

相较于对于常规的字段读取,获取一个数组中的某个值通常是依靠数组位置的索引来实现的,在路径表达式中,可以使用 [arrayIndex] 来指代某个一个位置的索引

🌰:‘$.a[2]’ 代表获取 a 字段下的数组中的第 3 个数值,例如 {“a”:[1,3,5,7],“b”:1} 将会返回数字 5

🔖小贴士
在 SQLite 当中,数组的位置索引是从 0 开始的
# 依然使用上面的代码构造数组
# 现在我们只看每个分组下的第一个和第四个 ID 
query = '''
SELECT 
    json_extract(passengerInfo,'$.Age') AS Age
    ,json_extract(json_group_array(PassengerId),'$[0]')        -- 注意这里没有 . 了 
    ,json_extract(json_group_array(PassengerId),'$[0]','$[3]') -- 查询数组中的多个值,如果数组长度不够则自动返回 NULL
FROM example_table_json
WHERE Survived==1 AND json_extract(passengerInfo,'$.Age') IS NOT NULL
      AND Age>40

GROUP BY json_extract(passengerInfo,'$.Age') 
'''
result = connection.execute(query).fetchall()
result
[(41.0, 273, '[273,null]'),
 (42.0, 289, '[289,622]'),
 (43.0, 780, '[780,null]'),
 (44.0, 195, '[195,null]'),
 (45.0, 188, '[188,707]'),
 (47.0, 872, '[872,null]'),
 (48.0, 461, '[461,713]'),
 (49.0, 53, '[53,797]'),
 (50.0, 260, '[260,527]'),
 (51.0, 766, '[766,null]'),
 (52.0, 450, '[450,null]'),
 (53.0, 572, '[572,null]'),
 (54.0, 497, '[497,null]'),
 (55.0, 16, '[16,null]'),
 (56.0, 648, '[648,null]'),
 (58.0, 12, '[12,null]'),
 (60.0, 367, '[367,null]'),
 (62.0, 571, '[571,null]'),
 (63.0, 276, '[276,null]'),
 (80.0, 631, '[631,null]')]

在 MySQL 和 PostgreSQL 中,你需要使用不同的方法来实现从分组后的数组中选取特定元素(例如,每个分组的第一个和第四个 ID)。SQLite 中的 json_extract 在这种用法上有一些特殊的灵活性,但在 MySQL 和 PostgreSQL 中,我们需要采用不同的策略。

MySQL

在 MySQL 中,你可以使用 JSON_ARRAYAGG 来构造数组,然后使用 JSON_EXTRACT 来提取特定索引的元素。但请注意,MySQL 中的 JSON_EXTRACT 索引是从 0 开始的。此外,应用 JSON_UNQUOTE: 为了获取不带引号的纯字符串。

MySQL 查询可能如下所示:

SELECT 
    JSON_UNQUOTE(JSON_EXTRACT(passengerInfo, '$.Age')) AS Age,
    JSON_UNQUOTE(JSON_EXTRACT(JSON_ARRAYAGG(PassengerId), '$[0]')) AS FirstID,
    JSON_UNQUOTE(JSON_EXTRACT(JSON_ARRAYAGG(PassengerId), '$[3]')) AS FourthID
FROM example_table_json
WHERE Survived=1 
      AND JSON_EXTRACT(passengerInfo, '$.Age') IS NOT NULL
      AND JSON_EXTRACT(passengerInfo, '$.Age') > 40
GROUP BY JSON_EXTRACT(passengerInfo, '$.Age');
PostgreSQL

在 PostgreSQL 中,你可以使用 array_agg 来创建一个数组,然后使用数组索引来提取特定的元素。在 SQL 中,数组索引通常从 1 开始。

PostgreSQL 查询可能如下所示:

SELECT 
    passengerInfo ->> 'Age' AS Age,
    (array_agg(PassengerId))[1] AS FirstID,
    (array_agg(PassengerId))[4] AS FourthID
FROM example_table_json
WHERE Survived=1 
      AND passengerInfo ->> 'Age' IS NOT NULL
      AND (passengerInfo ->> 'Age')::int > 40
GROUP BY passengerInfo ->> 'Age';

注意:

  • MySQL 和 PostgreSQL 在处理 JSON 和数组时的语法和行为有所不同。在实际应用中,可能需要根据你的具体数据库版本和配置进行调整。
  • 确保在 PostgreSQL 查询中正确处理数据类型转换,特别是在与数字进行比较时。
🔖小贴士
如果查询多个数组中的值时,返回的是一个 字符串形式的列表(查询单个值则返回值本身)
2.5.12 数组当中的路径表达式:倒序查看
# 如果需要倒序查看,可以使用 # 这个特殊符号,同时索引的序号从 -1 开始 
# 例如:#-1 代表数组中的最后一个数
query = '''
SELECT 
    json_extract(passengerInfo,'$.Age') AS Age
    ,json_extract(json_group_array(PassengerId),'$[#-1]')     
FROM example_table_json
WHERE Survived==1 AND json_extract(passengerInfo,'$.Age') IS NOT NULL
      AND Age>40

GROUP BY json_extract(passengerInfo,'$.Age')
'''
result = connection.execute(query).fetchall()
result
[(41.0, 338),
 (42.0, 866),
 (43.0, 780),
 (44.0, 524),
 (45.0, 857),
 (47.0, 872),
 (48.0, 863),
 (49.0, 797),
 (50.0, 661),
 (51.0, 858),
 (52.0, 821),
 (53.0, 572),
 (54.0, 775),
 (55.0, 16),
 (56.0, 880),
 (58.0, 269),
 (60.0, 588),
 (62.0, 830),
 (63.0, 484),
 (80.0, 631)]

在这段代码中,你正在尝试从每个分组中选择最后一个 PassengerId。SQLite 提供了一种通过使用 '$[#-1]' 索引来直接访问 JSON 数组最后一个元素的便捷方法。然而,MySQL 和 PostgreSQL 并不直接支持这种语法。我们需要找到等效的方法来达到同样的目的。

MySQL 实现

在 MySQL 中,我们可以先使用 JSON_ARRAYAGG 构建一个数组,然后使用 JSON_EXTRACT 函数配合 JSON_LENGTH 函数来访问数组的最后一个元素。

SELECT 
    JSON_UNQUOTE(JSON_EXTRACT(passengerInfo, '$.Age')) AS Age,
    JSON_UNQUOTE(JSON_EXTRACT(JSON_ARRAYAGG(PassengerId), CONCAT('$[', JSON_LENGTH(JSON_ARRAYAGG(PassengerId)) - 1, ']'))) AS LastPassengerID
FROM example_table_json
WHERE Survived=1 
      AND JSON_EXTRACT(passengerInfo, '$.Age') IS NOT NULL
      AND JSON_EXTRACT(passengerInfo, '$.Age') > 40
GROUP BY JSON_EXTRACT(passengerInfo, '$.Age');

在这里,JSON_LENGTH(JSON_ARRAYAGG(PassengerId)) - 1 计算数组中最后一个元素的索引,然后我们使用 CONCAT 构建正确的 JSON 路径。

PostgreSQL 实现

PostgreSQL 中的实现更直接,因为数组可以通过普通的 SQL 数组索引来访问。我们可以使用 array_agg 函数来创建一个数组,并使用数组索引来访问最后一个元素。

SELECT 
    passengerInfo ->> 'Age' AS Age,
    (array_agg(PassengerId))[array_length(array_agg(PassengerId), 1)] AS LastPassengerID
FROM example_table_json
WHERE Survived=1 
      AND passengerInfo ->> 'Age' IS NOT NULL
      AND (passengerInfo ->> 'Age')::int > 40
GROUP BY passengerInfo ->> 'Age';

在这里,array_length(array_agg(PassengerId), 1) 计算数组的长度,我们使用这个长度作为索引来获取最后一个元素。

请注意,这些查询可能会在性能上有所不同,特别是在处理大型数据集时。在实际部署之前,建议进行适当的性能测试。

🔖小贴士
# 符号实际上代表的是当前数组的长度
✍️ 小练习 4:利用上面的代码,查询每一组中倒数第二个与正数第三个ID
# ...你的代码...
query = '''
SELECT 
    json_extract(passengerInfo,'$.Age') AS Age,
    json_extract(json_group_array(PassengerId),'$[#-2]'),
    json_extract(json_group_array(PassengerId),'$[2]') -- 注意第三个是'$[2]'而非'$[3]'
FROM example_table_json
WHERE Survived==1 AND json_extract(passengerInfo,'$.Age') IS NOT NULL
      AND Age>40

GROUP BY json_extract(passengerInfo,'$.Age')
'''
result = connection.execute(query).fetchall()
result
[(41.0, 273, None),
 (42.0, 708, 433),
 (43.0, None, None),
 (44.0, 415, 524),
 (45.0, 707, 441),
 (47.0, None, None),
 (48.0, 755, 646),
 (49.0, 600, 600),
 (50.0, 527, 459),
 (51.0, 766, None),
 (52.0, 592, 821),
 (53.0, None, None),
 (54.0, 514, 775),
 (55.0, None, None),
 (56.0, 648, None),
 (58.0, 196, 269),
 (60.0, 367, None),
 (62.0, 571, None),
 (63.0, 276, None),
 (80.0, None, None)]
2.5.2: 计算数组长度:json_array_length()
# json_array_length
query = '''
SELECT 
    json_extract(passengerInfo,'$.Age') AS Age
    ,json_array_length(json_group_array(PassengerId))
FROM example_table_json
WHERE Survived==1 AND json_extract(passengerInfo,'$.Age') IS NOT NULL
      AND Age>40

GROUP BY json_extract(passengerInfo,'$.Age') 
'''
result = connection.execute(query).fetchall()
result
[(41.0, 2),
 (42.0, 6),
 (43.0, 1),
 (44.0, 3),
 (45.0, 5),
 (47.0, 1),
 (48.0, 6),
 (49.0, 4),
 (50.0, 5),
 (51.0, 2),
 (52.0, 3),
 (53.0, 1),
 (54.0, 3),
 (55.0, 1),
 (56.0, 2),
 (58.0, 3),
 (60.0, 2),
 (62.0, 2),
 (63.0, 2),
 (80.0, 1)]

在这段代码中,你正在尝试计算每个年龄组中的 PassengerId 数量。SQLite 的 json_array_length 函数直接提供了这个功能。然而,在 MySQL 和 PostgreSQL 中,我们需要采用不同的方法来实现相同的结果。

MySQL 实现

在 MySQL 中,我们可以使用 JSON_ARRAYAGG 来聚合 PassengerId,然后使用 JSON_LENGTH 函数来计算数组的长度。

MySQL 查询可能如下所示:

SELECT 
    JSON_UNQUOTE(JSON_EXTRACT(passengerInfo, '$.Age')) AS Age,
    JSON_LENGTH(JSON_ARRAYAGG(PassengerId)) AS PassengerCount
FROM example_table_json
WHERE Survived=1 
      AND JSON_EXTRACT(passengerInfo, '$.Age') IS NOT NULL
      AND JSON_EXTRACT(passengerInfo, '$.Age') > 40
GROUP BY JSON_EXTRACT(passengerInfo, '$.Age');

这里,JSON_ARRAYAGG(PassengerId) 创建了一个包含每个年龄组中的 PassengerId 的 JSON 数组,而 JSON_LENGTH 则用于计算这个数组的长度。

PostgreSQL 实现

在 PostgreSQL 中,我们可以使用 array_agg 函数来创建一个数组,然后使用 array_length 函数来计算数组的长度。

PostgreSQL 查询可能如下所示:

SELECT 
    passengerInfo ->> 'Age' AS Age,
    array_length(array_agg(PassengerId), 1) AS PassengerCount
FROM example_table_json
WHERE Survived=1 
      AND passengerInfo ->> 'Age' IS NOT NULL
      AND (passengerInfo ->> 'Age')::int > 40
GROUP BY passengerInfo ->> 'Age';

在这里,array_agg(PassengerId) 创建了一个包含每个年龄组中的 PassengerId 的数组,array_length(array_agg(PassengerId), 1) 则计算这个数组的长度。

这两种方法都能够有效地实现 SQLite 中 json_array_length 函数的功能。在实际使用中,你可能需要根据具体的数据库版本和设置进行一些调整。

2.5.3 其他 JSON 函数在数组上的应用方法
# json_insert,json_replace,json_set
# 往数组当中添加一个值
query = '''
SELECT 
    json_extract(passengerInfo,'$.Age') AS Age
    ,json_group_array(PassengerId)
    ,json_insert(json_group_array(PassengerId),'$[#]',1001)  -- # 符号代表在数组的最后插入数据
    ,json_replace(json_group_array(PassengerId),'$[0]',2002) -- 替换数组的第一个数据至 2002
    ,json_set(json_group_array(PassengerId),'$[3]',3003)     -- 更新数组的第四个数据至 3003,注意若数组的长度<=2,则不会有更新
FROM example_table_json
WHERE Survived==1 AND json_extract(passengerInfo,'$.Age') IS NOT NULL
      AND Age>40
GROUP BY json_extract(passengerInfo,'$.Age') 
'''
result = connection.execute(query).fetchall()
result
[(41.0, '[273,338]', '[273,338,1001]', '[2002,338]', '[273,338]'),
 (42.0,
  '[289,381,433,622,708,866]',
  '[289,381,433,622,708,866,1001]',
  '[2002,381,433,622,708,866]',
  '[289,381,433,3003,708,866]'),
 (43.0, '[780]', '[780,1001]', '[2002]', '[780]'),
 (44.0,
  '[195,415,524]',
  '[195,415,524,1001]',
  '[2002,415,524]',
  '[195,415,524,3003]'),
 (45.0,
  '[188,339,441,707,857]',
  '[188,339,441,707,857,1001]',
  '[2002,339,441,707,857]',
  '[188,339,441,3003,857]'),
 (47.0, '[872]', '[872,1001]', '[2002]', '[872]'),
 (48.0,
  '[461,557,646,713,755,863]',
  '[461,557,646,713,755,863,1001]',
  '[2002,557,646,713,755,863]',
  '[461,557,646,3003,755,863]'),
 (49.0,
  '[53,454,600,797]',
  '[53,454,600,797,1001]',
  '[2002,454,600,797]',
  '[53,454,600,3003]'),
 (50.0,
  '[260,300,459,527,661]',
  '[260,300,459,527,661,1001]',
  '[2002,300,459,527,661]',
  '[260,300,459,3003,661]'),
 (51.0, '[766,858]', '[766,858,1001]', '[2002,858]', '[766,858]'),
 (52.0,
  '[450,592,821]',
  '[450,592,821,1001]',
  '[2002,592,821]',
  '[450,592,821,3003]'),
 (53.0, '[572]', '[572,1001]', '[2002]', '[572]'),
 (54.0,
  '[497,514,775]',
  '[497,514,775,1001]',
  '[2002,514,775]',
  '[497,514,775,3003]'),
 (55.0, '[16]', '[16,1001]', '[2002]', '[16]'),
 (56.0, '[648,880]', '[648,880,1001]', '[2002,880]', '[648,880]'),
 (58.0,
  '[12,196,269]',
  '[12,196,269,1001]',
  '[2002,196,269]',
  '[12,196,269,3003]'),
 (60.0, '[367,588]', '[367,588,1001]', '[2002,588]', '[367,588]'),
 (62.0, '[571,830]', '[571,830,1001]', '[2002,830]', '[571,830]'),
 (63.0, '[276,484]', '[276,484,1001]', '[2002,484]', '[276,484]'),
 (80.0, '[631]', '[631,1001]', '[2002]', '[631]')]

要在 MySQL 和 PostgreSQL 中实现 SQLite 中的 json_insertjson_replacejson_set 功能,我们需要使用这些数据库各自的 JSON 函数。请注意,MySQL 和 PostgreSQL 的 JSON 处理能力与 SQLite 略有不同,所以转换可能需要一些调整。

MySQL 实现

MySQL 支持类似的 JSON 函数,如 JSON_ARRAY_APPENDJSON_REPLACEJSON_SET

MySQL 查询可能如下所示:

SELECT 
    JSON_UNQUOTE(JSON_EXTRACT(passengerInfo, '$.Age')) AS Age,
    JSON_ARRAYAGG(PassengerId) AS PassengerIds,
    JSON_ARRAY_APPEND(JSON_ARRAYAGG(PassengerId), '$', 1001) AS Inserted,
    JSON_REPLACE(JSON_ARRAYAGG(PassengerId), '$[0]', 2002) AS Replaced,
    JSON_SET(JSON_ARRAYAGG(PassengerId), '$[3]', 3003) AS SetArray
FROM example_table_json
WHERE Survived=1 
      AND JSON_EXTRACT(passengerInfo, '$.Age') IS NOT NULL
      AND JSON_EXTRACT(passengerInfo, '$.Age') > 40
GROUP BY JSON_EXTRACT(passengerInfo, '$.Age');

在这里:

  • JSON_ARRAY_APPEND 用于在数组末尾添加元素。
  • JSON_REPLACE 用于替换数组中的元素。
  • JSON_SET 用于设置数组中的元素,如果该元素不存在则添加。
PostgreSQL 实现

PostgreSQL 的 JSON 处理方式与 MySQL 和 SQLite 不同,它没有直接的函数来修改 JSON 数组。因此,你可能需要使用 SQL 函数或者在应用层进行这些操作。

一个基本的实现可能包括将 array_agg 构建的数组转换为 JSON 数组,然后在应用层进行修改。但请注意,这种方法可能在性能上不是最佳选择,尤其是在处理大型数据集时。

SELECT 
    passengerInfo ->> 'Age' AS Age,
    array_to_json(array_agg(PassengerId)) AS PassengerIds
FROM example_table_json
WHERE Survived=1 
      AND passengerInfo ->> 'Age' IS NOT NULL
      AND (passengerInfo ->> 'Age')::int > 40
GROUP BY passengerInfo ->> 'Age';

这个查询只返回了原始的 PassengerId 数组。对于 json_insertjson_replacejson_set 的等效操作,你可能需要在应用层处理或者使用一些复杂的 PL/pgSQL 函数。

这些转换展示了在不同的数据库系统中处理 JSON 数据的差异。在实际部署时,可能需要对性能和可维护性进行权衡。

✍️ 小练习 6:计算每个年龄的人均船费(Fare),其中分母(人数)用 json_array_length 计算
# ...your code...
query = '''
SELECT
    json_extract(passengerInfo,'$.Age') as Age,
    sum(json_extract(ticketInfo,'$.Fare'))/json_array_length(json_group_array(PassengerId)) as Fare
FROM example_table_json
WHERE 
    Survived==1 AND 
    json_extract(passengerInfo,'$.Age') IS NOT NULL AND
    Age>40
GROUP BY json_extract(passengerInfo,'$.Age')
'''
result = connection.execute(query).fetchall()
result
[(41.0, 77.0),
 (42.0, 59.72778333333333),
 (43.0, 211.3375),
 (44.0, 31.208333333333332),
 (45.0, 47.84334),
 (47.0, 52.5542),
 (48.0, 47.63473333333334),
 (49.0, 62.17295000000001),
 (50.0, 85.63416),
 (51.0, 52.254149999999996),
 (52.0, 67.42223333333334),
 (53.0, 51.4792),
 (54.0, 53.555566666666664),
 (55.0, 16.0),
 (56.0, 59.32915),
 (58.0, 108.84443333333336),
 (60.0, 77.225),
 (62.0, 45.25),
 (63.0, 43.7729),
 (80.0, 30.0)]

3. JSON 数据转化

在上一部分当中,我们熟悉了如何从已有的 JSON 数据中,通过一系列的函数对于特定的字段进行提取或修改。

但是这一系列的操作有一个基本的前提,即我们拿到手的数据就是一个 JSON 数据结构的数据。

那么如果我们拿到的就是普通的数据,该如何将他们变为 JSON 结构的数据呢?上文中提供了 json_array_group() 这个函数来通过 GROUP BY 阶段的分组将普通数据变为数组,这里我们将介绍更多的相关函数

同时,又有一些场景下我们希望能够将一个数组“展开”成为独立的数据行,这里也会简单介绍一些相关的函数

# 这里我们使用的是 example_table 这张表,是不含 JSON 字符串格式数据的
# 1. 通过 json_object 函数构造 json 数据结构
# 为了节省空间,我们这里只构造 passengerInfo
query = '''
SELECT 
    PassengerId
    ,Survived
    /*
    json_object 的输入格式:json_object(字段名A,字段值A,字段名B,字段值B...)
    */
    ,json_object('Sex',Sex,'Age',Age,'SibSp',SibSp,'Parch',Parch) AS passengerInfo 
FROM example_table
'''
result = connection.execute(query).fetchone()
result


(1, 0, '{"Sex":"male","Age":22.0,"SibSp":1,"Parch":0}')
# 2. 通过 json_array 函数构造数组
query = '''
SELECT 
    PassengerId
    ,Survived
    /*
    json_array 的输入格式:json_array(字段值A,字段值B...)
    */
    ,json_array(Sex,Age,SibSp,Parch) AS passengerInfo -- 可以操作,但是不推荐
    
FROM example_table
'''
result = connection.execute(query).fetchone()
result
(1, 0, '["male",22.0,1,0]')
# 3. 展开一个数组
# 通过 json_each 的方式展开一个数组
# 这里我们仍然采用 2.5 当中的代码片段
# 这里我们用一个【子查询】来表示我们的结果
# 关于【子查询】和更多结构化查询语句的知识,敬请期待教程:
query = '''
SELECT Age,json_each.value -- json_each.value 代表的是列中每个具体的数值
FROM 
/*
-- 子查询部分,将某个查询的结果用作下一个查询的输入
*/
(    SELECT 
        json_extract(passengerInfo,'$.Age') AS Age
        ,json_group_array(PassengerId) AS Ids
    FROM example_table_json
    WHERE Survived==1 AND json_extract(passengerInfo,'$.Age') IS NOT NULL
        AND Age>40
    GROUP BY json_extract(passengerInfo,'$.Age')
    ) AS tmp                                                   
/*
-- 子查询结束,用 tmp 命名子查询的结果
*/
    ,json_each(tmp.Ids) -- 用 json_each 函数展开 Ids 列

'''
result = connection.execute(query).fetchall()
result
[(41.0, 273),
 (41.0, 338),
 (42.0, 289),
 (42.0, 381),
 (42.0, 433),
 (42.0, 622),
 (42.0, 708),
 (42.0, 866),
 (43.0, 780),
 (44.0, 195),
 (44.0, 415),
 (44.0, 524),
 (45.0, 188),
 (45.0, 339),
 (45.0, 441),
 (45.0, 707),
 (45.0, 857),
 (47.0, 872),
 (48.0, 461),
 (48.0, 557),
 (48.0, 646),
 (48.0, 713),
 (48.0, 755),
 (48.0, 863),
 (49.0, 53),
 (49.0, 454),
 (49.0, 600),
 (49.0, 797),
 (50.0, 260),
 (50.0, 300),
 (50.0, 459),
 (50.0, 527),
 (50.0, 661),
 (51.0, 766),
 (51.0, 858),
 (52.0, 450),
 (52.0, 592),
 (52.0, 821),
 (53.0, 572),
 (54.0, 497),
 (54.0, 514),
 (54.0, 775),
 (55.0, 16),
 (56.0, 648),
 (56.0, 880),
 (58.0, 12),
 (58.0, 196),
 (58.0, 269),
 (60.0, 367),
 (60.0, 588),
 (62.0, 571),
 (62.0, 830),
 (63.0, 276),
 (63.0, 484),
 (80.0, 631)]
# 3 分步执行1
query = '''
SELECT 
    json_extract(passengerInfo,'$.Age') AS Age,
    json_group_array(PassengerId) AS Ids
FROM example_table_json
WHERE Survived = 1 AND
    json_extract(passengerInfo,'$.Age') IS NOT NULL AND
    json_extract(passengerInfo,'$.Age') > 40
GROUP BY json_extract(passengerInfo,'$.Age')
'''
result = connection.execute(query).fetchall()
result
[(41.0, '[273,338]'),
 (42.0, '[289,381,433,622,708,866]'),
 (43.0, '[780]'),
 (44.0, '[195,415,524]'),
 (45.0, '[188,339,441,707,857]'),
 (47.0, '[872]'),
 (48.0, '[461,557,646,713,755,863]'),
 (49.0, '[53,454,600,797]'),
 (50.0, '[260,300,459,527,661]'),
 (51.0, '[766,858]'),
 (52.0, '[450,592,821]'),
 (53.0, '[572]'),
 (54.0, '[497,514,775]'),
 (55.0, '[16]'),
 (56.0, '[648,880]'),
 (58.0, '[12,196,269]'),
 (60.0, '[367,588]'),
 (62.0, '[571,830]'),
 (63.0, '[276,484]'),
 (80.0, '[631]')]
# 3 分步执行2
query = '''
SELECT Age,json_each.value -- json_each.value
FROM
(SELECT 
    json_extract(passengerInfo,'$.Age') AS Age,
    json_group_array(PassengerId) AS Ids
FROM example_table_json
WHERE Survived = 1 AND
    json_extract(passengerInfo,'$.Age') IS NOT NULL AND
    json_extract(passengerInfo,'$.Age') > 40
GROUP BY json_extract(passengerInfo,'$.Age')
) AS tmp,json_each(tmp.Ids)
'''
result = connection.execute(query).fetchall()
result
[(41.0, 273),
 (41.0, 338),
 (42.0, 289),
 (42.0, 381),
 (42.0, 433),
 (42.0, 622),
 (42.0, 708),
 (42.0, 866),
 (43.0, 780),
 (44.0, 195),
 (44.0, 415),
 (44.0, 524),
 (45.0, 188),
 (45.0, 339),
 (45.0, 441),
 (45.0, 707),
 (45.0, 857),
 (47.0, 872),
 (48.0, 461),
 (48.0, 557),
 (48.0, 646),
 (48.0, 713),
 (48.0, 755),
 (48.0, 863),
 (49.0, 53),
 (49.0, 454),
 (49.0, 600),
 (49.0, 797),
 (50.0, 260),
 (50.0, 300),
 (50.0, 459),
 (50.0, 527),
 (50.0, 661),
 (51.0, 766),
 (51.0, 858),
 (52.0, 450),
 (52.0, 592),
 (52.0, 821),
 (53.0, 572),
 (54.0, 497),
 (54.0, 514),
 (54.0, 775),
 (55.0, 16),
 (56.0, 648),
 (56.0, 880),
 (58.0, 12),
 (58.0, 196),
 (58.0, 269),
 (60.0, 367),
 (60.0, 588),
 (62.0, 571),
 (62.0, 830),
 (63.0, 276),
 (63.0, 484),
 (80.0, 631)]

要在 MySQL 和 PostgreSQL 中实现 SQLite 的 json_object 函数,我们需要使用这两种数据库各自的函数来构造 JSON 对象。

MySQL 实现

1.代码同sqlite
2.代码同sqlite
3.

MySQL 不直接支持将 JSON 数组展开为行,但你可以使用 JSON_TABLE 函数(MySQL 8.0.4 及更高版本支持)来实现类似的功能。

MySQL 查询可能如下所示:

SELECT Age, jt.value
FROM (
    SELECT 
        JSON_UNQUOTE(JSON_EXTRACT(passengerInfo, '$.Age')) AS Age,
        JSON_ARRAYAGG(PassengerId) AS Ids
    FROM example_table_json
    WHERE Survived = 1 
        AND JSON_EXTRACT(passengerInfo, '$.Age') IS NOT NULL
        AND JSON_EXTRACT(passengerInfo, '$.Age') > 40
    GROUP BY JSON_EXTRACT(passengerInfo, '$.Age')
) AS tmp
CROSS JOIN JSON_TABLE(
    tmp.Ids,
    '$[*]' COLUMNS (
        value INT PATH '$'
    )
) AS jt;
PostgreSQL 实现
SELECT 
    PassengerId,
    Survived,
    jsonb_build_object('Sex', Sex, 'Age', Age, 'SibSp', SibSp, 'Parch', Parch) AS passengerInfo 
FROM example_table;
SELECT 
    PassengerId,
    Survived,
    jsonb_build_array(Sex, Age, SibSp, Parch) AS passengerInfo 
FROM example_table;

在 PostgreSQL 中,你可以使用 jsonb_array_elements 函数(如果你使用的是 jsonb 类型)或 json_array_elements(如果使用 json 类型)来展开 JSON 数组为一系列行。

PostgreSQL 查询可能如下所示:

SELECT Age, jsonb_element.value
FROM (
    SELECT 
        passengerInfo ->> 'Age' AS Age,
        jsonb_agg(PassengerId) AS Ids
    FROM example_table_json
    WHERE Survived = 1 
        AND passengerInfo ->> 'Age' IS NOT NULL
        AND (passengerInfo ->> 'Age')::int > 40
    GROUP BY passengerInfo ->> 'Age'
) AS tmp
CROSS JOIN jsonb_array_elements(tmp.Ids) as jsonb_element;

在这里,我们使用 jsonb_array_elements(或 json_array_elements,取决于你的 JSON 类型)来将 JSON 数组转换为可查询的行。

请注意,这些查询可能需要根据你的具体数据库版本和配置进行调整。此外,由于 JSON 处理在不同数据库中的实现差异,这些查询可能在性能和行为上有所不同。

4. 小结

在这一章节当中,我们详细地展开了在 SQLite 当中如何有效地处理复杂的数据结构,并且以 JSON 数据为例子,详细介绍了如何索引 JSON 里面的字段信息,如果通过 SQL 语句操作这些复杂的数据结构,以及最重要的是如何能够自行构筑的 JSON 数据结构

需要再次强调的是,在现代的数据库软件应用当中(例如 MySQL,PostgresSQL,SQLServer等)对于复杂的数据结构有着更好的底层支持和更加复杂的内置函数(在 SQLite 当中,JSON文件仅是以 TEXT 类型储存的)

相比于详尽地介绍 SQLite 的函数信息,本章更重要的两个 take-away 是:

1. 复杂的数据结构在数据分析的当中是有其独特价值的

2. 复杂的数据结构是可以通过原生 SQL 支持的,不一定需要外部工具的支持

5. 闯关题

STEP1:根据要求完成题目:
Q1:查询 example_table_json 中的 passengerInfo 字段时,下列哪个操作一定会改变字段值

Q1:查询 example_table_json 中的 passengerInfo 字段时,下列哪个操作一定会改变字段值

A . json_extract(passengerInfo,‘$.Sex’)

B . json_insert(passengerInfo,‘$.Sex’,‘unknown’) json_insert 遇到了已有的字段名将不会更新该字段

C . json_replace(passengerInfo,‘.Name’)) 在 JSON 当中替换匹配的键所对应的值

D . json_set(passengerInfo,‘$.Age’,‘2002’) 即对于匹配的值进行替换,对于不匹配的值将进行插入。

# ...your code...
a1 = 'D'   # 在 = 后面填入你的结果,如 a1 = 'A';注意区分大小写
Q2:参考 2.5 中的代码,按照 ticketInfo 中的 Pclass 分组并查看每个组内的幸存情况。请问下面哪个判断是正确的:

A . Pclass 为 1的分组中的前三名乘客全部生还

B . Pclass 为 2 的分组中,前 20 名乘客的生还率超过了 50%

C . Pclass 为 3 的分组中,前 5 名乘客无一生还

D . 三个分组中各自的 第 10 名乘客合计 2人生还

# A参考代码
query='''SELECT
    json_group_array(PassengerId) AS PassengerIds
    ,json_group_array(Survived) AS SurvivedValues
    ,ROUND(SUM(CASE WHEN Survived = 1 THEN 1 ELSE 0 END)*1.0 / COUNT(*), 2)  --存活占比
FROM (
    SELECT 
        PassengerId,
        survived
    FROM example_table_json
    WHERE json_extract(ticketInfo, '$.Pclass') = 1  -- 第n组
    -- 在计算时,若使用group by分组、分组后PassengerId,survived由于需要json_group_array()进行查询数据,且数据类型为字符串,不便sql统计
    -- 所以只需要使用where筛选组数为 n
    LIMIT 5 -- 前m行数据
) t'''
result = connection.execute(query).fetchall()
result
[('[2,4,7,12,24]', '[1,1,0,1,1]', 0.8)]
# A分步执行
query = '''
SELECT 
    PassengerId,
    survived
FROM example_table_json
WHERE json_extract(ticketInfo,'$.Pclass') = 1
ORDER BY PassengerId ASC
LIMIT 3
'''
result = connection.execute(query).fetchall()
result
[(2, 1), (4, 1), (7, 0)]
# A,第1组前三个乘客的存活率
query = '''
SELECT 
    json_group_array(PassengerId) AS PassengerIds,
    json_group_array(Survived) AS SurvivedValues,
    ROUND(SUM(CASE WHEN Survived=1 THEN 1 ELSE 0 END)*1.0 / COUNT(*),2) --存活占比
FROM(
    SELECT 
        PassengerId,
        Survived
    FROM example_table_json
    WHERE json_extract(ticketInfo,'$.Pclass') = 1
    ORDER BY PassengerId ASC
    LIMIT 3
) as tmp
'''
result = connection.execute(query).fetchall()
result
[('[2,4,7]', '[1,1,0]', 0.67)]
# B,第2组前20个乘客的存活率
query = '''
SELECT 
    json_group_array(PassengerId) AS PassengerIds,
    json_group_array(Survived) AS SurvivedValues,
    ROUND(SUM(CASE WHEN Survived=1 THEN 1 ELSE 0 END)*1.0 / COUNT(*),2) --存活占比
FROM(
    SELECT 
        PassengerId,
        Survived
    FROM example_table_json
    WHERE json_extract(ticketInfo,'$.Pclass') = 2
    ORDER BY PassengerId ASC
    LIMIT 20
) as tmp
'''
result = connection.execute(query).fetchall()
result
[('[10,16,18,21,22,34,42,44,54,57,59,67,71,73,79,85,99,100,118,121]',
  '[1,1,1,0,1,0,0,1,1,1,1,1,0,0,1,1,1,0,0,0]',
  0.6)]
# C,第3组前5个乘客的存活率
query = '''
SELECT 
    json_group_array(PassengerId) AS PassengerIds,
    json_group_array(Survived) AS SurvivedValues,
    ROUND(SUM(CASE WHEN Survived=1 THEN 1 ELSE 0 END)*1.0 / COUNT(*),2) --存活占比
FROM(
    SELECT 
        PassengerId,
        Survived
    FROM example_table_json
    WHERE json_extract(ticketInfo,'$.Pclass') = 3
    ORDER BY PassengerId ASC
    LIMIT 5
) as tmp
'''
result = connection.execute(query).fetchall()
result
[('[1,3,5,6,8]', '[0,1,0,0,0]', 0.2)]
# D 各组第10名乘客的生存率
query='''
SELECT
    json_extract(ticketInfo,'$.Pclass'),
    json_extract(json_group_array(PassengerId),'$[9]') AS PassengerIds,
    json_extract(json_group_array(Survived),'$[9]') AS SurvivedValues,
    ROUND(SUM(CASE WHEN Survived = 1 THEN 1 ELSE 0 END)*1.0 / COUNT(*), 2)  --存活占比
FROM example_table_json
GROUP BY json_extract(ticketInfo,'$.Pclass')
'''
result = connection.execute(query).fetchall()
result
[(1, 36, 0, 0.63), (2, 57, 1, 0.47), (3, 15, 0, 0.24)]
# ...your code...
a2 = 'B'  # 在 '' 中填入你的结果,如 a2 = 'A';注意区分大小写
Q3:参考 2.5 中的代码,按照 passengerInfo 中的 Age 分组后(去掉 Age 为 None 的组),查看每个组的生存情况,将每个组的倒数第三位的数字设置为 -1 。请问最后一共加入了多少 -1
# ...your code..
a3 = '56'   # 在 '' 中填入你的结果,如 a3 = 317;注意区分大小写
# 题目相当于 '人员数量大于等于3的组的数量是多少?',这样子理解就比较容易了
query = '''
SELECT COUNT(*)
FROM
(SELECT 
    json_extract(passengerInfo,'$.Age') AS Age
FROM example_table_json
WHERE json_extract(passengerInfo,'$.Age') IS NOT NULL
GROUP BY json_extract(passengerInfo,'$.Age') 
-- 此句也可运行但不如下边的语句实用,HAVING json_array_length(json_group_array(PassengerId))>=3
HAVING COUNT(*) >= 3) AS t
'''
result = connection.execute(query).fetchall()
result
[(56,)]
STEP2:将结果保存为 csv 文件

csv 需要有两列,列名:id、answer。其中,id 列为题号,如 q1、q2;answer 列为 STEP1 中各题你计算出来的结果。💡 这一步的代码你无需修改,直接运行即可。

# 生成 csv 作业答案文件
def save_csv(answers):
    import pandas as pd
    
    if len(answers)!=3:
        raise Exception("请检查你的答案数列中是否包含了 3 道题的答案。\n")
    df = pd.DataFrame({"id": ["q1","q2","q3"], "answer": answers})
    df.to_csv("answer_workflow_1_2.csv", index=None)
save_csv([a1,a2,a3])  # 该csv文件在左侧文件树project工作区下,你可以自行右击下载或者读取查看

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值