在当今数据驱动的世界中,组织越来越依赖数据分析来获得有价值的见解并做出明智的决策。数据建模在这一过程中发挥着至关重要的作用,为构建和组织数据提供了坚实的基础,以支持有效的分析。此外,理解数据建模和规范化的概念对实现分析的全部潜力、从复杂数据集中获取可操作的见解至关重要。
数据建模涉及定义系统中数据实体的结构、关系和属性。数据建模的一个重要方面是数据的规范化。数据规范化是一种消除数据冗余并提高数据完整性的技术。它涉及将数据分解为逻辑单元并组织到单独的表中,从而减少数据重复,提高整体数据库效率。规范化确保数据以结构化和一致的方式存储,这对于准确的分析和可靠的结果至关重要。
在分析方面,数据建模为创建分析模型提供了坚实的基础。分析师可以通过了解实体和数据结构之间的关系,设计捕捉相关信息并支持所需分析目标的有效模型。换句话说,一个设计良好的数据模型使分析师能够执行复杂的查询、连接表格并聚合数据以生成有意义的见解。
理解数据建模和规范化对于实际数据分析至关重要。分析师可能难以访问和正确解释没有适当数据模型的数据,这可能导致不正确的结论和无效的决策。此外,缺乏规范化可能导致数据异常、不一致性以及难以聚合数据,从而妨碍分析过程。
在本书中,我们将SQL和dbt作为支持有效的分析工程项目的两个核心技术进行了重点介绍,这也适用于设计和实现有效的数据模型。背后的原因是,SQL赋予用户通过其强大的查询功能定义表、操作数据和检索信息的能力。其无与伦比的灵活性和多用途性使其成为构建和维护数据模型的强大工具,使用户能够表达错综复杂的关系并轻松访问特定的数据子集。
与SQL相辅相成的是dbt,在这个叙述中起着核心作用,将数据建模推向一个全新的水平。它作为构建和协调复杂数据流程的综合性框架。在这个框架中,用户可以定义转换逻辑,应用基本的业务规则,并构建可重用的模块化代码组件,即所谓的模型。值得注意的是,dbt超越了独立功能:它与版本控制系统无缝集成,使协作变得轻松,并确保数据模型保持一致性、可审计性和轻松可重现性。
SQL和dbt在数据建模中的另一个关键方面是它们对测试和文档编制的重视,尽管存在一些值得澄清的区别。在数据建模的背景下,测试涉及验证数据模型的准确性、可靠性和对业务规则的遵循。虽然值得注意的是,dbt的测试功能与软件开发中传统的单元测试有所不同,但它们有着相似的目的。与传统的单元测试不同,dbt提供了可与分析师习惯运行的验证查询相媲美的验证查询。这些验证查询检查数据质量、数据完整性和对定义规则的遵守,为模型的输出提供信心。此外,dbt在文档编制方面表现出色,为分析师和利益相关者提供了宝贵的资源。这份文档简化了理解驱动数据模型的基本逻辑和假设的过程,增强了透明度,并促进了有效的协作。
总体而言,SQL和dbt使数据专业人员能够创建强大、可扩展和易于维护的数据模型,推动深刻的分析和明智的决策。通过利用这些工具,组织可以释放其数据的全部潜力,在当今数据驱动的环境中取得创新,并获得竞争优势。将二者结合在同一数据架构和策略中为数据建模带来了显著的优势。
数据建模简介
在数据库设计的世界中,创建一个结构化和有组织的环境对于有效地存储、操作和利用数据至关重要。数据库建模通过提供表示特定现实或业务的蓝图,并支持其过程和规则,对实现这一目标发挥着重要作用。
然而,在我们深入探讨创建这一蓝图之前,我们应该专注于理解业务的细微差别。了解业务的运营、术语和流程对于创建准确而有意义的数据模型至关重要。通过通过面谈、文件分析和流程研究收集需求,我们能够深入了解业务的需求和数据要求。在这个收集过程中,我们应该注重自然的沟通方式------书面语言。通过用明确的句子表达业务事实,我们确保业务的表示准确且不受解释。将复杂的句子分解成具有主语、动词和宾语的简单结构有助于简洁地捕捉业务现实。
除了这些基本做法之外,值得注意的是该领域的专家,比如 Lawrence Corr 在他的畅销书《敏捷数据仓库设计》(DecisionOne Press)中,倡导在数据模型设计的初始阶段采用更多的技术,如白板绘图和画布。这些额外的策略可以为这一过程增添细微差别,允许更全面地探索业务需求,并确保最终的数据模型与业务的目标和复杂性紧密契合。
一旦理解阶段完成,我们就进入数据库建模的三个基本步骤:
- 概念阶段
- 逻辑阶段
- 物理阶段
这些步骤构成了创建一个强大且有组织的数据库结构的过程。让我们以图书出版商的例子来说明这个过程。
建模的概念阶段
建模的概念阶段需要进行几个基本步骤。首先,有必要明确数据库的目的和目标,澄清它需要解决的具体问题或要求。接下来是通过面谈利益相关者和主题专家来收集需求,全面了解所需的数据元素、关系和约束。随后进行实体分析和定义,涉及识别要在数据库中表示的关键对象或概念,并定义它们的属性和关系。
我们在设计数据库外观的初始草图时进行轻度规范化。通过这样做,确保了所识别的实体和关系之间的完整性,并通过围绕概念上相关的语义结构组织实体和属性来减少冗余。识别键,包括主键和外键,对于保持唯一性和在表之间建立关系至关重要。
这些数据库设计通常通过图表、文字描述或其他捕捉设计和概念的方法创建。在可视化表示数据库概念方面,最常用的工具之一是实体-关系图(ERD)。使用ERD模型创建的可视化模型作为一种图表表示,有效地描述了要建模的实体、它们之间的关系以及关系的基数。通过采用ERD模型,我们可以直观地描述数据库结构,包括实体作为主要组件、实体之间的连接或关联以及关系的数量或程度。
让我们简单地设计一个数据库的概念。想象一下,O'Reilly公司的目标是跟踪先前出版的书籍和作者,以及尚未出版的新书的发布日期。我们通过一系列面谈与出版商的经理进行了沟通,开始准确了解需要存储在数据库中的数据。主要目标是识别所涉及的实体、它们之间的关系以及每个实体的属性。请记住,这个练习是举例说明的,是故意简化的。在这个书籍管理的子宇宙中,我们识别了三个不同的实体:
-
书籍
- 代表O'Reilly出版的一本书。属性可能包括book_id、title、publication_date、ISBN、price和一个特定的category。面谈者表示在这个模型中,一本书可能只有一个category。
-
作者
- 代表为O'Reilly写书的作者。属性可能包括author_id、author_name、email和bio。
-
类别
- 代表书籍类别,可以包含诸如category_id(唯一标识符)和category_name等属性。
下一步将是识别实体之间的关系。在数据库设计中,实体之间可能存在几种类型的关系,而关系的类型可称为关系的基数。例如,在一对一关系中,我们可能会有一个与一个作者关联的Book实体,其中每本书都与一个作者相关联,反之亦然。在一对多关系中,考虑将Category实体链接到Book实体,其中每本书只能属于一个类别,但每个类别可以有多本书。相反,在多对一关系中,考虑将Publisher实体与Book实体相连接,其中同一出版商出版多本书。最后,在多对多关系中,我们可能会有一个与Reader实体相关联的Book实体,表示多个读者可以拥有多本书。继续我们的练习,我们还确定了两个明确的关系:
-
书籍-类别关系
- 建立书籍和类别之间的连接。一本书可以有一个类别,而一个类别可以有多本书。这种关系表示为一对多关系。
-
书籍-作者关系
- 建立书籍和作者之间的连接。一本书可以有多个作者,而一个作者可以写多本书。这种关系表示为多对多关系。在这种关系中,特定书籍的出版发生在这里。
在识别关系时,通常使用能够代表实体之间真实交互的关系名称是常见的。例如,我们可以称之为Classifies而不是Book-Category,因为类别对书籍进行分类,或者我们可以称之为Publishes而不是Book-Author,因为作者出版书籍。
现在我们对实体、属性和关系有了一些了解,我们可以使用ERD设计我们的数据库。通过这样做,我们可以直观地表示实体、关系和基数,如图2-1所示。
正如我们所观察到的,实体被表示为白色矩形框,代表真实世界中的对象或概念,如Book或Author。关系被表示为菱形,说明实体之间的关系。
属性被表示为阴影框,并描述实体的属性或特征。例如,可能是Name或Publish date。此外,属性可以被分类为键属性(带下划线的阴影框),用于唯一标识实体,或者非键属性(非下划线的阴影框),提供有关实体的附加信息。在设计这类图表时可能存在更多类型的属性,但我们将坚持基本原则。
ERD中的其他组件包括基数和参与约束。基数定义关系中实例的数量,通常用符号表示,例如1、M或N,分别表示一对一或一对多关系。(N表示关系的数量不确定。)
数据建模的逻辑阶段
在建模的逻辑阶段,重点是对数据进行规范化,消除冗余,提高数据完整性,并优化查询性能。结果是一个经过规范化的逻辑模型,准确反映了实体之间的关系和依赖关系。
这个阶段可以分为两个步骤。首先,Entity-Relationship 模式的重构侧重于根据特定标准优化模式。这一步与任何特定的逻辑模型都没有直接关联。第二步将优化后的 ERD 转化为具体的逻辑模型。
假设我们已经决定将 ERD 映射到关系数据库模型(这将是我们的情况)------而不是文档或图数据库------概念 ERD 练习中的每个实体都被表示为一个表。每个实体的属性成为相应表的列。主键约束指示了每个表的主键列。此外,多对多关系由单独的联接表表示,该表保存引用相应实体的外键。
通过使用关系模型将概念 ERD 练习转化为逻辑模式,我们建立了一个结构化的实体、属性和关系表示。这个逻辑模式可以成为在特定数据库管理系统(DBMS)中实现数据库的基础,同时保持独立于任何特定系统。为了有效实现这种转化,适用所有的规范化步骤,但我们想分享一个有效的算法:
- 将实体 E 转换为表 T。
- E 的名称成为 T 的名称。
- E 的主键成为 T 的主键。
- E 的简单属性成为 T 的简单属性。
在涉及关系时,我们也可以分享一些步骤:
-
N:1 关系 在表 T1 中定义一个引用表 T2 主键的外键。这建立了两个表之间的连接,表示 N:1 关系。与关系相关的属性(Attrs)被映射并包含在表 T1 中。
-
N:N 关系 创建一个特定的交叉引用表来表示关系 REL。REL 的主键被定义为表 T1 和 T2 的主键的组合,这两个主键在交叉引用表中充当外键。与关系相关的属性(Attrs)被映射并包含在交叉引用表中。
现在让我们将这些规则应用于我们之前的概念模型;请参见图2-2。
在我们的示例中,我们有一些实体,正如我们的算法建议的那样,直接映射到表。这种情况包括 Authors、Books 和 Category。
我们确定了 Books 和 Category 之间的一对多关系,其中一本书有一个类别,但一个类别可以有多本书。为了映射这种关系,我们在 books 表中创建一个外键,用于引用相应的类别。
我们还有一个 N:N 的关系。在这种情况下,我们必须创建一张新表(交叉引用表)来存储这种关系。在我们的例子中,我们创建了 Publishes 表,其主键成为相关实体(Book ID 和 Author ID)之间的复合主键。与此同时,关系的属性成为这个交叉引用表的属性。
建模的物理阶段
我们现在准备将规范化的逻辑模型转换为物理数据库设计,这一步称为物理阶段,或物理模型创建。这一步定义了存储结构、索引策略和数据类型,以确保高效的数据存储和检索。虽然逻辑模型侧重于概念表示,但物理模型处理实现细节,以确保顺畅的数据管理。
在我们的案例中,让我们继续使用先前的逻辑模型,并假设我们将使用MySQL数据库引擎。示例2-1显示了图书数据库的物理模型。
sql
CREATE TABLE category (
category_id INT PRIMARY KEY,
category_name VARCHAR(255)
);
CREATE TABLE books (
book_id INT PRIMARY KEY,
ISBN VARCHAR(13),
title VARCHAR(50),
summary VARCHAR(255),
category_id INT,
FOREIGN KEY (category_id) REFERENCES category(category_id)
);
CREATE TABLE authors (
author_id INT PRIMARY KEY,
author_name VARCHAR(255),
date_birth DATETIME
);
CREATE TABLE publishes (
book_id INT,
author_id INT,
publish_date DATE,
planned_publish_date DATE,
FOREIGN KEY (book_id) REFERENCES books(book_id),
FOREIGN KEY (author_id) REFERENCES authors(author_id)
);
在示例2-1中,我们创建了四个表:category、books、authors和publishes。物理设计方面微调了表结构、数据类型和约束,以与MySQL数据库系统对齐。
例如,在category表中,我们可以将category_id列的数据类型指定为INT,确保其适用于存储整数值,并将其定义为主键,因为它标识表上的唯一记录。类似地,category_name列可以定义为VARCHAR(255),以容纳可变长度的类别名称。
在books表中,可以为列(如book_id(INT)、ISBN(VARCHAR(13))、title(VARCHAR(50))和summary(VARCHAR(255)))分配适当的数据类型和长度。此外,category_id列可以配置为引用category表中的category_id列的外键。注意,每个ISBN代码由13个字符长度的字符串组成。因此,我们不需要比这更大的字符串。
类似地,在authors表中,可以为列(如author_id(INT)、author_name(VARCHAR(255))和date_birth(DATETIME))定义数据类型,所有这些都符合预期的值域。
在publishes表中,我们强调我们定义了外键约束,以建立books表中的book_id列和authors表中的author_id列之间的关系。同时,外键由它关联的两个表的主键组成。
经过所有这些步骤,我们成功地从需求转向概念,建立了一个逻辑关系模型,并在MySQL中完成了该模型的实际实现,从而建立了我们的数据库。
数据规范化过程
数据规范化技术包含多个步骤,每个步骤旨在将数据组织成逻辑和高效的结构。示例2-2展示了包含一些相关属性的图书表。
sql
CREATE TABLE books (
book_id INT PRIMARY KEY,
title VARCHAR(100),
author VARCHAR(100),
publication_year INT,
genre VARCHAR(50)
);
规范化的第一步,称为第一范式(1NF),要求通过将数据拆分为更小的原子单元来消除重复的组。我们将创建一个名为authors的表,其中包含作者ID和作者的姓名。books表现在引用作者ID,而不是重复存储全名,如示例2-3所示。
sql
-- 表 Authors
CREATE TABLE authors (
author_id INT PRIMARY KEY,
author_name VARCHAR(100)
);
-- 表 Books
CREATE TABLE books (
book_id INT PRIMARY KEY,
title VARCHAR(100),
publication_year INT,
genre VARCHAR(50),
author_id INT,
FOREIGN KEY (author_id) REFERENCES authors(author_id)
);
进入第二范式(2NF),我们检查数据中的依赖关系。我们观察到出版年份在功能上依赖于书籍ID,而流派则依赖于作者ID。为了符合2NF,我们将books表拆分为三个表:
- books,包含book ID和title
- authors,包含author ID和name
- bookDetails,存储book ID、publication year和genre
这确保每一列仅依赖于主键,如示例2-4所示。
sql
-- 表 Authors
CREATE TABLE authors (
author_id INT PRIMARY KEY,
author_name VARCHAR(100)
);
-- 表 Books
CREATE TABLE books (
book_id INT PRIMARY KEY,
title VARCHAR(100),
);
-- 表 book details
CREATE TABLE bookDetails (
book_id INT PRIMARY KEY,
author_id INT,
genre VARCHAR(50),
publication_year INT,
FOREIGN KEY (author_id) REFERENCES authors(author_id)
);
第三范式(3NF)专注于消除传递依赖。我们意识到genre可以通过bookDetails表从book ID派生出来。为了解决这个问题,我们创建了一个名为genres的新表,其中包含genre ID和genre name。现在,bookDetails表引用genre ID,而不是直接存储genre name,如示例2-5所示。
sql
CREATE TABLE authors (
author_id INT PRIMARY KEY,
author_name VARCHAR(100)
);
CREATE TABLE books (
book_id INT PRIMARY KEY,
title VARCHAR(100),
);
CREATE TABLE genres (
genre_id INT PRIMARY KEY,
genre_name VARCHAR(50)
);
CREATE TABLE bookDetails (
book_id INT PRIMARY KEY,
author_id INT,
genre_id INT,
publication_year INT,
FOREIGN KEY (author_id) REFERENCES authors(author_id),
FOREIGN KEY (genre_id) REFERENCES genres(genre_id)
);
这些产生的规范化结构(第三范式)通常用于运营系统,也称为在线事务处理系统(OLTP),旨在高效处理和存储事务并检索事务数据,如客户订单、银行交易或工资单。重要的是要强调,如果有必要,我们可以应用进一步的规范化步骤,例如第四范式(4NF)和第五范式(5NF),以解决复杂的数据依赖关系,并确保更高水平的数据完整性。
数据规范化对于在OLTP系统中高效处理和存储个体交易至关重要。在这个过程中,数据被分成更小、冗余较少的部分,以实现这一目标,为OLTP系统带来了几个优势。数据规范化以减少数据冗余和提高数据完整性为重点,因为数据被组织成多个表,每个表都有特定的目的。这些表通过主键和外键相互连接,确保每个表中的记录是唯一的,并且相同的字段不会在多个表中复制,除了关键字段或系统字段(例如ID或创建时间戳)。
数据规范化之所以重要的另一个原因是它增强并最大化性能。这些规范化的数据库被设计为通过最小化数据冗余和建立在表之间有明确定义关系的情况下,高效处理快速读写,从而使数据库能够以极快的性能处理大量事务。这对于操作性系统中及时执行操作至关重要。
最后但并非最不重要的一点是,规范化的数据库专注于仅存储当前数据,以便数据库表示可用的最新信息。在存储客户信息的表中,每个记录始终反映客户的最新详细信息,例如名字、电话号码和其他相关数据,确保数据库准确表示当前事务状态。
然而,当涉及到分析项目或系统时,这种范式有些不同。通常,用户希望能够检索他们需要的数据,而不必进行大量连接,这是规范化过程的自然结果。而OLTP系统优化了写操作,以避免在像Web应用程序这样的实时系统中增加延迟,分析系统的用户希望进行读优化,以尽快获取分析数据。与存储实时数据的规范化事务数据库不同,分析数据库预计包含实时和非实时数据,并充当过去数据的历史存档。而且通常,分析数据库预计包含来自多个OLTP系统的数据,以提供对业务流程的集成视图。
这些差异确实是关键的,因为它们为数据组织、保留和利用的不同要求奠定了基础。然而,重要的是要澄清,我们刚刚探讨的主要涉及性能优化和遵循OLTP数据库设计的最佳实践的领域只是更广泛的分析工程领域的一个方面。
为了提供一个更清晰的路线图,让我们确立我们的旅程始于对这种基础数据建模类型的探索,这构成了OLTP系统的基础。随后,我们将转向讨论针对OLAP环境优化的数据建模方法。通过做出这种区分,我们的目标是全面了解数据建模的两个方面,为深入研究分析工程方法学及其在后续章节中的应用做好准备。
维度数据建模
数据建模是设计和组织数据库以高效存储和管理数据的基本方面。正如我们之前讨论的,它涉及定义系统内数据实体的结构、关系和属性。
数据建模的一种常见方法是维度建模,其重点是对数据进行建模以支持分析和报告需求。维度建模特别适用于数据仓库和业务智能应用。它强调创建包含可测量数据的事实表和提供描述性上下文的维度表的维度模型。通过使用维度建模技术,如星型模式和雪花模式,可以以简化复杂查询的方式组织数据,并实现高效的数据分析。
数据建模与维度建模之间的关系在于它们的互补性质。数据建模为捕获和结构化数据提供基础,而维度建模提供了一种专门用于支持分析和报告需求的建模技术。共同作用下,这些方法使组织能够设计健壮而灵活的数据库,既有助于事务处理,又支持深入的数据分析。
要理解维度建模,我们首先应该向被认为是数据仓库和维度建模奠基人的两位个人致敬:Bill Inmon 和 Ralph Kimball。他们被认为是企业范围信息收集、管理和决策支持领域的先驱。
他们在数据仓库领域做出了重要贡献,各自倡导不同的哲学和方法。Inmon 提议创建一个涵盖整个企业的集中式数据仓库,旨在生成全面的 BI 系统。另一方面,Kimball 建议创建多个专注于特定部门的小型数据集市,实现部门级别的分析和报告。他们不同的观点导致了在数据仓库设计和实施策略方面截然不同的技术。
除了他们不同的方法之外,Inmon 和 Kimball 还提出了在数据仓库背景下构造数据的不同方法。Inmon 主张在企业数据仓库中使用关系(ERD)模型,特别是第三范式(3NF)。相反,Kimball 的方法在维度数据仓库中使用多维模型,利用星型模式和雪花模式。
Inmon 认为在关系模型中构造数据可以确保企业范围的一致性。这种一致性使得在维度模型中相对轻松地创建数据集市成为可能。另一方面,Kimball 认为在维度模型中组织数据有助于信息总线,使用户更有效地理解、分析、聚合和探索数据的不一致性。此外,Kimball 的方法使得可以直接从分析系统访问数据。相反,Inmon 的方法限制了分析系统仅能从企业数据仓库中访问数据,需要与数据集市进行交互。
在接下来的部分,我们将深入探讨三种建模技术:星型模式、雪花模式和新兴的数据仓库。数据仓库是由 Dan Linstedt 在2000年引入的,近年来逐渐崭露头角。它采用了更加规范化的结构,虽然与 Inmon 的方法不完全一致,但有一些相似之处。
使用星型模式进行建模
星型模式是关系型数据仓库中广泛使用的建模方法,特别适用于分析和报告。它涉及将表分类为维度表或事实表,以有效地组织和表示业务单元及相关观察或事件。
维度表用于描述要建模的业务实体。这些实体可以包括产品、人员、地点和概念,包括时间等各个方面。在星型模式中,通常会找到一个日期维度表,为分析提供全面的日期集。维度表通常由一个或多个关键列组成,这些列作为每个实体的唯一标识符,并包含额外的描述性列,提供有关实体的更多信息。
另一方面,事实表存储发生在业务中的观察或事件。这些包括销售订单、库存水平、汇率、温度等可测量的数据。事实表包含维度键列,这些键列指向维度表,并包含数值测量列。维度键列确定了事实表的维度,指定了在分析中包括哪些维度。例如,存储销售目标数据的事实表可能包含日期和产品键列,表示分析包括与时间和产品相关的维度。
事实表的粒度取决于其维度键列中的值。例如,如果销售目标事实表中的日期列存储表示每个月第一天的值,那么表的粒度就是按月/产品级别。这意味着事实表以每个产品为单位捕获销售目标数据。
通过使用星型模式,以维度表表示业务单元和以事实表捕获观察或事件,公司可以高效进行复杂的分析并获得有意义的见解。星型模式为查询和聚合数据提供了清晰而直观的结构,使得分析和理解数据集内维度和事实之间的关系变得更加容易。
回到我们的书籍表,我们将按照建模步骤开发一个简单的星型模式模型。第一步是识别维度表。但首先,让我们回顾一下例子 2-6 中的基本表。
sql
-- 这是我们的基本表
CREATE TABLE books (
book_id INT PRIMARY KEY,
title VARCHAR(100),
author VARCHAR(100),
publication_year INT,
genre VARCHAR(50)
);
我们应该识别书籍表中的所有单个维度(与特定业务实体相关的属性),并为每个属性创建单独的维度表。在我们的示例中,就像在规范化步骤中一样,我们识别了三个实体:书籍、作者和流派。让我们看一下物理模型,具体请参见示例 2-7。
sql
-- 创建维度表
CREATE TABLE dimBooks (
book_id INT PRIMARY KEY,
title VARCHAR(100)
);
CREATE TABLE dimAuthors (
author_id INT PRIMARY KEY,
author VARCHAR(100)
);
CREATE TABLE dimGenres (
genre_id INT PRIMARY KEY,
genre VARCHAR(50)
);
在命名维度表时,建议使用反映它们所代表的实体的描述性和直观的名称。例如,如果我们有一个代表书籍的维度表,我们可以将其命名为 dimBook 或简称 books。同样,可以为代表作者、流派或其他实体的维度表使用相关且不言自明的名称,如 dimAuthor 或 dimGenre。
对于事实表,建议使用指示正在捕获的测量或事件的名称。例如,如果我们有一个记录图书销售的事实表,我们可以将其命名为 factBookSales 或 salesFact。这些名称表明该表包含与图书销售相关的数据。
现在,我们可以创建一个名为 factBookPublish 的事实表,如示例 2-8 所示,以捕获出版数据。
sql
-- 创建事实表
CREATE TABLE factBookPublish (
book_id INT,
author_id INT,
genre_id INT,
publication_year INT,
FOREIGN KEY (book_id) REFERENCES dimBooks (book_id),
FOREIGN KEY (author_id) REFERENCES dimAuthors (author_id),
FOREIGN KEY (genre_id) REFERENCES dimGenres (genre_id)
);
此代码创建了一个新的事实表 factBookPublish,其中包含与维度相关的测量或事件的列。在这种情况下,只有出版年份。外键约束建立了事实表与维度表之间的关系。
通过星型模式模型表示图书数据集,我们现在具有进行各种分析操作和提取有价值见解的坚实基础。星型模式的维度结构允许高效且直观地查询,使我们能够从不同的角度探索数据。完成建模过程后,我们应该得到一个类似于图 2-3 的模型,其外形类似于星星,因此得名星型模式。
使用这个模型,我们现在可以通过应用诸如流派、作者或出版年份等筛选器轻松分析图书出版情况。例如,我们可以快速检索特定流派的总出版物数量。通过将维度表与事实表连接,如示例 2-9 所示,我们可以轻松地了解书籍、作者、流派和销售之间的关系。
sql
-- 用于检索特定流派的总出版物示例。
SELECT COALESCE(dg.genre, 'Not Available'), -- 或 '-1'
COUNT(*) AS total_publications
FROM factBookPublish bp
LEFT JOIN dimGenres dg ON dg.genre_id = bp.genre_id
GROUP BY dg.genre;
正如您所看到的,我们在连接事实表和维度表时使用了 LEFT JOIN,这是相当常见的。它确保将事实表中的所有记录都包含在结果中,而不管在维度表中是否存在匹配的记录。这一考虑是重要的,因为它承认了并非每个事实记录都一定在每个维度中有对应的条目。
通过使用 LEFT JOIN,您保留了来自事实表的所有数据,同时用来自维度表的相关属性丰富它。这使您能够基于各种维度进行分析和聚合,并从不同的角度探索数据。然而,我们必须处理任何缺失的对应关系。因此,我们使用 COALESCE 运算符,通常用于设置默认值,如 -1 或 Not available。
LEFT JOIN 还允许增量更新维度。如果向维度表添加新记录,LEFT JOIN 仍将包括现有的事实记录,并将它们与可用的维度数据关联起来。这种灵活性确保您的分析和报告在维度数据随时间演变时仍然保持一致。
总体而言,星型模式的简单性和反规范化结构使其有利于聚合和汇总。您可以生成各种报告,例如随时间变化的销售趋势、畅销的流派或按作者划分的收入。此外,星型模式还支持逐层深入和逐层卷积操作,使您能够深入了解更详细的信息或卷积到更高层次的聚合,以全面查看数据。
这种建模技术还与集成到数据可视化工具和 BI 平台无缝对接。通过将模型连接到诸如Tableau、Power BI或Looker等工具,您可以创建视觉上引人入胜的仪表板和交互式报告。这些资源使利益相关者能够迅速把握洞察力,并在一瞥间做出数据驱动的决策。
然而,值得注意的是,前面的示例并没有充分突显星型模式所倡导的反规范化方面。例如,如果您的数据集严格遵循每本书一个流派的情景,您有机会通过直接在统一的 dimBooks 表中 consoli流派信息,推动反规范化并简化数据访问。
使用雪花模式进行建模
在雪花模式中,数据模型相比星型模式更加规范化。它通过将维度表进一步拆分成多个相邻的表,包含了额外层次的规范化。这有助于提高数据完整性并减少数据冗余。
举例来说,考虑一个用于电子商务数据库的雪花模式。我们有一个包含客户信息的维度表 customers,其中包括ID、姓名和地址等信息。在雪花模式中,我们可以将这个表拆分成多个相邻的表。
customers 表可以拆分为 customers 表和一个独立的 addresses 表。customers 表将包含特定于客户的属性,如ID和客户姓名。相反,addresses 表将包含与地址相关的信息,如ID以及客户的街道、城市和邮政编码。如果有多个客户共享相同的地址,我们只需在 addresses 表中存储一次地址信息并将其链接到相应的客户。
要从雪花模式检索数据,通常需要在相关的表上执行多次联接以获取所需的信息。例如,如果我们想查询客户姓名和地址,我们必须在ID字段上将 customers 表与 addresses 表进行联接。虽然雪花模式提供了更好的数据完整性,但由于额外的连接,它也需要更复杂的查询。然而,对于大型数据集和复杂关系,这种模式可以在数据管理方面提供更好的规范化和灵活性。
星型模式和雪花模式都是常见的数据仓库模式设计。在星型模式中,维度表是去规范化的,意味着它们包含冗余数据。星型模式提供了诸如更易于设计和实现以及更高效的查询等优势,因为它减少了 JOIN 操作。然而,由于去规范化的数据,它可能需要更多的存储空间,并且在更新和故障排除方面可能更具挑战性。
这是我们经常看到混合模型的原因之一,其中公司对星型模式进行建模,通常会对几个维度进行规范化,以采用不同的优化策略。选择在很大程度上取决于您独特的需求和要求。如果您在数据仓库解决方案中优先考虑简单性和效率,那么星型模式可能是理想的选择。该模式提供了易于实施和高效查询的优势,适用于简单的数据分析任务。然而,如果您预计数据需求会经常发生变化并且需要更大的灵活性,雪花模式可能更为适合,因为它允许更容易地适应不断变化的数据结构。
假设我们有一个表示全球特定客户位置的维度。在星型模式中对其进行建模的一种方式是创建一个包含所有位置层次结构的单个维度表,去规范化。示例2-10展示了在星型模式范 paradigm下的 dimLocation。
sql
-- 星型模式位置维度
CREATE TABLE dimLocation (
locationID INT PRIMARY KEY,
country VARCHAR(50),
city VARCHAR(50),
State VARCHAR(50)
);
示例2-11则按照雪花模式对位置维度进行建模。
sql
-- 雪花模式位置维度
CREATE TABLE dimLocation (
locationID INT PRIMARY KEY,
locationName VARCHAR(50),
cityID INT
);
CREATE TABLE dimCity (
cityID INT PRIMARY KEY,
city VARCHAR(50),
stateID INT
);
CREATE TABLE dimState (
stateID INT PRIMARY KEY,
state VARCHAR(50),
countryID INT
);
CREATE TABLE dimCountry (
countryID INT PRIMARY KEY,
country VARCHAR(50)
);
在雪花模式示例中,位置维度被拆分成四个表:dimLocation、dimCity、dimState 和 dimCountry。这些表使用主键和外键进行连接,以建立它们之间的关系。
一个重要的主题是,尽管我们有四个表来表示位置维度,但只有具有最高层次结构的表通过其主键连接到事实表(或事实表)。所有其他层次的细节遵循从最高到最低粒度的血统。图2-4说明了这种情况。
使用Data Vault进行建模
Data Vault 2.0是一种建模方法,不属于维度建模,但仍值得一提。它的方法结合了3NF元素和维度建模,以创建一个逻辑的企业数据仓库。它旨在通过提供灵活和可扩展的模式来处理各种数据类型,包括结构化、半结构化和非结构化数据。其最为突出的特点之一是专注于构建基于业务键集成原始数据的模块化和增量的Data Vault模型。这种方法确保数据仓库能够适应不断变化的业务需求和不断演进的数据集。
更深入地说,这种建模技术提供了一种可扩展和灵活的数据仓储和分析解决方案。它设计用于处理大规模数据、不断变化的业务需求和不断演变的数据源。Data Vault的模型由三个主要组件组成:中心(Hubs)、关联(Links)和卫星(Satellites)。
-
中心代表业务实体,充当存储唯一标识符(业务键)的中心点。每个中心对应于一个特定的实体,例如客户、产品或位置。中心表包含业务键列以及与实体相关的任何描述性属性。通过将业务键与描述性属性分离,Data Vault能够轻松跟踪对描述信息的更改,而不会损害业务键的完整性。
sql-- 中心表表示书籍 CREATE TABLE Hub_Books ( book_id INT PRIMARY KEY, title VARCHAR(100) );
-
关联捕获业务实体之间的关系。它们被创建来表示多对多关系或复杂的关联。关联表包含参与中心的外键,形成连接实体之间的桥梁。这种方法允许对复杂关系建模,而无需复制数据或创建不必要的复杂性。
sql-- 关联表表示书籍的作者关系 CREATE TABLE Link_BookAuthors ( book_id INT, author_id INT, PRIMARY KEY (book_id, author_id), FOREIGN KEY (book_id) REFERENCES Hub_Books(book_id), FOREIGN KEY (author_id) REFERENCES Hub_Authors(author_id) );
-
卫星存储与中心和关联相关的上下文特定属性。它们包含不属于业务键但提供有关实体的有价值上下文的附加描述性信息。卫星通过外键与相应的中心或关联关联,允许存储时变数据和保留历史记录。多个卫星可以与一个中心或关联相关联,每个卫星捕获不同时间点或不同视角的特定属性。
sql-- 卫星表表示书籍的描述信息 CREATE TABLE Satellite_BookDetails ( book_id INT, summary VARCHAR(255), publication_year INT, PRIMARY KEY (book_id), FOREIGN KEY (book_id) REFERENCES Hub_Books(book_id) );
Data Vault架构促进了可追溯性、可扩展性和可审计性,同时为数据集成、分析和数据治理提供了坚实的基础。通过使用中心、关联和卫星,组织可以构建支持其分析需求、适应不断变化的业务需求并保持可靠的数据更改历史记录的Data Vault。
Example 2-12. 使用Data Vault 2.0对书籍表进行建模
sql
-- 这是我们的基本表
CREATE TABLE books (
book_id INT PRIMARY KEY,
title VARCHAR(100),
author VARCHAR(100),
publication_year INT,
genre VARCHAR(50)
);
在Data Vault建模中,我们开始识别业务键,这是每个实体的唯一标识符。在这种情况下,书籍表的主键book_id作为业务键。
现在是模型和创建我们的第一个表的时候了:hub表,它存储唯一的业务键及其对应的哈希键以保持稳定性。Example 2-13 创建了hub表。
Example 2-13. 创建Hub表
sql
CREATE TABLE hubBooks (
bookKey INT PRIMARY KEY,
bookHashKey VARCHAR(50),
Title VARCHAR(100)
);
在hub表中,我们将每本书的唯一标识符存储为主键(bookKey),并为稳定性存储哈希键(bookHashKey)。Title列包含有关书籍的描述信息。
接下来是我们的satellite表,如Example 2-14所示,它捕获额外的书籍详细信息并保留历史更改。
Example 2-14. 创建Satellite表
sql
CREATE TABLE satBooks (
bookKey INT,
loadDate DATETIME,
author VARCHAR(100),
publicationYear INT,
genre VARCHAR(50),
PRIMARY KEY (bookKey, loaddate),
FOREIGN KEY (bookKey) REFERENCES hubBooks(bookKey)
);
通过将核心书籍信息分离到hub表并将历史详细信息存储在satellite表中,我们确保可以随时间捕获对作者、出版年份或流派等属性的更改,而无需修改现有记录。
在Data Vault模型中,我们可能还有其他表,比如link表用于表示实体之间的关系或其他satellite表用于捕获特定属性的历史更改。
单体数据建模
直到最近,数据建模的主要方法围绕着创建庞大的SQL脚本。在这种传统方法中,通常一个单独的SQL文件,往往延伸数千行,封装了整个数据建模过程。为了实现更复杂的工作流程,从业者可能会将文件分成多个SQL脚本或存储过程,然后通过Python脚本按顺序执行。使工作流程变得更加复杂的是,这些脚本通常在组织内鲜为人知。因此,即使另一个人希望以类似的方式进行数据建模,他们通常会从头开始,放弃利用现有工作的机会。
这种方法可以被描述为单体或传统的数据建模方法,其中每个数据使用者都独立重建其从原始源数据到数据转换的过程。在这一范 paradigm 中,存在一些显著的挑战,包括脚本的版本控制缺失、视图之间依赖关系的管理任务繁重,以及从原始数据源到最终报告阶段创建新视图或表的常见做法,破坏了可重用性。此外,幂等性的概念未统一应用于大型表,有时导致冗余和反填充------虽然这是常见的,但通常被证明是复杂和劳动密集的工作。
在当今快速发展的数据工程世界中,尤其是在SQL转换的背景下,单体数据模型提出了一个工程师们难以应对的重大挑战。考虑以下情景:你发现生产系统中出现了问题,结果发现最初似乎是一个简单的更改已经引发了一系列错误,这一连串的错误蔓延到整个基础设施。这种噩梦般的情景以高度互连的系统和一个微小的更改充当连锁反应的催化剂为特征,对许多数据专业人员来说是一个熟悉的问题。
设计数据模型时,我们要避免的正是与单体数据模型相关的风险。你不希望紧密耦合的数据模型使调试和实施更改变得艰巨,因为每个更改都可能干扰整个数据流水线。缺乏模块化会阻碍在当今数据驱动的景观中至关重要的灵活性、可扩展性和可维护性。
在单体数据模型中,所有组件都紧密相连,使得问题的识别和隔离成为一项具有挑战性的工作。实质上,这种传统的设计数据系统的方法往往将整个系统统一为一个单一的(尽管不总是紧密结合的)单元。
模型的这种相互关联意味着表面上不相关的更改可能会产生意想不到的后果,影响整个系统。这种复杂性不仅使故障排除变得更加困难,还增加了引入错误或忽视关键依赖关系的风险。所有的数据和功能都是如此紧密集成和相互依赖,以至于很难修改或更新系统的任何一个部分而不影响整个系统。
此外,数据模型中的缺乏模块化阻碍了适应不断变化的业务需求的能力。在数据需求不断变化的动态环境中,单体模型成为进展的瓶颈。整合新的数据源、扩展基础设施或集成新技术和框架变得越来越具有挑战性。
而且,对单体数据模型的维护和更新变得耗时且资源密集。由于系统内部的复杂依赖关系,每一次更改都带来更大的风险。对于不经意地破坏关键组件的担忧导致了一种过于谨慎的方法,减缓了开发周期并抑制了创新。
在当今的数据工程环境中,单体数据模型带来的挑战是显著的。相互依赖性、灵活性不足以及维护和可扩展性方面的困难要求转向模块化数据模型。
通过采用模块化,数据工程师可以在数据基础设施中实现更大的灵活性、稳健性和适应性,以应对快速演变的数据生态系统的复杂性。通过摆脱单体结构,组织可以实现其数据的全部潜力,推动创新,并在我们生活的数据驱动世界中获得竞争优势。
dbt在采用模块化方法并克服单体模型的挑战方面发挥了重要作用。它使我们能够通过将单一数据模型分解为具有各自SQL代码和依赖关系的个体模块来提高可维护性、灵活性和可扩展性。这种模块化结构使我们能够独立地处理各个模块,更容易开发、测试和调试数据模型的特定部分。这消除了意外更改影响整个系统的风险,使引入更改和更新更加安全。在接下来的小节中,dbt中的模块化主题将受到更多关注,第四章将全面探讨dbt。
构建模块化数据模型
过去的例子突显了dbt和数据模型模块化,总体上,如何有助于改善数据开发过程。然而,为什么这不是数据工程师和科学家的默认选择呢?事实上,在过去几十年里,软件开发领域的工程师和架构师选择了新的方式来利用模块化简化其编码过程。与其一次处理一大段代码,模块化将编码过程分解为各种步骤。这种方法相对于替代策略具有几个优势。
模块化的一个主要优势是其增强可管理性的能力。在开发大型软件程序时,保持专注于单个编码片段可能是具有挑战性的。然而,通过将其分解为单独的任务,这项工作变得更加可管理。这有助于开发人员保持专注并防止他们感到被项目的庞大所压倒。
模块化的另一个优势是它对团队编程的支持。与将一项庞大的工作分配给单个程序员不同,它可以分配给一个团队。每个程序员被分配为整个程序的一部分执行特定的任务。最终,所有程序员的工作合并在一起,创建最终的程序。这种方法加速了开发过程并允许在团队内进行专业化。
模块化还有助于提高代码质量。将代码分解为小部分并将责任分配给个别程序员可以提高每个部分的质量。当程序员专注于其分配的部分而不担心整个程序时,他们可以确保其代码的无缺陷性。因此,在集成所有部分时,整体程序包含错误的可能性较小。
此外,模块化使已被证明在实际中有效的代码模块的重复使用成为可能。通过将程序分解为模块,基本方面被打破。如果某段代码对于特定任务有效,则无需重新发明它。相反,可以重复使用相同的代码,从而节省程序员的时间和精力。在需要类似功能的情况下,这可以在整个程序中重复,进一步简化开发。
此外,模块化的代码非常有组织,这提高了其可读性。通过基于任务组织代码,程序员可以根据其组织方案轻松找到和引用特定的部分。这提高了多个开发人员之间的协作,因为他们可以遵循相同的组织方案更有效地理解代码。
模块化的所有优势最终导致了可靠性的提高。易于阅读、调试、维护和共享的代码具有较少的错误。在开发需要共享代码或在未来互操作的大型项目中,这变得至关重要。模块化使以可靠的方式创建复杂软件成为可能。
尽管模块化在软件工程领域是必不可少的,并在数据空间中被遗弃,直到最近才被采纳。其背后的原因是需要在数据架构和软件工程之间实现更清晰的界限。然而,最近该行业已经演变成两者的混合体,因为前述优势在数据分析和工程方面同样适用。
正如模块化简化编码过程一样,它也可以简化数据模型的设计和开发。通过将复杂的数据结构分解为模块化组件,数据工程师可以更好地在不同粒度级别上管理和操作数据。这种模块化方法实现了高效的数据集成、可伸缩性和灵活性,使整体数据架构更容易更新、维护和增强。
同时,模块化促进了对已经被证明在实际中有效的数据模块的重复使用,确保在数据模型中和减少冗余的各个部分保持一致和准确。总体而言,模块化原则为有效的数据建模和工程提供了坚实的基础,增强了数据系统的组织性、可访问性和可靠性。因此,模块化数据建模是设计高效和可扩展数据系统的强大技术。通过将复杂的数据结构分解为较小的可重用组件,开发人员可以构建更健壮和可维护的系统。这是设计高效和可扩展数据系统的强大技术,而dbt和SQL都提供了有效的工具来帮助我们实现这一技术。
总之,模块化数据建模的核心原则可以定义如下:
- 分解
将数据模型分解为更小、更易管理的组件
- 抽象
将数据模型的实现细节隐藏在接口背后
- 可重用性
创建可在系统的多个部分之间重用的组件
这种数据建模可以通过使用规范化、数据仓库和数据虚拟化技术来实现。例如,使用规范化技术,数据根据其特征和关系分隔到表中,形成了模块化的数据模型。
另一种选择是利用dbt,因为它有助于自动化创建模块化数据模型的过程,提供了支持模块化数据建模原则的几个功能。例如,dbt允许我们通过将数据模型拆分为更小的可重用组件来解决分解的问题,从而提供了创建可重用宏和模块化模型文件的方式。它还通过提供用于与数据源交互的简单一致的接口,为抽象数据模型的实现细节提供了一种方式。
此外,dbt通过提供一种定义和在各种模型之间重用通用代码的方式,鼓励可重用性。此外,dbt通过提供一种测试和记录数据模型的方式,有助于提高可维护性。最后,dbt允许您通过为模型定义和测试不同的物化策略来优化性能,最终允许您微调数据模型的各个组件的性能。
然而,重要的是要承认,模块化也带来潜在的缺点和风险。集成系统通常比模块化系统更容易优化,无论是因为减少数据移动和内存使用,还是因为数据库优化器能够在幕后改进SQL。有时创建视图来创建表可能会导致模型不够优化。然而,考虑到模块化的好处,这种权衡通常是值得的。模块化会创建更多文件,这可能意味着更多的对象需要拥有、管理,并可能被淘汰。如果没有成熟的数据治理策略,这可能导致模块化但无所有者的表激增,当出现问题时可能难以管理。
使用dbt实现模块化数据模型
正如我们先前所强调的,构建模块化数据模型是开发健壮且易于维护的数据基础设施的重要方面。然而,随着项目规模和复杂性的增长,管理和编排这些模型的过程可能变得复杂。
这就是强大的数据转换工具dbt发挥作用的地方。通过将模块化数据建模的原则与dbt的功能结合起来,我们可以轻松地在数据基础设施中实现全新的效率和可伸缩性水平。
采用这种模块化方法后,组织内的每个数据生产者或消费者都能够在他人完成的基础数据建模工作的基础上进行扩展,无需在每个场合都从源数据开始。
将dbt整合到数据建模框架中后,透视发生了变化,将数据模型的概念从一个整体实体转变为一个独立的组件。每个模型的每个贡献者开始识别可以在各种数据模型之间共享的转换。这些共享的转换被提取并组织成基础模型,可以在多个上下文中高效地引用。
正如图2-5所示,跨多个实例使用基本数据模型,而不是每次都从头开始,简化了数据建模中DAG的可视化。这种模块化的多层结构阐明了数据建模逻辑层之间如何构建以及显示依赖关系。然而,值得注意的是,仅仅采用像dbt这样的数据建模框架并不能自动确保模块化的数据模型和易于理解的DAG。
您的DAG的结构取决于团队的数据建模理念、思维过程以及表达这些理念的一致性。为了实现模块化的数据建模,请考虑诸如命名约定、可读性、调试和优化的原则。这些原则可以应用于dbt中的各种模型,包括分段模型、中间模型和Mart模型,以提高模块化并保持结构良好的DAG。
让我们通过了解dbt如何通过Jinja语法通过引用数据模型({{ ref() }})来实现模型的可重用性,开始迈向利用dbt进行模块化数据建模的旅程。
引用数据模型
通过采用dbt的功能,如模型引用和Jinja语法,数据工程师和分析师可以在模型之间建立清晰的依赖关系,增强代码的可重用性,并确保其数据流程的一致性和准确性。在这个背景下,Jinja是一种模板语言,允许SQL代码中进行动态和程序化的转换,为自定义和自动化数据转换提供了强大的工具。这种强大的模块化和dbt功能的组合使团队能够构建灵活且易于维护的数据模型,加快开发过程,并实现利益相关者之间的无缝协作。
为了充分利用dbt的功能并确保准确的模型构建,关键是使用{{ ref() }}语法进行模型引用。通过这种方式引用模型,dbt可以自动检测和建立基于上游表的模型之间的依赖关系。这实现了数据转换流水线的平稳可靠执行。
另一方面,{{ source() }} Jinja语法应该谨慎使用,通常仅限于从数据库中选择原始数据的初始阶段。避免直接引用非dbt创建的表,因为它们可能阻碍dbt工作流的灵活性和模块化。相反,应该集中精力通过使用{{ ref() }} Jinja语法在模型之间建立关系,确保对上游表的更改正确传播到下游,并保持清晰而一致的数据转换流程。通过遵循这些最佳实践,dbt实现了高效的模型管理,并促进了分析工作流的可扩展性和可维护性。
例如,假设我们有两个模型:orders和customers,其中orders表包含有关客户订单的信息,而customers表存储客户详细信息。我们希望在这两个表之间执行联接,以使用客户信息丰富订单数据(示例2-15)。
sql
-- 在orders.sql文件中
SELECT
o.order_id,
o.order_date,
o.order_amount,
c.customer_name,
c.customer_email
FROM
{{ ref('orders') }} AS o
JOIN
{{ ref('customers') }} AS c
ON
o.customer_id = c.customer_id
-- 在customers.sql文件中
SELECT
customer_id,
customer_name,
customer_email
FROM
raw_customers
此示例演示了在SQL查询中使用ref()函数引用模型。情境涉及两个模型文件:orders.sql和customers.sql。在orders.sql文件中,编写了一个SELECT语句,以从orders模型检索订单信息。{{ ref('orders') }}表达式引用了orders模型,使查询能够使用在该模型中定义的数据。查询使用customer_id列将orders模型与customers模型进行联接,检索额外的客户信息,如名称和电子邮件。
在customers.sql文件中,编写了一个SELECT语句,以从raw_customers表中提取客户信息。该模型表示在进行任何转换之前的原始客户数据。
dbt中的这种引用机制使得能够创建模块化且相互连接的模型,这些模型相互构建以生成有意义的见解和报告。为了说明其必要性,让我们考虑一个实际的例子:想象一下,您正在处理一个复杂的数据集,例如每周的产品订单。如果没有结构化的方法,管理这些数据可能会很快变得混乱。您可能最终会得到一个纷乱的SQL查询网络,这使得跟踪依赖关系、维护代码和确保数据准确性变得具有挑战性。
通过将数据转换过程组织成从源到mart表的不同层次,您可以获得多个好处。这简化了数据管道,使其更易于理解和管理。它还允许渐进式改进,因为每个层次都专注于特定的转换任务。这种结构化方法增强了数据工程师和分析师之间的协作,减少了错误,并最终产生了更可靠和有深度的报告。
暂存数据模型
暂存层在数据建模中扮演着关键的角色,因为它作为更复杂数据模型的模块化构建的基础。每个暂存模型对应于一个源表,与原始数据源存在一对一的关系。保持暂存模型简单并在该层内尽量减少转换是重要的。可接受的转换包括类型转换、列重命名、基本计算(如单位转换)以及使用条件语句(如CASE WHEN)进行分类。
暂存模型通常会以视图的形式实现,以保留数据的及时性并优化存储成本。这种方法允许引用暂存层的中间或商业智能模型访问最新的数据,同时节省空间和成本。在暂存层避免连接操作以防止冗余或重复计算是明智的。连接操作更适用于后续层,这里建立了更复杂的关系。
此外,在暂存层中应避免聚合,因为它们可能对有价值的源数据进行分组并潜在地限制对其的访问。暂存层的主要目的是为后续的数据模型创建基本的构建块,从而在下游转换中提供灵活性和可伸缩性。遵循这些指导方针,暂存层成为在模块化数据架构中构建强大数据模型的可靠而高效的起点。
在dbt中使用暂存模型使我们能够在代码中采用"不重复自己"(DRY)的原则。通过遵循dbt的模块化和可重用的结构,我们的目标是在可能的情况下将模型推得尽可能远。这种方法帮助我们避免重复代码,从而减少复杂性和计算开销。
例如,假设我们一直需要将以分为单位的货币值转换为以美元为单位的浮点数。在这种情况下,在暂存模型中早期执行除法和类型转换更为高效。这样,我们可以在下游引用转换后的值,而不必多次重复相同的转换。通过利用暂存模型,我们可以优化代码重用,并以一种可扩展且高效的方式简化数据转换过程。
假设我们有一个名为raw_books的源表,其中包含原始图书数据。现在,我们想要创建一个名为stg_books的暂存模型,以在进一步处理之前对数据进行转换和准备。在我们的dbt项目中,我们可以创建一个名为stg_books.sql的新的dbt模型文件,并定义生成暂存模型的逻辑,如示例2-16所示。
示例2-16. 暂存模型
sql
/* 这应该是文件stg_books.sql,并且查询原始表以创建新模型 */
SELECT
book_id,
title,
author,
publication_year,
genre
FROM
raw_books
在这个例子中,像stg_books这样的暂存模型从raw_books表中选择相关列。它可以包括基本的转换,如重命名列或转换数据类型。通过创建暂存模型,您将初始的数据转换与下游处理分开。这确保了数据质量、一致性,并符合标准,然后再进行进一步的使用。暂存模型为数据管道中的中间和商业智能层的更复杂的数据模型提供了基础。它们简化了转换,保持了数据的完整性,并提高了dbt项目的可重用性和模块化性。
基础数据模型
在dbt中,基础数据模型通常用作分段模型,但根据项目的具体需求,它们也可以包含额外的转换步骤。这些模型通常设计为直接引用输入到数据仓库中的原始数据,并在数据转换过程中发挥关键作用。一旦你创建了分段或基础模型,dbt项目中的其他模型可以引用它们。
在dbt文档中,从"base"模型变为"staging"模型反映了不受"base"名称限制的愿望,因为它暗示了构建数据模型的第一步。新术语允许更灵活地描述这些模型在dbt框架中的角色和目的。
中间数据模型
中间层在数据建模中扮演着关键角色,它通过将来自分段层的原子构建块组合起来,创建更复杂且有意义的模型。这些中间模型代表对业务具有重要意义但通常不会通过仪表板或应用程序直接展示给最终用户的构造。为了保持分离和优化性能,建议将中间模型存储为短暂模型。短暂模型不会直接在数据库或数据集上创建,而是它们的代码被插入到引用它们的模型中,作为通用表达式(CTEs)。然而,有时将它们实体化为视图更为合适。短暂模型不能直接选择,这使得故障排除具有挑战性。此外,通过运行操作调用的宏不能引用短暂模型。因此,将特定的中间模型实体化为短暂模型或视图取决于具体的用例,但建议从短暂实体化开始。
如果选择将中间模型实体化为视图,将它们放置在与dbt配置文件中定义的主模式之外的自定义模式中可能是有益的。这有助于组织模型并有效地管理权限。
中间层的主要目的是将不同的实体汇聚在一起,吸收最终 Mart 模型的复杂性。这些模型促进整体数据模型结构的可读性和灵活性。重要的是要考虑在其他模型中引用中间模型的频率。多个模型引用同一中间模型可能表明存在设计问题。在这种情况下,将中间模型转换为宏可能是提高模块化性并保持更清晰设计的合适解决方案。
通过有效利用中间层,可以使数据模型更具模块性和可管理性,确保吸收复杂性的同时保持组件的可读性和灵活性。
举例来说,假设我们有两个分段模型,stg_books 和 stg_authors,分别表示书籍和作者数据。现在我们想要创建一个名为int_book_authors的中间模型,将来自两个分段模型的相关信息组合起来。在我们的dbt项目中,我们可以创建一个名为int_book_authors.sql的新dbt模型文件,如示例2-17所示,并定义生成中间模型的逻辑。
示例2-17. 中间模型
sql
-- 这应该是文件 int_book_authors.sql
-- 引用分段模型
WITH
books AS (
SELECT *
FROM {{ ref('stg_books') }}
),
authors AS (
SELECT *
FROM {{ ref('stg_authors') }}
)
-- 组合相关信息
SELECT
b.book_id,
b.title,
a.author_id,
a.author_name
FROM
books b
JOIN
authors a ON b.author_id = a.author_id
在示例2-17中,int_book_authors模型使用{{ ref() }} Jinja语法引用了分段模型stg_books和stg_authors。这确保了dbt可以正确推断模型的依赖关系,并根据上游表构建中间模型。
Mart 模型
数据管道的顶层由 Mart 模型组成,它们负责通过仪表板或应用程序将业务定义的实体集成和呈现给最终用户。这些模型汇总来自多个源的所有相关数据,并将其转化为一个统一的视图。
为了确保最佳性能,通常会将 Mart 模型实体化为表格。实体化模型能够加快查询的执行速度,并更快地向最终用户提供结果。如果实体化表格的创建时间或成本是一个问题,可以考虑将其配置为增量模型,允许在包含新数据时进行高效的更新。
在 Mart 模型中,简单性是关键,应避免过多的连接操作。如果在 Mart 模型中需要多个连接,请重新考虑设计,并考虑重构中间层。通过保持 Mart 模型相对简单,您可以确保查询执行的效率,并维护数据管道的整体性能。
让我们以图书出版分析的数据 Mart 为例。我们有一个名为 int_book_authors 的中间模型,其中包含原始的图书数据,包括每本书的作者信息(示例2-18)。
示例2-18. Mart 模型
sql
-- 这应该是文件 mart_book_authors.sql
{{
config(
materialized='table',
unique_key='author_id',
sort='author_id'
)
}}
WITH book_counts AS (
SELECT
author_id,
COUNT(*) AS total_books
FROM {{ ref('int_book_authors') }}
GROUP BY author_id
)
SELECT
author_id,
total_books
FROM book_counts
我们首先设置模型的配置,指定它应该被实体化为表格。将唯一键设置为 author_id 以确保唯一性,并且基于 author_id 进行排序。
接下来,我们使用一个名为 book_counts 的 CTE 来聚合图书数据。我们选择 author_id 列,并计算与每位作者关联的书籍数量,数据来源是分段模型 stg_books。最后,SELECT 语句从 book_counts CTE 检索聚合数据,返回每位作者的 author_id 和相应的书籍数量。由于这是一个实体化表格,该模型可以在需要时刷新,以反映原始数据的任何更改。
测试你的数据模型
在 dbt 中进行测试是确保数据模型和数据源准确性和可靠性的重要方面。dbt 提供了一个全面的测试框架,允许您使用 SQL 查询定义和执行测试。这些测试旨在识别不符合指定断言条件的行或记录,而不是检查特定条件的正确性。
dbt 有两种主要类型的测试:单一测试和通用测试。单一测试是特定和有针对性的测试,以 SQL 语句的形式编写并存储在单独的 SQL 文件中。它们允许您测试数据的特定方面,例如检查事实表中的 NULL 值的缺失或验证某些数据转换。通过单一测试,我们可以利用 Jinja 的强大功能基于我们的数据和业务需求动态定义断言。让我们通过分析示例 2-19 来看一下 dbt 中的单一测试。
示例 2-19. dbt 中的单一测试示例
yaml
version: 2
models:
- name: my_model
tests:
- not_null_columns:
columns:
- column1
- column2
在这个例子中,我们为 dbt 模型 my_model 定义了一个名为 not_null_columns 的单一测试。这个测试检查模型中特定的列是否包含 NULL 值。columns 参数指定要检查 NULL 值的列。在这种情况下,指定了 column1 和 column2。如果这些列中有任何一个包含 NULL 值,测试就会失败。
另一方面,通用测试更加灵活,可以应用于多个模型或数据源。它们在 dbt 项目文件中使用特殊语法定义。这些测试允许我们定义更全面的标准来验证我们的数据,例如检查表之间的数据一致性或确保特定列的完整性。此外,它们提供了一种灵活且可重用的方式来定义可以应用于 dbt 模型的断言。这些测试以 YAML (.yml) 文件的形式编写和存储,这使我们能够对查询进行参数化,并在各种上下文中轻松重用它们。
在通用测试中对查询进行参数化使您能够快速适应多种情景。例如,在将通用测试应用于不同的模型或数据集时,您可以指定不同的列名或条件参数。让我们通过示例 2-20 查看其中一个通用测试。
示例 2-20. dbt 中的通用测试示例
yaml
version: 2
tests:
- name: non_negative_values
severity: warn
description: Check for non-negative values in specific columns
columns:
- column_name: amount
assert_non_negative: {}
- column_name: quantity
assert_non_negative: {}
在这个例子中,通用测试以名称 non_negative_values 定义。在这里,我们可以观察要测试的列以及每个列的断言条件。该测试检查 amount 和 quantity 列中的值是否为非负数。通用测试允许您编写可重用的测试逻辑,可应用于 dbt 项目中的多个模型。
要在多个模型中重用通用测试,可以在每个单独模型的 YAML 文件的 tests 部分中引用它,如示例 2-21 所示。
示例 2-21. 重用通用测试
yaml
version: 2
models:
- name: my_model
columns:
- column_name: amount
tests: ["my_project.non_negative_values"]
- column_name: quantity
tests: ["my_project.non_negative_values"]
在这个例子中,定义了模型 my_model,指定了 amount 和 quantity 列,并分别指定了相应的测试。测试引用了命名空间为 my_project(假设 my_project 是您的 dbt 项目名称)的通用测试 non_negative_values。
通过在每个模型的 tests 部分中指定通用测试,您可以在多个模型中重用相同的测试逻辑。这种方法确保了数据验证的一致性,并允许您在不复制测试逻辑的情况下轻松将通用测试应用于不同模型中的特定列。
请注意,您必须确保通用测试的 YAML 文件位于 dbt 项目结构内的正确目录中,并且可能需要修改测试引用以匹配您项目的命名空间和文件夹结构。
生成数据文档
正确数据建模的另一个不可或缺的组成部分是文档。具体而言,确保组织中的每个人,包括业务用户,都能轻松理解和访问指标,如ARR(年度重复收入)、NPS(净推荐值)或甚至MAU(月活跃用户),对于实现基于数据的决策至关重要。
通过利用dbt的功能,我们可以记录这些指标的定义方式以及它们所依赖的具体源数据。这些文档成为任何人都可以访问的宝贵资源,促进透明度并实现自助数据探索。
当我们消除这些语义障碍并提供易于访问的文档时,dbt使得所有技术水平的用户都能够浏览和探索数据集,确保有价值的洞见面向更广泛的受众。
假设我们有一个名为nps_metrics.sql的dbt项目模型,该模型计算净推荐值。我们可以通过在SQL文件中使用Markdown语法的注释轻松记录此指标,如示例2-22所示。
示例2-22. 文档
sql
/* nps_metrics.sql
-- 该模型根据客户反馈计算产品的净推荐值(NPS)。
依赖关系:
- 该模型依赖于"feedback"模式中的"customer_feedback"表,
该表存储客户反馈数据。
- 它还依赖于"users"模式中的"customer"表,包含客户信息。
计算:
-- NPS是通过根据客户评级将客户反馈分为推荐者、中立者和批评者而计算的。
-- 推荐者:评级为9或10的客户。
-- 中立者:评级为7或8的客户。
-- 批评者:评级为0到6的客户。
-- 然后通过从批评者的百分比中减去推荐者的百分比来派生NPS。
*/
-- SQL查询:
WITH feedback_summary AS (
SELECT
CASE
WHEN feedback_rating >= 9 THEN 'Promoter'
WHEN feedback_rating >= 7 THEN 'Passive'
ELSE 'Detractor'
END AS feedback_category
FROM
feedback.customer_feedback
JOIN
users.customer
ON customer_feedback.customer_id = customer.customer_id
)
SELECT
(COUNT(*) FILTER (WHERE feedback_category = 'Promoter')
- COUNT(*) FILTER (WHERE feedback_category = 'Detractor')) AS nps
FROM
feedback_summary;
在这个例子中,注释提供了有关NPS指标的重要详细信息。它们指定了nps_metrics模型的依赖关系,解释了计算过程,并提到了在查询中涉及的相关表。
在记录模型之后,我们可以使用dbt命令行界面(CLI)运行以下命令(示例2-23)生成dbt项目的文档。
示例2-23. 运行文档生成
dbt docs generate
运行此命令将为整个dbt项目生成HTML文档,包括已记录的NPS指标。生成的文档可以托管并提供给组织中的用户,使他们能够轻松找到和理解NPS指标。
调试和优化数据模型
改善dbt性能的宝贵优化建议之一是仔细分析和优化查询本身。一种方法是利用查询规划器的功能,例如PostgreSQL(Postgres)查询规划器。了解查询规划器将帮助您识别查询执行中的潜在瓶颈和低效之处。
另一种有效的优化技术是通过将复杂查询分解为较小的组件,如CTEs。根据涉及的操作的复杂性和性质,这些CTEs可以转换为视图或表。涉及轻量计算的简单查询可以实体化为视图,而更复杂和计算密集型的查询可以实体化为表。dbt配置块可用于为每个查询指定所需的实体化方法。
通过有选择地使用适当的实体化技术,可以实现显著的性能改进。这可以导致更快的查询执行时间,减少处理延迟,并提高整体数据建模效率。特别是表实体化的使用已经显示出令人印象深刻的性能提升,可以根据情况显著提高速度。
实施这些优化建议将使dbt工作流更加精简和高效。通过优化查询并使用适当的实体化策略,您可以优化dbt模型的性能,实现更好的数据处理和更高效的数据转换。
让我们看一下示例2-24中的复杂查询。
sql
-- 复杂查询 1
SELECT column1, column2, SUM(column3) AS total_sum
FROM table1
INNER JOIN table2 ON table1.id = table2.id
WHERE column4 = 'some_value'
GROUP BY column1, column2
HAVING total_sum > 1000
该查询涉及表的连接、应用过滤器和执行聚合。在创建最终模型之前,让我们将其分解为多个CTE,如示例2-25所示。
sql
-- 使用CTE分解复杂查询以进行优化
-- CTE 1: 连接所需数据
WITH join_query AS (
SELECT table1.column1, table1.column2, table2.column3
FROM table1
INNER JOIN table2 ON table1.id = table2.id
)
-- CTE 2: 过滤行
, filter_query AS (
SELECT column1, column2, column3
FROM join_query
WHERE column4 = 'some_value'
)
-- CTE 3: 聚合和过滤结果
, aggregate_query AS (
SELECT column1, column2, SUM(column3) AS total_sum
FROM filter_query GROUP BY column1, column2
HAVING total_sum > 1000
)
-- 检索优化结果的最终查询,这将成为我们的模型
SELECT *
FROM aggregate_query;
join_query CTE专注于连接所需的表,而filter_query CTE对行进行过滤。然后,aggregate_query CTE执行聚合并应用最终的过滤条件。
通过将复杂查询拆分为单独的CTEs,您可以简化和组织逻辑以优化执行。这种方法提供了更好的可读性、可维护性和潜在的性能改进,因为数据库引擎可以为每个CTE优化执行计划。最终查询通过从aggregate_query CTE中选择列来检索优化结果。
现在让我们探讨在dbt中调试实体化模型的过程。一开始可能会是一项困难的任务,因为它需要进行彻底的验证。一个重要的方面是确保数据模型的显示符合预期,并且值与非实体化版本相匹配。
为了简化调试和验证过程,可能需要完全刷新整个表,并将其视为非增量。这可以通过使用dbt run --full-refresh命令来实现,该命令更新表并运行模型,就好像它是第一次执行一样。
在某些情况下,在前几天内同时对模型和增量模型进行完整更新可能是有帮助的。这种比较的方法允许验证两个版本之间的一致性,并最小化未来数据不一致的风险。当与生产中的一个经过良好验证的、可靠的数据模型一起工作时,这种技术特别有效,因为它增强了对已做更改的信心。通过比较更新后的模型和增量模型,我们可以确保更改的准确性,并减轻潜在的与数据相关的问题。
考虑一个具体的场景,有一个基于交易数据计算月度收入的实体化dbt模型。我们想要调试和验证该模型以确保其准确性。我们开始怀疑实体化模型生成的值可能与预期结果不符。为了进行故障排除,我们决定将表完全刷新,就好像它不是增量的一样。使用dbt full-refresh命令,我们触发了更新整个表并从头运行模型的过程。
在前几天内,我们还运行了一个并行过程,更新实体化模型和增量模型。这使我们能够比较两个版本之间的结果,并确保它们匹配。通过检查更新后的模型和增量模型的一致性,我们对所做更改的准确性有了信心。
例如,如果我们有一个在生产中运行了一段时间并被视为可靠的收入模型,比较更新后的模型和增量模型就更有意义。通过这种方式,我们可以确认对模型的更改没有导致计算收入数据中的任何意外不一致。此外,全面的测试对于确保数据模型的准确性和可靠性至关重要。在整个工作流中实施测试可以帮助早期发现问题,并为SQL查询的性能提供有价值的见解。
Medallion架构模式
数据仓库在决策支持和商业智能方面有着丰富的历史,但在处理非结构化、半结构化和高变异性数据时存在局限性。与此同时,数据湖出现作为存储各种数据格式的存储库,但它们缺乏关键功能,如事务支持、数据质量强制执行和一致性。
这阻碍了它们实现承诺的能力,并导致丧失与数据仓库相关的好处。为了满足公司不断发展的需求,需要一个灵活且高性能的系统,以支持SQL分析、实时监控、数据科学和机器学习等多样化的数据应用。然而,一些最近在人工智能领域的进展侧重于处理各种数据类型,包括半结构化和非结构化数据,而传统的数据仓库并未针对这些进行优化。
因此,组织通常使用多个系统,包括数据湖、数据仓库和专用数据库,这引入了由于系统之间的数据移动和复制而导致的复杂性和延迟。作为将所有这些传统系统合并为满足新市场需求的系统的自然结果,出现了一种新型系统:数据湖仓。
数据湖仓结合了数据湖和数据仓库的优势,在成本效益的云存储中直接实现类似仓库的数据结构和管理功能,采用Apache Delta Lake、Iceberg或Apache Hudi等开放格式。这些格式相对于CSV和JSON等传统文件格式具有各种优势。虽然CSV缺乏对列的类型定义,JSON提供了更灵活的结构,但其类型不一致。相较于这些,Parquet、Apache Avro和ORC(优化的行列格式)文件格式通过采用列导向的方式和更强类型化,但在某些情况下不符合ACID(原子性、一致性、隔离性、持久性)标准(除了ORC,在某些情况下)。相比之下,Delta Lake、Iceberg和Hudi通过添加ACID合规性和作为双向数据存储的能力来增强数据存储,从而实现了高修改吞吐量同时支持大量分析查询。与最初为本地Hadoop系统设计的传统格式Parquet不同,这些格式特别适用于现代基于云的数据系统。
湖仓提供了关键功能,如支持并发数据读取和写入的事务支持、模式强制执行和治理、直接的商业智能工具支持、可扩展性的存储和计算的解耦、具有高效数据访问的标准化存储格式和API的开放性、支持多样化数据类型以及与包括数据科学、机器学习和SQL分析在内的各种工作负载的兼容性。它们通常还提供端到端的流处理能力,消除了实时数据应用程序需要单独系统的需求。企业级湖仓系统包括安全性、访问控制、数据治理、数据发现工具和符合隐私法规的合规性等功能。实施湖仓使组织能够将这些基本功能整合到一个由数据工程师、分析工程师、科学家、分析师甚至机器学习工程师共享的单一系统中,从而可以共同开发新的数据产品。
在湖仓和新的开放格式的背景下,勋章架构应运而生。简单来说,这是一种在湖仓环境中战略性地构建数据的数据建模范式,旨在在数据经过不同迭代级别时迭代地提升数据质量。这种架构框架通常包括三个可辨识的层次,即青铜层、银层和金层,每一层代表数据精炼程度的递增:
- 青铜层
这作为来自外部源系统的数据的初始目的地。该层中的表镜像源系统表的结构,包括任何额外的元数据列以捕获加载日期/时间和进程ID等信息。这一层优先考虑高效的变更数据捕获(CDC),保持源数据的历史存档,确保数据血统,促进审计,并支持重新处理,而无需重新从源系统读取数据。
- 银层
在湖仓架构中,这一层在整合和完善从青铜层获取的数据方面发挥着关键作用。银层通过匹配、合并、一致化和清理等过程创建了一个全面的视图,涵盖了关键的业务实体、概念和交易。这包括主客户、商店、非重复交易和交叉参照表。银层作为自助式分析的全面数据源,为用户提供自由报告、高级分析和机器学习能力。通常观察到银层可以采用3NF数据模型、星型模式、数据仓库(Data Vault)甚至是雪花模式。类似于传统数据仓库,这是任何利用数据进行解决业务问题的项目和分析的有价值的资源。
- 金层
这一层提供了解决业务问题的有价值的见解。它从银层聚合数据,并将其提供给商业智能自由报告工具和机器学习应用程序。该层确保数据湖的可靠性、改进的性能和ACID事务,同时在云数据存储顶部统一流处理和批处理事务。
图2-6展示了在湖仓背景下的勋章架构,并显示了dbt如何支持创建这类系统。
通过从青铜到金层的过程,数据经历了多个步骤,如摄取、清理、增强和聚合过程,提供了无法计量的业务洞见。这种方法相较于传统的数据架构,如具有暂存和维度模型层的数据仓库,甚至是单一的数据湖,通常涉及更多的文件组织,而不是创建适当的语义层,代表了一项重大的进步。
勋章架构并不取代其他维度建模技术。每个层次中模式和表的结构可以根据数据更新的频率和类型,以及数据的预期用途而变化。相反,它指导数据应该如何跨三个层次组织,以实现更模块化的数据建模方法。
对于分析工程师来说,了解勋章架构的基础和湖仓背后的概念是有价值的,因为在某些情景中,这可能是他们花费大量时间的地方。这种参与可能包括设计要部署在勋章的一个层次中的模型结构,利用开放格式提供的接口,或者构建转换脚本(例如使用dbt等工具),以实现数据在架构各层之间的流转。
然而,值得注意的是,开放格式和湖仓的重要性可能取决于具体使用的数据架构。例如,在像Snowflake这样的架构中,数据可能主要被摄取到本地表中,而不是像Iceberg这样的开放格式,这使得理解湖仓更像是一个不错的选择,而不是分析工程的基本要求。
总结
数据建模在分析领域发展迅速,以满足多样化的业务洞见和报告需求。星型模式通过具有中心事实表,周围是维度表,提供了一种简单的查询方法。雪花模式通过进一步细分这些维度,提供了更深层次的细粒度。相比之下,Data Vault 方法优先考虑灵活性,以应对数据源迅速变化的环境。新的勋章设计将所有这些模型结合在一起,形成了各种分析需求的完整计划。
所有这些建模的进展都是为了解决特定的分析问题。中心目标是有效提供可操作的洞见,无论是在星型和雪花模式的性能改进中,还是在 Data Vault 的灵活性中。随着分析需求变得更加复杂,选择正确的建模方法变得至关重要,这不仅能使数据可用,还能确保数据具有意义并提供洞见。
分析工程师使用星型、雪花、Data Vault 或勋章等建模结构来创建和维护强大、可伸缩和高效的数据结构。他们的工作确保了数据的最佳组织,使其易于访问,并对数据分析师和科学家有所帮助。通过理解和应用这些模型,分析工程师为准确的洞见和明智的决策打下基础,从庞大的数据流中创建一致的数据集。