likes
comments
collection
share

使用SQL和dbt的分析工程——用于分析的SQL

作者站长头像
站长
· 阅读数 2

在庞大的数据和分析领域中,选择正确的工具和技术以高效地处理和操作数据至关重要。在经受时间考验并仍然处于前沿的工具之一是结构化查询语言(SQL)。它提供了一种强大而灵活的处理数据的方法,使其成为任何分析开发任务的首选工具。SQL是一种用于管理和操作关系型数据库的标准化编程语言,使数据专业人员能够高效地检索、存储、修改和分析存储在数据库中的数据。由于其直观的语法和在社区中的广泛接受,SQL已成为数据专业人员的标准语言,他们使用它与数据库进行交互,并从复杂的数据集中获取有价值的洞察。

SQL在今天数据驱动的世界中充当着数据消费和分析的支柱。企业在执行数据分析操作以获取竞争优势时高度依赖它。SQL的多功能性和丰富的功能使其成为分析专业人员的重要工具,使他们能够检索特定的数据子集,执行复杂的聚合,并连接多个表以发现数据中隐藏的模式和关系。

SQL的一个关键优势是它能够快速检索和操作数据,提供了广泛的查询能力。这使数据专业人员能够基于特定标准过滤、排序和分组数据,仅检索必要的数据,从而最小化资源使用并提高性能。此外,SQL支持数据操作,如插入、更新和删除记录,有助于在分析之前进行数据清理和准备任务。

使用SQL的另一个相关好处是它与各种分析工具和生态系统(如Python或BI平台)的无缝集成,使其成为数据专业人员的首选语言,并允许他们将SQL的强大功能与高级统计分析、机器学习算法和交互式可视化相结合。此外,云数据库和数据仓库的崛起进一步提升了SQL在分析消费中的重要性,Google BigQuery、Amazon Redshift和Snowflake等平台将SQL作为其主要查询语言支持。

在本章中,我们将讨论SQL语言作为最常用的分析语言之一的韧性。然后,我们将介绍数据库的基础知识,引入SQL作为与数据库进行交互的标准语言。我们还会探讨视图的创建和使用,它提供了简化复杂查询和抽象数据结构的强大机制。

随着对SQL的深入挖掘,我们将回顾窗口函数,使您能够执行高级计算和聚合。此外,我们将深入研究公共表达式(CTEs),它们提供了创建临时结果集和简化复杂查询的方法。最后,我们还将提供一个瞥见SQL用于分布式数据处理的部分,并以一个额外的部分展示SQL在训练机器学习模型方面的应用。

SQL的适应性

随着时间的推移,我们发现用SQL开发的数据工程流水线通常会持续多年,而查询和存储过程仍然是支持金融机构、零售公司甚至科学活动的几个关键系统的核心。然而,令人惊奇的是,SQL已被广泛使用,并不断演变以满足现代数据处理的需求,并引入了新的功能。此外,令人着迷的是,诸如dbt、DuckDB,甚至新的数据操作库Polars都通过SQL接口提供其功能。但这种流行背后的主要原因是什么呢?我们认为可以突出几个因素。

首先且最重要的是代码的可读性。这是数据工程的一个关键方面。SQL的语法虽然灵活,但允许根据上下文和具体要求同时进行命令式和声明式使用。许多查询涉及命令式任务,比如为用户检索特定数据或计算给定日期范围的结果。然而,在指定要获取的数据而不是规定如何检索它时,SQL的声明性特性就显得很突出。这种灵活性使得包括BI开发人员、业务分析师、数据工程师和数据科学家在内的各种用户能够理解和解释代码。与一些其他严格命令式的数据处理语言不同,SQL允许作者专注于描述期望的结果。这种自描述的特性使得SQL代码更具可读性和可理解性,促进了跨职能团队的有效协作。

另一个令人振奋的因素是,尽管SQL作为一个接口已经经受住了时间的考验,但实际上,在过去的几年里,支持它的引擎发生了巨大的变化。传统的SQL引擎得到了改进,而像Spark和Presto这样的分布式工具使SQL能够处理海量数据集。最近,DuckDB作为一款改变游戏规则的工具崭露头角,为SQL提供了在单台机器上进行极快的并行化分析查询的能力。由于其功能可与其他高性能替代方案媲美,DuckDB为各种规模的数据工程任务开辟了新的可能性。

然而,值得注意的是,并非所有基于SQL的系统都是相同的。例如,SQL Server通常用于仓储,但它是为OLTP设计的。另一方面,像Snowflake和Redshift这样的平台是专门的OLAP数据仓库。它们在处理大规模分析工作负载方面表现出色,并且针对复杂的查询和报告进行了优化。这些区别突显了SQL的多功能性,它可以适应各种数据库架构和用途。SQL仍然是一种统一语言,弥合了OLAP和OLTP系统之间的差距,促进了对跨数据库类型和技术的数据访问和分析。

数据类型是SQL的另一个显著优势,特别是在数据工程领域。经验丰富的数据工程师了解在各种编程语言和SQL引擎之间管理数据类型的挑战,这个过程可能繁琐且容易出错。然而,SQL引擎在强制执行强大的数据类型方面表现出色,确保数据在整个数据流水线中的一致处理。此外,SQL生态系统提供了诸如Apache Arrow之类的有价值的工具,用于解决由各种工具和数据库引起的兼容性问题。Arrow在包括R、Python和各种数据库在内的环境中促进了强大且一致的数据类型处理。选择与Arrow兼容的SQL引擎可以有效缓解许多数据类型方面的挑战,简化维护工作,减轻依赖管理的负担,从而使数据工程师能够更多地专注于他们的数据工程工作的核心方面。

SQL与软件工程最佳实践的兼容性是数据工程领域的一个重要优势。数据工程师经常处理复杂的SQL脚本,这是他们组织的数据流水线的重要组成部分。过去,维护和修改这样的脚本是一个主要的挑战,通常导致难以理解和修改的代码。然而,SQL工具的发展解决了这些挑战,并使得将SQL代码适应良好的技术实践变得更加容易。一个显著的进步是DuckDB的出现,这是一款专门为分析查询设计的SQL引擎。DuckDB独特的功能,比如没有依赖关系和对分析工作负载的优化,使得数据工程师可以进行单元测试,并促进SQL代码的快速迭代。这确保了SQL代码符合已建立的技术原则,提高了其可靠性和可维护性。

SQL生态系统中的另一个有用工具是CTEs,可用于将大型查询分解成更小、更易管理和可测试的部分。通过将复杂查询分解为语义上有意义的组件,数据工程师可以轻松验证和独立验证每个部分,推动更模块化和强大的开发过程。

其他的改进也有助于将SQL推向分析工程的前沿。Lambda函数使数据工程师能够直接将任意函数写入SQL语句。这种能力提高了SQL代码的灵活性和敏捷性,使其能够在数据处理过程中进行动态计算和转换。

窗口函数也长期以来被认为是SQL中的一种有价值的工具,因为它们通过将数据分成可管理的段提供了增强的分析能力。借助窗口函数,数据工程师可以在定义的数据子集上执行复杂的聚合、排名和统计计算,为分析和报告开辟了新的可能性。

最后但并非最不重要的是,现代SQL引擎已经整合了全文搜索、地理数据函数和用户定义函数等功能,进一步扩展了SQL的能力。这些增强功能针对特定的用例和领域特定的需求,使数据工程师能够在SQL环境中执行专业化的操作。

所有这些以及更多因素随着时间的推移促成了SQL的弹性,并鼓励许多人投资于学习和在日常分析活动中应用它。现在让我们回过头来重新审视SQL的核心概念。

数据库基础

对于分析和数据工程师来说,对数据库基础知识的深刻理解至关重要。数据库作为存储、组织和检索大量数据的支柱起着关键作用。随着时间的推移,数据库的发展为SQL的崛起和完善创造了条件,使其成为处理关系数据库的强大且广泛应用的语言。然而,在探讨数据库的具体性质之前,了解数据、信息和知识的更广泛背景至关重要,因为它们都存在于数据库中或源自数据库。

在这一背景的基础上,我们有DIKW金字塔,如图3-1所示。这个概念模型描述了数据、信息、知识和智慧之间的层次关系。通过一系列迭代过程,DIKW金字塔为我们提供了一个框架,用于理解如何将原始数据转化为可操作的智慧。

使用SQL和dbt的分析工程——用于分析的SQL

为了更好地理解DIKW金字塔,让我们分解每一层:

数据(Data) 原始的事实和数字,缺乏上下文和含义。数据可以被视为信息的构建块。数据的例子:1989年、教师、绿色。

信息(Information) 数据的有组织、有结构的表现形式,提供上下文并回答特定问题。信息的例子:

  • 我的数学老师是1989年出生的。
  • Albany Ave和Avenue J交叉口的红绿灯是绿色的。

知识(Knowledge) 当我们将信息与经验、专业知识和理解相结合时产生。它代表从分析和解释信息中获得的见解,使个人和组织能够做出明智的决策并采取适当的行动。知识的例子:

  • 由于我的数学老师是1989年出生的,他现在是成年人。
  • 我驶向的交通灯正在变绿。

智慧(Wisdom) 一种超越知识的深刻理解水平。智慧发生在个人和组织能够运用他们的知识并做出明智判断,产生积极影响和变革性见解的时候。智慧的例子:

  • 也许是我数学老师考虑开始制定养老储蓄计划的时候了。
  • 随着交通灯变绿,我可以前进了。

数据库在DIKW金字塔中发挥着关键的作用,充当着存储、管理和组织数据的基础。这使得将数据转化为有意义的见解成为可能,最终使企业获得必要的知识以做出明智的决策。

数据库的类型

数据库是现代数据管理系统的核心组成部分,提供了存储、组织和检索数据的结构化方法。为了更好地理解数据库是如何实现这一目标的,让我们首先探讨一下数据库的两个主要类别:关系数据库和非关系数据库。通过了解这两种类型之间的特点和区别,您将能够更好地为满足特定数据需求选择数据库解决方案。

图3-2显示了数据库的两个主要类别,在每个类别中映射了最常见的数据库类型。

使用SQL和dbt的分析工程——用于分析的SQL

  • 关系数据库

在这个最常见且广泛采用的数据库类别中,数据被组织成行和列的表格。关键是用来强制实施表之间的关系,而SQL用于查询和操作数据。关系数据库提供强大的数据完整性、事务可靠性和对ACID属性的支持,确保数据库事务是可靠的、保持数据完整性的,并且可以从故障中恢复。

  • 非关系数据库

也被称为NoSQL(不仅仅是SQL)数据库,非关系数据库作为管理大量无结构和半结构数据的一种选择而出现,具有可扩展性和灵活性。与关系数据库相比,非关系数据库不依赖于固定的模式。它们可以以各种格式存储数据,如键值对、文档、宽列存储或图形。非关系数据库优先考虑高性能、水平扩展和模式灵活性。它们非常适用于实时分析、处理无结构数据的应用程序和物联网(IoT)数据等场景。

我们可以把数据库想象成数据宇宙的一个子集,它被构建、设计和注入了数据,具有特定于您组织目标的目的。数据库是社会的一个重要组成部分。一些活动,比如列举的这些,广泛分布在整个社会,其中有一个数据库位于中心存储着数据:

  • 预订酒店
  • 预订机票
  • 在知名市场购买手机
  • 进入您喜欢的社交网络
  • 就医

但在实践中,这是什么样子呢?进入关系数据库,我们将数据组织成具有行和列的表。表代表我们宇宙中的一个实体,比如大学里的学生或图书馆中的一本书。列描述实体的属性。例如,学生有姓名或地址。书有标题或国际标准图书编号(ISBN)。最后,行就是数据本身。学生的姓名可以是Peter Sousa或Emma Rock。对于书的标题,一行可以是“使用SQL和dbt进行分析工程”。图3-3展示了一个带有相应列和行的表的示例。

使用SQL和dbt的分析工程——用于分析的SQL

另一个需要考虑的话题是我们如何与数据建立关系并确保一致性。在关系数据库中,我们可以通过使用键来强制表之间的连接,这是一个要强调的重要因素。在关系数据库中强制执行这些关系和连接涉及实施机制,以在相关表之间维护数据的完整性和一致性。这些机制维护表之间的关系,防止不一致性或数据异常。

强制关系的一种方法是使用主键和外键。我们将在后面详细讨论这个问题,但现在,图3-4展示了表之间的相互关系。用例是一个大学,一个或多个学生可以注册一个或多个课程。了解这些类型的数据库为我们下一个话题做好了准备:数据库管理系统(DBMS)。在下一节中,我们将深入探讨DBMS的功能和重要性,它是一种软件工具,能够在各种类型的数据库中实现高效的数据存储、检索和管理。

使用SQL和dbt的分析工程——用于分析的SQL

数据库管理系统

DBMS(数据库管理系统)是一种软件系统,它使数据库的创建、组织、管理和操作变得可能。它为用户和应用程序提供了一个接口和一组工具,使其能够与数据库进行高效的数据存储、检索、修改和删除交互。

DBMS充当用户或应用程序与底层数据库之间的中介。它抽象了与数据库交互的复杂性,提供了一种方便且标准化的数据处理方式。它充当一个软件层,负责处理数据的存储、检索和管理,同时确保数据的完整性、安全性和并发控制。

DBMS(数据库管理系统)的主要功能包括以下几个方面:

  • 数据定义

DBMS允许用户通过创建和修改数据库模式来定义数据的结构和组织。它支持定义表、列、关系和约束,以规范存储在数据库中的数据。

  • 数据操作

用户可以使用查询语言(通常是SQL)对数据库中存储的数据执行操作。DBMS提供插入、检索、更新和删除数据的机制,实现对数据库内容的高效和受控操作。

  • 数据安全性和完整性

DBMS提供机制来确保数据安全性,通过强制执行访问控制策略实现。它支持定义用户角色和权限,限制对敏感数据的访问。此外,DBMS通过实施约束和验证来保证数据的完整性,维护数据的一致性和准确性。

  • 数据并发和事务管理

DBMS处理多用户或多应用程序对数据库的并发访问,确保数据保持一致并受到冲突的保护。它提供事务管理功能,确保一组操作可靠一致地执行,遵循ACID属性。

  • 数据恢复和备份

DBMS具有确保数据持久性和可恢复性的功能。它提供数据备份和还原机制,以便在系统故障或灾难发生时进行数据恢复。

一些常见的关系型和非关系型数据库管理系统可以在表3-1中找到。

使用SQL和dbt的分析工程——用于分析的SQL

与数据库“交流”

从外部的角度来看,通过数据库管理系统(DBMS)与数据库进行交互提供了四种语言:

  1. 数据定义语言(DDL):用于处理模式,如表的创建。
  2. 数据操作语言(DML):用于处理数据。
  3. 数据控制语言(DCL):用于管理对数据库的权限。
  4. 事务控制语言(TCL):用于处理数据库中发生的事务。

图3-5显示了与数据库交互时使用的主要语言及其主要命令。

使用SQL和dbt的分析工程——用于分析的SQL

在本书中,我们的主要重点将是通过学习如何查询、操作和定义数据库结构来提供关于SQL的坚实基础,因此我们将讨论DDL和DML。与DCL和TCL相关的管理任务将不在讨论范围内。

使用DDL创建和管理数据结构

DDL(数据定义语言)用于定义数据库中对象的结构,包括定义表、索引、序列、别名等的命令和语法。

最常见的DDL命令包括以下内容:

CREATE:创建新的数据库对象,如表、视图、索引或约束。它指定对象的名称及其结构,包括列、数据类型和任何其他属性。

DROP:删除现有的数据库对象。它永久删除指定的对象及所有相关数据。

ALTER:修改现有数据库对象的结构。可以用它来添加、修改或删除表的列、约束或其他属性,为适应数据库模式的变化提供了灵活性。

RENAME:重命名现有的数据库对象,如表或列。它提供了一种在不更改结构或数据的情况下更改对象名称的方法。

TRUNCATE:快速从表中删除所有数据,同时保留表结构。与使用DELETE命令逐行删除所有行相比,它更快,因为它释放数据页面而不记录单个行的删除。

CONSTRAINT:在表列上定义约束,通过指定数据必须满足的规则或条件来确保数据的完整性和有效性。

INDEX:在表的一个或多个列上创建索引。通常,索引通过创建一个排序结构,允许更快的数据搜索和排序,从而提高数据检索操作的性能。

在深入进行实际应用案例之前,有一些主题需要详细讨论,并且我们需要介绍一个额外的主题。事实上,大多数DDL命令在某种程度上是不言自明的,只要我们稍后在代码中看到它们,就会很容易理解。尽管如此,对CONSTRAINT命令的讨论应该稍微详细一些,以介绍其特殊性。

如前所述,约束是数据必须满足的规则或条件,以确保其完整性。通常,这些约束应用于列或表。最常见的约束如下:

主键(Primary key):主键约束确保表中的列或列组合唯一标识每一行,防止重复和空值。它对于数据完整性至关重要,通常用作相关表中外键约束的引用。

外键(Foreign key):外键约束指定两个表之间的关系。它确保一个表中列或列组合中的值与另一个表中的主键值匹配,有助于保持引用完整性并强制实施跨相关表的数据一致性。

唯一(Unique):唯一约束确保列或列组合中的值是唯一的,不允许重复。与主键不同,唯一约束可以允许空值,但如果列具有唯一约束,则只允许一个空值。

检查(Check):检查约束对列中允许的值强加条件。通常使用这些约束来实施业务规则、领域特定要求或数据上的任何其他自定义条件。

非空(Not null):非空约束确保列不包含空值,因此具有此约束的特定列必须为每个插入或更新的行提供一个值。这有助于强制执行数据完整性,避免意外的空值。

最后,还有一个要讨论的问题:数据类型,用于分类可以存储在列或变量中的数据。这些字段可能因数据库引擎而异。在我们的情况下,我们将保持简单,使用MySQL数据类型作为参考:

整数(Integer):没有小数部分的整数。最常见的是INT、SMALLINT、BIGINT、TINYINT。可能的值示例:1、156、2012412、2。

十进制(Decimal):带有小数部分的数字。其中一些常见的是DECIMAL、NUMERIC、FLOAT、DOUBLE。可能的值示例:3.14、94.5482、5161.17620。

布尔(Boolean):二进制值。传统上写作BOOLEAN、BOOL、BIT、TINYINT。用于存储true/false或0/1值。

日期(Date):大多数情况下都很好理解,但其格式可能有所不同。声明为DATE,其中一种常用的标准格式是2023-07-06。

时间(Time):您也可以决定时间数据类型的格式。在数据库中写作TIME,其中一种常见格式是18:34:59。

时间戳(Timestamp):日期和时间的组合。通常,我们使用TIMESTAMP或DATETIME。示例:2017-12-31 18:34:59。

文本(Text):最通用的数据类型。但它只能包含字母或字母、数字或任何其他字符的混合。通常声明为CHAR、VARCHAR、NVARCHAR、TEXT。请注意选择正确的文本数据类型是相关的,因为每种类型都有指定的最大长度。文本的示例:“hello world”,“porto1987”,“Hélder”,“13,487*5487+588”。

既然您对DDL命令和最常见的数据库数据类型有了更好的了解,让我们为管理O'Reilly图书创建一个数据库。这与第二章中的示例一致,当时我们介绍了一个用于追踪图书的O'Reilly数据库,但现在让我们从物理模型的创建开始。

值得注意的是,对于数据工程师来说,精通所有类型的SQL命令是至关重要的,因为他们负责数据库设计(DDL)和数据操作(DML)。分析师主要关注DML SQL命令,通常仅限于用于数据分析的SELECT查询。另一方面,分析工程师通常使用DML和一些DDL SQL命令的组合,尽管他们通常通过诸如dbt之类的工具来抽象DDL操作。

首先,让我们创建数据库本身。在您的MySQL客户端中,执行示例3-1中的命令。

示例3-1:创建数据库

-- 创建OReillyBooks数据库语句
CREATE DATABASE OReillyBooks;

有了创建的数据库,执行示例3-2中的代码。

示例3-2:创建数据库,第2部分

-- 使用数据库
USE OReillyBooks;

-- 创建表
-- 表:Authors
CREATE TABLE authors (
  author_id INT PRIMARY KEY,
  author_name VARCHAR(100)
);

-- 表:Books
CREATE TABLE books (
  book_id INT PRIMARY KEY,
  book_title VARCHAR(100),
  author_id INT,
  rating DECIMAL(10,2),
  FOREIGN KEY (author_id) REFERENCES Authors(author_id)
);

-- 表:Category
CREATE TABLE category (
  category_id INT PRIMARY KEY,
  category_name VARCHAR(50)
);

-- 表:bookCategory
CREATE TABLE book_category (
  book_id INT,
  category_id INT,
  FOREIGN KEY (book_id) REFERENCES books(book_id),
  FOREIGN KEY (category_id) REFERENCES category(category_id)
);

总的来说,这两个示例创建了一个名为OReillyBooks的数据库,并定义了四个表:authors、books、category和book_category(表示图书和类别之间的多对多关系)。每个表都有其自己的列和约束,如主键和外键。

最后,为了测试其他DDL命令,假设有一个新的需求,现在我们还需要存储publication_year,它表示一本特定书籍的出版年份。执行这样的操作的语法如示例3-3所示。

示例3-3:ALTER TABLE语法

-- 添加新列
ALTER TABLE table_name
ADD column_name datatype [column_constraint];

-- 修改现有列的数据类型
ALTER TABLE table_name
ALTER COLUMN column_name [new_datatype];

-- 重命名列
ALTER TABLE table_name
RENAME COLUMN old_column_name TO new_column_name;

-- 向列添加新约束
ALTER TABLE table_name
ADD CONSTRAINT constraint_name constraint_type (column_name);

-- 修改现有约束
ALTER TABLE table_name
ALTER CONSTRAINT constraint_name [new_constraint];

-- 删除现有列
ALTER TABLE table_name
DROP COLUMN column_name;

根据示例3-3中显示的语法,符合我们需求的修改是添加新列。现在,通过执行示例3-4中的代码段,添加publication_year

示例3-4:添加出版年份

-- 向books表添加publication_year列
ALTER TABLE books
ADD publication_year INT;

使用DML操纵数据

DML是数据库管理的一个重要组成部分。该语言允许在数据库系统中进行数据选择、插入、删除和更新。其主要目的是检索和操作驻留在关系型数据库中的数据,涵盖了几个关键的命令。

使用INSERT插入数据

INSERT命令简化了将新数据插入表中的过程。通过此命令,用户可以轻松地将一条或多条记录插入到数据库中的特定表中。通过使用INSERT,可以通过包含额外条目来扩展表的内容。这个命令在向初始为空的表中添加记录方面非常重要,但也允许不断地增加数据库中现有的数据。示例3-5显示了该命令的标准语法。

INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...);

INSERT INTO语句指定将插入数据的表,其中table_name表示表的名称。组件(column1column2,...)是可选的,允许指定要插入数据的列。如果省略了列,假定将为表中的所有列提供值。VALUES关键字指示将插入指定列的值的开始。在VALUES子句(value1value2,...)中,我们有要插入到相应列中的实际值。确保提供的值数量与指定的列数量相匹配非常重要。这是确保值在插入过程中正确映射到相应列的唯一方法。如果不遵守这一点,大多数数据库引擎将引发错误。

现在,让我们扩展我们的用例,这个用例我们从“使用DML操作数据”(第82页)开始,向先前创建的表中插入数据。为此,请执行示例3-6中的命令。

-- 将数据插入authors表
INSERT INTO authors (author_id, author_name) VALUES
(1, 'Stephanie Mitchell'),
(2, 'Paul Turner'),
(3, 'Julia Martinez'),
(4, 'Rui Machado'),
(5, 'Thomas Brown');

-- 将数据插入books表
INSERT INTO books (book_id, book_title,
author_id, publication_year,
rating)
VALUES
(1, 'Python Crash Course', 1, 2012, 4.5), 
(2, 'Learning React', 2, 2014, 3.7),
(3, 'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow',
3, 2017, 4.9),
(4, 'JavaScript: The Good Parts', 4, 2015, 2.8),
(5, 'Data Science for Business', 5, 2019, 4.2);

-- 将数据插入category表
INSERT INTO category (category_id, category_name) VALUES
(1, 'Programming'),
(2, 'Machine Learning'),
(3, 'Data Science'),
(4, 'Software Engineering'),
(5, 'Algorithms'),
(6, 'Computer Science');

-- 将数据插入book_category表
INSERT INTO book_category (book_id, category_id) VALUES
(1, 1),
(2, 1),
(3, 2),
(4, 1),
(5, 3);

此代码创建了几个INSERT语句,每个都针对特定的表。我们首先向authors表中插入数据。每一行代表一位作者,具有author_id和author_name列,分别表示作者的唯一标识符和姓名。

然后,我们将数据插入books表。每一行代表一本书,具有book_id,book_title和author_id列,分别表示书的唯一标识符、标题和作者标识符。author_id列链接到authors表中的author_id列,以建立书和作者之间的关系。请注意,由于引用完全不存在的作者而无法插入书籍,这是由于参照完整性的缘故。

我们还创建了一个category表,以根据其内容类型正确分类书籍。每一行代表一个类别,具有category_id和category_name列,分别表示类别的唯一标识符和名称。

最后,我们的中间表book_category存储了书籍及其相应类别之间的关系。每一行表示此关系的一次出现,其中book_id和category_id列分别表示书籍和类别的标识符。这些列建立了书籍和类别之间的多对多关系。

让我们查看我们插入的数据。逐行执行示例3-7中的代码。我们将在下一节详细讨论SELECT语句,但目前检查每个表中的数据就足够了。

Example 3-7. 查询authors、book_category、books和category表的SELECT语句

-- 查询authors表
SELECT * FROM authors;

-- 查询book_category表
SELECT * FROM book_category;

-- 查询books表
SELECT * FROM books;

-- 查询category表
SELECT * FROM category;

使用SELECT查询数据

"SELECT"是SQL中最基本的DML命令之一。该命令允许你从数据库中提取特定的数据。当执行此语句时,它会检索所需的信息并将其组织成一个结构化的结果表,通常称为结果集。这个结果集包含符合指定条件的数据,使用户能够轻松访问和分析所选信息。在Example 3-8中,我们可以分析该命令的(最简单的)语法。

Example 3-8. SELECT语句的语法

SELECT column1, column2, ...
FROM table_name;

该结构的SELECT部分指示要从表中检索的特定列或表达式。FROM组件指定将从中检索数据的表。关于这个命令,我们还有很多要详细讨论的内容,包括数据过滤和相应的运算符,以及数据分组或连接。在接下来的章节中,我们将讨论每个属性。

使用WHERE过滤数据 可选的WHERE子句允许用户定义检索到的数据必须满足的条件,有效地根据指定的条件过滤行。这是SQL查询的基本部分,允许您从表中过滤和检索特定子集的数据。Example 3-9显示了带有WHERE子句的SELECT语句的语法。 Example 3-9. SELECT语句带有WHERE子句的语法

SELECT column1, column2, ...
FROM table_name
WHERE condition;

为了正确理解如何在SQL中编写条件并充分过滤数据,我们首先必须熟悉SQL中的单个或多个条件的运算符。

SQL运算符。SQL运算符经常用于WHERE子句中,以建立过滤数据的条件。这些运算符允许您在SQL中比较满足定义条件的值和表达式。表3-2总结了最常见的运算符。

使用SQL和dbt的分析工程——用于分析的SQL

为了更好地理解它们的应用,让我们探讨比较运算符和逻辑运算符的例子。为了简化事情,因为我们没有深入研究SQL的其他元素,比如连接,让我们以图书表为用例的源。

作为条件和逻辑运算符的初始示例,让我们尝试找到早于2015年出版的书籍。然后,让我们只找到2017年出版的书,最后找到标题中包含“Python”的书。示例3-10包含了三个代码片段,帮助您解决这个挑战。

示例 3-10. 使用条件运算符选择数据

-- 发布于2015年之前的图书
SELECT
  book_title,
  publication_year
FROM books
WHERE publication_year < 2015;

-- 发布于2017年的图书
SELECT
  book_title,
  publication_year
FROM books
WHERE publication_year = 2017;

-- 标题中包含"Python"的图书
SELECT
  book_title,
  publication_year
FROM books
WHERE book_title LIKE '%Python%';

示例 3-10 展示了三个使用条件运算符的例子。请随意运行代码并测试之前介绍的其他例子。

最后,为了熟悉逻辑运算符,让我们搜索2012年或2015年之后出版的图书。示例 3-11 将帮助完成这个任务。

示例 3-11. 使用逻辑运算符选择数据

-- 发布于2012年或2015年之后的图书
SELECT
  book_title,
  publication_year
FROM books
WHERE publication_year = 2012 OR publication_year > 2015;

还需要注意的是,这些运算符并不仅限于 WHERE 子句。它们还可以与其他过滤技术一起使用,比如我们将在下一部分介绍的 HAVING 子句。

对数据进行聚合操作使用 GROUP BY

GROUP BY子句是SQL中的一个可选功能,用于将结果集按一个或多个列分组。通常与聚合函数一起使用,GROUP BY计算在指定列或列中共享相同值的行的子集。换句话说,使用GROUP BY子句时,结果集被分成多个组,其中每个组表示给定聚合列或列的唯一值组合。正如所述,GROUP BY通常与聚合函数一起用于这些组,为数据提供有价值的洞察力。表3-3中显示了一些最常见的聚合函数。

使用SQL和dbt的分析工程——用于分析的SQL

GROUP BY通常用于趋势分析和汇总报告,比如月度销售报告和季度用户访问报告等。GROUP BY子句的通用语法如示例3-12所示。

示例3-12. 带有GROUP BY子句的SELECT语句的语法

SELECT column1, column2, ...
FROM table_name
GROUP BY column1, column2, ...

现在让我们在一个简单的用例中应用这些函数。使用表book_category,让我们分析每个类别平均图书的数量。为了帮助您完成这个挑战,让我们看一下示例3-13。

示例3-13. 选择和聚合数据

SELECT
  category_id,
  COUNT(book_id) AS book_count
FROM bookCategory
GROUP BY category_id;

我们使用了COUNT()聚合函数,但根据所需的用例,也可以使用其他聚合函数。最后,这是一个简单的例子,因为我们只看到了category_id,但如果我们有类别名称的话会更好;然而,该字段仅在类别表中可见。要包含它,我们需要知道如何使用连接。我们将在第90页的“使用INNER、LEFT、RIGHT、FULL和CROSS JOIN连接数据”中进一步讨论这个问题。

最后,我们来看HAVING过滤器。作为与GROUP BY密切相关的可选子句,HAVING过滤器对分组数据应用条件。与WHERE子句相比,HAVING过滤器在聚合后对行进行过滤,而在WHERE子句中,过滤在分组操作之前发生。然而,与WHERE语句中一样,HAVING使用相同的运算符,比如“等于”和“大于”等。

HAVING过滤器的SQL语法如示例3-14所示。

示例3-14. 带有GROUP BY子句和HAVING过滤器的SELECT语句的语法

SELECT column1, column2, ...
FROM table_name
GROUP BY column1, column2, ...
HAVING condition

让我们看一下HAVING过滤器的实际应用。参考示例3-13,现在我们只想要那些至少有两本图书出版的类别。示例3-15将帮助您完成这个任务。

示例3-15. 选择和聚合数据,应用HAVING过滤器

SELECT
  category_id,
  COUNT(book_id) AS book_count
FROM bookCategory
GROUP BY category_id
HAVING COUNT(book_id) >= 2;

通过利用GROUP BY子句和HAVING过滤器,您可以有效地组织和总结数据,对聚合数据集执行计算,帮助您发现数据中的模式、趋势和关系,促进数据分析,支持决策过程。

使用ORDER BY对数据进行排序

ORDER BY子句是SQL中的排序语句,通常用于以特定顺序组织查询结果,使数据更容易分析和解释。它根据一个或多个列对查询的结果集进行排序,允许您为每个列指定排序顺序,即升序(默认)和降序。

ORDER BY子句的基本语法如示例3-16所示。 示例3-16. 带有ORDER BY子句的SELECT语句的语法

SELECT column1, column2, ...
FROM table_name
ORDER BY column1 [ASC|DESC], column2 [ASC|DESC], ...

在之前的用例中,一个明显的例子是按年份出版的书籍。查看表时,很难确定哪些是最新的和最老的书籍。ORDER BY子句大大简化了这种分析。为了测试这个子句,请执行示例3-17中的代码片段,并查看有无ORDER BY的结果。需要注意的是,如果未明确声明ASC/DESC的顺序,SQL将默认使用ASC。

示例3-17. 带有ORDER BY子句的SELECT语句

SELECT
  book_title,
  publication_year
FROM books
ORDER BY publication_year DESC;

总的来说,ORDER BY子句允许您按照期望的顺序排列结果集,以最好地适应数据的探索和分析,简化捕捉有意义的数据。

使用INNER、LEFT、RIGHT、FULL和CROSS JOIN连接数据

连接是SQL中的一种机制,用于合并来自多个表的数据。理解并使用连接可以极大地提高您从复杂数据集中提取有价值的见解并做出更明智的决策的能力。本节将引导您了解SQL中可用的连接类型、它们的语法以及用法。

SQL有几种类型的连接。每一种连接都允许您根据指定的条件从多个表中合并数据。在看到连接实际操作之前,让我们通过向数据集添加一个新的作者来扩展它。这个作者将没有任何书籍。要执行此操作,请运行示例3-18中的语句。

示例3-18. 插入没有书籍的作者

INSERT INTO authors (author_id, author_name) VALUES
(6, 'John Doe');

我们创建一个没有任何书籍的作者是为了探索我们将要介绍的几种连接。以下是SQL中最常见的连接类型。

INNER JOIN(内连接)。INNER JOIN仅返回两个表中基于指定连接条件的匹配行。如果我们将Venn图中的圆A和圆B分别表示为每个数据集,在INNER JOIN中,我们将只看到包含两个表的匹配值的重叠区域。让我们查看图3-6以更好地可视化Venn图。

使用SQL和dbt的分析工程——用于分析的SQL

INNER JOIN的代码语法如示例3-19所示。 示例3-19. INNER JOIN的语法

SELECT
  columns
FROM Table_A
INNER JOIN Table_B ON join_condition;

为了看到INNER JOIN的实际效果,让我们只收集有书籍的作者。示例3-20展示了所需的代码。 示例3-20. 仅收集有书籍的作者

SELECT
  authors.author_id,
  authors.author_name,
  books.book_title
FROM authors
INNER JOIN books ON authors.author_id = books.author_id;

图3-7显示了查询的输出。

使用SQL和dbt的分析工程——用于分析的SQL

通过分析结果,我们可以快速识别缺失的作者John Doe。正如您可能记得的,我们创建了一个没有任何书籍的作者,因此在使用INNER JOIN时,预期他将被省略。

LEFT JOIN(或LEFT OUTER JOIN)。返回左表的所有行以及右表中匹配的行。如果没有匹配,右表的列将包含null值。类似于前面的练习,一个左侧的圆A和一个右侧的圆B分别表示每个数据集。在LEFT JOIN中,左侧的圆包含左表的所有行,重叠区域表示基于连接条件的匹配行。右侧的圆包含右表中的非匹配行,结果集中的这些行由null值表示。请看图3-8以更好地可视化Venn图。

使用SQL和dbt的分析工程——用于分析的SQL

LEFT JOIN的代码语法如示例3-21所示。 示例3-21. LEFT JOIN的语法

SELECT
  columns
FROM Table_A
LEFT JOIN Table_B ON join_condition;

为了测试LEFT JOIN,让我们保持与之前相同的用例,将作者与他们的书籍关联起来,但现在我们希望列出所有作者及其各自的书籍,同时也必须包括没有任何书籍的作者。执行示例3-22中的代码片段。

SELECT
  authors.author_id,
  authors.author_name,
  books.book_title
FROM authors
LEFT JOIN books ON authors.author_id = books.author_id;

查询输出如图3-9所示。

使用SQL和dbt的分析工程——用于分析的SQL

与INNER JOIN相比,LEFT JOIN使我们能够看到作者John Doe。这是因为在LEFT JOIN中,左表authors完全显示,而右表books仅显示与authors相交的结果。

RIGHT JOIN(或RIGHT OUTER JOIN)。RIGHT JOIN返回右表的所有行以及左表中匹配的行。如果没有匹配,左表的列将包含null值。继续考虑一个Venn图,其中圆A(左)和圆B(右)分别表示每个数据集。在RIGHT JOIN中,右侧的圆包含右表的所有行,重叠区域表示基于连接条件的匹配行。左侧的圆包含左表中的非匹配行,结果集中的这些行由null值表示。最后,看一下图3-10以更好地可视化Venn图。

使用SQL和dbt的分析工程——用于分析的SQL

RIGHT JOIN的代码语法如示例3-23所示。

SELECT
  columns
FROM Table_A
RIGHT JOIN Table_B ON join_condition;

首先,让我们将培训情境化,看看RIGHT JOIN的实际效果。在这种情况下,我们想要看到所有的书和它们的作者,所以执行示例3-24中的代码。

示例3-24. 收集书籍及其作者

SELECT
  authors.author_id,
  authors.author_name,
  books.book_title
FROM authors
RIGHT JOIN books ON authors.author_id = books.author_id;

查询输出如图3-11所示。

使用SQL和dbt的分析工程——用于分析的SQL

通过分析查询输出,我们看到了所有书籍及其各自的作者。由于我们没有没有作者的书籍,我们无法看到任何书籍和作者之间存在书籍没有作者的交集。

FULL JOIN(或FULL OUTER JOIN)。在此连接中,从两个表中返回所有行。它结合了LEFT JOIN和RIGHT JOIN的结果。如果没有匹配,将包含来自非匹配表的列的null值。在一个Venn图中,圆A(左)和圆B(右)分别表示每个数据集,FULL JOIN的图表将显示基于连接条件的匹配行的重叠区域,而每个圆的非重叠部分包括各自表中的非匹配行。最终,生成的结果集包括来自两个表的所有行,对于非匹配的行,包含null值。让我们看一下图3-12以更好地可视化。

使用SQL和dbt的分析工程——用于分析的SQL

FULL JOIN的代码语法如示例3-25所示。 示例3-25. FULL JOIN的语法

SELECT
  columns
FROM Table_A
FULL JOIN Table_B ON join_condition;

CROSS JOIN(或叉积连接)。CROSS JOIN返回两个表的笛卡尔积,将第一个表的每一行与第二个表的每一行组合在一起。它不需要连接条件。在CROSS JOIN的Venn图中,我们没有重叠的圆圈,因为它将来自圆A(左)和圆B(右)的每一行组合在一起。结果集包括来自两个表的所有可能的行组合,如图3-13所示。

使用SQL和dbt的分析工程——用于分析的SQL

CROSS JOIN的代码语法如示例3-26所示。

示例3-26. CROSS JOIN的语法

SELECT
  columns
FROM Table_A
CROSS JOIN Table_B;

在示例3-27中展示了作者表和书籍表的CROSS JOIN。 示例3-27. 作者表和书籍表的CROSS JOIN

SELECT
  *
FROM authors
CROSS JOIN books;

总的来说,SQL连接提供了在条件下合并来自多个表的数据的灵活性。了解它们的用法和语法使您能够提取所需的信息,并为跨表的相关数据建立关系。通过Venn图可视化连接有助于解释表数据如何在连接条件下重叠和组合,通过突出显示结果集中匹配和不匹配的行,清晰地表示在连接操作期间表之间关系的情况。

使用UPDATE更新数据

UPDATE命令允许我们在数据库中修改现有表中的记录。通过执行此命令,用户可以有效地更新和修改存储在特定记录中的数据。UPDATE使得可以对表中的一个或多个记录进行更改,确保数据准确地反映最新信息。通过利用此命令,用户可以无缝地修改表的内容,允许根据需要进行数据的精炼、更正或更新。示例3-28显示了此命令的语法。

示例3-28. UPDATE语句的语法

UPDATE table_name
SET column1 = value1, column2 = value2, ...
WHERE condition;

UPDATE关键字用于指定将要更新的表,table_name表示要修改的表的名称。SET关键字指示将更新列并为其分配新值。在SET子句中,column1 = value1, column2 = value2… 指定要更新的列及其相应的新值。最后,WHERE子句是可选的,允许指定行必须满足的条件以进行更新。它根据指定的条件过滤行。

为了在实际中测试UPDATE语句,假设我们在书名中有一个拼写错误:我们想要将“Learning React”更正为“Learning React Fundamentals”。查看books表,我们可以看到Learning React的book_id = 2。您可以参考示例3-29中的代码来了解如何执行此更新。

示例3-29. 更新books表

UPDATE books
SET book_title = 'Learning React Fundamentals'
WHERE book_id = 2;

就是这样。如果再次查看books表的数据,您将看到新的名称(图3-14)。

使用SQL和dbt的分析工程——用于分析的SQL

使用DELETE删除数据

DELETE命令提供了根据指定的条件选择性删除某些记录或删除表中所有记录的能力。DELETE在数据维护中起着关键作用,允许用户通过删除不必要或过时的记录,有效地管理和清理表的内容。该命令确保了数据的完整性,并通过消除冗余或无关的信息来优化数据库。示例3-30显示了此命令的语法。

示例3-30. DELETE语句的语法

DELETE FROM table_name
WHERE condition;

DELETE FROM部分指示将删除数据的具体表,table_name指示表的名称。可选的WHERE子句通过允许用户定义必须满足的条件来执行关键功能,以便删除行。通过使用此子句,可以基于特定条件过滤行。如果不使用WHERE子句,将删除表中的所有行。最后,condition指的是行必须满足的特定条件,以符合删除的资格。

为了在实际中应用此命令,让我们想象一下我们将不会发布任何来自计算机科学类别的书籍。通过查看category_id,我们可以看到它是数字6。现在让我们执行示例3-31,看看会发生什么。

示例3-31. 从category表中删除一个类别

DELETE FROM Category
WHERE category_id = 6;

如果一切顺利,您应该能够选择category表,并看到我们不再有计算机科学类别,如图3-15所示。

使用SQL和dbt的分析工程——用于分析的SQL

最后,您还可以使用另一种数据管理技术,称为软删除,来“删除”数据。这种技术不是永久性地擦除记录,而是在数据库中设置一个标志或属性,指示该记录应被视为已删除。这样可以保留历史数据,确保在需要时可以轻松恢复,并通过保持更改的审计跟踪来支持合规性。

存储查询作为视图

视图是数据库中由查询定义的虚拟表。它类似于普通表,由命名列和数据行组成。然而,与表不同,视图在数据库中不会物理存储数据值。相反,当访问视图时,它会动态地从其查询中引用的表中检索数据。在示例3-32中,我们看到了创建视图的通用语法。

示例3-32. VIEW语法

CREATE VIEW view_name AS
SELECT column1, column2, ...
FROM table_name
WHERE condition;

使用我们的OReillyBooks数据库,示例3-33创建了一个用于分析每位作者创建的书籍数量的视图。

示例3-33. 用于books数据库的视图

CREATE VIEW author_book_count AS
SELECT authors.author_id,
authors.author_name,
COUNT(books.book_id) AS book_count
FROM authors
LEFT JOIN books ON authors.author_id = books.author_id
GROUP BY authors.author_id, authors.author_name;

然后,我们可以查询author_book_count视图以分析每位作者创建的书籍数量;见示例3-34。

示例3-34. 查询books数据库中的视图

SELECT * FROM author_book_count;

视图的主要目的之一是作为底层表的过滤器。定义视图的查询可以涉及来自同一数据库或不同数据库的一个或多个表或其他视图。事实上,可以创建视图来 consoli,从多个异构来源整合数据,允许您在组织中的不同服务器上合并存储特定区域数据。

视图通常用于简化和定制每个用户对数据库的感知。通过定义视图,您可以向不同的用户提供数据的专注和定制视图,隐藏不必要的细节,并提供更直观的界面。此外,视图可以作为安全机制,允许用户通过视图访问数据,而无需直接访问底层基本表。这提供了一个额外的控制层,并确保用户只看到他们被授权查看的数据。

在示例3-35中,我们基于books表创建了renamed_books视图。我们在SELECT语句中使用列别名来将列重命名为某个更熟悉于特定用户的内容,而不改变表结构。我们甚至可以在相同数据上创建不同的视图,具有不同的命名约定,具体取决于受众。

示例3-35. 重命名列的视图

CREATE VIEW renamed_books AS
SELECT
id AS BookID,
title AS BookTitle,
rating AS BookRating
FROM books;

此外,当表的架构发生变化时,视图也是很有帮助的。您可以创建一个视图来模拟旧表结构,而不是修改现有的查询和应用程序,为访问数据提供向后兼容的接口。通过这种方式,您可以在更改底层数据模型的同时保持兼容性。

虽然视图提供了许多优势,但它们也具有一定的限制和潜在的危险。一个限制是依赖于底层表结构,我们先前强调过这是一个好处;然而,它也是一个诅咒。如果基础表结构发生更改,则必须相应地更新视图定义,这可能会导致维护开销增加。此外,视图可能会影响查询性能,特别是对于涉及多个表或复杂计算的复杂视图。

为了避免不必要的开销,持续优化视图查询并学会有效使用执行计划以阻止低效率是至关重要的。另一个危险是可能创建过于复杂或低效的视图,导致性能差和难以随时间推移维护或修改视图。此外,视图可以通过限制对特定列或行的访问来提供数据安全的错觉。然而,它们并不提供绝对的安全性,如果未经授权的用户获得对视图的访问权限,仍然可以访问底层数据。为确保数据保护,我们必须实施适当的数据库级安全措施。最后,如果没有得到适当维护,视图可能导致潜在的数据完整性问题,因为它们可能不强制执行约束或引用完整性,就像物理表一样。

总体而言,虽然视图提供了有价值的功能,但我们应该理解并尽量减少它们的限制和潜在风险,以确保其有效而安全的使用。

在示例3-36中,我们演示了由于连接数众多以及来自不同表的各种列的包含,视图的复杂性增加,使得一眼难以阅读和理解。解决这个问题的一个有趣方法是通过使用CTE,我们将在下一节中描述。

示例3-36. 复杂视图

CREATE VIEW complex_books_view AS
SELECT
b.book_id, 
b.book_title,
b.author_id,
b.rating,
b.publication_year,
a.author_id,
a.author_name,
c.category_id,
c.category_name
FROM books b
INNER JOIN authors a ON a.author_id = b.author_id
LEFT JOIN bookCategory bc ON bc.book_id = b.book_id
LEFT JOIN category c ON c.category_id = bc.category_id;

公共表达式(CTE)

许多数据分析师和开发人员都面临理解复杂 SQL 查询的挑战。在处理复杂的业务逻辑和多个上游依赖关系时,很常见的情况是难以理解特定查询组件的目的和依赖关系。加之意外的查询结果可能会让分析人员不确定查询的哪一部分导致了差异,这更增添了困扰。在这种情况下,公共表达式(CTE)提供了一个宝贵的解决方案。

CTE 为简化复杂查询和提高查询可维护性提供了一个强大的工具。作为临时结果集,CTE 通过将复杂的查询分解为可管理的块来增强 SQL 代码的可读性。

示例3-37展示了在 SQL 中创建 CTE 的通用语法。尽管看起来复杂,但它遵循简单的模式。

示例3-37. CTE 语法

WITH cte_name (column1, column2, ..., columnN) AS (
-- 查询定义在这里
)
SELECT column1, column2, ..., columnN
FROM cte_name
-- 额外的查询操作在这里

使用 WITH 关键字声明 CTE,并给表达式一个名称。如果需要,还可以指定列,或使用 * 字符。

在 AS 关键字之后通过编写定义 CTE 的查询来定义 CTE 查询。这个查询可以根据需要是简单或复杂的,包括过滤、连接、聚合或任何其他 SQL 操作。

在后续查询中使用 CTE,通过名称引用 CTE,就好像它是一个实际的表一样。您可以从 CTE 中选择列,或对 CTE 的数据执行其他操作。

添加更多的查询操作——在查询中串联 CTE。这一步是可选的。我们可以包含额外的查询操作,如过滤、排序、分组或连接,以进一步操作从 CTE 检索的数据。

示例3-38使用 books 表作为参考创建了一个 CTE。

示例3-38. 一个简单的 CTE

WITH popular_books AS (
SELECT title,
author,
rating
FROM books
WHERE rating >= 4.5
)
SELECT title,
author
FROM popular_books
ORDER BY rating DESC;

与派生表和数据库视图类似,CTE 提供了几个优势,使得查询编写和维护更加轻松。通过将复杂查询拆分为较小的可重用块,CTE 增强了代码的可读性,并简化了整体查询结构。让我们来比较使用 CTE 与仅使用子查询的区别。为此练习,我们使用了一个包含所有图书销售的虚构销售表,如示例3-39所示。该表通过 book_id 主键与 books 表相连接。

示例3-39. 没有 CTE 的查询

SELECT pb.book_id,
pb.title,
pb.author,
s.total_sales
FROM (
SELECT book_id,
title,
author
FROM books
WHERE rating >= 4.6
) AS pb
JOIN sales s ON pb.book_id = s.book_id
WHERE s.year = 2022 ORDER BY s.total_sales DESC
LIMIT 5;

此代码使用子查询而不是 CTE 来获取 2022 年销售排名前五的畅销书。现在,让我们使用 CTE 看看在示例3-40中可读性如何提高。

示例3-40. 使用 CTE 的查询

WITH popular_books AS (
SELECT book_id,
title,
author
FROM books
WHERE rating >= 4.6
),
best_sellers AS (
SELECT pb.book_id,
pb.title,
pb.author,
s.total_sales
FROM popular_books pb
JOIN sales s ON pb.book_id = s.book_id
WHERE s.year = 2022
ORDER BY s.total_sales DESC
LIMIT 5
)
SELECT *
FROM best_sellers;

我们创建了两个层次的 CTE。popular_books 是第一个 CTE,它从 books 表中选择 book_id、title 和 author 列,过滤评级为 4.6 或更高的图书。请注意,这个 CTE 专注于一个明确的责任:获取评价最高的图书。

然后有了 best_sellers,第二个建立在第一个 CTE 基础之上。它从 popular_books 中选择 book_id、title、author 和 total_sales 列,并根据 book_id 列与 sales 表进行连接。此外,它筛选出发生在 2022 年的销售,按总销售额降序排序,并将输出限制为销售排名前五的图书。同样,这个 CTE 关注另一个明确的责任:获取销售排名前五的畅销书,但仅限于预先选择评级为 4.6 的图书。

最后,主查询从 best_sellers 中选择所有列,并检索组合的结果。我们可以在这个主查询上应用额外的聚合或过滤,但最好的做法是保持代码简单,只专注于选择最终分析所需的属性。

CTE 的一个常见用例是在单个查询中多次引用派生表。CTE 通过允许一次定义派生表并多次引用,消除了冗余代码的需要。这提高了查询的清晰度,并降低了由于代码重复而产生错误的机会。为了看到它的实际应用,让我们看一下示例3-41,其中我们将继续使用我们虚构的销售表。

示例3-41. 使用 CTE 和派生表的查询

WITH high_ratings AS (
SELECT book_id,
title,
rating
FROM books
WHERE rating >= 4.5
),
high_sales AS (
SELECT book_id,
count(book_id) AS nbr_sales
FROM sales
GROUP BY book_id
)
SELECT hr.title,
hr.rating,
hs.sales
FROM high_ratings hr
JOIN high_sales hs ON hr.book_id = hs.book_id;

正如我们所看到的,在这种情况下使用 CTE,通过一次定义派生表(high_ratings 和 high_sales),我们消除了冗余代码的需要。通过这种策略,我们可以在主查询或任何后续 CTE 中多次引用这些表。

另一种 CTE 突显优势的情况是作为创建永久数据库视图的替代方案。有时创建视图可能并非必要或可行。在这种情况下,CTE 可以用作临时和动态的替代,通过允许在单个查询的范围内定义和引用结果集,提供灵活性和简便性。我们可以在示例3-42 中看到,在这种情况下使用 CTE,我们避免了创建永久数据库视图的需要。

示例3-42. 使用 CTE 避免创建永久视图

WITH filtered_books AS (
SELECT title,
author
FROM books
WHERE rating > 4.0
) SELECT *
FROM filtered_books;

当相同的计算必须在查询组件之间执行时,CTE 也非常有用。与在多个地方重复计算不同,CTE 允许一次定义计算并根据需要重用。这促进了代码的可重用性,减少了维护工作,提高了查询性能。让我们从示例3-43 开始。

示例3-43. 使用 CTE 促进代码的可重用性

WITH total_sales AS (
SELECT customer_id,
SUM(sales_amount) AS total_amount
FROM sales
GROUP BY customer_id
)
SELECT ts.customer_id,
ts.total_amount,
avg(total_amount) AS avg_amount
FROM total_sales ts;

我们可以看到,通过使用 total_sales CTE,总销售额的计算在 CTE 中只定义了一次,并在主查询中用于计算平均值,展示了第一个聚合的可重用性用于另一个聚合。

总之,CTE 允许我们通过将复杂问题拆分为更小、更易处理的部分来解决复杂问题。通过利用 CTE,我们可以更模块化和可读地组织和构造查询。它们为我们提供了一种解决复杂问题的方式,允许我们定义中间结果集并在单个查询中多次引用它们。这消除了冗余代码的需要,促进了代码的可重用性,减少了维护工作,降低了由于代码重复而产生错误的机会。

窗口函数

窗口函数是在分析数据集的分区或窗口时提高效率、降低查询复杂性的有用工具。它们提供了一种替代更复杂的 SQL 概念(例如派生查询)的方法,使得执行高级分析操作变得更容易。

窗口函数的一个常见用例是在给定窗口内对结果进行排名,这允许对每个组进行排名或根据特定标准创建相对排名。此外,窗口函数允许访问同一窗口内另一行的数据,这对于生成一段时间内的报告或比较相邻行之间的数据非常有用。

同时,窗口函数简化了在给定窗口内进行聚合的计算,使得如累积总计等计算更加容易。使用窗口函数使查询更加高效、简化,并且更有意义,使分析师和数据科学家能够在不使用复杂子查询或过程逻辑的情况下执行对数据分区的复杂分析。最终,窗口函数增强了SQL的分析能力,并为数据分析提供了一种多功能的工具。

更切实地理解窗口函数的方式是将其视为在与当前行相关的一组表行上执行的计算。它类似于聚合函数,但不会将行分组为单个输出行。相反,每行保留其单独的身份。窗口函数可以访问查询结果中不仅仅是当前行的数据。

窗口函数的语法,如示例3-44所示,包含几个组件。首先,我们使用SELECT语句指定要包含在结果集中的列。这些列可以是表中可用列的任意组合。接下来,我们选择要使用的窗口函数。标准窗口函数包括SUM()、COUNT()、ROW_NUMBER()、RANK()、LEAD()、LAG()等等。我们可以使用这些函数对特定列或一组列执行计算或应用聚合操作。

示例3-44. 窗口函数语法

SELECT column1,
       column2,
       ...,
       window_function() OVER (PARTITION BY column1,
                               column2,
                               ... ORDER BY column3, column4, ...)
FROM table_name;

要定义窗口函数计算的窗口框架,使用OVER子句。在OVER子句内部,有两个主要组件:PARTITION BY和ORDER BY。

PARTITION BY子句根据一个或多个列将行分成分区。然后,窗口函数分别应用于每个分区。当我们希望在表内的不同数据组上执行计算时,这是非常有用的。

ORDER BY子句允许您指定一个或多个列,以确定每个分区内的顺序。基于此顺序应用窗口函数。它有助于定义窗口函数将处理的数据的逻辑顺序或顺序。在OVER子句内结合PARTITION BY和ORDER BY子句允许我们精确控制窗口函数如何作用于数据,使我们能够在不改变整个结果集的情况下对表中的特定窗口或子集的行执行计算或应用聚合函数。

窗口函数的一个实际示例是计算累计总计。在给定的查询中,running_count列显示了基于图书出版年份的顺序计数。窗口函数ROW_NUMBER() OVER (ORDER BY publication_year)为每本书分配一个行号,按出版年份排序。这段代码在示例3-45中,查询输出在图3-16中显示。

示例3-45. 窗口函数示例

SELECT book_id,
       book_title,
       publication_year,
       ROW_NUMBER() OVER (ORDER BY publication_year) AS running_count
FROM books;

使用SQL和dbt的分析工程——用于分析的SQL

使用窗口函数,您还可以使用像COUNT()和AVG()这样的聚合函数,这些函数在“使用GROUP BY进行数据聚合”第87页有介绍。这些函数可以类似于常规聚合,但它们作用于指定的窗口。

窗口函数提供了额外的功能,例如ROW_NUMBER()、RANK()和DENSE_RANK()用于对行进行编号和排名,NTILE()用于确定百分位数或四分位数,以及LAG()和LEAD()用于访问先前或后续行的值。

使用SQL和dbt的分析工程——用于分析的SQL

表3-4总结了多种类型的窗口函数。

为了深入了解每种函数的作用,我们将以publication_year列为基础进行实验,并尝试使用一系列函数。

在第一个示例中,我们想要按升序排列的顺序对最新到最旧的图书进行排名。让我们看一下Example 3-46中的片段。

Example 3-46. 窗口函数—RANK()

SELECT book_id,
       book_title,
       publication_year,
       RANK() OVER (ORDER BY publication_year) AS rank
FROM books;

在使用RANK()函数时,一个重要的考虑因素是它根据指定的条件为窗口内的每一行分配一个唯一的排名,但如果多行共享相同的值并被分配相同的排名,后续的排名将被跳过。例如,如果两本书的publication_year相同,则下一个排名将增加与具有相同排名的行数相同的数量。如果不想要重复的排名,即不同的行共享相同的排名,您可能希望改用ROW_NUMBER()。

在Example 3-47中,我们想要按publication_year对数据进行分桶。

Example 3-47. 窗口函数—NTILE()

SELECT book_id,
       book_title,
       publication_year,
       NTILE(3) OVER (ORDER BY publication_year) AS running_ntile
FROM books;

NTILE()通常用于在指定数量的组中均匀分布行,或者当您需要为进一步的分析或处理划分数据时。这有助于诸如数据分割、百分位数计算或创建等大小样本的任务。

最后,我们想知道先前发布的书籍的publication_year。为此,我们使用LAG()函数,如Example 3-48中所示。

Example 3-48. 窗口函数—LAG()

SELECT book_id,
       book_title,
       publication_year,
       LAG(publication_year) OVER (ORDER BY publication_year) AS previous_year
FROM books;

SQL中的LAG()函数允许您在窗口框架内访问先前行的数据。它基于OVER子句中指定的排序检索指定列在前一行的值。

SQL用于分布式数据处理

随着企业迈向云端,它们面临着一个共同的挑战。它们现有的关系数据库,作为关键应用程序的基础,无法充分发挥云的潜力,并且难以有效扩展。很明显,数据库本身正在成为一个瓶颈,阻碍了转型的速度和效率。因此,组织正在寻找一种解决方案,将已被证明的关系数据存储(如Oracle、SQL Server、Postgres和MySQL)的可靠性与云的可扩展性和全球范围相结合。

为了满足这些需求,一些公司已经转向NoSQL数据库。尽管这些替代方案通常满足可扩展性的要求,但它们往往不适用于事务性数据库。这种限制的原因在于它们的设计,因为它们最初并非从根本上设计为提供真正的一致性。尽管特定的NoSQL解决方案最近引入了处理某些类型挑战的先进技术,但它们受到各种警告,最终无法提供银行或医院等关键工作负载所需的隔离级别。

鉴于传统关系数据库和NoSQL存储的缺点,公司转向一种被称为分布式SQL的有前途的解决方案。这种创新的方法在多个物理节点上部署单个逻辑数据库,可以在单个数据中心或根据需要分布在多个数据中心。通过利用分布式体系结构的强大功能,分布式SQL将弹性可扩展性与坚韧不拔的弹性相结合。

分布式SQL的关键优势之一是其无缝扩展的能力,以满足现代云环境不断发展的需求。随着数据量的增长和用户需求的增加,组织可以轻松地向分布式部署中添加额外的节点,使数据库能够水平扩展。这种弹性扩展确保即使在繁重的工作负载下,性能仍然保持最佳,并消除了传统关系数据库经常面临的限制。

与此同时,分布式SQL提供了无与伦比的弹性。由于数据分布在多个节点上,它天生具有容错性。如果一个节点失败或变得不可用,系统可以自动将查询转发到其余健康的节点,确保对关键数据的不间断访问。这种强大的弹性显著降低了停机和数据丢失的风险,提高了数据库的整体可靠性。其分布式特性还支持全球范围和数据可用性。组织可以在不同的地理区域部署节点,以战略方式将它们靠近最终用户,减少延迟。这种地理分布的方法确保数据可以从全球任何地方快速访问,促进高效的数据传递,使组织能够服务于全球用户群。

本书的重点不在于实际的分布式处理引擎,也不在于它们的工作原理;相反,我们仅仅涉及它们为我们提供的接口,以便进行交互。它们中的大多数最终都会公开API或SDK。然而,一些更侧重于数据分析的框架使用SQL作为接口语言。实际上,分布式处理和SQL已经成为一种强大的组合,SQL作为一种便利和熟悉的接口,用于利用分布式计算能力。

Spark、Hadoop和Dask等分布式处理框架提供了在多台机器或集群上处理大规模数据的基础设施。这些框架分发工作负载并并行计算,实现更快速、更高效的数据处理。另一方面,SQL提供了一种声明性和直观的方式来表达数据操作。用户可以利用他们的SQL技能,通过将SQL作为分布式处理的接口来利用分布式计算框架的能力。这种方法允许无缝扩展、高效的数据处理,并能够处理庞大数据集上的复杂分析任务,同时使用熟悉的SQL语法。

这种组合使用户能够以简单而高效的方式执行高级数据分析和处理任务。这种强大组合的示例包括DuckDB、dbt本身,甚至是FugueSQL。这些接口充当分布式计算引擎之上的一层,允许用户编写SQL查询,并利用他们对SQL语法和语义的熟悉性。DuckDB专注于通过利用分布式计算的强大功能,实现SQL查询的高效和可扩展的执行。它允许用户使用SQL制定其分析和数据处理工作流程,而底层的分布式处理引擎负责在多台机器的多个集群上执行并行操作。

然而,尽管存在这些SQL接口,它们经常与Python代码一起使用。即使在Spark文档中,Python代码仍然是必需的,用于各种任务,如数据转换、DataFrame加载和执行SQL查询后的后处理。对Python代码的依赖源于标准SQL缺乏表达在分布式计算环境中用户通常执行的许多操作的语法结构。因此,仅使用SQL通常不足以表达全面的端到端工作流程。

让我们通过一个例子深入探讨。假设我们需要创建一个SQL查询来了解O'Reilly作者自成立以来销售的所有单位。这将是一个直观的查询,如示例3-49所示。

-- 检索畅销的O'Reilly书籍
SELECT Title,
       UnitsSold
FROM Sales
WHERE Publisher = 'O''Reilly'
ORDER BY UnitsSold DESC
LIMIT 5

此时,SQL查询为我们提供了所需的聚合结果。然而,如果我们想执行其他数据操作或将结果集成到外部系统中,通常需要使用Python或其他编程语言。

例如,我们可以将聚合的结果与存储在一个单独数据集中的客户人口统计数据进行连接,以获得更深入的见解。这个操作通常需要编写Python代码来执行数据合并和后处理步骤。此外,如果我们打算可视化结果或将其导出到其他格式,再次需要Python代码来完成这些任务。

一个常见的用例实际上是将数据公开为API,而SQL本身不提供此类功能。示例3-50显示了如何将SQL与Python结合使用以实现端到端流程。

# 一个基本的FastAPI应用
from fastapi import FastAPI
import duckdb

app = FastAPI()

@app.get("/top_books")
def get_top_books():
    # 建立到DuckDB数据库的连接
    conn = duckdb.connect()

    # 执行SQL查询
    query = '''
    SELECT Title, UnitsSold
    FROM sales
    WHERE Publisher = "O'Reilly"
    ORDER BY UnitsSold DESC
    LIMIT 5
    '''
    result = conn.execute(query)

    # 将查询结果转换为字典列表
    books = []
    for row in result:
        book = {
            "title": row[0],
            "units_sold": row[1]
        }
        books.append(book)

    # 以JSON格式返回结果
    return {"top_books": books}

我们已经开发了一个FastAPI应用程序,并设置了一个可以通过/top_books路由访问的单一端点GET。简单来说,端点是我们可以用来从应用程序中检索信息的特定网络地址(URL)。当有人在他们的Web浏览器中或通过应用程序访问此URL时,它将触发我们定义的特定功能get_top_books的执行。此功能包含有关当有人从/top_books端点检索信息时要执行的操作的说明。本质上,这就好像我们有一个特定的按钮,当按下时,会导致我们的应用程序执行特定的操作,例如提供畅销书的列表。

在函数内部,我们通过使用duckdb.connect()建立了与DuckDB数据库的连接。然后使用连接对象上的execute()方法执行SQL查询。查询从sales表中选择标题和销售单位,按销售单位降序排序,并限制为前五本书。

然后,查询结果被转换为字典列表;每个字典代表一本书及其标题和销售单位。最后,将结果作为JSON返回,通过将其包装在具有键top_books的字典中。

通过充分利用这两种语言,我们可以通过友好的 SQL 接口创建和操作数据,并通过出色的 FastAPI 框架将其公开为 API。在下一节中,我们将探讨三个知名的 Python 框架,它们通过类似 SQL 的接口抽象访问分布式数据处理:DuckDB、FugueSQL 和 Polars。

使用 DuckDB

进行数据操作 在涉及数据处理库时,大多数数据科学家对于 pandas 都非常熟悉,这是 Python 中主要的数据处理库。Pandas 以其简单性、多功能性和管理多种数据格式和大小的能力而闻名。它为数据操作提供了直观的用户界面。熟悉 SQL 的人赞赏其强大的功能,使用户能够使用简洁的语法执行复杂的数据转换。然而,在某些情况下,必须在执行速度与工具的易用性或表达能力之间进行权衡。当处理超过内存限制或需要复杂数据处理操作的大型数据集时,这个困境变得尤为困难。

在这种情况下,与其使用 pandas,不如使用 SQL 可能是一个更好的解决方案。这就是 DuckDB 发挥作用的地方。DuckDB通过提供一个快速高效的 SQL 查询执行引擎,能够处理大型数据集上的复杂查询,结合了 pandas 和 SQL 的优势。它与 pandas DataFrames 无缝集成,并允许在 DataFrames 上直接执行查询,无需频繁的数据传输。使用 DuckDB,数据科学家可以在使用 pandas 的同时充分发挥 SQL 的威力,平衡了性能和易用性。

此外,我们看到一些公司正在趋势中决定将 dbt 与 DuckDB 结合起来,以取代 Spark 作为数据处理引擎。当然,这必须根据具体情况来判断,但它确实为分析师提供了支持更复杂的数据转换的机会,可以在数据管道中以临时或自动化的方式运行。

安装 DuckDB

DuckDB 是一个非常轻量级的数据库引擎,可以在主机进程中运行,不需要外部依赖。安装非常简单,只需几个简单的步骤。

要安装 DuckDB,我们有几个选项,具体取决于操作系统和我们想要进行的安装类型。现在,让我们看看如何使用 pip 包管理器安装 DuckDB,如示例 3-51 所示。

示例 3-51. 安装 DuckDB

pip install duckdb

这就是全部。现在我们可以像使用任何其他库一样在 Python 中使用 DuckDB。示例 3-52 展示了如何轻松地将 pandas DataFrame 加载到 DuckDB 中,对数据进行操作,并将结果存储回 DataFrame 中。 示例 3-52. 使用 DuckDB

import pandas as pd
import duckdb

mydf = pd.DataFrame({'a': [1, 2, 3]})
result = duckdb.query("SELECT SUM(a) FROM mydf").to_df()

正如我们所看到的,该代码导入了 pandas 库并将其命名为 pd,还导入了 DuckDB 库。这使得代码能够访问这些库提供的功能。接下来,创建了一个名为 mydf 的 pandas DataFrame,其中包含一个名为 a 的列,包含值为 [1, 2, 3] 的三行。代码的下一行使用 DuckDB 接口执行了一个 SQL 查询。查询是 SELECT SUM(a) FROM mydf,它计算了 mydf DataFrame 中列 a 的值的总和。SQL 查询的结果存储在 result 变量中。通过在 DuckDB 查询结果上使用 to_df() 方法,将数据转换为 pandas DataFrame。这使得可以使用 pandas 中丰富的函数和方法进行进一步的数据操作或分析。

使用 DuckDB 运行 SQL 查询

现在我们已经看到了一个简单的例子,让我们更仔细地了解 DuckDB 的一些核心特性。与传统系统不同,DuckDB 直接在应用程序中运行,消除了对外部进程或客户端/服务器架构的需求。这种范式与 SQLite 的进程内模型密切相关,确保了 SQL 查询的无缝集成和高效执行。

这种方法的重要性还延伸到 OLAP 领域,这是一种技术,它在最小化对事务系统的影响的同时,对大型企业数据库进行复杂分析。与其他面向 OLAP 的数据库管理系统一样,DuckDB 通过利用其创新的矢量化查询执行引擎处理复杂的分析工作负载。其面向列的方法改进了性能和可扩展性,使其成为处理分析查询的理想选择。

DuckDB 的一个显著优势是其自包含的设计。与传统数据库不同,DuckDB 不需要您安装、更新或维护任何外部依赖项或服务器软件。这种简化的、自包含的架构简化了部署,并实现了应用程序与数据库之间的快速数据传输。其结果是一个非常响应迅捷且高效的系统。

DuckDB 的另一个有趣特性是它的技术能力得益于辛勤工作且能干的开发人员,他们确保了其稳定性和成熟性。通过对来自主要系统的数百万个查询进行严格测试,验证了 DuckDB 的性能和可靠性。它遵循 ACID 特性原则,支持二级索引,并提供强大的 SQL 功能,证明了其多功能性和适用于苛刻的分析工作负载。

DuckDB 与流行的数据分析框架(如 Python 和 R)集成,实现了无缝高效的交互式数据分析。而且,它不仅支持 Python 和 R,还为 C、C++ 和 Java 提供了 API,使其可以在多种编程语言和环境中使用。以其卓越的性能和灵活性而闻名,非常适用于高效处理和查询大量数据。对于分析师来说,使用 DuckDB 运行 SQL 查询是一项有价值的技能。分析师可以利用 DuckDB 的强大功能轻松执行复杂的 SQL 查询,并从数据中获取有价值的见解。

现在我们已经更多地了解了 DuckDB,让我们进行一步一步的练习,以阐明其中一些好处。我们将使用之前使用过的书籍分析查询。首先,导入我们需要的库,即 pandas 和 DuckDB,如示例 3-53 所示。

示例 3-53. 在 DuckDB 中导入库

import duckdb
import pandas as pd

下一步是连接到 DuckDB 的内存数据库(示例 3-54)。

示例 3-54. 连接到 DuckDB

con = duckdb.connect()

让我们从创建一个虚构的 pandas DataFrame 开始,用 DuckDB 进行操作。执行示例 3-55 中的代码。

示例 3-55. 加载数据文件

import pandas as pd
data = [
    {'Title': 'Python for Data Analysis', 'Author': 'Wes McKinney', 'Publisher': "O'Reilly", 'Price': 39.99, 'UnitsSold': 1000},
    {'Title': 'Hands-On Machine Learning', 'Author': 'Aurélien Géron', 'Publisher': "O'Reilly", 'Price': 49.99, 'UnitsSold': 800},
    {'Title': 'Deep Learning', 'Author': 'Ian Goodfellow', 'Publisher': "O'Reilly", 'Price': 59.99, 'UnitsSold': 1200},
    {'Title': 'Data Science from Scratch', 'Author': 'Joel Grus', 'Publisher': "O'Reilly", 'Price': 29.99, 'UnitsSold': 600}
]
df = pd.DataFrame(data)

现在在这里,我们将 DuckDB 引入我们的代码。具体而言,我们从 DataFrame 创建了一个 DuckDB 表。通过使用连接并给它一个名称(在这种情况下是 'sales'),如示例 3-56 所示。这使我们能够使用 SQL 查询和操作数据。

示例 3-56. 创建 DuckDB 表

con.register('sales', df)

有了我们可以查询的表,我们现在可以执行任何需要的分析任务。例如,我们可以计算 O'Reilly 图书的总收入,如示例 3-57 所示。

示例 3-57. 应用分析查询

query_total_revenue = """
SELECT SUM(Price * UnitsSold) AS total_revenue
FROM sales
WHERE Publisher = "O'Reilly"
"""
total_revenue = con.execute(query_total_revenue).fetchall()[0][0]

如果我们对获取结果不感兴趣,而是希望将执行结果存储为一个 DataFrame,我们可以在执行后直接调用 duckdb 的 df() 函数。示例 3-58 创建了一个名为 df_total_revenue 的 DataFrame,我们可以使用 DuckDB 的 SQL 接口和 pandas 进行操作。

示例 3-58. 调用 df() 函数

query_total_revenue = """
SELECT SUM(price * unitsSold) AS total_revenue
FROM sales
WHERE publisher = "O'Reilly"
"""
df_total_revenue = con.execute(query_total_revenue).df()

最后但同样重要的是,我们可以使用 Python 中的任何可用数据可视化库绘制结果,如示例 3-59 所示。

示例 3-59. 数据可视化

# 创建条形图
plt.bar("O'Reilly", total_revenue)
# 设置图表标题和轴标签
plt.title("O'Reilly 图书总收入")
plt.xlabel("出版商")
plt.ylabel("总收入")

回到 pandas,它提供了一个 pandas.read_sql 命令,允许在现有数据库连接上执行 SQL 查询,然后加载到 pandas DataFrame 中。虽然这种方法适用于轻量级操作,但它不适用于密集的数据处理任务。传统的关系数据库管理系统(如 Postgres 和 MySQL)按顺序处理行,导致长时间执行和显著的 CPU 开销。另一方面,DuckDB 是专为在线分析处理而设计的,采用了列矢量化的方法。这个决策使 DuckDB 能够有效地并行处理磁盘 I/O 和查询执行,从而实现显著的性能提升。

在内部,DuckDB 使用 Postgres SQL 解析器,并与 Postgres 具有完全兼容性的 SQL 函数。这使用您熟悉的 SQL 函数,同时利用 DuckDB 的高效列处理。由于其专注于性能和效率,DuckDB 对于运行 SQL 查询和资源密集型的数据处理任务是一个引人注目的解决方案,尤其是与传统的关系数据库管理系统相比。

使用 Polars 进行数据操作

与 DuckDB 一样,Polars 也专注于解决在处理大型数据集时 pandas 的低性能和低效性问题。Polars 是一个高性能的 DataFrame 库,完全用 Rust 编写,其中一个关键优势是它不使用 DataFrame 的索引。与 pandas 不同,pandas 依赖于可能经常是多余的索引,Polars 消除了对索引的需求,简化了 DataFrame 操作,使其更直观和高效。

此外,Polars 使用 Apache Arrow 数组来进行内部数据表示。这与 pandas 使用 NumPy 数组(pandas 2.0 可能会改变这一点)形成对比。使用 Arrow 数组在加载时间、内存使用和计算方面提供了显著的优势。Polars 利用这种高效的数据表示轻松处理大型数据集,并更高效地执行计算。

Polars 的另一个优势是其对并行操作的支持。Polars 用 Rust 编写,这是一种以性能和并发性为重点的语言,可以利用多线程并行运行多个操作。这种增强的并行能力使数据处理任务更快速、更可扩展。最后,它还引入了一种强大的优化技术,称为惰性评估。在执行 Polars 中的查询时,库会检查和优化查询,并寻找加速执行或减少内存使用的机会。这个优化过程提高了查询的整体性能,增强了数据处理的效率。相比之下,pandas 只支持急切评估,其中表达式一经遇到就立即被评估。

对于分析工程师来说,使用 Polars 进行数据操作具有很大的价值,因为它具有独特的能力。Polars 的设计强调性能和可扩展性,使其非常适用于高效处理大量数据。使用大型数据集的分析工程师可以从其内存高效的操作和并行处理支持中受益,从而实现更快的数据转换。与 Rust 生态系统的集成也使其成为与基于 Rust 的数据管道一起工作的分析师的有价值工具,提供兼容性和易用性。查询优化能力、先进的数据操作功能以及对多个数据源的支持使 Polars 成为工具箱的有价值的补充,使其能够以高效和灵活的方式处理复杂的数据任务。

安装 Polars

要安装 Polars,我们有几个选项,具体取决于操作系统和我们想要进行的安装类型,但让我们看一下示例 3-60,它演示了如何通过使用 pip 包管理器简单地安装 Polars 的例子。

示例 3-60. 安装 Polars

pip install polars

这将立即使 Polars 库在我们的 Python 上下文中可用。让我们通过执行示例 3-61 中的代码片段来测试它。

示例 3-61. Polars DataFrame

import polars as pl

df = pl.DataFrame(
{
'Title': ['Python Crash Course', 'Hands-On Machine Learning',
'Data Science for Business', 'Learning SQL',
'JavaScript: The Good Parts', 'Clean Code'],
'UnitsSold': [250, 180, 320, 150, 200, 280],
'Publisher': ["O'Reilly", "O'Reilly", "O'Reilly", "O'Reilly",
"O'Reilly", "O'Reilly"],
}
)

df

我们有一个包含三列的 DataFrame:Title、UnitsSold 和 Publisher。Title 列表示各种 O'Reilly 书籍的标题。UnitsSold 列指示每本书销售的数量,而 Publisher 列指定所有书籍都是由 O'Reilly 出版的。

使用 Polars,我们可以对这个 DataFrame 执行各种操作,以了解 O'Reilly 的图书销售情况。无论是计算总收入、分析按书名或作者分类的销售情况,还是识别畅销书,如示例 3-62 所示,Polars 都为数据分析提供了一个多才多艺且高效的平台。

示例 3-62. Polars DataFrame—畅销书

# 按 UnitsSold 列降序排序 DataFrame
top_selling_books = df.sort(by="UnitsSold", reverse=True)
# 获取畅销书的标题和销售数量
top_books_data = top_selling_books.select(["Title", "UnitsSold"]).limit(5).to_pandas()
print("畅销的 O'Reilly 书籍:")
print(top_books_data)

正如你所看到的,我们使用 sort 方法根据 UnitsSold 列以降序排序 DataFrame。然后,我们使用 limit 方法选择前五本书。最后,我们使用 to_pandas() 将结果的 DataFrame 转换为 pandas DataFrame,以便更容易地打印和显示。

虽然这很有趣,显示了在语法上与 pandas 的相似性,但我们确实提到了 Polars 具有将其功能暴露为 SQL 的能力。实际上,Polars 提供了多种在其框架内利用 SQL 能力的方法。

就像 pandas 一样,Polars 无缝集成了外部库,比如 DuckDB,使您能够充分利用它们的 SQL 功能。您可以从 DuckDB 或 pandas 导入数据到 Polars 中,在导入的数据上执行 SQL 查询,并将 SQL 操作与 Polars DataFrame 操作无缝结合。这种集成提供了一个全面的数据分析和操作生态系统,同时兼具 SQL 和 Polars 的优势。

在示例 3-63 中,我们使用 duckdb.connect() 创建了一个 DuckDB 连接。然后,我们创建了一个 Polars DataFrame df,其中包含了 Title、Author、Publisher、Price 和 UnitsSold 列,表示了 O'Reilly 图书的数据。通过使用 con.register(),我们将这个 DataFrame 注册为 DuckDB 中的名为 books 的表。接下来,我们使用 con.execute() 在 books 表上执行 SQL 查询,选择了 Title 和 UnitsSold 列,并使用 Publisher = "O'Reilly" 进行过滤。结果以元组列表的形式返回。我们将结果转换为一个具有指定列名的 Polars DataFrame result_df

示例 3-63. 带有 DuckDB 的 Polars DataFrame

import polars as pl
import duckdb

# 创建 DuckDB 连接
con = duckdb.connect()

# 创建一个包含 O'Reilly 图书数据的 Polars DataFrame
df = pl.DataFrame({
    'Title': ['Python for Data Analysis', 'Hands-On Machine Learning', 'Deep Learning', 'Data Science from Scratch'],
    'Author': ['Wes McKinney', 'Aurélien Géron', 'Ian Goodfellow', 'Joel Grus'],
    'Publisher': ["O'Reilly", "O'Reilly", "O'Reilly", "O'Reilly"],
    'Price': [39.99, 49.99, 59.99, 29.99],
    'UnitsSold': [1000, 800, 1200, 600]
})

# 在 DuckDB 中注册 DataFrame 作为表
con.register('books', df)

# 使用 Polars 在 DuckDB 表上执行 SQL 查询
result = con.execute("SELECT Title, UnitsSold FROM books WHERE Publisher = 'O''Reilly'")

# 将结果转换为 Polars DataFrame
result_df = pl.DataFrame(result, columns=['Title', 'UnitsSold'])

# 打印结果
print(result_df)

# 关闭 DuckDB 连接
con.close()

Polars 还提供了在不依赖外部库的情况下执行 SQL 查询的本机支持。使用 Polars,您可以直接在代码中编写 SQL 查询,利用 SQL 语法执行数据转换、聚合和过滤操作。这使您能够在 Polars 框架内充分发挥 SQL 的强大功能,从而提供了一种方便高效的处理结构化数据的方法。

在 Polars 中使用 SQL 是一个简单而直接的过程。您可以按照以下步骤在 Polars DataFrame 上执行 SQL 操作。首先,创建一个 SQL 上下文,设置执行 SQL 查询的环境。这个上下文使您能够在 Polars 框架内无缝使用 SQL,如示例 3-64 所示。

示例 3-64. 创建 SQL 上下文

# 创建包含 O'Reilly 图书数据的 Polars DataFrame
df = pl.DataFrame({
    'Title': ['Python for Data Analysis', 'Hands-On Machine Learning', 'Deep Learning', 'Data Science from Scratch'],
    'Author': ['Wes McKinney', 'Aurélien Géron', 'Ian Goodfellow', 'Joel Grus'],
    'Publisher': ["O'Reilly", "O'Reilly", "O'Reilly", "O'Reilly"],
    'Price': [39.99, 49.99, 59.99, 29.99],
    'UnitsSold': [1000, 800, 1200, 600]
})

# 创建 SQL 上下文
sql = pl.SQLContext()

示例 3-65 展示了接下来的步骤:注册您想要查询的 DataFrame。

示例 3-65. 注册 DataFrame

# 在上下文中注册 DataFrame
sql.register('df', df)

通过为 DataFrame 提供一个名称,您为 SQL 查询建立了一个参考点。这个注册步骤确保 DataFrame 与一个可识别的标识符相关联。

一旦 DataFrame 被注册,您可以使用 Polars 提供的 query() 函数在其上执行 SQL 查询。这个函数以 SQL 查询作为输入,并返回一个 Polars DataFrame 作为结果。这个 DataFrame 包含符合 SQL 查询中指定条件的数据。让我们看一下示例 3-66。

示例 3-66. 运行分析查询

# 运行您的 SQL 查询
result_df = sql.execute(
"""
select
*
from df
where Title = 'Python for Data Analysis'
"""
).collect()

通过将 SQL 与 Polars 整合,具有深厚 SQL 知识的数据专业人员可以轻松利用 Polars 的强大和高效功能。他们可以利用现有的 SQL 技能,直接应用于 Polars 框架内的数据分析和操作任务。这种无缝集成使用户能够在使用他们熟悉的 SQL 语法的同时,利用库的优化查询执行引擎。

使用 FugueSQL 进行数据操作

Fugue 是一个强大的统一界面,用于在流行的分布式框架(如 Spark、Dask 和 Ray)上无缝运行 Python、pandas 和 SQL 代码。通过 Fugue,用户可以在最小的代码更改下充分发挥这些分布式系统的潜力。

Fugue 的主要用例围绕着将现有的 Python 和 pandas 代码并行化和扩展到流行的分布式框架上运行。通过将代码无缝过渡到 Spark、Dask 或 Ray,用户可以利用这些系统的可扩展性和性能优势,而无需重写大量代码。

与我们的讨论相关的是,Fugue 提供了一个独特的功能,称为 FugueSQL,它允许用户通过先进的 SQL 接口在 pandas、Spark 和 Dask DataFrames 上定义端到端的工作流。它结合了熟悉的 SQL 语法和调用 Python 代码的能力。这为用户提供了一个强大的工具,用于简化和自动化其数据处理工作流程。

FugueSQL 提供了多种好处,可以在多种场景中利用,包括作为 Fugue 项目整体目标的一部分进行并行代码执行,或在单台机器上独立查询。无论我们是在使用分布式系统还是在本地机器上进行数据分析,它都允许我们高效地查询我们的 DataFrames。

安装 Fugue 和 FugueSQL 我们有多种选项可以安装 Fugue,取决于我们的操作系统和安装类型。示例 3-67 使用 pip install。

示例 3-67. 安装 Fugue

pip install fugue

Fugue 提供了各种安装额外功能,增强其功能并支持不同的执行引擎和数据处理库。这些安装额外包括以下内容:

  • sql: 这个额外的功能启用了对 FugueSQL 的支持。虽然 Fugue 的非 SQL 功能在没有这个额外功能的情况下仍然可以工作,但如果您打算使用 FugueSQL,安装这个是必要的。要实现这一点,请执行示例 3-68 中的代码片段。

示例 3-68. 安装 FugueSQL

pip install "fugue[sql]"
  • spark: 安装这个额外功能会为 Fugue 添加对 Spark 作为 ExecutionEngine 的支持。通过此额外功能,用户可以利用 Spark 的功能来执行其 Fugue 工作流。要添加此额外功能,请运行示例 3-69 中的代码。

示例 3-69. 安装 FugueSpark

pip install "fugue[spark]"
  • dask: 这个额外的功能启用了对 Dask 作为 Fugue 中的 ExecutionEngine 的支持。通过安装这个额外的功能,用户可以在 Fugue 框架内利用 Dask 的分布式计算能力。
  • ray: 安装这个额外的功能会为 Fugue 添加对 Ray 作为 Fugue 中的 ExecutionEngine 的支持。通过这个额外的功能,用户可以在其 Fugue 工作流中利用 Ray 的高效任务调度和并行执行能力。
  • duckdb: 这个额外的功能启用了对 DuckDB 作为 Fugue 中的 ExecutionEngine 的支持。通过安装这个额外的功能,用户可以在 Fugue 框架内利用 DuckDB 的高速内存数据库来进行高效的查询执行。
  • polars: 安装这个额外的功能提供了对 Polars DataFrames 和使用 Polars 库的扩展的支持。通过这个额外的功能,用户可以在 Fugue 中利用 Polars 的特性和功能进行数据处理。
  • ibis: 启用这个额外的功能允许用户将 Ibis 集成到 Fugue 工作流中。Ibis 提供了一个富有表达力且强大的接口,用于处理类似 SQL 的查询,通过安装这个额外的功能,用户可以将 Ibis 的功能整合到他们的 Fugue 工作流中。
  • cpp_sql_parser: 启用这个额外的功能使用 CPP(C++)antlr 解析器用于 Fugue SQL,相比纯 Python 解析器,它提供了显著更快的解析速度。虽然对于主要的 Python 版本和平台已经提供了预构建的二进制文件,但这个额外的功能可能需要在其他平台上即时构建时使用 C++ 编译器。

实际上,我们可以在单个 pip install 命令中安装前面提到的多个额外功能。在示例 3-70 中,我们一次性安装了 duckdb、polars 和 spark 的额外功能。

示例 3-70. 安装多个 Fugue 额外功能

pip install "fugue[duckdb,spark,polars]"

另一个有趣的额外功能与笔记本有关。FugueSQL 对 Jupyter Notebooks 和 JupyterLab 都有一个笔记本扩展。此扩展提供语法高亮。我们可以运行另一个 pip install 命令来安装这个扩展(示例 3-71)。

示例 3-71. 安装笔记本扩展

pip install fugue-jupyter
fugue-jupyter install startup

第二个命令 fugue-jupyter install startup 将 Fugue 注册在 Jupyter 启动脚本中,这样每当您打开 Jupyter Notebooks 或 JupyterLab 时,它都会对您可用。

如果您已经安装了 Fugue 并使用 JupyterLab,则 %%fsql 单元格魔术已默认自动注册。这意味着您可以在 JupyterLab 环境中直接使用单元格魔术,无需任何额外步骤。然而,如果您使用的是传统的 Jupyter Notebooks,或者 %%fsql 单元格魔术未注册,您可以通过在笔记本中使用示例 3-72 中的命令来启用它。

示例 3-72. 启用笔记本扩展

from fugue_notebook import setup
setup(is_lab=True)

使用 FugueSQL 运行 SQL 查询

FugueSQL 是专门为希望使用 Python DataFrames(如 pandas、Spark 和 Dask)的 SQL 用户设计的。FugueSQL 提供了一个 SQL 接口,可以解析并在您选择的底层引擎上运行。这对于更希望专注于定义逻辑和数据转换而不是处理执行复杂性的数据科学家和分析师尤其有益。

但它也专为 SQL 爱好者的需求量身定制,为他们提供了使用 SQL 在流行的数据处理引擎(如 pandas、Spark 和 Dask)上定义端到端工作流的能力。这样,SQL 爱好者可以利用他们的 SQL 技能轻松编排复杂的数据流水线,而无需在不同的工具或语言之间切换。

Fugue 为主要使用 pandas 并希望利用 Spark 或 Dask 处理大型数据集的数据科学家提供了一个实用的解决方案。使用 Fugue,他们可以轻松地扩展其 pandas 代码,并无缝过渡到 Spark 或 Dask,以最小的努力实现分布式计算的潜力。例如,如果有人使用 FugueSQL 与 Spark,该框架将使用 SparkSQL 和 PySpark 来执行查询。尽管 FugueSQL 支持非标准的 SQL 命令,但重要的是要强调 Fugue 仍然完全与标准 SQL 语法兼容。这种兼容性确保了 SQL 用户可以轻松地切换到 Fugue 并利用他们现有的 SQL 知识和技能,无需进行主要的自定义或复杂的调整。

最后,Fugue正在证明对于经常面临代码维护问题的大数据项目的数据团队来说是一项有价值的资产。通过采用 Fugue,这些团队可以从一个统一的界面中受益,该界面简化了在分布式计算平台上执行代码的过程,确保在开发过程中始终保持一致性、效率和可维护性。

示例 3-73 展示了使用 FugueSQL 的端到端示例。

示例 3-73. FugueSQL 完整示例

import pandas as pd
from pyspark.sql import SparkSession
from fugue.api import fugue_sql_flow

data = [
    {'Title': 'Python for Data Analysis', 'Author': 'Wes McKinney', 'Publisher': "OReilly", 'Price': 39.99, 'UnitsSold': 1000},
    {'Title': 'Hands-On Machine Learning', 'Author': 'Aurélien Géron', 'Publisher': "OReilly", 'Price': 49.99, 'UnitsSold': 800},
    {'Title': 'Deep Learning', 'Author': 'Ian Goodfellow', 'Publisher': "OReilly", 'Price': 59.99, 'UnitsSold': 1200},
    {'Title': 'Data Science from Scratch', 'Author': 'Joel Grus', 'Publisher': "OReilly", 'Price': 29.99, 'UnitsSold': 600}
]

# 将数据保存为 Parquet
df = pd.DataFrame(data)
df.to_parquet("/tmp/df.parquet")

# Fugue 使用 pandas 引擎
import fugue.api as fa
query = """
LOAD "/tmp/df.parquet"
SELECT Author, COUNT(Title) AS NbBooks
GROUP BY Author
PRINT
"""
pandas_df = fa.fugue_sql(query, engine="pandas")

# Fugue 使用 Spark 引擎
query = """
LOAD "/tmp/df.parquet"
SELECT Author, COUNT(Title) AS NbBooks
GROUP BY Author
PRINT
"""
spark_df = fa.fugue_sql(query, engine="spark")

# Fugue 使用 DuckDB
query = """
df = LOAD "/tmp/df.parquet"
res = SELECT *
FROM df
WHERE Author = 'Wes McKinney'
SAVE res OVERWRITE "/tmp/df2.parquet"
"""
fa.fugue_sql(query, engine="duckdb")

import duckdb
with duckdb.connect() as conn:
    df2 = conn.execute("SELECT * FROM '/tmp/df2.parquet'").fetchdf()
print(df2.head())

此示例创建了一个 FugueSQLWorkflow 实例。我们使用 workflow.df() 方法将 pandas DataFrame df 注册为一个表。然后,我们在 workflow.run() 方法中编写 SQL 查询以对数据执行各种操作。FugueSQLWorkflow 是 Fugue 库提供的一个类,作为执行 FugueSQL 代码的入口点。它允许我们在不需要显式数据转换或处理底层执行引擎的情况下定义和执行对各种数据源的 SQL 查询,如前所述。

此示例演示了三个查询:

  • 计算 O'Reilly 书籍的总收入
  • 计算 O'Reilly 书籍的平均价格
  • 检索畅销的 O'Reilly 书籍

结果存储在 result 对象中,我们可以通过使用 first() 和 collect() 方法访问数据。

最后,我们将结果打印到控制台。请注意,在 SQL 查询中使用两个单引号 ('') 来转义出版商名称 "O'Reilly" 中的单引号,以确保正确的语法。

有人可能会想知道 FugueSQL 是否是 pandasql 的替代品或进化版本。我们会认为,虽然 pandasql 仅支持 SQLite 作为后端,但 FugueSQL 支持多个本地后端,如 pandas、DuckDB、Spark 和 SQLite。当使用 FugueSQL 与 pandas 后端时,SQL 查询直接转换为 pandas 操作,消除了数据传输的需要。同样,DuckDB 对 pandas 有很好的支持,减少了数据传输的开销。因此,pandas 和 DuckDB 都是 FugueSQL 本地数据处理的推荐后端。总的来说,FugueSQL 是一个很好的框架,可以利用 SQL 语法,具有在分布式处理和规模化数据操作方面的附加功能。

总的来说,Fugue、DuckDB 和 pandas 都是提供高效数据处理能力的强大工具。然而,无论使用哪种技术,都必须认识到良好的数据建模对于成功实现可扩展性至关重要。没有设计良好的数据模型,任何系统都将难以有效处理大规模的数据处理。

健壮的数据模型的基础确保数据被结构化、组织,并针对分析和操作进行了优化。通过了解数据实体之间的关系、定义适当的数据类型,并建立高效的索引策略,我们可以创建一个可扩展的架构,最大程度地提高性能,并在不同平台和工具之间实现无缝的数据操作。因此,尽管 Fugue、DuckDB 和 pandas 有助于实现高效的数据处理,但适当的数据建模对于实现可扩展性至关重要。这也是我们在第二章中介绍数据建模的主要原因之一。

奖励内容:使用 SQL 训练机器学习模型

这个标题可能让你感觉我们正在推动类似 SQL 的能力的极限,但实际上,由于一个非常特定的库——dask-sql,我们可以在 SQL 中使用 Python 机器学习生态系统。

Dask-sql 是一个最近开发的 SQL 查询引擎,处于实验阶段,它基于基于 Python 的 Dask 分布式库。它具有独特的能力,能够无缝集成 Python 和 SQL,使用户能够执行分布式和可伸缩的计算。这个创新性的库为利用 Python 和 SQL 在数据分析和处理中的优势提供了机会。

我们可以运行 pip install 命令来安装该扩展,如示例 3-74 所示。

示例 3-74. 安装 dask-sql

pip install dask-sql

在示例 3-75 中,我们通过 c = Context() 创建了 Context 类的一个实例。通过这个实例,我们为 SQL 查询初始化了一个新的执行上下文。这个上下文可以用来对我们的数据执行 SQL 查询,并执行诸如过滤、聚合和连接之类的操作,但它还可以应用由 Dask 提供的一种特殊类型的命令,用于训练和测试机器学习模型。

示例 3-75. 从 dask_sql 导入上下文

from dask_sql import Context
c = Context()

现在我们已经具备了加载要处理的数据集的所有工具。在示例 3-76 中,我们使用 Dask 的 read_csv() 函数读取 Iris 数据集。一旦数据加载完成,我们就可以将数据视为 Dask DataFrame 进行访问和操作。

下一步是在 dask-sql 上下文中将已加载的 Dask DataFrame(df)注册为名为 iris 的表。使用 Context 类的 create_table 方法来注册表。完成这一步后,我们可以使用 SQL 语法查询数据。

示例 3-76. 将数据加载为 Dask DataFrame 并注册为表

# 加载数据:下载 Iris 数据集
df = dd.read_csv('https://datahub.io/machine-learning/iris/r/iris.csv')
# 注册 Dask 表
c.create_table("iris", df)

有了准备好的数据,我们现在可以使用训练组件来训练机器学习模型。首先,我们使用 CREATE OR REPLACE MODEL 语句,这是 dask-sql 的一个扩展,允许在 SQL 上下文中定义和训练机器学习模型。

在这种情况下,聚类模型命名为 clustering,并使用 scikit-learn 库的 KMeans 算法创建模型。这是一种用于对数据点进行聚类的热门无监督学习算法。有趣的是,dask-sql 允许我们使用来自第三方库(如 scikit-learn)的模型类。n_clusters 参数设置为 3,表示算法应该在数据中识别三个聚类。

在示例 3-78 中,我们展示了用于模型的训练数据来自在上下文 c 中注册的 iris 表。SELECT 语句指定了用于训练的特征,包括 iris 表中的 sepallength、sepalwidth、petallength 和 petalwidth 列。

示例 3-78. 创建我们的聚类模型

# 训练:使用 sklearn.cluster.KMeans 算法创建我们的聚类模型
c.sql("""
CREATE OR REPLACE MODEL clustering WITH (
model_class = 'sklearn.cluster.KMeans',
wrap_predict = True,
n_clusters = 3
) AS (
SELECT sepallength, sepalwidth, petallength, petalwidth
FROM iris
)
""")

我们现在可以通过运行 SHOW MODELS 命令(示例 3-79)来验证我们的模型是否已经创建,这类似于传统 SQL 引擎中经常使用的 SHOW TABLES。后者显示数据库某个模式中的所有表,而前者列出了在 dask-sql 上下文中创建和可用的所有模型。

示例 3-79. 显示模型列表

# 显示已在上下文中训练和存储的模型列表
c.sql("""
SHOW MODELS
""")

另一个有趣的命令是 DESCRIBE MODEL MODEL_NAME(示例 3-80),它显示用于训练该模型的所有超参数。

示例 3-80. 获取某个模型的所有超参数

# 获取已训练模型的超参数
c.sql("""
DESCRIBE MODEL clustering
""")

在示例 3-81 中,我们展示了 dask-sql 中最引人注目的命令之一——PREDICT 命令。它使用最近创建的 clustering 模型来预测 df DataFrame 的行的聚类类别。带有 PREDICTSELECT 语句将经过训练的机器学习模型应用于来自某个表的新数据点,这是在 SQL 上下文中进行的。 在这种情况下,PREDICT 命令用于将聚类模型应用于 iris 表的前 100 行。MODEL 子句指定要使用的模型的名称,即 clustering。PREDICT 命令内部的 SELECT 语句指定要用于预测的特征,这些特征与模型训练步骤中使用的相同特征相同,如示例 3-81 所示。

示例 3-81. 进行预测

-- 预测:通过将预测应用于 df 的行来测试最近创建的模型 —
-- 在这种情况下,将每个观察分配到一个聚类
SELECT * FROM PREDICT (
MODEL clustering,
SELECT sepallength, sepalwidth, petallength, petalwidth FROM iris
LIMIT 100
)

dask-sql 的另一个有趣的功能是其实验组件。它通过使用 CREATE EXPERIMENT 语句运行实验,以尝试为聚类模型尝试不同的超参数值。 在示例 3-82 中,实验命名为 first_experiment。它使用来自 scikit-learn 的 GridSearchCV 类,这是一种用于超参数调整的常见技术。在这种情况下进行调整的超参数是聚类的数量(n_clusters),这只是为了展示该功能。tune_parameters 参数指定了要尝试的 n_clusters 超参数的值范围。在此示例中,实验将尝试三个值(2、3 和 4),表示我们期望获得的聚类数。 在机器学习项目的实际情况下,我们应该专注于选择模型的最相关超参数。这取决于问题是分类还是回归任务以及使用的算法类型。

示例 3-82. 超参数调整

-- 超参数调整:运行一个试验以尝试不同的参数
c.sql("""
CREATE EXPERIMENT first_experiment WITH (
model_class = 'sklearn.cluster.KMeans',
experiment_class = 'GridSearchCV',
tune_parameters = (n_clusters = ARRAY [2, 3, 4]),
experiment_kwargs = (n_jobs = -1),
target_column = 'target'
) AS (
SELECT sepallength, sepalwidth, petallength, petalwidth, class AS target
FROM iris
LIMIT 100
)
""")

最后但并非最不重要的是,我们有一个 EXPORT MODEL 语句,如示例 3-83 所示。在这种情况下,通过将 format 参数设置为 pickle,将模型导出为 pickle 格式。Pickle 是一种特定于 Python 的二进制序列化格式,允许保存和加载 Python 对象。 location 参数指定了导出模型文件应保存的路径和文件名。在这个例子中,模型保存在当前目录下,文件名为 clustering.pkl

示例 3-83. 导出模型为 pickle 文件

-- 导出模型:将其导出为 pickle 文件以在其他上下文中使用
c.sql("""
EXPORT MODEL clustering WITH (
format ='pickle',
location = './clustering.pkl'
)
""")

总的来说,dask-sql 是一个用于机器学习目的的强大而有前景的工具,为大型数据集上的数据操作和机器学习操作提供了 SQL 接口。使用 dask-sql,我们可以利用熟悉的 SQL 语法来查询和转换数据,以及通过使用诸如 scikit-learn 之类的流行库训练和评估机器学习模型。它允许我们注册数据表,应用于数据预处理的 SQL 查询,并在 SQL 上下文中创建和训练机器学习模型。 然而,我们必须强调,dask-sql 仍处于试验阶段,尽管它是一个对想要探索机器学习领域的 SQL 爱好者来说非常迷人的工具,但随着其成长和成熟,必须谨慎使用。

总结

在结束本章时,让我们考虑数据库和SQL的显著历程以及它们对我们过去和未来的不可否认的影响。SQL仍然是在不断发展的数据领域中可靠而坚定的组成部分,将经过验证的技术与现代分析见解相结合,从而确保一个乐观的未来。

我们的探索表明,从清晰的表结构到适应紧迫业务需求的复杂模型,SQL的重要性仍然持久不衰,数据库经历着持续的创新。

然而,值得承认的是,这些工具的有效性取决于使用它们的人的技能。对于分析工程师而言,持续的教育和灵活性至关重要。SQL、数据库管理和数据分析领域不断发展。为了取得成功,我们必须保持更新,保持好奇心,并自信地面对挑战。

随着数据领域的迅速扩展,数据工程、分析和数据科学中的角色之间的区别变得更加明显。虽然这些角色有一些重叠和融合的领域,但数据的庞大和复杂性推动了对专业技能和专业知识的需求。本章的结论提醒我们,分析工程领域既广泛又引人入胜。在每个查询和数据库中,都存在一个探索和创新的新机会,这是由于对今天数据领域的复杂性的需求而推动的。