SQL-和-DBT-分析工程-全-
SQL 和 DBT 分析工程(全)
原文:
zh.annas-archive.org/md5/f7242202b4f3c518e7bf628069e4ae7a译者:飞龙
前言
- 在不断发展的商业世界中,一个令人着迷的概念被称为分析工程。它迅速成为热门话题,受到经理们的追捧,由 IT 公司呈现,并受到用户的欣赏,因为他们对其提供的可能性感到惊叹。但在兴奋之余,许多人并不知道分析工程究竟是什么。他们认为这是关于创建数据管道、设计令人惊艳的可视化和使用先进算法。哦,他们错了多么!
你可以把这个非凡的分析工程世界想象成细致入微的侦探夏洛克·福尔摩斯与天才工程师托尼·斯塔克(更为人所知的钢铁侠)的结合。想象一下夏洛克·福尔摩斯出色的解决问题的能力与钢铁侠尖端技术的结合。这种组合定义了分析技术的真正力量和潜力。
-
但要小心:如果你认为分析工程仅限于数据管道和可视化,那么你错过了夏洛克·福尔摩斯的深刻演绎思维,他代表了数据分析师或业务分析师所带来的贡献。这个领域是分析调查与软件工程师或数据工程师的技术相结合的地方,由托尼·斯塔克代表。
-
暂停一下,思考一下数据在你的业务中的重要性。你为什么寻求它?答案在于追求知识。分析技术被用来将原始数据转化为可行动派见解,这些见解作为明智决策的基础。它是一个强大的支持系统,提供揭示你业务实际情况的事实。然而,它不会替你做决策,而是提供你需要的信息,帮助你的业务取得成功。
-
在你投入到创建令人印象深刻的钢铁侠分析技术装备之前,接受夏洛克·福尔摩斯的智慧。利用他敏锐的观察力来识别和理解你所面临挑战的核心。不要被视觉化和算法的吸引力所迷惑,只因他人对其着迷。记住,分析工程不仅仅是技术:它是一个管理工具,只有与组织的战略和目标保持一致,它才会成功。确保你的关键绩效指标与你业务的实际情况保持一致,这将确保你的分析工程努力的结果准确、有影响力,并且不会让你失望。
-
分析工程的伟大冒险并不是从构建数据管道或选择先进算法开始。不,朋友,它始于对你组织知识空白的深刻反省。弄清楚为什么那些知识重要,以及如何利用它来推动你的业务成功。将分析的变革力量作为你的指南,它将在广阔的数据海洋中指引你通向成功的道路。
在追求分析工程的过程中,请永远记住福尔摩斯的故事。当一辆简朴的自行车足以胜任时,不要建造一架奢华的飞机。让问题的复杂性和其上下文的细微差别指导你的努力。记住,分析不仅仅是技术,它是管理的灯塔,是一种必须用目的和精准性使用的无价工具。让它成为你通往成功之路上的不变伴侣。
为什么我们写这本书
在今天信息丰富的时代,由于技术的快速增长和创新的不懈追求,重要的知识、概念和技术往往会在其中变得模糊。在这种动态变革期间,一些重要的概念有时会被无意间忽视。这种忽视并不是因为它们的相关性在减弱,而是因为进步的速度太快。
在数据管理的背景下,经常被忽视的一个基本概念就是数据建模。值得注意的是,数据建模包括各种方法,包括 Kimball、概念、逻辑和物理建模等。我们意识到在这种多样化的景观中强调数据建模的重要性的紧迫性,这也是我们撰写本书的一个关键原因。在这些页面中,我们旨在揭示数据建模的复杂性和各个维度,以及它如何支撑更广泛的分析工程领域。
随着时间的推移,数据建模在确保稳固的数据管理系统中的重要性逐渐从大众意识中淡化。这并非因为它变得过时,而是由于行业关注焦点的转移。新词汇、工具和方法涌现出来,使得基本原则变得不那么重要。传统实践向承诺快速和高效的现代解决方案转变,有时导致基础强度的丧失。
分析工程的兴起导致了一次复苏。这不仅仅是一个充满花哨词汇的趋势,而是对商业智能领域原则的回归。不同之处在于,现代工具、基础设施和技术现在可用于更有效地实施这些原则。
那么,为什么我们觉得有必要记录我们的思想呢?主要有两个原因。首先,强调像数据建模这样的成熟概念的持久价值和重要性至关重要。尽管这些方法可能已经存在一段时间了,但它们为现代技术的发展提供了坚实的基础。我们的第二个意图是强调分析工程不是独立的实体,而是从商业智能的遗产自然演变而来的。通过整合这两者,组织可以构建更具弹性的数据价值链,确保其数据不仅广泛而且可操作,从而最终提高其效用。
本书不仅仅是对过去的情感回顾或对现在的评论。它是未来的蓝图。我们的目标是帮助组织重新审视其基础,欣赏旧技术和新技术的优势,并将它们整合为全面的数据管理方法。我们将深入探讨数据建模和转换的细节,解释其重要性,并探讨它如何与现代分析工程工具互动。我们旨在为读者提供全面的理解,使他们能够加强其数据管理流程并充分利用其数据的潜力。
读者对象
本书旨在为处理复杂数据管理和分析世界的专业人士、学生和爱好者而设计。无论您是一位经验丰富的老将回忆起数据建模的基本原则,还是一位渴望理解从商业智能到现代分析工程转变的初学分析师,我们的叙述都确保清晰和方向明确。
寻求加强其数据处理流程的组织将在本书讨论的结合了成熟原则和现代工具的方法中找到巨大价值。简言之,如果您希望通过将过去的优势与现在的创新相结合来充分利用您的数据,本书将为您提供指导。
本书的组织结构
我们将本书分为六章:
第一章,“分析工程”
本章追溯了数据管理从传统的基于 SQL 的系统发展到诸如 Apache Airflow 和 dbt 等创新工具的演变,每一种工具都改变了我们处理和查看数据的方式。分析工程师角色连接数据工程和分析,确保我们的洞察可靠且可操作。尽管工具和角色发生了变化,但数据的重要性和价值仍然至关重要。然而,挑战依然存在,例如数据质量、高效存储以及在 Redshift 等平台上优化计算资源的任务负载平衡,或者在 Snowflake 上设计具有适当大小仓库的高效作业。数据建模,即通过结构化数据反映现实场景,是这些解决方案的核心。
第二章,“用于分析的数据建模”
本章深入探讨了在当今以分析驱动的景观中数据建模的关键作用。我们将调查它如何帮助构建有效分析的数据结构,并探讨数据规范化在减少重复性方面的重要性。虽然我们强调规范化的重要性,但值得注意的是,各种建模方法(如 Kimball 和 One Big Table)根据特定的用例提倡不同的方法,包括去规范化。通过理解这些基本原则,并考虑建模方法的广泛谱系,分析师可以有效地探索数据,确保获得实质性的洞察和明智的决策。在没有强大的数据模型的情况下,无论是按照上下文规范化还是去规范化,分析过程可能会不一致和不准确。
第三章,“SQL 用于分析”
本章探讨了作为首选分析语言的 SQL 的持久力量。我们将从数据库的基础和 SQL 作为与数据库交互的主要语言开始。我们的旅程将涵盖视图在简化查询中的有用性,窗口函数在高级计算中的强大功能,以及通用表达式在优化复杂查询中的灵活性。我们还将讨论 SQL 在分布式数据处理中的作用,并以 SQL 在机器学习模型训练中的激动人心的应用结束。
第四章,“使用 dbt 进行数据转换”
本章详细探讨了 dbt 在初步介绍之外的内容。我们将审视 dbt 在数据分析生命周期中的关键角色,并演示它如何将原始数据转换为结构化和可访问的模型。我们的探索将遍历 dbt 项目结构,讨论模型构建、文档编制和测试等功能,同时提供有关 dbt 文件的洞察,包括 YAML 文件。在本章结束时,您将全面了解 dbt,能够将其无缝整合到您的分析工作流程中。
第五章,“dbt 高级主题”
本章中,我们将深入探讨 dbt 的高级方面。除了视图或表之外,我们还将讨论 dbt 中模型的各种实现方式,包括使用临时模型、数据快照以及实现增量模型以避免常规全量数据加载。此外,我们还将提升我们的分析代码,专注于通过 Jinja、宏和包等技术来优化其效率,以保持 DRY(不重复自己)。最后,我们还将介绍 dbt 语义层,它在原始数据和有意义的洞察之间起着关键作用的桥梁角色。
第六章,“构建端到端分析工程用例”
此结束章节汇总了您在使用 dbt 和 SQL 进行分析工程学习过程中所学到的所有内容。在深入探讨前几章节中的概念、技术和最佳实践之后,我们现在将通过从头开始设计一个完整的分析工程用例,采用 dbt 和 SQL 的能力来实施和部署一个全面的分析解决方案。数据建模将成为焦点。我们的目标是通过整合前几章的见解来演示一个完整的分析工作流程,从数据摄取到报告。在这个过程中,我们将克服普遍存在的挑战,并提供有效应对策略。
本书使用的约定
本书使用以下排版约定:
斜体
表示新术语、URL、电子邮件地址、文件名及文件扩展名。
常量宽度
用于程序清单,以及在段落内引用程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。
常量宽度粗体
显示用户应该按字面输入的命令或其他文本。
常量宽度斜体
显示应由用户提供的值或由上下文确定的值替换的文本。
提示
此元素表示提示或建议。
注
此元素表示一般注释。
使用代码示例
补充材料(代码示例、练习等)可在https://github.com/helder-russa/dbt-analytics-engineer下载。
如果您在使用代码示例时遇到技术问题或困惑,请发送电子邮件至bookquestions@oreilly.com。
本书旨在帮助您完成工作。通常情况下,如果本书提供了示例代码,您可以在自己的程序和文档中使用它。除非您复制了代码的大部分内容,否则不需要联系我们以获得许可。例如,编写一个使用本书多个代码片段的程序不需要许可。销售或分发 O’Reilly 图书中的示例代码需要许可。通过引用本书并引用示例代码来回答问题不需要许可。将本书大量示例代码整合到您产品的文档中需要许可。
我们感谢,但通常不需要署名。署名通常包括标题、作者、出版商和 ISBN。例如:“SQL 与 dbt 的分析工程,作者 Rui Machado 和 Hélder Russa(O’Reilly)。版权所有 2024 Rui Pedro Machado 和 Hélder Russa,978-1-098-14238-4。”
如果您认为您使用的代码示例超出了合理使用范围或上述许可的范围,请随时通过permissions@oreilly.com与我们联系。
O’Reilly 在线学习
注
40 多年来,O’Reilly Media已经为企业提供技术和业务培训、知识和见解,帮助它们取得成功。
我们独特的专家和创新者网络通过图书、文章和我们的在线学习平台分享他们的知识和专业知识。O’Reilly 的在线学习平台为您提供按需访问实时培训课程、深入学习路径、交互式编码环境,以及来自 O’Reilly 和其他 200 多家出版商的广泛文本和视频。欲了解更多信息,请访问https://oreilly.com。
如何联系我们
请将有关本书的评论和问题发送至出版商:
-
O’Reilly Media, Inc.
-
Gravenstein Highway North 1005
-
Sebastopol, CA 95472
-
800-889-8969(美国或加拿大)
-
707-829-7019(国际或本地)
-
707-829-0104(传真)
-
support@oreilly.com
我们为本书创建了一个网页,列出勘误、示例和任何额外信息。您可以访问此页面:https://oreil.ly/analytics-engineering-SQL-dbt。
有关我们的图书和课程的新闻和信息,请访问https://oreilly.com。
在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media
在 Twitter 上关注我们:https://twitter.com/oreillymedia
在 YouTube 上观看我们:https://youtube.com/oreillymedia
致谢
我想特别向我的妻子 Ana 和我的两个可爱女儿 Mimi 和 Magui 发送一条特别的信息。你们每天都激励着我相信自己,并坚定不移地追求我的梦想,因为我为自己取得的成就,也是为了我们。最重要的是,我希望向我的女儿们展示,只要我们有决心,任何事情都是可能的。最后,我需要感谢我的朋友和合作者 Hélder,因为他的坚韧和不屈不挠让这个梦想得以实现。
Rui Machado
我想要感谢我的(未来的)妻子一直以来的陪伴。在最艰难的时刻,她的耐心和鼓励是我坚强的支柱。同时,特别感谢我的父母。没有他们的支持和努力,我肯定无法继续我的学业和追逐梦想,也就不可能完成这本书。再次真心感谢他们。最后,感谢所有给予我支持的匿名和非匿名的朋友们,以及合作者 Rui,他们的积极反馈和建设性意见极大丰富了本书的内容。
Hélder Russa
第一章:分析工程
分析的发展历程包括许多重要的里程碑和技术,这些里程碑和技术塑造了今天的分析领域。它始于 1980 年代数据仓库的出现,为组织和分析商业数据建立了基础框架。计算机科学家比尔·因蒙(Bill Inmon)在 1980 和 1990 年代持续发表作品,被广泛认为是为数据仓库提供了第一个坚实的理论基础。
随后的发 在 Ralph Kimball,另一位数据仓库和商业智能(BI)领域的主要贡献者,于 1996 年发表了他的影响力巨著《数据仓库工具箱》之后,数据仓库和商业智能的发展迎来了又一波浪潮。Kimball 的工作为维度建模奠定了基础,标志着分析发展中的另一个关键里程碑。Inmon 和 Kimball 的贡献,跨越 20 世纪末,发挥了在塑造数据仓库和分析领域的关键作用。
在 2000 年初,谷歌和亚马逊等科技巨头的崛起,催生了对更先进的数据处理解决方案的需求,促成了 Google 文件系统和 Apache Hadoop 的发布。这标志着大数据工程时代的到来,专业人士利用 Hadoop 框架处理海量数据。
像亚马逊网络服务(AWS)这样的公共云提供商的崛起,彻底改变了软件和数据应用的开发与部署方式。AWS 的一个开创性产品是亚马逊 Redshift,于 2012 年推出。它代表了一种在线分析处理(OLAP)与传统数据库技术的有趣结合。在 Redshift 的早期阶段,数据库管理员需要管理如清理和扩展等任务,以维持最佳性能。
随着时间的推移,云原生技术不断发展,Redshift 本身也经历了重大提升。在保留其核心优势的同时,Redshift 的新版本,以及 Google BigQuery 和 Snowflake 等云原生平台,简化了许多管理任务,向各类企业提供了先进的数据处理能力。这一发展突显了云数据处理生态系统内持续的创新。
现代数据堆栈,包括诸如 Apache Airflow、数据构建工具(dbt)和 Looker 等工具,进一步改变了数据工作流。随着这些进步,"大数据工程师"这个术语已经过时,为数据工程师更广泛和更包容的角色让路。这种转变得到了 Maxime Beauchemin 的影响深远的文章的认可——他是 Apache Superset 和 Airflow 的创造者之一,也是 Facebook 和 Airbnb 的首批数据工程师之一——尤其是他的文章"数据工程师的崛起",强调了数据工程在行业中日益重要的地位。所有这些数据领域的快速发展都导致了数据专业人员角色的重大变化。随着数据工具的出现,简单任务正变成战略任务。
如今的数据工程师拥有多方面的角色,包括数据建模、质量保证、安全性、数据管理、架构设计和编排。他们越来越多地采纳软件工程的实践和概念,如功能化数据工程和声明式编程,以增强他们的工作流程。虽然 Python 和结构化查询语言(SQL)在数据工程师中表现突出,但值得注意的是,程序设计语言的选择在这个领域可以因项目的具体需求和偏好而广泛变化。工程师们可能利用其他语言,如 Java(通常用于管理 Apache Spark 和 Beam)、Scala(在 Spark 和 Beam 生态系统中也很普遍)、Go 等。在大型组织中,Java 和 SQL 等语言的组合也是数据工程师中常见的。
组织日益向分散的数据团队、自助服务平台和替代数据存储选项转移。随着数据工程师不得不适应所有这些市场变化,我们经常看到一些人承担更加技术化的角色,专注于平台的启用。其他数据工程师更接近业务,设计、实施和维护将原始数据转化为高价值信息的系统,因此适应这个快速发展的行业,每天都为市场带来新工具,促使了分析工程的奇妙世界的产生。
在本章中,我们介绍了分析工程领域及其在数据驱动决策过程中的角色。我们讨论了分析工程在今天数据驱动世界中的重要性,以及分析工程师的主要角色。此外,我们将探讨分析工程生命周期如何用于管理分析过程,并如何确保生成的数据和见解的质量和准确性。我们还将讨论正在塑造分析工程领域的当前趋势和技术,从历史到现在,涉及到像数据网格这样的新兴概念,并讨论全球采用的诸多数据建模技术之间的基本选择,同时也触及了抽取、加载和转换(ELT)与抽取、转换和加载(ETL)策略。
数据库及其对分析工程的影响
长期以来,数据越来越成为公司关注的焦点,这些公司希望在竞争中保持领先、改善内部流程,或仅仅理解其客户的行为。随着新工具、新工作方式和数据科学、商业智能等新知识领域的出现,如今完全调查和理解数据景观变得越来越困难。
技术的自然进步导致了数据分析、可视化和存储工具的过剩供应,每种工具都提供独特的功能和能力。然而,这些工具的加速部署导致了碎片化的景观,个人和组织需要保持与最新技术发展的同步,同时在如何使用它们上做出明智选择。有时这种丰富会造成困惑,并需要持续的学习和适应。
工作实践的演变伴随着工具的多样化。动态和敏捷的方法论取代了数据管理和分析的传统方法。迭代实践和跨功能协作为数据项目引入了灵活性和速度,但同时也在协调各种团队和角色之间的工作流程上带来挑战。有效的沟通和对齐至关重要,因为数据流程的各个方面融合在一起,这要求对这些新型工作实践有全面的理解。
数据科学和商业智能(BI)等专业领域也增加了数据领域的复杂性。数据科学家应用先进的统计和机器学习技术来检测复杂模式,而商业智能专家则从原始数据中提取有价值的信息,产生实用的见解。这些专业领域引入了精细的技术,需要定期的技能发展和学习。成功采用这些实践需要致力于教育,并灵活掌握技能获取的方法。
随着数据在数字领域的传播,它带来了意想不到的数量、种类和速度。数据的涌入,以及现代数据源(如物联网设备和无组织文本)的复杂特性,使得数据管理变得更加严峻。将数据整合、转换和评估数据精度的细节变得更加明显,强调了需要确保可靠和精确洞见的强大方法。
数据世界的多面性增加了其复杂性。作为各个领域技能汇聚的结果,包括计算机科学、统计学和领域特定的熟练程度,需要一种协作和沟通的策略。这种跨学科的互动突显了有效团队合作和知识分享的重要性。
但情况并非始终如此。几十年来,电子表格一直是存储、管理和分析各级别数据的标准技术,无论是用于业务运营管理还是用于分析以理解它。然而,随着企业变得更加复杂,对数据相关决策的需求也在增加。其中第一个改变的形式是被称为数据库的革命。数据库可以定义为有组织的、结构化信息或数据的集合,通常以电子方式存储在计算机系统中。这些数据可以是文本、数字、图像或其他类型的数字信息。数据以一种便于访问和检索的方式存储,使用一组预定义的规则和结构,称为模式。
数据库在分析中是不可或缺的,因为它们提供了一种有效存储、组织和检索大量数据的方式,允许分析人员轻松访问他们需要进行复杂分析所需的数据,以获得其他情况下难以获得的洞见。此外,可以配置数据库以确保数据完整性,这保证了正在分析的数据准确和一致,从而使分析更可靠和值得信赖。
在分析中使用数据库的最常见方法之一是数据仓库技术,即构建和使用数据仓库。数据仓库是一个大型的、集中式的数据存储,旨在简化数据使用。数据仓库中的数据通常来自多种来源,例如事务系统、外部数据源和其他数据库。然后对数据进行清洗、转换,并集成到一个统一的数据模型中,通常遵循星型模式或数据仓库等维度建模技术。
数据库在分析中的另一个重要用途是数据挖掘过程。数据挖掘使用统计和机器学习技术来发现大型数据集中的模式和关系。通过这种方式,可以识别趋势、预测未来行为以及进行其他类型的预测。
数据库技术和数据科学家在数据科学的兴起中发挥了关键作用,通过提供一种有效存储、组织和检索大量数据的方式,使数据科学家能够处理大数据集并专注于重要的事务:从数据中获取知识。
使用 SQL 和其他编程语言(如 Python 或 Scala),可以与数据库进行交互,使数据科学家能够执行复杂的数据查询和操作。此外,数据可视化工具如 Tableau 和 Microsoft Power BI 与数据库引擎轻松集成,使数据科学家能够以清晰直观的方式呈现其发现。
随着大数据的出现和存储及处理大规模数据集的需求增长,出现了各种数据库技术以满足不同的需求。例如,数据分析师经常依赖数据库进行广泛的应用,包括数据仓库、数据挖掘以及与 Tableau 等 BI 工具的集成。
然而,深入探讨这些用例以理解分析工程的必要性至关重要。当将 BI 工具直接连接到运营数据库(在线事务处理[OLTP]副本)时,性能和可伸缩性可能受到限制。这种方法对于较小的数据集和简单的查询可能效果良好,但随着数据量的增长和分析复杂性的增加,可能会导致性能瓶颈和子优化的查询响应时间。
这就是分析工程的发挥作用之处。分析工程师是优化数据工作流程、转换和聚合数据的专家,确保数据以适合分析任务的正确格式存在。他们设计和维护数据管道,从各种来源 ETL 数据到优化的数据仓库或数据湖中。通过这样做,他们帮助组织克服直接 OLTP 连接的限制,利用像 Tableau 这样的工具进行更快速、更高效的数据分析。本质上,分析工程弥合了原始数据与可操作洞见之间的差距,确保数据分析师和科学家能够有效地处理大规模、复杂的数据集。
云计算及其对分析工程的影响
在过去几十年中,世界面临了一系列具有重大技术影响的复杂挑战。经济衰退推动了金融技术和风险管理系统的创新。地缘政治紧张局势要求在保护关键基础设施和敏感数据方面进行网络安全的进步。全球健康危机凸显了先进数据分析和预测建模在疾病监测和管理中的重要性。此外,迫切需要应对气候变化推动了先进的可再生能源技术和可持续工程解决方案的发展,以实现气候目标。
在面对这些挑战时,追求利润和增长仍然是全球企业的关键驱动力。然而,人力劳动时间的价值已经具备了新的维度,这导致企业运营方式和云计算如何容纳它们发生了显著变化。这种变化反映在越来越多地采用减少对全职支持人员(如数据库管理员)依赖的托管和无服务器产品上。
随着公司适应这一变化的格局,创新、差异化和商业模型与战略的可持续性已成为寻求在快速变化的世界中成功的公司的重要考量。在这种情况下,信息技术和系统行业发现了在帮助组织克服这个充满不确定性和压力的世界中增强其能力的好机会。操作模型的合理化变得迫在眉睫,需要重新评估数据中心和定价结构。此外,产品和服务的提供必须主要侧重于易用性、低延迟、提高安全性、更广泛的实时工具、更多的集成、更智能化、更少的代码以及更快的上市时间。
组织已经意识到投资于创新工具、推动数字转型以及采用以数据为中心的决策方法的重要性,以实现更大的灵活性和竞争优势。为实现这些目标,许多公司正在专注于利用来自内部和外部来源的精心策划的数据。这些精心构建的数据可以为业务绩效提供宝贵的见解。
在行业中,以可访问的格式创建、可视化和分析互联的业务数据的做法通常被称为数据分析。从历史上看,它也被称为商业智能,这两个术语密切相关。虽然商业智能是分析的一个子集,专注于面向业务的决策,但数据分析涵盖了更广泛的范围,包括产品分析、运营分析和其他几个专业领域。商业智能和数据分析在通过数据驱动的见解帮助组织获得竞争优势方面都发挥着关键作用。
尽管数据分析为改进和重塑业务战略、监控绩效提供了诸多好处,但其需要在服务器、软件许可证以及数据工程师、数据科学家和数据可视化专家等专业人员方面进行重大的财务投入。在经济危机期间,与 IT 硬件、软件和专业人员相关的高前期和运营成本被认为是不切实际和不吸引人的。
因此,设在公司自有场地上并由公司自行管理数据分析基础设施的本地解决方案,对于对此概念不熟悉的分析新手来说,吸引力已大不如前。本地解决方案通常需要大量投资于硬件、软件和持续维护。与基于云的数据分析解决方案相比,它们也缺乏灵活性和可伸缩性。偏好转移促使新的基于云的数据分析解决方案为满足传统数据分析的类似业务需求铺平了道路。然而,与基于本地服务器和软件依赖相比,基于云的解决方案利用云计算服务加速部署并减少基础设施成本。
云计算在各行各业的日益普及,促使微软、谷歌和亚马逊等软件供应商开发了先进的数据分析和数据仓库工具。这些工具设计为在云计算范式下运行,并利用共享网络资源,以实现更大的可访问性和简化的部署。这一趋势的生动例子是微软的综合数据分析平台,Microsoft Fabric。
同时,dbt Labs 的 dbt 产品,在本书稍后会详细讨论,作为一种多功能混合产品脱颖而出。dbt 与 Hadoop 类似,是一种开源解决方案,使用户能够根据其特定需求进行部署,无论是在云端还是本地。在其云端版本中,dbt 与包括 Microsoft Azure、Google Cloud Platform (GCP) 和 AWS 在内的主要云平台无缝集成。这种开源性质使得组织能够根据其独特的需求和基础设施偏好定制其部署方案。
尽管基于云的数据分析解决方案和平台是全球趋势和现代数据平台的核心概念,但重要的是要认识到,云计算解决方案带来了既有利也有风险的影响,这些风险不容忽视。这些风险包括潜在的安全问题、服务器的物理位置以及从特定提供商迁移所带来的成本。
尽管如此,云技术目前正在改变组织部署和构建信息系统和技术解决方案的方式,数据分析也不例外。因此,认识到转向云端不久将不再是一种选择,而是一种必要性,变得至关重要。理解作为服务的分析解决方案的好处至关重要。否则,如果不解决这一过渡问题,提供给决策者的及时信息在缺乏灵活性和可伸缩性的本地解决方案中可能会变得越来越具挑战性。
然而,尽管云技术带来了多种好处,如规模经济和灵活性,它们也带来了信息安全问题。数据集中存储在云基础设施中,使其成为未经授权攻击的吸引目标。在云数据环境中成功,组织必须理解并减轻与云计算相关的风险。关键风险包括数据隐私、控制丧失、数据不完整或不安全删除、未经授权的内部访问、数据可用性和复杂的成本计算。
数据隐私是一个重大关注点,因为验证供应商是否按照法律和标准处理数据是具有挑战性的,尽管供应商的公共审计报告可以帮助建立信任。在非集成的场景中,数据在各种系统和数据中心之间流动,数据安全风险倍增,增加了截取和同步化的风险。另一个重要的风险是供应商依赖性,这种依赖性发生在数据管理责任完全落在一个服务提供商身上,从而限制了迁移到其他解决方案的能力。这种依赖性最终限制了组织对决策的控制和对数据的授权。虽然这些只是一些已知的风险,但我们已经可以理解,组织需要有效地掌握这些风险,以有效地享受基于云的数据分析解决方案的好处。这需要仔细考虑、遵循安全标准和最佳实践,并持续控制成本以衡量投资回报。
如果所有风险在正确的数据战略中得到有效解决和缓解,这份战略详细描述了组织如何管理其信息资产,包括云战略、技术、流程、人员和相关规则,那么相比没有数据战略的组织,组织可以获得显著的竞争优势。通过专注于云计算并利用云数据平台,组织可以将原始数据转化为有意义的见解,加速建立坚实的数据基础的过程。这使得相关数据的高效采集、结构化和分析成为可能,甚至支持 AI 技术的采用,同时在比传统方法更短的时间内和更低的成本下创造价值。
有趣的是,云数据平台、分析和 AI 之间的关系是相互促进的。实施云数据平台加速了分析驱动的架构的采用,并实现了 AI 计划的全面运作。它赋予组织使用所有相关数据的能力,获得企业范围的见解,并开启新的商业机会。通过消除管理多个工具的需求,组织可以专注于数据现代化,加速洞察的发现,并从现有技术合作中获益,从而推动其 AI 之旅。
这也是为什么说,云计算已经成为现代数据平台以及基于云的分析和人工智能平台的核心组成部分,这些平台每天都在不断增长,从而促成了这个行业的颠覆。
数据分析生命周期
数据分析生命周期是将原始数据转化为有价值且易消化的数据产品的一系列步骤。这些产品可以是良好管理的数据集,仪表盘,报告,API 甚至是 Web 应用程序。换句话说,它描述了如何创建,收集,处理,使用和分析数据,以实现特定的产品或业务目标。
组织动态的不断复杂化直接影响数据处理方式。许多人必须使用相同的数据,但目标不同。虽然高级主管可能只需了解几个顶层关键绩效指标来跟踪业务绩效,而中级管理人员可能需要更详细的报告来支持日常决策。
这突显了基于相同数据基础创建和维护数据产品时需要一种受管制和标准化方法的必要性。鉴于组织必须在其数据治理,技术和管理流程方面做出许多决策,遵循一种结构化方法对于记录和持续更新组织的数据战略至关重要。
因此,数据分析生命周期是理解和映射创建和维护分析解决方案涉及的阶段和过程的重要框架(图 1-1)。它是数据科学和分析的重要概念,并提供了一种结构化方法来管理创建有效分析解决方案所需的各种任务和活动。

图 1-1. 数据分析生命周期
数据分析生命周期通常包括以下阶段:
问题定义
分析周期的第一阶段涉及理解需要解决的问题。这包括识别业务目标,可用数据以及解决问题所需的资源。
数据建模
在确定业务需求并完成数据源评估后,您可以根据最符合您需求的建模技术开始对数据进行建模。您可以选择菱形策略,星型模式,数据仓库或完全非规范化技术。所有这些概念将在第二章中讨论。
数据摄入和转换
下一阶段是摄取和准备来自源系统的数据,以匹配创建的模型。根据整体信息架构,您可以选择写入模式,其中您将更多的精力放在将原始数据直接转换为您的模型中,或者读取模式,在这种模式下,您摄取和存储数据的时候进行最小的转换,并将大量转换移到数据平台的下游层面。
数据存储和结构化
一旦设计并可能实施数据管道,您需要决定使用的文件格式——简单的 Apache Parquet 或更高级的格式如 Delta Lake 或 Apache Iceberg——以及分区策略和存储组件——基于云的对象存储如 Amazon Simple Storage Service (S3),或者像 Redshift、BigQuery 或 Snowflake 这样的更类似数据仓库的平台。
数据可视化和分析
一旦数据可用,下一步是探索它、可视化它,或者创建直接支持决策或启用业务流程监控的仪表板。这个阶段非常业务导向,应与业务利益相关者密切协调。
数据质量监控、测试和文档化
尽管在分析生命周期的最后阶段,数据质量应该是一个端到端的关注点,并且在整个流程中通过设计来保证。它涉及实施所有质量控制,以确保利益相关者可以信任您的公开数据模型,记录所有转换和语义含义,并确保在数据继续流动的过程中沿着管道进行适当的测试。
注意
使用 dbt,这些组件中的几个可以更轻松、更高效地部署,因为它允许我们并行跨生命周期构建它们。文档编制、测试和质量成为并行执行的常见任务。这将在第四章中广泛阐述。
分析生命周期是一个关键概念,使组织能够以结构化和一致的方式处理数据工程、科学和分析过程。通过遵循结构化的过程,组织可以确保他们解决正确的问题,使用正确的数据,并构建准确可靠的数据产品,最终实现更好的决策和更好的业务结果。
分析工程师的新角色
如前所述,数据科学家和分析师现在可以轻松访问他们需要执行复杂分析并获得洞察力的数据。然而,随着存储和分析的数据量继续增长,对组织来说拥有数据专家来帮助他们管理数据并提供所需的基础设施变得越来越重要。
最近成立的一类专门数据工程师分支,称为分析工程师,在开发和维护数据库和数据管道中发挥着重要作用,使数据科学家和分析师能够专注于更高级的分析任务。分析工程师负责设计、构建和维护数据架构,使组织能够将数据转化为有价值的见解,并做出数据驱动的决策。
此外,从传统的强制写入模式的 ETL 过程转向 ELT 模式,即先写后读的方法,意味着数据现在在被转换之前就已经进入了数据仓库。这为那些既非常了解业务又具备技术技能,能将原始数据建模为清洁、明确定义数据集的超级技术分析师,即分析工程师,提供了机会。如果您在数据仓库和 ETL 范式的世界中寻找这些类型的技能,将需要同时具备软件工程和数据分析技能——这将更难找到。
分析工程师充当了数据平台工程师和专注于将数据转化为见解型数据产品的数据分析师之间的桥梁。他们的工作是创建经过良好测试、最新、有文档记录的数据集,整个组织可以用来回答他们自己的问题。他们具备足够的技术能力,能够应用软件开发最佳实践,如版本控制和持续集成/持续部署(CI/CD),但也需要能够有效地与利益相关者沟通。
我们可以类比到土木工程:数据平台工程师是分析项目的基础,负责确保基础设施的稳固性,包括管道、电气系统和结构基础。他们为接下来的所有工作奠定了基础。
分析工程师可以被类比为建筑师。他们利用数据工程师创建的坚实基础,设计符合业务模型的结构,从出色的仪表板到有价值的数据模型,构建一切。他们弥合了技术基础设施与业务目标之间的差距。
在这个类比中,数据分析师则扮演室内设计师的角色。他们步入建筑内部,不仅确保内容与用户一致,而且使其用户友好并根据数据消费者的特定需求定制。这些角色共同合作,创建一个全面和功能齐全的分析环境。
看看数据分析生命周期,数据平台工程师建立平台并将原始数据导入企业级数据存储中。另一方面,分析工程师将原始数据转换,以匹配业务需要支持决策的分析数据模型。
分析工程师的责任
随着数据量和复杂性以及其多样化应用的增长,分析工程师的角色变得越来越重要。这包括从设计和实现数据存储和检索系统,到创建和维护数据管道,再到开发和部署机器学习模型。在这个动态的景观中,分析工程师在利用日益增长的数据资源并在各种应用中最大化其价值方面发挥着关键作用。
基于最新的角色趋势,主要职责之一是设计和实现高效的数据存储和检索系统。这包括与数据库和数据仓库技术合作,设计能处理大型和复杂数据集的数据模型和结构。另一个直接的职责是创建和维护数据管道,从各种来源提取数据,进行转换,并将其加载到中央库进行分析。
对于大多数分析工程师来说,开发和使用机器学习模型的过程可能不太明显,但仍在进行中。这包括与数据科学家合作,理解他们的需求,选择和实现适当的算法,并确保模型经过正确的训练和部署,并使用正确的训练和测试数据集。在这种情况下,分析工程师会合作建立适当的数据管道,以持续提供适当的训练和测试数据给数据科学家。
此外,分析工程师还负责监控和维护机器学习模型的性能,通过帮助结构化离线评估和将模型特定的指标与业务指标结合,进行在线监控。
分析工程师通常精通编程语言和工具,如 Python、R、SQL 和 Spark,用于实施数据管道、数据模型和机器学习模型。他们还应熟悉像 AWS、GCP 或 Azure 等云计算平台,以部署和扩展他们的解决方案。
在观察分析工程师在几家公司中的职责时,可以包括以下内容:
-
设计和实现数据存储和检索系统,如数据库和数据仓库,以处理大型和复杂数据集。创建和维护数据管道,从各种来源提取、转换和加载数据到中央库进行分析。
-
通过执行数据质量检查、跟踪数据流动并实施数据安全措施,确保数据准确、完整、一致和可访问。
-
利用 AWS、GCP 或 Azure 等云计算平台部署和扩展分析解决方案,以及数据基础设施的可伸缩性、安全性和成本优化。
-
优化数据存储和检索系统的性能,数据管道以及机器学习模型,确保它们能够处理大量和复杂性的数据。
-
使用 Python、R、SQL 和 Spark 等编程语言和工具实现数据管道、数据模型和机器学习模型。
-
与数据科学家合作,了解他们的需求,选择并实施适当的算法,确保机器学习模型得到正确训练和部署。监控和维护机器学习模型的性能,并根据需要进行故障排除和优化。
-
保持与数据工程、机器学习和分析的最新技术和趋势同步,并持续寻求改进组织的数据基础设施和分析能力的机会。
分析师的角色广泛,需要结合技术技能、问题解决能力和对业务需求的理解。分析工程师必须熟悉数据科学的技术和业务方面,并能够弥合数据科学家和 IT 之间的鸿沟。
在数据网格中启用分析
数据网格是一种现代框架,概述了组织的数据战略。它使业务域团队能够负责其数据及提供对其的访问的服务,而不仅仅依赖于中心数据团队。它将单片数据架构分解为一组独立的、自治的数据服务,实现了更精细的扩展、更多的自治和更好的数据管理。它在处理不同类型的数据时提供了更大的灵活性,并促进了实验、创新和协作文化。借助数据网格,企业应能更快速地移动并更快速地响应业务变化的需求。
数据网格方法论作为一种架构模式的出现,彻底改变了分析师与数据基础设施互动的方式。通过将单片数据架构分解为一系列独立的、自治的数据服务,这些服务可以独立开发、部署和运营,团队可以更细粒度和轻松地解决可扩展性、可管理性和数据架构自治性的挑战。
采用这种新颖的方法,团队可以更细粒度地扩展其数据基础设施,降低数据孤岛和重复数据的风险。每个业务域团队也拥有更多的自治权,可以根据特定需求选择最佳工具和技术,但利用中心提供的服务来管理整个数据生命周期。这使他们能够更快速、更灵活地响应业务变化的需求。此外,数据网格方法还提供了处理结构化、半结构化和非结构化数据的更大灵活性。通过分解单片数据架构并实现数据服务的清晰映射,还能促进更好的数据治理实践。
在数据网格组织中,分析工程师可以通过专注于构建和维护支持多个团队和应用程序需求的独立、自治的数据服务来提供价值,例如共享数据模型,良好治理和文档化,以确保数据的轻松发现、可访问性和安全性。
在数据网格工作的另一个有意义的方面是确保数据治理和安全性,这可能包括实施数据访问控制、数据排序和数据质量检查等数据政策和程序,以确保数据安全和高质量。此外,分析工程师应与数据所有者和利益相关者合作,了解并遵守所有数据存储和管理的法规要求。
在数据网格中工作需要与传统的单片式数据架构有不同的思维方式。分析工程师必须摆脱数据是集中资源的观念,而将其视为分布式自治服务,各团队可以使用。
数据产品
我们一直在使用的另一个重要概念是数据产品。这些是可访问的应用程序,提供访问数据驱动洞见的支持,将支持业务决策过程甚至自动化它们。在内部,它们可能包含用于检索、转换、分析和解释数据的组件。另一个重要方面是,数据产品应该以一种方式暴露它们的数据,使其可以被其他内部或外部应用程序或服务访问和使用。
一些数据产品的例子如下:
-
一个 REST API,允许用户查询特定的业务数据模型。
-
从各种来源摄取和处理数据的数据管道。
-
存储和管理大量结构化和非结构化数据的数据湖。
-
帮助用户理解和传达数据洞见的数据可视化工具。
数据产品也可以包括微服务。这些是小型、独立和专注的服务,可以独立开发、部署和扩展。它们可以通过 API 访问,并在整个企业中重复使用。
dbt 作为数据网格的促进者。
dbt 是一个开源工具,帮助数据工程师、分析工程师和数据分析师通过提供创建、测试和管理数据服务的方式构建数据网格。它允许团队定义、测试和构建数据模型,并为这些模型创建清晰和明确定义的接口,以便其他团队和应用程序可以轻松使用。
支持创建数据网格的 dbt 特性包括以下内容:
数据建模能力
数据建模能力允许团队使用简单且熟悉的基于 SQL 的语法定义其数据模型,使得数据工程师和数据分析师可以轻松地共同定义和测试数据模型。
数据测试能力
dbt 提供了一个测试框架,允许团队测试其数据模型,并确保其准确和可靠。这有助于在开发过程的早期发现错误,并确保数据服务的高质量。
数据文档
dbt 使数据模型和服务能够被文档化,以便其他团队和应用程序能够轻松理解和使用。
数据跟踪能力
数据跟踪能力使团队能够追踪数据模型的来源。这使得理解数据的使用方式和来源变得容易。
数据治理能力
数据治理能力使得执行数据访问控制、数据血统和数据质量检查等数据治理政策成为可能,有助于确保数据安全和高质量。
尽管数据分析工程的主要焦点是设计和实施数据模型,但重要的是要注意,数据跟踪和治理能力可以显著增强分析工程流程的有效性。在需要数据模型追踪数据来源并遵守严格的数据治理政策的情况下,这些能力尤其有价值。采用这些实践和治理模型,包括数据网格,可能会因数据环境的具体需求和复杂性而有所不同。许多成功的 dbt 部署从较简单的单星模式数据模型开始,并且随着时间推移可能探索像数据网格这样的高级概念。
数据分析工程的核心
数据转换 将数据从一种格式或结构转换为另一种,使其更加有用或适合特定的应用或目的。这个过程是必要的,因为它使组织能够将原始、非结构化的数据转换为有价值的见解,从而可以指导业务决策,改进运营并推动增长。
数据转换是分析生命周期中的关键步骤,组织必须拥有有效和高效执行此任务的工具和技术非常重要。数据转换的一些示例包括数据清理和准备、数据聚合和汇总,以及通过添加额外信息丰富数据。由于 dbt 允许组织快速、轻松地执行复杂的数据转换任务,并且可以与其他工具(如 Airflow)集成用于端到端数据管道管理,因此 dbt 在数据转换中的使用非常广泛。
对于分析师和企业利益相关者来说,dbt 是现场。当数据被转换并以易于使用的形式交付时,对企业和利益相关者的价值得以体现。
提示
现场(Gemba) 是一个日语术语,意思是“真实的地方”。在企业界,现场指的是价值创造的地方。
在 ETL 策略中,数据转换通常在数据加载到目标系统(如数据仓库或数据湖)之前执行。数据从各种来源提取,转换以匹配目标系统的结构和格式,然后加载到目标系统中。这个过程确保数据在系统和应用程序之间是一致且可用的。
相比之下,ELT 策略代表了一种更新颖、更灵活的数据处理方法。在这种策略中,数据首先被提取并加载到目标系统中,然后再进行转换。ELT 提供了多种优势,包括增加的灵活性和支持更广泛数据应用能力,超过了传统 ETL 范式。其一个显著的优势在于,它能够在目标系统内直接进行各种数据转换和实时洞察。这种灵活性使组织能够更快速地从数据中获得可操作的洞察,并适应不断变化的分析需求。
然而,需要指出的是,ELT 可能伴随着更高的存储和摄入成本,因为需要存储原始或最小转换后的数据。许多企业认为这些成本是合理的,因为它们为其运营带来了重大价值,特别是灵活性。因此,随着基于云的数据仓库解决方案的出现以及它们提供的改进的数据转换和处理能力,ELT 越来越受欢迎。
无论使用哪种策略,如果没有适当的数据清洗、转换和标准化,数据可能会变得不准确、不完整或难以使用,从而导致糟糕的决策。
传统流程
传统上,传统的 ETL 过程往往复杂、耗时,并需要专门的技能来开发、实施和维护。它们通常还需要大量的手工编码和数据操作,使其容易出错且难以扩展。
此外,这些过程通常缺乏灵活性,无法适应不断变化的业务需求或新的数据来源。随着数据量、种类和速度的增长,传统的 ETL(抽取-转换-加载)过程变得越来越不足够,因此正在被更现代、更灵活的 ELT(抽取-加载-转换)方法所取代。
过去,通常使用自定义脚本或专门的基于可视化的 ETL 工具来执行 ETL。这些脚本或工具从各种来源(如平面文件或数据库)提取数据,对数据进行必要的转换,然后将数据加载到目标系统,如数据仓库中。
一个传统 ETL 过程的示例可能是使用 SQL 脚本和编程语言(如 Java 或 C#)来从关系数据库提取数据,使用编程语言转换数据,然后将转换后的数据加载到数据仓库中。另一个例子是使用专用 ETL 工具如 Oracle Data Integrator 或 IBM InfoSphere DataStage 来在系统间提取、转换和加载数据。这些传统 ETL 过程可能很复杂,难以维护和扩展,并且通常需要专门的开发团队。
使用 SQL 和存储过程进行 ETL/ELT
在过去,特定的数据平台使用存储过程在关系数据库管理系统(RDBMS)如 SQL Server 或 Oracle 中进行 ETL。存储过程是预先编写的 SQL 代码,可以存储在数据库引擎中,以便可以重复使用该代码。根据是数据流入还是流出,脚本会在源数据库或目标数据库中执行。
假设你想要创建一个简单的存储过程来从表中提取数据,转换数据,并将其加载到另一个表中,如示例 1-1 所示。
示例 1-1. 提取数据的 SQL 过程
CREATE PROCEDURE etl_example AS
BEGIN
-- Extract data from the source table
SELECT * INTO #temp_table FROM source_table;
-- Transform data
UPDATE #temp_table
SET column1 = UPPER(column1),
column2 = column2 * 2;
-- Load data into the target table
INSERT INTO target_table
SELECT * FROM #temp_table;
END
该存储过程首先使用SELECT INTO语句从源表中提取所有数据并将其存储在临时表(#temp_table)中。然后它使用UPDATE语句将column1的值改为大写并将column2的值加倍。最后,存储过程使用INSERT INTO语句将#temp_table中的数据加载到target_table中。
注意
如果你对 SQL 语法不熟悉,不必担心。第三章专门为您提供使用基础知识。
需要注意的是这只是一个基础示例,实际的 ETL 过程通常要复杂得多,并涉及许多步骤,如数据验证、处理空值和错误以及记录处理结果。
虽然可以使用存储过程进行 ETL 过程,但需要注意使用它们可能会有一些影响,比如需要专业知识和专长来编写和维护这些过程,以及灵活性和可扩展性的缺乏。此外,使用存储过程进行 ETL 可能会使与其他系统和技术集成以及解决 ETL 过程中出现的问题变得困难。
使用 ETL 工具
正如先前提到的,ETL 工具是一种软件应用程序,通过提供可视化界面、软件开发工具包(SDK)或编程库以及预打包的代码和工件,加速构建摄取和转换管道的过程。这些工具可以用于从各种来源提取、转换和加载数据到目标位置,如数据仓库或数据湖。许多组织通常使用它们来自动化将数据从各个系统和数据库传输到中央数据仓库或数据湖的过程,以便进行分析。
Airflow 是一个流行的开源平台,用于管理和调度数据管道。由 Airbnb 开发,近年来因其灵活性和可伸缩性而广受欢迎。Airflow 允许用户使用 Python 代码定义、调度和监视数据管道,使其对数据工程师和科学家而言易于创建。
示例 1-2 展示了一个简单的 Airflow DAG。一个有向无环图(DAG)是一个没有有向环路的有向图。
示例 1-2. 一个 Airflow DAG
from airflow import DAG
from airflow.operators.bash_operator import BashOperator
from datetime import datetime, timedelta
default_args = {
'owner': 'me',
'start_date': datetime(2022, 1, 1),
'depends_on_past': False,
'retries': 1,
'retry_delay': timedelta(minutes=5),
}
dag = DAG(
'simple_dag',
default_args=default_args,
schedule_interval=timedelta(hours=1),
)
task1 = BashOperator(
task_id='print_date',
bash_command='date',
dag=dag,
)
task2 = BashOperator(
task_id='sleep',
bash_command='sleep 5',
retries=3,
dag=dag,
)
task1 >> task2
此代码定义了一个名为simple_dag的 DAG,每小时运行一次。它有两个任务,print_date和sleep。第一个任务执行date命令,打印当前日期和时间。第二个任务执行sleep 5命令,使任务休眠五秒。第二个任务的重试次数设置为 3 次。因此,如果任务失败,它将在放弃之前重试三次。这两个任务由运算符>>连接。这也意味着task2依赖于task1,并且只有在task1成功完成后才会执行。
Airflow 是一个用于调度和管理 ETL 管道的高效工具,但它也有一些局限性。首先,Airflow 的设置和管理可能非常复杂,特别是对于大型或复杂的管道。其次,它并没有专门为数据转换而设计,可能需要额外的工具或定制代码来执行某些类型的数据操作。
注意
dbt 可以通过提供一组最佳实践和数据转换的约定,以及用于执行和管理数据转换的简单直接的界面,来解决这些 Airflow 的局限性。它还可以与 Airflow 集成,提供一个易于设置和管理的完整的 ETL/ELT 解决方案,同时提供对数据管道的高度灵活性和控制。
dbt 革命
dbt 是一个开源的命令行工具,在数据分析行业中越来越受欢迎,因为它简化和优化了数据转换和建模的过程。另一方面,Airflow 是一个强大的开源平台,用于以编程方式创建、调度和监控工作流。当 dbt 与 Airflow 集成时,数据管道可以更高效地管理和自动化。Airflow 可以用来调度 dbt 运行,而 dbt 可以用来执行管道中的数据转换任务。
这种集成使团队能够管理从数据提取到加载到数据仓库的整个数据管道,确保数据始终保持最新和准确。集成使得自动化数据管道任务、调度和监控管道以及及时解决问题变得更加容易。
为了说明构建简单 dbt 模型的简易性,想象一下你想要建立一个模型,通过将每个订单的收入相加来计算公司的总收入。模型可以通过一个 dbt 模型文件来定义,该文件指定了计算的 SQL 代码以及任何所需的依赖关系或参数。示例 1-3 展示了模型文件可能的样子。
示例 1-3. 一个 dbt 模型
{{ config(materialized='table') }}
select
sum(orders.revenue) as total_revenue
from {{ ref('orders') }} as orders
dbt 的主要优势之一是,分析工程师可以使用一种简单的高级语言编写可重用、可维护和可测试的数据转换代码,从而消除了编写 SQL 的复杂性。这促进了团队在数据项目上的协作,并减少了数据管道中错误的风险。
dbt 的另一个好处是,它能够实现更高效的数据管道管理。通过与像 Airflow、Dagster 或 Prefect 等编排工具以及 dbt Labs 自己的 dbt Cloud 产品集成,dbt 赋予团队有效地规划、调度和监控数据管道的能力。这确保数据始终保持最新和准确。dbt 与 Airflow 等编排工具的协同作用允许无缝地刷新数据并部署新的逻辑,类似于软件工程中的 CI/CD 实践。这种集成确保了随着新数据的可用性或转换的更新,数据管道可以有效地编排和执行,以提供可靠和及时的洞察。
总体来说,对于希望提高其数据分析能力并简化数据管道的组织来说,dbt 正变得越来越普及。尽管它仍然是一种相对较新的技术,但许多公司正在使用它,并认为它是数据专业人士的宝贵工具。第 4 章 将更深入地介绍 dbt 及其能力和特性。
总结
在最近几十年中,数据管理领域经历了深刻的变革,从基于结构化方法的数据存储和访问(如基于 SQL 的存储过程)转向了更灵活和可扩展的工作流。这些现代工作流得到了像 Airflow 和 dbt 这样强大工具的支持。Airflow 支持动态编排,而 dbt 将分析代码提升到了生产级软件的水平,引入了数据测试和转换的创新方法。
在这个充满活力的环境中,新角色如数据分析工程师应运而生,站在数据工程与数据分析的交汇点上,确保提供强大的洞察力。尽管工具和角色不断演进,数据的内在价值仍然不变。然而,数据管理正在演变为一门不仅关注数据本身,还关注运用数据的专业人士的学科。
尽管取得了这些进展,核心挑战仍然存在:获取关键数据、维护最高的数据质量标准、高效存储数据以及在数据交付中满足利益相关者的期望。数据价值链的核心在于数据建模的复兴。高效的数据建模不仅限于数据收集;它将数据结构化和组织起来,以反映现实世界的关系和层次。第二章 将深入探讨数据建模及其在分析工程中的关键角色。
在本章中,我们探讨了数据管理的演变、数据分析工程师角色的出现,以及数据网格和 ELT 与 ETL 策略之间的区别。这一多样的主题旨在提供对数据领域的全面概述。
第二章:数据建模与分析
在当今数据驱动的世界中,组织越来越依赖数据分析来获取宝贵的见解并做出明智的决策。数据建模在此过程中扮演了至关重要的角色,为结构化和组织数据提供了坚实的基础,以支持有效的分析。此外,理解数据建模和规范化的概念对于实现分析的全部潜力并从复杂数据集中获得可操作的见解至关重要。
数据建模是定义系统内数据实体的结构、关系和属性。数据建模的一个重要方面是数据的规范化。数据规范化是消除数据冗余并提高数据完整性的技术。它涉及将数据分解为逻辑单元,并将其组织成单独的表格,从而减少数据重复,并提高整体数据库效率。规范化确保数据以结构化和一致的方式存储,这对于准确的分析和可靠的结果至关重要。
在分析领域,数据建模为创建分析模型提供了坚实的基础。分析师可以通过理解实体和数据结构之间的关系来设计有效的模型,捕捉相关信息并支持所需的分析目标。换句话说,一个设计良好的数据模型使分析师能够执行复杂的查询,连接表格,并聚合数据以产生有意义的见解。
理解数据建模和规范化对于实际数据分析至关重要。分析师如果没有合适的数据模型,可能会难以访问和正确解释数据,从而导致错误的结论和无效的决策。此外,缺乏规范化可能导致数据异常、不一致性和难以聚合数据,阻碍分析过程。
在这本书中,我们将 SQL 和 dbt 作为两种核心技术突出展示,以支持有效的分析工程项目,这也适用于设计和实施有效的数据模型。背后的原因是,SQL 赋予用户定义表格、操作数据并通过其强大的查询能力检索信息的能力。其无与伦比的灵活性和多功能性使其成为构建和维护数据模型的强大工具,赋予用户表达复杂关系和轻松访问特定数据子集的能力。
与 SQL 相辅相成,dbt 在这一叙述中起着核心作用,将数据建模艺术推向全新高度。它作为一个全面的框架,用于构建和编排复杂的数据管道。在这个框架内,用户可以定义转换逻辑,应用关键业务规则,并编写可重复使用的模块化代码组件,称为模型。值得注意的是,dbt 不仅限于独立功能:它与版本控制系统无缝集成,使协作变得轻松,并确保数据模型保持一致性、可审计性和易复现性。
SQL 和 dbt 在数据建模中的另一个关键方面是它们对测试和文档的重视,尽管有一些值得澄清的区别。在数据建模的背景下,测试涉及验证数据模型的准确性、可靠性和遵守业务规则。虽然需要注意的是,dbt 的测试能力与软件开发中传统的单元测试有所不同,但它们的目的类似。dbt 提供的验证查询不同于传统的单元测试,它们类似于分析人员习惯运行的查询。这些验证查询检查数据质量、数据完整性以及遵守定义的规则,为模型的输出提供信心。此外,dbt 在文档编制方面表现出色,为分析人员和利益相关者提供了宝贵的资源。这些文档简化了理解驱动数据模型的基础逻辑和假设,增强了透明度并促进了有效的协作。
SQL 和 dbt 共同赋能数据专业人士创建强大、可扩展和可维护的数据模型,推动洞察性分析和明智的决策。通过利用这些工具,组织可以释放数据的全部潜力,在当今数据驱动的景观中获得竞争优势。将这两者结合在同一数据架构和策略中,对数据建模带来了显著的优势。
数据建模简介
在数据库设计领域,创建结构化和有组织的环境对于有效存储、操作和利用数据至关重要。数据库建模通过提供代表特定现实或业务的蓝图,并支持其过程和规则,对实现这一目标起到了重要作用。
然而,在我们深入创建这个蓝图之前,我们应该专注于理解业务的微妙差别。理解业务的运营、术语和流程对于创建准确和有意义的数据模型至关重要。通过采访、文档分析和流程研究收集需求,我们深入了解业务的需求和数据要求。在这个收集过程中,我们应该专注于自然的沟通方式——书面语言。通过清晰的句子表达业务事实,确保业务的表达准确且没有歧义。将复杂的句子分解为主语、谓语和宾语的简单结构,有助于简洁地捕捉业务实际情况。
除了这些基本实践之外,还值得注意的是,像劳伦斯·科尔在他的畅销书《敏捷数据仓库设计》(DecisionOne Press)中提倡的进一步技术,如白板绘图和画布设计,在数据模型设计的初始阶段可以增加细节,允许更全面地探索业务需求,并确保最终的数据模型与业务目标和复杂性无缝对接。
一旦理解阶段完成,我们将进入数据库建模的三个基本步骤:
-
概念阶段
-
逻辑阶段
-
物理阶段
这些步骤构成了创建稳健且良好组织的数据库结构的旅程。让我们以图书出版商为例来说明这个过程。
建模的概念阶段
数据库建模的概念阶段需要几个关键步骤。首先,需要确定数据库的目的和目标,并明确它需要解决的具体问题或要求。接下来是通过采访利益相关者和主题专家来收集需求,全面了解所需的数据元素、关系和约束条件。随后进行实体分析和定义,这涉及识别要在数据库中表示的关键对象或概念,并定义它们的属性和关系。
我们在设计数据库外观的初始草图时,首先进行轻度规范化,以确保识别的实体和关系之间的完整性,并通过将实体和属性组织在概念上相关的语义结构周围来减少冗余。识别包括主键和外键在内的键是保持唯一性和在表之间建立关系的关键。
这些数据库设计通常通过图表、文字描述或其他捕捉和有效传达数据库设计和概念的方法创建。最常用的视觉表示数据库概念的工具之一是实体-关系图(ERD)。使用 ERD 创建的视觉模型作为有效描述被建模实体、它们的关系以及这些关系的基数的图表表示。通过采用 ERD 模型,我们可以视觉描述数据库结构,包括实体作为主要组成部分、实体之间的连接或关联,以及关系的数量或程度。
让我们来做一个非常简单的数据库概念设计。想象一下,O'Reilly 的目标是跟踪以前出版的书籍和作者,以及尚未出版的新书的发布日期。我们与出版商的经理进行一系列的访谈,并开始准确理解需要存储在数据库中的数据。主要目标是识别涉及的实体、它们之间的关系以及每个实体的属性。请记住,这个练习是说明性的,并且是有意简化的。我们在书籍管理的这个子领域中识别了三个不同的实体:
书籍
此实体代表 O'Reilly 出版的一本书。属性可能包括book_id、title、publication_date、ISBN、price,以及一个特定的类别。采访者表示,在这个模型中,一本书可能只有一个类别。
作者
此实体代表为 O'Reilly 写书的作者。属性可能包括author_id、author_name、email,以及bio。
类别
此实体代表书籍类别,可以包含诸如category_id作为唯一标识符和category_name等属性。
下一步是识别实体之间的关系。在数据库设计中,实体之间可以存在几种类型的关系,关系的类型可以称为关系的基数。例如,在一对一的关系中,我们可以有一个书籍实体连接到一个作者实体,其中每本书都与一个作者相关联,反之亦然。在一对多的关系中,考虑一个类别实体与一个书籍实体连接,其中每本书只能属于一个类别,但每个类别可以有多本书。相反,在多对一的关系中,想象一个出版商实体连接到一个书籍实体,同一个出版商出版多本书。最后,在多对多的关系中,我们可以有一个书籍实体与一个读者实体相关联,表示多个读者可能拥有多本书。继续我们的练习,我们还确定了两个明确的关系:
书-类别关系
建立书籍和类别之间的连接。一本书可以有一个类别,一个类别可以有多本书。这种关系表示为一对多关系。
书籍-作者关系
建立书籍和作者之间的关系。一本书可以有多位作者,一个作者可以写多本书。这种关系表示为多对多关系。在这个关系中,特定书籍的出版发生。
在识别关系时,通常使用能够代表实体间真实交互的关系名称。例如,我们可以称之为分类而不是 Book-Category,因为类别对书籍进行分类,或者我们可以称之为出版而不是 Book-Author,因为作者出版了书籍。
现在我们对实体、属性和关系有了一个概念,我们可以使用 ERD 设计我们的数据库。通过这样做,我们可以直观地表示实体、关系和基数,如图 2-1 所示。

图 2-1. 书籍数据库的 ERD 示例
如我们所见,实体以白色矩形框表示,代表现实世界的对象或概念,如 Book 或 Author。关系以菱形表示,说明实体之间的关系。
属性表示为阴影框,描述实体的属性或特征。例如,名称或出版日期。此外,属性可以分类为关键属性(带有下划线的阴影框),这些属性唯一标识实体,或非关键属性(非下划线的阴影框),提供有关实体的额外信息。在设计这类图表时还存在更多类型的属性,但我们将坚持基础知识。
ERD 中的其他组成部分包括基数和参与约束。基数定义关系中实例的数量,通常用符号 1、M 或 N 表示,表示一对一或一对多关系,分别表示未确定数量的关系(N 表示)。
建模的逻辑阶段
在建模的逻辑阶段,重点是将数据规范化,消除冗余,提高数据完整性,并优化查询性能。结果是一个规范化的逻辑模型,准确反映实体之间的关系和依赖。
该阶段可分为两步。首先,实体关系模式的重构侧重于根据特定标准优化模式。此步骤与任何特定的逻辑模型无关。第二步将优化的 ERD 转换为特定的逻辑模型。
假设我们已决定将 ER 图映射到关系数据库模型(这将是我们的情况),而不是文档或图形数据库,那么每个从概念性 ER 图练习中得到的实体将被表示为一个表。每个实体的属性将成为相应表的列。为每个表的主键列指示主键约束。此外,多对多关系由单独的连接表表示,这些表包含引用相应实体的外键。
将概念性的 ER 图练习转换为使用关系模型的逻辑模式后,我们建立了实体、它们的属性以及它们之间关系的结构化表示。这个逻辑模式可以作为在特定数据库管理系统(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。

图 2-2:图书数据库的逻辑 ERD 示例
在我们的示例中,有一些实体,如我们的算法所建议的那样,直接映射为表。这种情况包括作者、书籍和类别。
我们确定了图书和类别之间的 1:N 关系,其中一本书对应一个类别,但一个类别可以有多本书。为了映射这种关系,我们在books表中创建一个外键,引用相应的类别。
我们还有一个 N:N 的关系。在这种情况下,我们必须创建一个新的表(交叉参照表),用于存储这种关系。在我们的案例中,我们创建了一个名为 Publishes 的表,其主键成为相关实体(Book ID 和 Author ID)的复合键。与此同时,关系的属性成为这个交叉参照表的属性。
建模的物理阶段
现在我们准备将归一化的逻辑模型转换为物理数据库设计,这一步称为物理阶段或物理模型创建。这一步定义了存储结构、索引策略和数据类型,以确保数据的高效存储和检索。而逻辑模型侧重于概念表示,物理模型则处理平滑数据管理所需的实施细节。
在我们的案例中,让我们继续从前面的逻辑模型,并假设我们将使用 MySQL 数据库引擎。示例 2-1 显示了图书数据库的物理模型。
示例 2-1. 物理模型中的图书数据库
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)
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 author(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 说明了包含几个相关属性的books表。
示例 2-2. books表需要归一化
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 中所示。
示例 2-3. 1NF 中的books表
-- Table Authors
CREATE TABLE authors (
author_id INT PRIMARY KEY,
author_name VARCHAR(100)
);
-- Table 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,包含书籍 ID 和标题 -
authors,包含作者 ID 和姓名 -
bookDetails,存储书籍 ID、出版年份和流派
这确保每一列仅依赖于主键,如示例 2-4 所示。
示例 2-4. 2NF 中的books表
-- Table Authors
CREATE TABLE authors (
author_id INT PRIMARY KEY,
author_name VARCHAR(100)
);
-- Table Books
CREATE TABLE books (
book_id INT PRIMARY KEY,
title VARCHAR(100),
);
-- Table 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)侧重于消除传递依赖。我们意识到流派可以通过bookDetails表从书籍 ID 派生出来。为了解决这个问题,我们创建了一个名为genres的新表,包含流派 ID 和流派名称,而bookDetails表现在引用流派 ID 而不是直接存储流派名称(示例 2-5)。
示例 2-5. 3NF 中的books表
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)
);
这些产生的标准化结构(3NF)通常用于操作系统中,也称为在线事务处理系统(OLTP),旨在有效处理和存储交易,并检索交易数据,例如客户订单、银行交易甚至工资单。重要的是要强调,如果需要,我们可以应用进一步的规范化步骤,如第四范式(4NF)和第五范式(5NF),以解决复杂的数据依赖关系,并确保更高水平的数据完整性。
数据规范化对于实现 OLTP 系统中个体事务的高效处理和存储至关重要。在这个过程中,数据被划分为较小、冗余较少的部分,以实现这一目标,并为 OLTP 系统带来多个优势。数据规范化以减少数据冗余和提高数据完整性而闻名,因为数据被组织成多个表,每个表都有特定的用途。这些表通过主键和外键进行链接以建立它们的关系,确保每个表中的记录是唯一的,并且同一字段不会在多个表中重复,除了关键字段或系统字段,如 ID 或创建时间戳。
数据归一化相关的另一个原因是它增强和最大化了性能。这些归一化数据库通过最小化数据冗余并在表之间建立明确定义的关系,设计成能够高效处理快速读写操作,从而可以处理大量的事务并具有闪电般的性能。这对于操作时间关键的事务系统非常重要。
最后但同样重要的是,归一化数据库专注于仅存储当前数据,以便数据库反映最新可用信息。在存储客户信息的表中,每条记录始终反映客户的最新详细信息,如名字、电话号码和其他相关数据,确保数据库准确反映当前事务状态。
然而,当涉及到分析项目或系统时,范式有些不同。通常,用户希望能够在不进行大量连接的情况下检索所需的数据,这是归一化过程的自然结果。虽然 OLTP 系统经过优化,以避免在诸如 Web 应用等实时系统中出现延迟增加的写操作,但分析系统的用户希望进行读取优化,以尽快获取其分析数据。与存储实时数据的归一化事务性数据库不同,分析数据库预计包含实时和非实时数据,并充当过去数据的历史存档。而且,通常期望分析数据库包含来自多个 OLTP 系统的数据,以提供业务流程的集成视图。
正是这些差异至关重要,因为它们支撑着数据组织、保留和利用的不同要求。然而,重要的是澄清我们刚刚探讨的主要是规范化在性能优化和遵循 OLTP 数据库设计最佳实践方面的基础。虽然这一基础非常宝贵,但它只代表了分析工程更广阔景观中的一个方面。
为了提供更清晰的路线图,让我们确立我们的旅程从探索这种基础数据建模开始,这种建模形成了 OLTP 系统的基础。接下来,我们将转向讨论针对 OLAP 环境优化的数据建模方法。通过做出这种区分,我们旨在全面理解数据建模的两个方面,为深入探讨分析工程方法论及其在后续章节中的应用做好铺垫。
维度数据建模
数据建模是设计和组织数据库以高效存储和管理数据的基本方面。正如我们之前讨论的,它涉及在系统内定义数据实体的结构、关系和属性。
数据建模的一种流行方法是维度建模,其重点在于为支持分析和报告需求而进行数据建模。维度建模特别适用于数据仓库和 BI 应用程序。它强调创建包含事实表和提供描述性上下文的维度表的维度模型。通过使用维度建模技术,如星型模式和雪花模式,数据可以以简化复杂查询并实现高效数据分析的方式进行组织。
数据建模与维度建模之间的关系在于它们的互补性质。数据建模为捕获和结构化数据提供基础,而维度建模则提供了一种专门的建模数据以支持分析和报告需求的技术。这些方法共同促使组织设计健壮且灵活的数据库,从而便于事务处理和深入数据分析。
要理解维度建模,我们首先应该向被认为是数据仓库和维度建模之父的两位先驱致敬:分别是 Bill Inmon 和 Ralph Kimball。他们被公认为企业级信息收集、管理和决策支持分析的先驱。
它们对数据仓库话题的重要讨论做出了贡献,每种方法都主张不同的哲学和方法。Inmon 提议创建一个涵盖整个企业的集中式数据仓库,旨在生成全面的 BI 系统。另一方面,Kimball 建议创建多个小型数据集市,专注于特定部门,从而实现部门级别的分析和报告。他们不同的观点导致了数据仓库的对比设计技术和实施策略。
除了他们不同的方法之外,Inmon 和 Kimball 还提出了在数据仓库的数据结构化背景下使用不同的方法。Inmon 主张在企业数据仓库中使用关系(ERD)模型,特别是第三范式(3NF)。相反,Kimball 的方法在维度数据仓库中采用多维模型,利用星型模式和雪花模式。
Inmon 主张在关系模型中组织数据可确保企业范围内的一致性。这种一致性有助于在维度模型中相对轻松地创建数据集市。另一方面,Kimball 认为在维度模型中组织数据有助于信息总线,允许用户更有效地理解、分析、聚合和探索数据不一致性。此外,Kimball 的方法使得可以直接从分析系统访问数据。相比之下,Inmon 的方法限制了分析系统仅从企业数据仓库访问数据,需要与数据集市互动来检索数据。
提示
数据集市是数据仓库的一个特定部分,旨在满足特定部门或业务单位的独特需求。
在接下来的章节中,我们将深入探讨三种建模技术:星型模式、雪花模型和新兴的数据仓库。数据仓库,由 Dan Linstedt 于 2000 年推出,近年来已经获得了迅速发展。它遵循更规范化的结构,虽然与 Inmon 的方法并不完全一致,但相似。
使用星型模式建模
星型模式是关系数据仓库中广泛使用的建模方法,尤其适用于分析和报告目的。它将表格分类为维度表和事实表,以有效组织和表示业务单位及相关观察或事件。
维度表用于描述要建模的业务实体。这些实体可以包括产品、人员、地点和概念等各个方面,包括时间。在星型模式中,您通常会找到一个日期维度表,为分析提供全面的日期集合。维度表通常包括一个或多个键列,这些列作为每个实体的唯一标识符,以及提供有关实体的进一步信息的附加描述列。
另一方面,事实表存储发生在业务中的观察或事件。这些包括销售订单、库存水平、汇率、温度和其他可测量数据。事实表包含维度键列,这些列指向维度表,以及数值测量列。维度键列确定事实表的维度,并指定分析中包含的维度。例如,存储销售目标的事实表可能包含日期和产品键的维度键列,表明分析包括与时间和产品相关的维度。
事实表的粒度由其维度键列中的值决定。例如,如果销售目标事实表中的日期列存储表示每个月第一天的值,那么表的粒度就在月份/产品级别。这意味着事实表捕获了每个产品的月度销售目标数据。
通过在星型模式中结构化数据,其中维度表表示业务单元,而事实表捕获观察或事件,公司可以有效地执行复杂分析并获得有意义的洞见。星型模式为查询和聚合数据提供了清晰和直观的结构,使得分析和理解数据集中维度与事实之间的关系变得更加容易。
回到我们的books表,我们将遵循建模步骤来开发一个简单的星型模型。第一步将是识别维度表。但首先,让我们记住我们的基础表在示例 2-6 中。
示例 2-6. 我们星型模型的基础表
-- This is our base table
CREATE TABLE books (
book_id INT PRIMARY KEY,
title VARCHAR(100),
author VARCHAR(100),
publication_year INT,
genre VARCHAR(50)
);
我们应该在books表中识别所有单独的维度(与特定业务实体相关的属性),并为每个创建单独的维度表。在我们的示例中,就像在规范化步骤中一样,我们识别了三个实体:books、authors 和 genres。让我们看看与示例 2-7 相对应的物理模型。
示例 2-7. 我们星型模型的维度表
-- Create the dimension tables
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 所示,来捕获出版数据。
示例 2-8. 我们星型模型的事实表
-- Create the fact table
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-3. 星型模型
使用这种模型,我们现在可以通过应用诸如流派、作者或出版年份等筛选器轻松分析图书出版情况。例如,我们可以快速检索特定流派的总出版物数量。通过将维度表与事实表连接,如在示例 2-9 中所示,我们可以轻松地深入了解书籍、作者、流派和销售之间的关系。
示例 2-9. 从星型模式检索数据
-- Example for retrieving the total publications for a specific genre.
SELECT COALESCE(dg.genre, 'Not Available'), -- Or '-1'
COUNT(*) AS total_publications
FROM factBookPublish bp
LEFT JOIN dimGenres dg ON dg.genre_id = bp.genre_id
GROUP BY g.genre;
正如您所见,当将事实表与维度表连接时,我们使用了LEFT JOIN。这是相当常见的。它确保包括结果中来自事实表的所有记录,无论维度表中是否存在匹配记录。这一考虑很重要,因为它承认并非每个事实记录必定在每个维度中都有对应的条目。
通过使用LEFT JOIN,您可以保留事实表中的所有数据,同时使用维度表中的相关属性进行丰富。这样可以基于不同的维度进行分析和聚合,从不同的视角探索数据。然而,我们必须处理任何缺失的对应关系。因此,我们使用COALESCE运算符,通常用于设置默认值,如-1或不可用。
LEFT JOIN还允许增量维度更新。如果向维度表添加新记录,则LEFT JOIN仍将包括现有的事实记录,并将它们与可用的维度数据关联起来。这种灵活性确保您的分析和报告在维度数据随时间演变的同时保持一致。
总体而言,星型模式的简单性和去规范化结构使其非常适合进行聚合和汇总。您可以生成各种报告,例如随时间变化的销售趋势、畅销流派或按作者的收入。此外,星型模式还支持逐步深入和上卷操作,使您可以深入了解更详细的信息或上卷到更高层次的聚合,从而全面查看数据。
这种建模技术还能无缝对接数据可视化工具和 BI 平台。通过将您的模型连接到诸如 Tableau、Power BI 或 Looker 等工具,您可以创建视觉上吸引人的仪表板和交互式报告。这些资源使利益相关者能够迅速掌握见解,并在一瞥间做出数据驱动的决策。
然而,值得注意的是,上述示例并未充分突出星型模式所倡导的去规范化方面。例如,如果您的数据集严格遵循每本书一个流派的情况,您可以通过直接在统一的dimBooks表中 consolida 流派信息来进一步简化模型,促进去规范化并简化数据访问。
采用雪花模式建模
在雪花模式中,数据模型比星型模式更归一化。它通过将维度表拆分为多个连续表来增加归一化级别。这有助于提高数据完整性并减少数据冗余。例如,考虑一个电子商务数据库的雪花模式。我们有一个维度表customers,包含客户信息,如 ID、姓名和地址。在雪花模式中,我们可以将这个表拆分为多个连续的表。
customers表可以拆分为customers表和单独的addresses表。customers表将包含特定于客户的属性,如 ID 和客户姓名。相比之下,addresses表将包含与地址相关的信息,如 ID、客户的街道、城市和邮政编码。如果几个客户具有相同的地址,则我们只需在addresses表中存储一次地址信息,并将其链接到相应的客户即可。
要从雪花模式检索数据,通常需要在相关表上执行多次连接操作,以获取所需的信息。例如,如果我们想查询客户姓名和地址,我们必须将customers表与addresses表在 ID 页上进行连接。虽然雪花模式提供了更好的数据完整性,但由于额外的连接,它也需要更复杂的查询。然而,由于提供了更好的归一化和数据管理灵活性,因此这种模式对于大数据集和复杂关系可能是有益的。
星型模式和雪花模式都是两种常见的数据仓库模式设计。在星型模式中,维度表是非归一化的,意味着它们包含冗余数据。星型模式提供了诸如更易于设计和实施、由于较少的JOIN操作而更高效的查询等优点。然而,由于冗余数据可能需要更多的存储空间,并且在更新和故障排除时可能更具挑战性。
这是我们经常看到混合模型的原因之一,公司在其中建模星型模式,并常常为不同的优化策略归一化少量维度。选择主要取决于您独特的需求和要求。如果您在数据仓库解决方案中优先考虑简单性和效率,星型模式可能是理想选择。这种模式提供了易于实施和高效的查询,适合简单的数据分析任务。然而,如果您预期数据需求经常变化,并且需要更多的灵活性,则雪花模式可能更好,因为它允许更容易地适应不断演变的数据结构。
想象一下,我们有一个表示全球特定客户位置的维度。在星型模式中建模它的一种方法是创建一个包含所有位置层次结构的单个维度表,使其反规范化。示例 2-10 展示了在星型模式范例下的 dimLocation。
示例 2-10. 星型模式位置维度
CREATE TABLE dimLocation (
locationID INT PRIMARY KEY,
country VARCHAR(50),
city VARCHAR(50),
State VARCHAR(50)
);
示例 2-11 模型按照雪花模式处理位置维度。
示例 2-11. 雪花模式位置维度
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 说明了这种情况。

图 2-4. 雪花模式模型
使用 Data Vault 进行建模
Data Vault 2.0 是一种建模方法,不属于维度建模,但仍然值得一提。其方法结合了 3NF 元素和维度建模,以创建一个逻辑企业数据仓库。它旨在通过提供灵活和可扩展的模式来处理包括结构化、半结构化和非结构化数据在内的各种数据类型。其最突出的特点之一是专注于基于业务键构建模块化和增量的 Data Vault 模型。这种方法确保数据仓库能够适应不断变化的业务需求和不断发展的数据集。
更深入地说,这种建模技术提供了一个可扩展和灵活的数据仓库和分析解决方案。它旨在处理大数据量、不断变化的业务需求和不断发展的数据源。Data Vault 的模型由三个主要组件组成:hub、link 和 satellite。
Hubs 代表业务实体,并作为存储称为业务键的唯一标识符的中心点。每个 Hub 对应于特定的实体,如客户、产品或位置。Hub 表包含业务键列以及与实体相关的任何描述性属性。通过将业务键与描述性属性分开,Data Vault 能够轻松跟踪描述信息的变化,而不会损害业务键的完整性。
链接捕获业务实体之间的关系。它们被创建来表示多对多关系或复杂的关联。链接表包含参与的中心的外键,形成连接实体之间的桥梁。这种方法允许对复杂关系进行建模,而无需复制数据或创建不必要的复杂性。
卫星存储与中心和链接相关的特定上下文属性。它们包含不属于业务键但提供有关实体的有价值上下文信息的附加描述信息。通过外键,卫星与相应的中心或链接相关联,允许存储时变数据并保留历史记录。可以将多个卫星与中心或链接关联,每个卫星捕获不同时间点或不同视角的特定属性。
数据仓库架构推动可追溯性、可扩展性和审计性,同时为数据集成、分析和数据治理提供坚实的基础。通过中心(hubs)、链接(links)和卫星(satellites),组织可以构建支持其分析需求的数据仓库,适应不断变化的业务需求,并维护可靠的历史数据变更记录。
回到我们的books表,让我们按照三个建模步骤开发简单的数据仓库模型。第一步是识别业务键并创建相应的中心和卫星表。在这种情况下,我们只有一个业务实体,因此不会使用链接。示例 2-12 展示了books表的数据仓库建模。
示例 2-12. 使用数据仓库 2.0 建模books表
-- This is our base table
CREATE TABLE books (
book_id INT PRIMARY KEY,
title VARCHAR(100),
author VARCHAR(100),
publication_year INT,
genre VARCHAR(50)
);
在数据仓库建模中,我们首先要识别业务键,即每个实体的唯一标识符。在这种情况下,books表的主键book_id充当业务键。
现在是时候对我们的第一张表进行建模和创建了:中心表,它存储唯一的业务键及其对应的哈希键以确保稳定性。示例 2-13 创建了中心表。
示例 2-13. 创建中心(hub)
CREATE TABLE hubBooks (
bookKey INT PRIMARY KEY,
bookHashKey VARCHAR(50),
Title VARCHAR(100)
);
在中心表中,我们将每本书的唯一标识符存储为主键(bookKey),并使用哈希键(bookHashKey)来保持稳定性。Title列包含关于书籍的描述信息。
接下来是我们的卫星表,显示在示例 2-14,它捕获了额外的书籍详情并保持了历史更改。
示例 2-14. 创建卫星(satellite)
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)
);
通过将核心图书信息分离到中心表中,并在卫星表中存储历史细节,我们确保可以随时间捕获诸如作者、出版年份或流派等属性的变化,而不会修改现有记录。
在数据仓库模型中,我们可能会有额外的表,比如链接表来表示实体之间的关系,或者其他卫星表来捕获特定属性的历史变化。
单片数据建模
直到最近,数据建模的主流方法围绕创建大量的 SQL 脚本展开。在这种传统方法中,一个单独的 SQL 文件通常会延伸数千行,封装了整个数据建模过程。为了实现更复杂的工作流程,从业者们可能会将文件分割成多个 SQL 脚本或存储过程,然后通过 Python 脚本顺序执行。为了使工作流程更加复杂,这些脚本通常在组织内部鲜为人知。因此,即使另一个人希望以类似的方式进行数据建模,他们也会从头开始,错失利用现有工作的机会。
这种方法可以恰当地描述为数据建模的单片或传统方法,其中每个数据消费者都独立地从原始数据重构他们的数据转换。在这种范式内部,存在一些显著的挑战,包括脚本的版本控制缺失、管理视图之间依赖关系的艰巨任务,以及从原始数据源到最终报告阶段制作新视图或表的常见做法,损害了可重用性。此外,幂等性概念并不统一地应用于大表中,有时会导致冗余和回填操作,虽然这些操作很常见,但通常复杂且耗时。
在当今快速发展的数据工程世界中,特别是在 SQL 转换的背景下,单片数据模型构成了工程师们需要应对的重大挑战。考虑以下情景:你发现你的生产系统出了问题,最初看似简单的改变引发了一连串的错误,这些错误蔓延至整个基础设施。这个噩梦般的情景以高度互联的系统和微小的改动作为导火索,形成了连锁反应,对许多数据专业人士来说是一个令人不安但又熟悉的问题。
在设计数据模型时,我们要避免的正是与单片数据模型相关的风险。你最不希望的是紧密耦合的数据模型,这使得调试和实施变更变得艰巨,因为每一次变更都有可能干扰整个数据管道。缺乏模块化会妨碍在当今数据驱动的景观中至关重要的灵活性、可扩展性和可维护性。
在整体数据模型中,所有组件紧密相连,使得问题的识别和隔离成为一项具有挑战性的任务。本质上,这种传统的数据系统设计方法倾向于将整个系统统一为一个单一的单位,尽管并非始终内聚。
模型的这种相互连接性意味着看似无关的变更可能会对整个系统产生意外后果。这种复杂性不仅使故障排除变得更加困难,还增加了引入错误或忽视关键依赖性的风险。所有数据和功能都如此紧密集成和相互依赖,以至于修改或更新系统的任何一个部分都变得非常困难。
此外,数据模型缺乏模块化会阻碍适应不断变化的业务需求能力。在数据需求动态变化的环境中,整体模型成为进展的瓶颈。整合新数据源、扩展基础设施或集成新技术和框架变得日益具有挑战性。
此外,对整体数据模型的维护和更新变得耗时且资源密集。由于系统内复杂的依赖关系,每次变更都存在更高的风险。对于可能不经意地破坏关键组件的担忧导致过度谨慎的方法,从而减缓开发周期并抑制创新。
在当今数据工程领域,整体数据模型带来的挑战是显著的。相互依赖性、缺乏灵活性以及在维护和扩展方面的困难,迫使我们转向模块化数据模型。通过采用模块化,数据工程师可以在其数据基础设施中实现更大的灵活性、鲁棒性和适应性,以应对快速演变的数据生态复杂性。通过摆脱整体结构,组织可以实现数据的全部潜力,在我们生活的数据驱动世界中推动创新,并获得竞争优势。
dbt 在采用模块化方法和克服整体模型挑战方面发挥了重要作用。通过将单一数据模型拆分为各自具有独立 SQL 代码和依赖项的模块,它使我们能够提高可维护性、灵活性和可扩展性。这种模块化结构使我们能够独立处理每个模块,从而更容易开发、测试和调试数据模型的特定部分。这消除了意外更改影响整个系统的风险,使引入变更和更新更加安全。
dbt 中模块化的主题将在接下来的子章节中得到更多关注,并且第四章将深入探讨 dbt 的全面探索。
构建模块化数据模型
前面的例子突显了 dbt 和数据模型模块化在改进数据开发过程中的贡献。然而,为何数据工程师和科学家没有将此视为必备技能?事实是,在过去几十年的软件开发世界中,工程师和架构师选择了新的方式来利用模块化简化编码过程。模块化将编码过程从处理一个大代码块变成多个步骤。这种方法相较于替代策略具有几个优势。
模块化的一个主要优势是增强了可管理性。在开发大型软件程序时,集中精力处理一个大块编码可能会具有挑战性。但通过将其分解为个别任务,工作变得更加可管理。这有助于开发者保持专注,并防止他们因项目的庞大而感到不知所措。
模块化的另一个优点是支持团队编程。与将一个大任务交给单个程序员不同,可以将其分配给团队。每个程序员作为整体程序的一部分分配了特定的任务。最终,来自所有程序员的工作被结合起来创建最终程序。这种方法加速了开发过程,并允许团队内部的专业化。
模块化还有助于提升代码质量。将代码分解为小部分,并将责任分配给个别程序员,增强了每个部分的质量。当程序员专注于他们分配的部分,而不必担心整个程序时,他们可以确保代码的完美性。因此,当所有部分集成时,整体程序较少可能包含错误。
此外,模块化还能够使已被证明有效的代码模块得以重复利用。通过将程序分解为模块,可以打破基本的方面。如果某段代码在特定任务中运行良好,就无需重新发明。相反,可以重复使用相同的代码,节省程序员的时间和精力。在需要类似特性时,可以在整个程序中重复此过程,进一步简化开发。
此外,模块化的代码高度组织,提升了其可读性。通过根据任务组织代码,程序员可以轻松找到并引用特定的部分,基于其组织方案。这提升了多个开发者之间的协作,因为他们可以遵循相同的组织方案,更高效地理解代码。
所有模块化的优势最终都导致了改进的可靠性。代码更易阅读、调试、维护和共享,减少错误。在需要多名开发者共享代码或在未来与他人代码接口的大型项目中,这变得至关重要。模块化使得能够可靠地创建复杂软件。
尽管模块化在软件工程界是必须且已知的,但在数据领域,它在过去几年中才开始被重视。这背后的原因是需要在数据架构和软件工程之间更清晰的界定。然而,最近,行业已经演变成两者融合的形式,因为前述的优势也适用于数据分析和工程。
正如模块化简化了编码过程,它也可以简化数据模型的设计和开发。通过将复杂数据结构分解为模块化组件,数据工程师可以更好地管理和操作不同粒度的数据。这种模块化方法支持高效的数据集成、可扩展性和灵活性,使得对整体数据架构的更新、维护和增强更加容易。
与此同时,模块化促进了数据模块的重用,确保数据模型的一致性和准确性,减少了冗余。总体而言,模块化原则为有效的数据建模和工程提供了坚实的基础,增强了数据系统的组织性、可访问性和可靠性。
因此,模块化数据建模是设计高效可扩展数据系统的强大技术。开发人员可以通过将复杂数据结构分解为可重用的小组件来构建更健壮和易维护的系统。这是设计高效可扩展数据系统的强大技术,而 dbt 和 SQL 都提供了有效的工具来帮助我们实施这一技术。
总结:模块化数据建模的核心原则可以定义如下:
分解
将数据模型分解为更小、更易管理的组件
抽象化
隐藏数据模型实现细节的接口背后
可重用性
创建可以在系统多个部分重复使用的组件
这种数据建模可以通过标准化、数据仓库化和数据虚拟化技术来实现。例如,使用标准化技术,根据数据特征和关系将数据分离到表中,从而实现模块化数据模型。
另一个选择是利用 dbt,因为它有助于自动化创建模块化数据模型的过程,提供支持模块化数据建模原则的多个功能。例如,dbt 允许我们通过将数据模型拆分为更小的可重复使用组件来解决分解问题,从而提供了创建可重复使用的宏和模块化模型文件的方式。它还通过提供简单一致的数据源工作界面,抽象化数据模型的实现细节。
此外,dbt 通过提供一种定义和重复使用各种模型中通用代码的方式,鼓励可重用性。此外,dbt 通过提供一种测试和记录数据模型的方式,有助于提高可维护性。最后,dbt 允许您通过定义和测试不同的模型实现策略来优化性能,从而最终允许您调整数据模型的各个组件的性能。
然而,必须承认,模块化也带来潜在的缺点和风险。集成系统通常比模块化系统更易优化,无论是由于数据移动和内存使用的最小化,还是由于数据库优化器在幕后改进 SQL 的能力。创建视图然后创建表有时可能导致次优模型。然而,考虑到模块化的好处,这种权衡往往是值得的。模块化会创建更多文件,这可能意味着更多的对象需要拥有、治理和可能废弃。如果没有成熟的数据治理策略,这可能会导致模块化但未拥有的表的增多,在问题出现时管理起来可能会很具挑战性。
使用 dbt 实现模块化数据模型
正如我们之前强调的,构建模块化数据模型是开发健壮和可维护数据基础设施的重要方面。然而,随着项目规模和复杂性的增长,管理和协调这些模型的过程可能变得复杂起来。
这就是强大的数据转换工具如 dbt 的用武之地。通过将模块化数据建模原则与 dbt 的功能结合,我们可以轻松地在我们的数据基础设施中实现全新的效率和可伸缩性水平。
采用这种模块化方法后,组织内的每个数据生产者或消费者都能够在其他人已完成的基础数据建模工作基础上进行扩展,无需在每次使用源数据时都从头开始。
将 dbt 集成到数据建模框架中后,会发生一种视角转变,将数据模型的概念从单片实体转变为独立组件。每个模型的单独贡献者开始识别可以在各种数据模型中共享的转换。这些共享的转换被提取并组织成基础模型,允许在多个上下文中高效地引用它们。
正如图 2-5 所示,跨多个实例使用基本数据模型,而不是每次从头开始,简化了数据建模中 DAG 的可视化。这种模块化的多层结构阐明了数据建模逻辑层如何相互构建并显示依赖关系。但是,需要注意的是,仅仅采用像 dbt 这样的数据建模框架并不能自动确保模块化的数据模型和易于理解的 DAG。

图 2-5. dbt 模块化
您的 DAG 的结构取决于团队的数据建模理念和思维过程,以及表达这些理念的一致性。为了实现模块化数据建模,考虑诸如命名规范、可读性以及调试和优化的便利性等原则非常重要。这些原则可以应用于 dbt 中的各种模型,包括分段模型、中间模型和 mart 模型,以提高模块化并保持良好结构的 DAG。
让我们从理解 dbt 如何通过使用引用数据模型的 Jinja 语法实现模型可重用性开始,从而开始利用 dbt 构建模块化数据模型的旅程:{{ ref() }}。
引用数据模型
通过采用 dbt 的功能,如模型引用和 Jinja 语法,数据工程师和分析师可以建立模型之间清晰的依赖关系,增强代码的可重用性,并确保数据管道的一致性和准确性。在这种情况下,Jinja是一种模板语言,允许在 SQL 代码中进行动态和程序化的转换,为定制和自动化数据转换提供了强大的工具。这种模块化和 dbt 能力的强大结合使团队能够构建灵活和可维护的数据模型,加快开发过程,并促进利益相关者之间的无缝协作。
要充分利用 dbt 的全部功能并确保准确的模型构建,关键在于使用{{ ref() }}语法进行模型引用。通过这种方式引用模型,dbt 可以自动检测并建立基于上游表的模型依赖关系。这样一来,数据转换管道的执行变得顺畅可靠。
另一方面,应该节俭地使用{{ source() }} Jinja 语法,通常仅限于从数据库中选择原始数据的初始阶段。避免直接引用非 dbt 创建的表是很重要的,因为这可能会影响 dbt 工作流的灵活性和模块化。相反,应重点建立模型之间的关系,通过使用{{ ref() }} Jinja 语法确保上游表的更改正确传播到下游,并保持清晰和连贯的数据转换过程。通过遵循这些最佳实践,dbt 能够实现有效的模型管理,并在分析工作流中促进可伸缩性和可维护性。
例如,假设我们有两个模型:orders 和 customers,其中orders表包含有关客户订单的信息,而customers表存储客户详细信息。我们希望在这两个表之间执行联接,以用客户信息丰富订单数据(示例 2-15)。
示例 2-15. 引用模型
-- In the orders.sql file
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
-- In the customers.sql file
-- 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 查询的混乱网,这样就很难跟踪依赖关系、维护代码并确保数据的准确性。
通过将数据转换过程组织成从源到数据仓库表的不同层,您将获得几个好处。这简化了数据管道,使其更易理解和管理。它还允许增量改进,因为每个层次都专注于特定的转换任务。这种结构化方法增强了数据工程师和分析师之间的协作,减少了错误,并最终产生了更可靠和见解深刻的报告。
分期数据模型
分阶段层在数据建模中扮演着关键角色,因为它作为构建更复杂数据模型的模块化基础。每个分阶段模型对应于源表,与原始数据源之间存在一对一的关系。保持分阶段模型简单并在该层内尽量减少转换是很重要的。可接受的转换包括类型转换、列重命名、基本计算(如单位转换)以及使用条件语句(如CASE WHEN)进行分类。
分阶段模型通常被实现为视图,以保持数据的时效性并优化存储成本。这种方法允许中间或市场模型引用分阶段层以访问最新的数据,同时节省空间和成本。建议避免在分阶段层进行连接,以防止冗余或重复的计算。连接操作更适合在后续层次建立更复杂的关系时使用。
此外,应避免在分阶段层进行聚合,因为它们可能会分组并潜在地限制对宝贵源数据的访问。分阶段层的主要目的是为后续数据模型创建基本构建块,在模块化数据架构中提供灵活性和可伸缩性。遵循这些准则,分阶段层成为构建健壮数据模型的可靠和高效起点。
在 dbt 中利用分阶段模型允许我们在代码中采用“不重复你自己”(DRY)原则。通过遵循 dbt 的模块化和可重用的结构,我们旨在尽可能将对特定组件模型一致需要的任何转换推到最上游。这种方法帮助我们避免重复代码,从而降低复杂性和计算开销。
例如,假设我们始终需要将整数形式的美分转换为浮点形式的美元。在这种情况下,在分阶段模型中早期执行除法和类型转换更为高效。这样一来,我们可以在下游引用转换后的值,而无需多次重复相同的转换过程。通过利用分阶段模型,我们优化了代码复用,并以可扩展和高效的方式简化了数据转换过程。
假设我们有一个名为raw_books的源表,其中包含原始书籍数据。现在我们想要创建一个名为stg_books的分阶段模型,以在进一步处理之前对数据进行转换和准备。在我们的 dbt 项目中,我们可以创建一个名为stg_books.sql的新 dbt 模型文件,并定义生成分阶段模型的逻辑,如示例 2-16 所示。
示例 2-16. 分阶段模型
/* This should be file stg_books.sql, and it queries the raw table to create
the new model */
SELECT
book_id,
title,
author,
publication_year,
genre
FROM
raw_books
在这个示例中,像stg_books这样的分阶段模型从raw_books表中选择相关列。它可以包括基本的转换,如重命名列或转换数据类型。通过创建分阶段模型,您可以将初始数据转换与下游处理分开。这确保了数据质量、一致性,并符合标准,以便进一步使用。分阶段模型作为数据管道中间和市场层更复杂数据模型的基础。它们简化转换过程,维护数据完整性,并提高了您的 dbt 项目的可重用性和模块化。
基础数据模型
在 dbt 中,基础模型通常作为分阶段模型,但根据项目的特定需求,它们也可以包含额外的转换步骤。这些模型通常设计为直接引用输入到数据仓库中的原始数据,它们在数据转换过程中扮演着至关重要的角色。一旦您创建了分阶段或基础模型,您的 dbt 项目中的其他模型可以引用它们。
dbt 文档中从“基础”模型更改为“分阶段”模型的变化反映了不受“基础”名称约束的愿望,该名称暗示建立数据模型的第一步。新的术语允许更灵活地描述这些模型在 dbt 框架中的角色和目的。
中间数据模型
中间层通过将分阶段层的原子构建模块组合起来,创建更复杂和有意义的模型,在数据建模中扮演着至关重要的角色。这些中间模型代表着对业务有意义的构造,但通常不会通过仪表板或应用程序直接向最终用户展示。
为了保持分离和优化性能,建议将中间模型存储为临时模型。临时模型不会直接在数据库或数据集上创建,而是它们的代码会插入到引用它们的模型中作为公共表达式(CTE)。然而,有时将它们材料化为视图更为合适。临时模型不能直接选择,这使得故障排除变得具有挑战性。此外,通过run-operation调用的宏不能引用临时模型。因此,将特定的中间模型作为临时模型还是视图材料化取决于具体的用例,但建议从临时材料化开始。
如果选择将中间模型材料化为视图,则将它们放置在 dbt 配置文件中定义的主模式之外的自定义模式可能会有所帮助。这有助于组织模型并有效地管理权限。
中间层的主要目的是汇集不同实体并吸收最终马特模型的复杂性。这些模型提升了整体数据模型结构的可读性和灵活性。重要的是要考虑在其他模型中引用中间模型的频率。多个模型引用同一中间模型可能表明存在设计问题。在这种情况下,将中间模型转换为宏可能是增强模块化和保持更清晰设计的合适解决方案。
通过有效利用中间层,数据模型可以变得更加模块化和可管理,确保在吸收复杂性的同时保持组件的可读性和灵活性。
假设我们有两个分期模型,stg_books和stg_authors,分别代表书籍和作者数据。现在我们想要创建一个名为int_book_authors的中间模型,将来自两个分期模型的相关信息组合在一起。在我们的 dbt 项目中,可以创建一个名为int_book_authors.sql的新 dbt 模型文件,如示例 2-17 所示,并定义生成中间模型的逻辑。
示例 2-17. 中间模型
-- This should be file int_book_authors.sql
-- Reference the staging models
WITH
books AS (
SELECT *
FROM {{ ref('stg_books') }}
),
authors AS (
SELECT *
FROM {{ ref('stg_authors') }}
)
-- Combine the relevant information
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 能够正确推断模型依赖关系,并基于上游表构建中间模型。
马特模型
数据管道的顶层由马特模型组成,负责通过仪表板或应用程序将业务定义的实体集成和呈现给最终用户。这些模型将来自多个来源的所有相关数据结合在一起,形成一个统一的视图。
为了确保最佳性能,马特模型通常被物化为表格。物化模型能够加快查询执行速度,并在向最终用户提供结果时具备更好的响应性。如果物化表格的创建时间或成本成为问题,可以考虑配置为增量模型,允许随着包含新数据的加入而进行高效更新。
简单性是马特模型的关键,应避免过多的连接。如果在马特模型中需要多个连接,请重新思考设计,并考虑重新构造中间层。通过保持马特模型相对简单,可以确保查询执行的高效性,并维护数据管道的整体性能。
让我们来考虑一个数据马特(data mart)的例子,用于书籍出版分析。我们有一个名为int_book_authors的中间模型,包含原始书籍数据,包括每本书的作者信息(示例 2-18)。
示例 2-18. 马特模型
-- This should be file 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 的强大功能根据数据和业务需求动态定义断言。让我们通过分析 Example 2-19 来看一个 dbt 中的单独测试。
Example 2-19. dbt 中的单独测试示例
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)文件形式编写和存储,允许我们对查询进行参数化,并在各种情境中轻松重用。通用测试中的查询参数化使您能够快速地将测试适应多种情况。例如,您可以在将通用测试应用于不同模型或数据集时指定不同的列名或条件参数。
让我们看看 Example 2-20 中的其中一个通用测试。
Example 2-20. dbt 中的通用测试示例
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 文件的测试部分引用它,如 Example 2-21 所示。
Example 2-21. 重用通用测试
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的通用测试non_negative_values(假设my_project是您的 dbt 项目名称)。
通过在每个模型的tests部分指定通用测试,您可以在多个模型中重复使用相同的测试逻辑。此方法确保了数据验证的一致性,并允许您轻松地将通用测试应用于不同模型中的特定列,而无需复制测试逻辑。
注意,您必须确保通用测试的 YAML 文件位于 dbt 项目结构内正确的目录中,并且可能需要修改测试引用以匹配项目的命名空间和文件夹结构。
生成数据文档
正确数据建模的另一个关键组成部分是文档。具体而言,确保组织中的每个人,包括业务用户,都能轻松理解和访问诸如 ARR(年度重复收入)、NPS(净推荐值)或者 MAU(月活跃用户)等指标,对于推动数据驱动决策至关重要。
通过利用 dbt 的功能,我们可以记录这些指标如何定义以及它们所依赖的具体源数据。这些文档成为任何人都可以访问的宝贵资源,促进透明度并实现自助数据探索。
当我们消除这些语义障碍并提供易于访问的文档时,dbt 使得所有技术水平的用户都能够浏览和探索数据集,确保宝贵的洞见对更广泛的受众可用。
假设我们有一个名为nps_metrics.sql的 dbt 项目模型,用于计算净推荐值。我们可以在 SQL 文件中使用 Markdown 语法通过注释轻松记录此指标,如 Example 2-22 所示。
Example 2-22. 文档
/* nps_metrics.sql
-- This model calculates the Net Promoter Score (NPS)
for our product based on customer feedback.
Dependencies:
- This model relies on the "customer_feedback"
table in the "feedback" schema, which stores customer feedback data.
- It also depends on the "customer" table in the "users"
schema, containing customer information.
Calculation:
-- The NPS is calculated by categorizing customer
feedback from Promoters, Passives, and Detractors
based on their ratings.
-- Promoters: Customers with ratings of 9 or 10.
-- Passives: Customers with ratings of 7 or 8.
-- Detractors: Customers with ratings of 0 to 6.
-- The NPS is then derived by subtracting the percentage
of Detractors from the percentage of Promoters.
*/
-- SQL Query:
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)运行以下命令来为我们的 dbt 项目生成文档(示例 2-23)。
示例 2-23. 运行文档生成
dbt docs generate
运行该命令将为您的整个 dbt 项目生成 HTML 文档,包括已记录的 NPS 指标。生成的文档可以托管,并向您的组织用户提供访问权限,使他们可以轻松找到和理解 NPS 指标。
调试和优化数据模型
改进 dbt 性能的宝贵建议是仔细分析和优化查询本身。其中一种方法是利用查询规划器的功能,例如 PostgreSQL(Postgres)查询规划器。理解查询规划器将帮助您识别查询执行中的潜在瓶颈和低效性。
另一种有效的优化技术是通过将复杂查询分解为更小的组件(如 CTE)来解构它们。根据操作的复杂性和性质,这些 CTE 可以转换为视图或表。简单的涉及轻量计算的查询可以作为视图物化,而复杂且计算密集型的查询可以作为表物化。dbt 配置块可以用于指定每个查询所需的物化方法。
通过选择适当的物化技术可以实现显著的性能改进。这可以导致更快的查询执行时间,减少处理延迟,并提高整体数据建模效率。特别是,使用表物化已显示出令人印象深刻的性能提升,可以根据场景显著提高速度。
实施这些优化建议将使 dbt 工作流更精简和高效。通过优化查询并使用适当的物化策略,您可以优化 dbt 模型的性能,从而实现更好的数据处理和更高效的数据转换。
让我们来看看 示例 2-24 中的复杂查询。
示例 2-24. 复杂查询 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)。
示例 2-25. 解构复杂查询 1
-- Deconstructing a complex query using CTEs for optimization
-- CTE 1: Joining required data
WITH join_query AS (
SELECT table1.column1, table1.column2, table2.column3
FROM table1
INNER JOIN table2 ON table1.id = table2.id
)
-- CTE 2: Filtering rows
, filter_query AS (
SELECT column1, column2, column3
FROM join_query
WHERE column4 = 'some_value'
)
-- CTE 3: Aggregating and filtering results
, aggregate_query AS (
SELECT column1, column2, SUM(column3) AS total_sum
FROM filter_query
GROUP BY column1, column2
HAVING total_sum > 1000
)
-- Final query to retrieve the optimized results, and this will be our model
SELECT *
FROM aggregate_query;
join_query CTE 专注于连接所需的表,而 filter_query CTE 应用过滤条件以缩小行数。然后,aggregate_query CTE 执行聚合并应用最终的筛选条件。
通过将复杂查询拆分为单独的 CTE,可以简化和组织逻辑以优化执行。这种方法允许更好的可读性、可维护性和潜在的性能改进,因为数据库引擎可以针对每个 CTE 优化执行计划。最终查询通过从aggregate_query CTE 选择列来检索优化的结果。
现在让我们探索在 dbt 中调试物化模型的过程。起初这可能是一个困难的任务,因为它需要彻底的验证。一个重要的方面是确保数据模型看起来如预期,并且值与非物化版本匹配。
为了便于调试和验证,可能需要完全刷新整个表,并将其视为非增量。这可以通过dbt run --full-refresh命令来完成,该命令会更新表并像第一次执行一样运行模型。
在某些情况下,首几天同时对模型和增量模型进行全面更新可能会有所帮助。这种比较的方法允许验证两个版本之间的一致性,并减少未来数据差异的风险。当与生产中经过时间验证且可靠的数据模型一起工作时,这种技术尤为有效,因为它增强了对已做更改的信心。通过比较更新和增量模型,我们可以确保更改的准确性,并减轻潜在的与数据相关的问题。
考虑一个示例场景,使用基于交易数据计算每月收入的物化 dbt 模型。我们希望调试和验证此模型以确保其准确性。我们开始怀疑物化模型生成的值可能与预期结果不符合。为了排除故障,我们决定完全刷新表格,就像它不是增量一样。使用dbt full-refresh命令,我们触发了更新整个表并从头运行模型的流程。
在最初的几天,我们还会运行一个并行过程来更新物化和增量模型。这使我们能够比较两个版本之间的结果,并确保它们匹配。通过检查更新模型和增量模型的一致性,我们增强了对所做更改准确性的信心。
例如,如果我们有一个经过时间验证并被认为是可靠的运行已久的收入模型,那么比较更新和增量模型的结果就更有意义。通过这种方式,我们可以确认对模型的更改未导致计算的收入数字出现意外差异。此外,全面的测试对确保数据模型的准确性和可靠性至关重要。在整个工作流程中实施测试可以帮助及早发现问题,并为 SQL 查询的性能提供宝贵的见解。
注意
所有这些 dbt 功能,从构建 dbt 模型到测试和文档编制,将在第四章和第五章中讨论和强化。
中勋体架构模式
数据仓库在决策支持和商业智能方面有着丰富的历史,但在处理非结构化、半结构化和高多样化数据时存在限制。与此同时,数据湖作为存储多样数据格式的仓库出现,但缺乏关键特性,如事务支持、数据质量强制执行和一致性。
这阻碍了它们实现承诺的能力,并导致丧失与数据仓库相关的好处。为了满足企业不断发展的需求,需要一个灵活且高性能的系统,支持 SQL 分析、实时监控、数据科学和机器学习等多样的数据应用。然而,一些最新的人工智能进展侧重于处理广泛的数据类型,包括半结构化和非结构化数据,而传统的数据仓库并未为此进行优化。
因此,组织通常使用多个系统,包括数据湖、数据仓库和专用数据库,这增加了数据在系统之间移动和复制导致的复杂性和延迟。作为将所有这些传统系统整合成能够满足新市场需求的系统的自然结果,出现了一种新类型的系统:数据湖仓。
数据湖仓 结合了数据湖和数据仓库的优势,通过在开放格式(如 Apache Delta Lake、Iceberg 或 Apache Hudi)的成本效益云存储上实现类似仓库的数据结构和管理特性。这些格式相对于 CSV 和 JSON 等传统文件格式具有各种优势。虽然 CSV 缺乏列类型化,JSON 提供更灵活的结构但类型不一致。Parquet、Apache Avro 和 ORC(优化的行列式文件格式)通过面向列和更强类型化来改进这些问题,但不符合 ACID(原子性、一致性、隔离性、持久性)要求(在某些情况下除外,ORC 符合 ACID)。相反,Delta Lake、Iceberg 和 Hudi 通过增加 ACID 合规性和作为双向数据存储的能力来增强数据存储,支持修改的高吞吐量同时支持大量分析查询。与最初为本地 Hadoop 系统设计的传统格式 Parquet 不同,这些格式特别适合现代基于云的数据系统。
湖屋提供关键功能,如并发数据读写的事务支持、模式执行和治理、直接 BI 工具支持、为了可伸缩性而解耦存储和计算、开放式的标准化存储格式和 API,以便高效地访问数据、支持多种数据类型,并兼容包括数据科学、机器学习和 SQL 分析在内的各种工作负载。它们还经常提供端到端流处理能力,消除了实时数据应用需要单独系统的需求。企业级湖屋系统包括安全性、访问控制、数据治理、数据发现工具,并符合隐私规定的合规性。实施湖屋使组织能够将这些基本功能整合到一个由数据工程师、分析工程师、科学家、分析师甚至机器学习工程师共享的单一系统中,从而协作开发新的数据产品。
在湖屋和新开放格式的背景下,勋章架构应运而生。简而言之,这是一种数据建模范式,用于在湖屋环境中战略性地组织数据,旨在通过不同层次的迭代逐步提升数据质量。这种架构框架通常包括三个可辨识的层次,即青铜层、银层和金层,每一层代表数据精炼程度的逐级提升:
青铜层
这作为来自外部源系统数据的初始目标。此层中的表反映了源系统表的结构,包括任何额外的元数据列,以捕获加载日期/时间和处理 ID 等信息。此层优先考虑高效的变更数据捕获(CDC),保持源数据的历史存档,确保数据血统,便于审计,并支持重新处理,无需重新读取源系统的数据。
银层
在湖屋架构中,这一层在整合和精炼从青铜层获取的数据方面发挥着重要作用。银层通过匹配、合并、符合和清洗等过程创建了一个全面的视图,涵盖了关键业务实体、概念和交易。这包括主客户、商店、非重复交易和交叉引用表。银层作为自助分析的全面数据源,赋予用户自由报告、高级分析和机器学习能力。通常观察到银层可以采用 3NF 数据模型、星型模式、数据仓库或者雪花模型。与传统数据仓库类似,这是任何利用数据解决业务问题的项目和分析的宝贵资源。
金层
此层提供了解决业务问题的宝贵见解。它从银层聚合数据,并将其提供给 BI 临时报告工具和机器学习应用程序。该层确保了数据湖的可靠性、提高了性能,并为云数据存储提供了 ACID 事务,同时还统一了流处理和批处理事务。
图 2-6 描述了湖屋中奖牌架构的情境,并展示了 dbt 如何支持这种系统的创建。

图 2-6. 奖牌架构的表现形式及其与 dbt 的关系
从青铜层到黄金层的进展中,数据经历了摄取、清洗、增强和聚合等多个步骤,提供了无数的业务洞见。这种方法相对于传统的数据架构(如具有分段和维度模型层的数据仓库,甚至仅是一个数据湖,通常涉及更多的文件组织而不是创建适当的语义层),表示了一个重要的进步。
注释
奖牌架构并不取代其他维度建模技术。每个层中模式和表的结构可以根据数据更新的频率和类型以及数据的预期使用而有所不同。相反,它指导数据应如何在三个层次之间组织,以实现更模块化的数据建模方法。
对于分析工程师来说,理解奖牌架构的基础和湖屋背后的概念是非常有价值的,因为在某些情况下,他们可能会花费大量时间在此处。这种参与可以包括在奖牌的某一层部署建模结构,利用开放格式提供的接口,或构建转换脚本(例如使用 dbt 等工具)以促进数据在架构各层之间的进展。
但是,需要注意的是,开放格式和湖屋在特定数据架构使用情况下的重要性可能有所不同。例如,在像 Snowflake 这样的架构中,数据可能主要被摄取到本地表中,而不是像 Iceberg 这样的开放格式,这使得理解湖屋更多地成为一种不可或缺的要求,而不是分析工程的基本需求。
摘要
在分析领域,数据建模已经在适应多样化的业务洞见和报告需求方面有了显著发展。星型模式通过将中心事实表环绕以维度表提供了简单的查询方法。雪花模式通过进一步分解这些维度提供了更深入的细粒度。相比之下,数据仓库方法优先考虑灵活性,以处理数据源快速变化的环境。新的奖牌设计结合了所有这些模型,形成了适应各种分析需求的完整计划。
所有建模进展都旨在解决特定的分析问题。其核心目标是高效提供可操作的洞察,无论是在星型和雪花模式的性能改进,还是数据仓库的多功能性。随着分析需求变得更加复杂,选择正确的建模方法不仅能使数据可用,还能确保数据具有意义并提供洞察。
分析工程师使用建模结构,如星型、雪花、数据仓库或勋章,创建和维护稳健、可扩展和高效的数据结构。他们的工作确保数据的最佳组织,使其易于访问,并对数据分析师和科学家有所帮助。分析工程师通过理解并应用这些模型,从海量数据流中创建连贯的数据集,为准确的洞察和明智的决策奠定基础。
第三章:分析用 SQL
在数据和分析的广阔领域中,选择正确的工具和技术以有效处理和操作数据至关重要。一个经受住时间考验并始终处于前沿的工具是结构化查询语言(SQL)。它提供了一种强大而多功能的处理数据的方法,使其成为处理任何分析开发任务的优秀首选。SQL 是一种用于管理和操作关系数据库的标准化编程语言,使数据专业人员能够高效地检索、存储、修改和分析存储在数据库中的数据。由于其直观的语法和社区的广泛接受,SQL 已经成为数据专家的标准语言,他们用它与数据库进行交互,并从复杂数据集中获得宝贵的见解。
SQL 在当今数据驱动的世界中作为数据消费和分析的支柱。企业在执行数据分析操作以获得竞争优势时高度依赖它。SQL 的多功能性和丰富功能使其成为分析专业人员的重要工具,使他们能够检索特定子集的数据,执行复杂的聚合操作,并连接多个表以发现数据中隐藏的模式和关系。
SQL 的关键优势之一是其快速检索和操作数据的能力,提供了广泛的查询功能。这使得数据专家能够基于特定的标准对数据进行筛选、排序和分组,仅检索必要的数据,从而最大限度地减少资源使用并提高性能。此外,SQL 还支持数据操作,如插入、更新和删除记录,有助于在分析之前进行数据清洗和准备任务。
使用 SQL 的另一个相关好处是它与各种分析工具和生态系统(如 Python 或 BI 平台)的无缝集成,使其成为数据专业人士的首选语言,并允许他们将 SQL 的强大功能与高级统计分析、机器学习算法和交互式可视化结合起来。此外,基于云的数据库和数据仓库的崛起进一步增强了 SQL 在分析消费中的相关性,像谷歌的 BigQuery、亚马逊的 Redshift 和 Snowflake 等平台支持 SQL 作为其主要查询语言。
在本章中,我们将讨论 SQL 语言作为最常用的分析语言之一的弹性。然后,我们将探讨数据库的基础知识,并介绍 SQL 作为与其交互的标准语言。我们还将研究视图的创建和使用,这为简化复杂查询和抽象数据结构提供了强大的机制。
当我们深入研究 SQL 时,我们将回顾窗口函数,这些函数使您能够进行高级计算和聚合。此外,我们还将深入研究 CTE(公共表达式),它们提供了创建临时结果集和简化复杂查询的方法。最后,我们还将提供 SQL 用于分布式数据处理的一瞥,最后以一个额外的部分展示 SQL 在训练机器学习模型中的应用。
SQL 的韧性
随着时间的推移,我们发现使用 SQL 开发的数据工程管道通常能够持续多年,而查询和存储过程仍然是支持金融机构、零售公司甚至科学活动的几个关键系统的核心。然而,令人惊讶的是,SQL 已被广泛应用并不断演进,以满足现代数据处理的需求,引入了新的功能。此外,令人着迷的是,诸如 dbt、DuckDB 甚至新的数据操作库 Polars 通过 SQL 接口提供其功能。但是,这种流行背后的主要原因是什么呢?我们认为可以强调几个因素。
首要的是代码的可读性。这是数据工程的一个关键方面。SQL 的语法虽然多才多艺,允许根据上下文和具体需求同时使用命令式和声明式。许多查询涉及命令式任务,比如为用户检索特定数据或计算给定日期范围内的结果。然而,SQL 的声明性质在指定需要的数据而非如何检索时表现出色。这种灵活性使得包括 BI 开发人员、业务分析师、数据工程师和数据科学家在内的广泛用户能够理解和解释代码。与一些其他严格的命令式数据处理语言不同,SQL 允许作者专注于描述所需的结果。这种自我记录特性使得 SQL 代码更易读、更易理解,促进跨功能团队的有效协作。
另一个令人兴奋的因素是,尽管 SQL 作为一个接口经过了多年的考验,但事实上它背后的引擎在过去几年里发生了巨大的进步。传统的 SQL 引擎得到了改进,而像 Spark 和 Presto 这样的分布式工具使得 SQL 能够处理海量数据集。近年来,DuckDB 已经成为一个变革者,通过在单台机器上进行极快的并行分析查询,为 SQL 赋予了新的功能。其功能与其他高性能替代方案不相上下,为各种规模的数据工程任务开辟了新的可能性。
然而,需要注意的是,并非所有基于 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 的出现和完善铺平了道路,SQL 成为处理关系型数据库的强大且广泛采用的语言。然而,在探讨数据库的具体特性之前,了解数据、信息和知识的更广泛背景至关重要,因为它们或者存在于数据库中,或者从数据库中获取。
在这个背景的基础上,我们有 DIKW 金字塔,如 图 3-1 所示。这个概念模型描述了数据、信息、知识和智慧之间的层级关系。通过一系列迭代过程,DIKW 金字塔提供了一个框架,用于理解如何将原始数据转化为可操作的智慧。

图 3-1. DIKW 金字塔
为了更好地理解 DIKW 金字塔,让我们分解每一层:
数据
缺乏上下文和含义的原始事实和数字。数据可以被视为信息的构建块。数据的例子:1989、教师、绿色。
信息
提供上下文并回答特定问题的数据的有序和结构化表示。信息的例子:
-
我的数学老师出生于 1989 年。
知识
当我们将信息与经验、专业知识和理解结合时,知识就产生了。它代表通过分析和解释信息获得的见解,使个人和组织能够做出明智的决策并采取适当的行动。知识的例子:
-
由于我的数学老师是 1989 年出生的,他已经成年了。
-
我正在驾驶的交通灯正在变绿。
智慧
超越知识的深度理解层次。智慧发生在个人和组织能够应用他们的知识并做出明智判断的时候,导致积极影响和转变性的见解。智慧的例子:
-
也许是时候让我的数学老师开始考虑退休储蓄计划了。
-
当交通灯变绿时,我可以前进了。
数据库在 DIKW 金字塔中发挥着至关重要的作用,作为存储、管理和组织数据的基础。这使得数据能够转化为有意义的见解,最终使企业获得必要的知识,以做出明智的决策。
数据库类型
数据库是现代数据管理系统的核心组成部分,提供了存储、组织和检索数据的结构化方法。为了更好地理解数据库如何实现这一点,让我们首先探讨数据库的两个主要类别:关系型和非关系型。通过理解这两种类型之间的特点和区别,您将更能够选择适合您特定数据需求的数据库解决方案。
图 3-2 显示了数据库的两个主要类别,在每个类别中映射了最常见的数据库类型。

图 3-2. 数据库类别及其类型
关系型数据库
在这个最常见且广泛采用的数据库类别中,数据被组织成行和列的表格。键用于强制表之间的关系,SQL 用于查询和操作数据。关系型数据库提供强大的数据完整性、事务可靠性和支持 ACID 属性,确保数据库事务可靠、保持数据完整性,并能从故障中恢复。
非关系型数据库
也被称为NoSQL(不仅仅是 SQL)数据库,非关系型数据库作为管理大量非结构化和半结构化数据的替代方案而出现,具有可扩展性和灵活性。与关系型数据库相比,非关系型数据库不依赖于固定的模式。它们可以以各种格式存储数据,例如键-值对、文档、宽列存储或图形。非关系型数据库优先考虑高性能、水平扩展性和模式灵活性。它们非常适合于实时分析、处理非结构化数据的应用以及物联网数据等场景。
注意
在接下来的几节中,我们将主要关注关系数据库,这是本章的整体目标的结果,即介绍 SQL 的基础知识。
我们可以把数据库想象成数据宇宙的一个子集 —— 它们被建立、设计和输入数据,这些数据具有特定于您的组织的特定目的。数据库是社会的重要组成部分。一些活动,例如列出的活动,广泛分布在整个社会中,并且一个数据库位于中心用于存储数据:
-
预订酒店
-
预订飞机票
-
在一个知名市场购买一部手机
-
输入您喜欢的社交网络
-
去看医生
但在实践中是什么样子呢?进入关系数据库,我们将数据组织成具有行和列的表。表代表我们宇宙中的一个实体,例如大学的学生或图书馆的书籍。列描述实体的属性。例如,学生有姓名或地址。一本书有标题或国际标准书号(ISBN)。最后,行是数据本身。学生的姓名可以是 Peter Sousa 或 Emma Rock。关于书名,一行可以是“使用 SQL 和 dbt 进行分析工程”。图 3-3 展示了一个带有其相应列和行的表的示例。

图 3-3. 表格及其行列示例
另一个需要考虑的主题是我们如何与数据建立关系并确保一致性。这是关系数据库中需要重点强调的一个重要因素,通过使用键可以在表之间强制建立连接。在关系数据库中执行这些关系和连接的操作涉及实施机制,以维护跨相关表的数据的完整性和一致性。这些机制维护表之间的关系,防止不一致或数据异常。
通过使用主键和外键来强制关系的一种方式。我们将介绍这一点,但现在,图 3-4 展示了表之间的相互关系。使用案例是一个大学,其中一个或多个学生可以注册一个或多个课程。
了解这些类型的数据库为我们的下一个主题铺平了道路:数据库管理系统(DBMS)。在下一节中,我们将更深入地探讨 DBMS 的功能和重要性,它们作为软件工具,能够在各种类型的数据库中实现高效的数据存储、检索和管理。

图 3-4. 表之间的相互关系
数据库管理系统
DBMS 是一个软件系统,它使数据库的创建、组织、管理和操作成为可能。它为用户和应用程序提供了一个接口和一组工具,用于与数据库交互,从而实现高效的数据存储、检索、修改和删除。
DBMS 充当用户或应用程序与底层数据库之间的中介。它抽象了与数据库交互的复杂性,提供了一种便捷和标准化的工作数据的方式。它作为软件层处理数据的存储、检索和管理,同时确保数据的完整性、安全性和并发控制。
DBMS 的主要功能包括以下几点:
数据定义
数据库管理系统 (DBMS) 允许用户通过创建和修改数据库模式来定义数据的结构和组织方式。它支持定义表、列、关系和约束,这些规定了数据库中存储的数据。
数据操作
用户可以通过查询语言(通常是 SQL)对存储在数据库中的数据执行操作。DBMS 提供了插入、检索、更新和删除数据的机制,允许高效和受控地操作数据库内容。
数据安全性和完整性
DBMS 提供机制来确保数据安全性,通过执行访问控制策略来实施。它支持定义用户角色和权限,限制对敏感数据的访问。此外,DBMS 通过实施约束和验证来保持数据的一致性和准确性。
数据并发性和事务管理
DBMS 处理多用户或应用程序对数据库的并发访问,确保数据保持一致性并受到保护,不会发生冲突。它提供事务管理功能,确保一组操作能够可靠和一致地执行,符合 ACID 特性。
数据恢复和备份
DBMS 包含功能以确保数据的持久性和可恢复性。它提供数据备份和恢复机制,允许在系统故障或灾难发生时进行数据恢复。
一些适用于关系型和非关系型数据库的常见 DBMS 可见于表 3-1。
表 3-1. 常见的 DBMS
| 关系型数据库 | 非关系型数据库 |
|---|---|
| Microsoft Access | MongoDB |
| Microsoft SQL Server | Apache Cassandra |
| Postgres | Apache CouchDB |
| MySQL | Redis |
| SQLite | Elasticsearch |
"与数据库交流"
从外部角度看,通过 DBMS 与数据库交互提供了四种类型的语言:
数据定义语言 (DDL)
处理模式,如表的创建
数据操作语言 (DML)
处理数据
数据控制语言 (DCL)
管理对数据库的权限
事务控制语言 (TCL)
处理发生在数据库中的事务
图 3-5 展示了与数据库交互时使用的主要语言及其主要命令。

图 3-5. 主要 SQL 命令
对于本书,我们的主要重点将是通过学习如何查询、操作和定义数据库结构来提供 SQL 的坚实基础,因此我们将讨论 DDL 和 DML。不涉及像 DCL 和 TCL 这样的管理任务相关的活动。
使用 DDL 创建和管理数据结构
DDL 是 SQL 的子集,是一种标准化的语言,用于创建和修改数据库中对象的结构。它包括定义表、索引、序列、别名等的命令和语法。
最常见的 DDL 命令如下:
CREATE
创建新的数据库对象,如表、视图、索引或约束。它指定对象的名称及其结构,包括列、数据类型和任何附加属性。
DROP
删除或删除现有数据库对象。它永久删除指定的对象及其所有相关数据。
ALTER
修改现有数据库对象的结构。您可以使用它来添加、修改或删除表的列、约束或其他属性。它提供了适应不断变化的需求调整数据库架构的灵活性。
RENAME
重命名现有的数据库对象,如表或列。它提供了一种在不改变对象结构或数据的情况下更改对象名称的方式。
TRUNCATE
快速从表中删除所有数据,同时保留表结构。与使用DELETE命令逐行删除数据相比,它更快速,因为它会释放数据页而不记录单个行的删除操作。
CONSTRAINT
定义表列的约束,通过指定数据必须满足的规则或条件,确保数据的完整性和有效性。
INDEX
在表的一个或多个列上创建索引。通常,索引通过创建排序结构来提高数据检索操作的性能,从而实现更快的数据搜索和排序。
在进行实际应用之前,我们需要详细讨论一些主题和一个额外的主题引入。事实上,大多数 DDL 命令在某种意义上是不言自明的,只要稍后在代码中看到它们,它们将很容易理解。尽管如此,我们需要稍微详细地讨论CONSTRAINT命令,以介绍其特殊性。
正如前面提到的,约束是数据必须满足的规则或条件,以确保其完整性。通常,这些约束适用于列或表。最常见的约束如下:
主键
主键约束确保表中的列或列组合唯一标识每一行,防止重复和空值。它对数据完整性至关重要,并且通常用作相关表中外键约束的参考。
外键
外键约束指定两个表之间的关系。它确保一个表中列或列组合中的值与另一个表中主键值匹配,有助于维护引用完整性并强制执行跨相关表的数据一致性。
唯一
唯一约束确保列或列组合中的值是唯一的,并且不允许重复。与主键不同,唯一约束可以允许空值,但如果列有唯一约束,则只允许一个空值。
检查
检查约束对列中允许的值施加条件。通常用于强制执行业务规则、特定领域的要求或数据上的任何其他自定义条件。
非空
非空约束保证列不包含空值,因此具有此约束的特定列必须为每个插入或更新的行提供一个值。这有助于强制数据完整性并避免意外的空值。
最后,还有一个要讨论的点:用于分类可以存储在列或变量中的数据类型。这些字段在不同的数据库引擎中可能有所不同。在我们的情况下,我们将简单地使用 MySQL 数据类型作为参考:
整数
没有分数部分的整数。最常见的是INT、SMALLINT、BIGINT、TINYINT。可能的值示例:1、156、2012412、2。
十进制
带有小数部分的数字。一些最常见的是DECIMAL、NUMERIC、FLOAT、DOUBLE。可能的值示例:3.14、94.5482、5161.17620。
布尔
二进制值。传统上写作BOOLEAN、BOOL、BIT、TINYINT。用于存储真/假或 0/1 值。
日期
大多数是不言而喻的,但格式可能有所不同。声明为DATE,常用的标准格式是 2023-07-06。
时间
您也可以决定时间数据类型的格式。在数据库中写作TIME,一个常见的格式是 18:34:59。
时间戳
日期和时间结合在一起。通常我们使用TIMESTAMP或DATETIME。示例:2017-12-31 18:34:59。
文本
最通用的数据类型。但它只能是字母或字母、数字或任何其他字符的混合。通常声明为CHAR、VARCHAR、NVARCHAR、TEXT。请注意,选择正确的文本数据类型很重要,因为每种类型都有指定的最大长度。文本示例:"hello world"、"porto1987"、"Hélder"、"13,487*5487+588"。
注释
我们将使用 MySQL 因其广泛的采用率。您可以通过MySQL 网站下载 MySQL Workbench。
现在,您对 DDL 命令和最常见的数据库数据类型有了更好的理解,让我们为管理 O'Reilly 图书创建一个数据库。这与第二章中的示例相符,当时我们介绍了一个用于追踪图书的 O'Reilly 数据库,但现在让我们开始创建物理模型。
作为一种说明,对于数据工程师来说,精通所有类型的 SQL 命令至关重要,因为他们负责数据库设计(DDL)和数据操作(DML)。分析师主要专注于 DML SQL 命令,通常仅限于用于数据分析的 SELECT 查询。另一方面,分析工程师通常使用 DML 和一些 DDL SQL 命令的组合,尽管他们经常通过工具如 dbt 来抽象 DDL 操作。
首先,让我们创建数据库本身。在您的 MySQL 客户端中,执行 示例 3-1 中的命令。
示例 3-1. 创建数据库
-- Create the OReillyBooks database statement
CREATE DATABASE OReillyBooks;
现在,已经创建了数据库,执行 示例 3-2 中的代码。
示例 3-2. 创建数据库,第二部分
-- Use the database
USE OReillyBooks;
-- Create the tables
-- Table: Authors
CREATE TABLE authors (
author_id INT PRIMARY KEY,
author_name VARCHAR(100)
);
-- Table: 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)
);
-- Table: Category
CREATE TABLE category (
category_id INT PRIMARY KEY,
category_name VARCHAR(50)
);
-- Table: 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 语法
-- Add a new column
ALTER TABLE table_name
ADD column_name datatype [column_constraint];
-- Modify a datatype of an existing column
ALTER TABLE table_name
ALTER COLUMN column_name [new_datatype];
-- Rename a column
ALTER TABLE table_name
RENAME COLUMN old_column_name TO new_column_name;
-- Add a new constraint to a column
ALTER TABLE table_name
ADD CONSTRAINT constraint_name constraint_type (column_name);
-- Modify an existing constraint
ALTER TABLE table_name
ALTER CONSTRAINT constraint_name [new_constraint];
-- Remove an existing column
ALTER TABLE table_name
DROP COLUMN column_name;
根据 示例 3-3 中显示的语法,适合我们需求的修改是添加一个新列。现在通过执行 示例 3-4 中的代码片段来添加 publication_year。
示例 3-4. 添加出版年份
-- Add publication_year to the books table
ALTER TABLE books
ADD publication_year INT;
使用 DML 操作数据
DML 在数据库管理中是一个重要组成部分。该语言使得在数据库系统中进行数据选择、插入、删除和更新成为可能。其主要目的是检索和操作驻留在关系数据库中的数据,同时涵盖了几个关键命令。
使用 INSERT 插入数据
INSERT 命令便于向表中添加新数据。通过此命令,用户可以轻松地将一个或多个记录插入到数据库中的特定表中。利用 INSERT,可以通过包含额外条目来扩展表的内容。该命令在向最初为空的表中添加记录时非常有用,同时也允许不断扩展数据库中现有的数据。示例 3-5 展示了该命令的标准语法。
示例 3-5. INSERT 语句的语法
INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...);
INSERT INTO 语句指定将插入数据的表,其中table_name代表表本身的名称。组件(__column1__, __column2__, ...)是可选的,允许指定要插入数据的列。如果省略列,则假定将为表中的所有列提供值。VALUES关键字表示要插入到指定列中的值列表的开始。在VALUES子句中,(__value1__, __value2__, ...)包含要插入到相应列中的实际值。确保提供的值数量与指定的列数相匹配至关重要,这是确保在插入过程中正确映射值到相应列的唯一方式。大多数数据库引擎如果不遵守此规则将引发错误。
现在让我们扩展我们的用例,我们从“使用 DML 操作数据”开始,并将数据插入到先前创建的表中。为此,请执行 示例 3-6 中的命令。
示例 3-6. 创建虚拟数据
-- Inserting data into the authors table
INSERT INTO authors (author_id, author_name) VALUES
(1, 'Stephanie Mitchell'),
(2, 'Paul Turner'),
(3, 'Julia Martinez'),
(4, 'Rui Machado'),
(5, 'Thomas Brown');
-- Inserting data into the books table
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);
-- Inserting data into the category table
INSERT INTO category (category_id, category_name) VALUES
(1, 'Programming'),
(2, 'Machine Learning'),
(3, 'Data Science'),
(4, 'Software Engineering'),
(5, 'Algorithms'),
(6, 'Computer Science');
-- Inserting data into the book_category table
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 语句,但目前仅检查每个表中的数据就足够了。
示例 3-7. 使用 SELECT 语句查询 authors、book_category、books 和 category 表
select * from authors;
select * from book_category;
select * from books;
select * from category;
用 SELECT 选择数据
SELECT是 SQL 中最基本的 DML 命令之一。此命令允许您从数据库中提取特定的数据。执行此语句时,它检索所需的信息并将其组织成结构化的结果表,通常称为结果集。这个结果集包含满足指定条件的数据,使用户能够轻松访问和分析所选信息。在示例 3-8 中,我们可以分析此命令的(最简单的)语法。
注意
如果您已经熟练掌握 SQL 和SELECT命令,并希望了解更高级的 SQL 语句,请参考“窗口函数”。如果您已经想要进入 dbt 世界,您可以在第四章找到相关内容。
示例 3-8. SELECT语句的语法
SELECT column1, column2, ...
FROM table_name;
此结构的SELECT部分指示从表中检索的具体列或表达式。FROM部分指定从中检索数据的表。我们还有更多内容可以详细说明这个命令,从数据过滤和相应的运算符到数据分组或连接。在接下来的部分,我们将讨论每个属性。
使用WHERE过滤数据
可选的WHERE子句允许用户定义检索数据必须满足的条件,有效地根据指定的条件过滤行。这是 SQL 查询的基本部分,允许您根据指定的条件过滤和检索表中的特定数据子集。示例 3-9 展示了WHERE语句的语法。
示例 3-9. 带有WHERE子句的SELECT语句语法
SELECT column1, column2, ...
FROM table_name
WHERE condition ;
要正确理解如何在 SQL 中编写条件并充分过滤数据,我们必须首先熟悉 SQL 运算符,这些运算符支持单个或多个条件。
SQL 运算符
SQL 运算符经常在WHERE子句中使用,用于建立过滤数据的条件。这些运算符允许您在 SQL 中比较符合定义条件的值和表达式。表 3-2 总结了最常见的运算符。
表 3-2. SQL 运算符
| 运算符 | 运算符类型 | 意义 |
|---|---|---|
= |
比较 | 等于 |
<> 或 != |
比较 | 不等于 |
< |
比较 | 小于 |
> |
比较 | 大于 |
<= |
比较 | 小于或等于 |
>= |
比较 | 大于或等于 |
LIKE '%expression%' |
比较 | 包含"expression" |
IN ("exp1", "exp2") |
比较 | 包含任何一个"exp1"或"exp2" |
BETWEEN |
逻辑 | 选择在给定范围内的值 |
AND |
逻辑 | 将两个或多个条件组合在一起,只有当所有条件都为真时才返回true |
OR |
逻辑 | 将两个或多个条件组合在一起,如果至少一个条件为真,则返回true |
NOT |
逻辑 | 对条件进行否定,如果条件为假则返回true,反之亦然 |
UNION |
集合 | 将两个 SELECT 语句的结果合并,去除重复的行。 |
UNION ALL |
集合 | 将两个 SELECT 语句的所有记录合并,但使用 UNION ALL 时,重复的行不会被消除。 |
为了更好地理解它们的应用,让我们探讨比较运算符和逻辑运算符的示例。为了简化事情,由于我们没有深入研究 SQL 的其他元素,比如连接,让我们使用 books 表作为我们用例的数据源。
作为条件和逻辑运算符的初始示例,让我们尝试找到早于 2015 年出版的书籍。然后,让我们仅找到 2017 年出版的书籍,最后是标题中包含“Python”的书籍。示例 3-10 包含了三个代码片段,帮助你解决这个挑战。
例 3-10. 使用条件运算符选择数据
-- Books published earlier than 2015
SELECT
book_title,
publication_year
FROM books
WHERE publication_year < 2015;
-- Books published in 2017
SELECT
book_title,
publication_year
FROM books
WHERE publication_year = 2017;
-- Books with "Python" in the title
SELECT
book_title,
publication_year
FROM books
WHERE book_title LIKE '%Python%';
示例 3-10 展示了使用条件运算符处理的三个示例。随意尝试代码并测试之前介绍的其他内容。
最后,为了熟悉逻辑运算符,让我们搜索 2012 年或之后出版的书籍。示例 3-11 将帮助你完成这个任务。
例 3-11. 使用逻辑运算符选择数据
-- Books published in 2012 or after 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 中。
表 3-3. 聚合函数
| 聚合函数 | 意义 |
|---|---|
COUNT() |
计算列中行数或非空值的数量。 |
SUM() |
计算列中数值的总和。 |
AVG() |
计算列的平均值。 |
MAX() |
从列中检索最大值。 |
MIN() |
从列中检索最小值。 |
DISTINCT |
虽然严格意义上不是聚合函数,DISTINCT 关键字通常与聚合函数一起在 SELECT 语句中使用,用于计算唯一的值。 |
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,但如果我们有类别名称会更好;然而,这个字段仅在category表中可见。要包含它,我们需要知道如何使用连接。我们将在“使用 INNER、LEFT、RIGHT、FULL 和 CROSS JOIN 连接数据”中进一步讨论这个问题。
最后,我们来看看HAVING过滤器。作为与GROUP BY紧密相关的可选子句,HAVING过滤器应用于分组数据。与WHERE子句相比,HAVING在聚合之后过滤行,而WHERE在分组操作之前过滤行。然而,相同的运算符,如“等于”和“大于”,等等,与WHERE语句中的应用相同。
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根据指定的连接条件仅返回两个表中匹配的行。如果我们想象一个 Venn 图,其中圆 A 和圆 B 分别代表每个数据集,在INNER JOIN中,我们只会看到包含两个表的匹配值的重叠区域。让我们查看图 3-6 以更好地可视化这个 Venn 图。

图 3-6. 展示INNER JOIN的 Venn 图
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 展示了查询结果。

图 3-7. 展示仅包含有书籍的作者的INNER JOIN查询结果
通过分析结果,我们可以快速识别缺失的作者 John Doe。正如您可能记得的那样,我们创建了他但没有任何书籍,因此在使用INNER JOIN时,预期他会被排除。
LEFT JOIN(或 LEFT OUTER JOIN)
返回左表的所有行和右表的匹配行。如果没有匹配,则包括来自右表的列的空值。与前一个练习类似,圆圈 A 表示左表,圆圈 B 表示右表的 Venn 图。在 LEFT JOIN 中,左圆圈包含左表的所有行,重叠区域表示基于连接条件的匹配行。右圆圈包含来自右表的非匹配行,结果集中用空值表示。查看 图 3-8 更好地可视化 Venn 图。

图 3-8. 展示了 LEFT JOIN 的维恩图。
LEFT JOIN 的代码语法在 示例 3-21 中。
示例 3-21. LEFT JOIN 的语法。
SELECT
columns
FROM Table_A
LEFT JOIN Table_B ON join_condition;
为了测试 LEFT JOIN,我们保持与之前相同的用例,将作者与其书籍关联,但现在我们要列出所有作者及其各自的书籍,并且必须包括没有书籍的作者。在 示例 3-22 中执行代码片段。
示例 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 中。

图 3-9. 展示了 LEFT JOIN 查询的作者及其相应书籍的输出。
与 INNER JOIN 相比,LEFT JOIN 允许我们看到作者 John Doe。这是因为在 LEFT JOIN 中,左表 authors 被完全显示,而右表 books 仅显示与 authors 交集的结果。
RIGHT JOIN(或 RIGHT OUTER JOIN)
右连接返回右表的所有行和左表的匹配行。如果没有匹配,则包括来自左表的列的空值。继续考虑每个数据集用圆圈 A(左)和圆圈 B(右)表示的 Venn 图。在 RIGHT JOIN 中,右圆圈包含右表的所有行,重叠区域表示基于连接条件的匹配行。左圆圈包含来自左表的非匹配行,结果集中用空值表示。最后,查看 图 3-10 更好地可视化 Venn 图。

图 3-10. 展示了 RIGHT JOIN 的维恩图。
RIGHT JOIN 的代码语法如 示例 3-23 所示。
示例 3-23. RIGHT JOIN 的语法。
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 中。

图 3-11. RIGHT JOIN 查询输出,显示了书籍及其对应作者
通过分析查询输出,我们可以看到所有书籍及其相应的作者。由于没有任何没有作者的书籍,我们无法看到books和authors之间的任何交集,即存在书籍但没有作者的情况。
FULL JOIN(或FULL OUTER JOIN)
在此连接中,从两个表中返回所有行。它结合了LEFT JOIN和RIGHT JOIN的结果。如果没有匹配项,将包括非匹配表中列的空值。在一个维恩图中,圆圈 A(左)和圆圈 B(右)分别表示每个数据集,FULL JOIN的图表将显示重叠区域,表示基于连接条件的匹配行,而每个圆圈的非重叠部分包括各自表中的非匹配行。最终生成的结果集包括来自两个表的所有行,对于非匹配行,将包括空值。让我们查看图 3-12 以更好地可视化。

图 3-12. 用于说明FULL JOIN的维恩图
FULL JOIN的代码语法见示例 3-25。
示例 3-25. FULL JOIN的语法
SELECT
columns
FROM Table_A
FULL JOIN Table_B ON join_condition;
注意
MySQL 不原生支持FULL JOIN。我们必须在LEFT JOIN和RIGHT JOIN语句之间执行UNION来实现它。这有效地结合了两个方向的数据,复制了FULL JOIN的行为。
CROSS JOIN
CROSS JOIN(或笛卡尔积连接)返回两个表的笛卡尔积,即将第一个表的每一行与第二个表的每一行组合。它不需要连接条件。在CROSS JOIN的维恩图中,我们没有重叠的圆圈,因为它组合了圆圈 A(左)和圆圈 B(右)的每一行。结果集包括来自两个表的所有可能的行组合,如图 3-13 所示。

图 3-13. 用于说明CROSS JOIN的维恩图
CROSS JOIN的代码语法见示例 3-26。
示例 3-26. CROSS JOIN的语法
SELECT
columns
FROM Table_A
CROSS JOIN Table_B;
展示了authors表和books表的CROSS JOIN,详见示例 3-27。
示例 3-27. authors表和books表的CROSS JOIN
SELECT
*
FROM authors
CROSS JOIN books;
总结一下,SQL 连接提供了根据条件从多个表中组合数据的灵活性。了解它们的用法和语法允许您提取所需的信息,并为跨表相关数据建立关系。通过维恩图可视化连接有助于解释表数据如何基于连接条件重叠和组合,突出显示结果集中匹配和不匹配的行,并清晰地展示连接操作期间表之间关系的表示。
使用 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)。

图 3-14. 更新 books 表
使用 DELETE 删除数据
DELETE 命令提供了根据指定条件选择性删除某些记录或删除表内所有记录的能力。DELETE 在数据维护中发挥着重要作用,允许用户通过删除不必要或过时的记录有效地管理和清理表的内容。此命令确保数据完整性,并通过消除冗余或无关信息来优化数据库。示例 3-30 展示了该命令的语法。
示例 3-30. DELETE 语句的语法
DELETE FROM table_name
WHERE condition;
DELETE FROM 部分指示了将要删除数据的具体表,而 table_name 则表示表的名称。可选的 WHERE 子句通过允许用户定义必须满足的行删除条件,发挥着重要作用。利用此子句,可以根据特定条件筛选行。如果不使用 WHERE 子句,则将删除表内的所有行。最后,condition 指的是行必须满足的具体条件,才能符合删除的资格。
为了实际地应用此命令,我们假设不会从计算机科学类别中发布任何书籍。通过查看category_id,我们可以看到它是 number 6. 让我们现在执行 示例 3-31 并查看发生了什么。
示例 3-31. 从category表中删除类别
DELETE FROM Category
WHERE category_id = 6
如果一切顺利,您应该能够选择category表,并看到我们不再拥有计算机科学类别,如 图 3-15 所示。

图 3-15. 从category表中删除的类别
最后,您还可以使用另一种数据管理技术,称为软删除,来“删除”数据。这种技术不会永久擦除记录,而是在数据库中设置一个标志或属性来指示应该视为已删除的记录。这样可以保留历史数据,在需要时确保轻松恢复,并通过保持变更审计跟踪来支持合规性。
将查询存储为视图
view is a virtual table in a database defined by a query. It is similar to a regular table, consisting of named columns and rows of data. However, unlike a table, a view does not physically store data values in the database. Instead, it retrieves data dynamically from the tables referenced in its query when the view is accessed.
在 示例 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;
视图的主要目的之一是作为底层表的过滤器。定义视图的查询可以涉及同一数据库或不同数据库中的一个或多个表或其他视图。事实上,可以创建视图来 cons 折各式数据,允许您综合组织中的不同服务器存储在数据 for specific region.
视图通常用于简化和定制每个用户对数据库的感知。通过定义视图,您可以向不同的用户呈现数据的专注和定制视图,隐藏不必要的细节,并提供更直观的界面。此外,视图还可以作为安全机制,允许用户通过视图访问数据,而无需直接访问底层基表。这提供了额外的控制层,并确保用户只看到他们被授权查看的数据。
在 示例 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;
公共表达式
许多数据分析师和开发人员面临理解复杂 SQL 查询的挑战。当处理复杂的业务逻辑和多个上游依赖时,特别是对特定查询组件的目的和依赖性不清楚时,很容易遇到困难。再加上意外的查询结果可能会让分析师不确定哪个查询部分导致了差异,这样的情况并不少见。公共表达式(CTEs)在这种情况下提供了宝贵的解决方案。
CTE 为简化复杂查询和提高查询可维护性提供了强大的工具。作为临时结果集,CTE 通过将复杂查询分解为可管理的块来增强 SQL 代码的可读性。
示例 3-37 展示了在 SQL 中创建 CTE 的通用语法。尽管看起来复杂,但它遵循简单的模式。
示例 3-37. CTE 语法
WITH cte_name (column1, column2, ..., columnN) AS ( 
-- Query definition goes here 
)
SELECT column1, column2, ..., columnN 
FROM cte_name 
-- Additional query operations go here 
使用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 和仅使用子查询的差异。在这个练习中,我们使用了一个虚构的sales表,展示了所有书籍销售情况,如示例 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 基础上的 CTE。它从popular_books选择book_id、title、author和total_sales列,并根据book_id列与sales表进行连接。此外,它筛选了 2022 年发生的销售,并按总销售额降序排列结果,并将输出限制为前五名畅销书。同样,该 CTE 专注于另一个明确的责任:基于销售获取评分为 4.6 的前五名畅销书。
最后,主查询从best_sellers中选择所有列,并检索合并的结果。我们可以在主查询上应用额外的聚合或过滤器,但将代码保持简单并仅专注于选择最终分析所需的属性是一种最佳实践。
CTE 的一个常见用例是在单个查询中多次引用派生表。CTE 通过允许一次定义派生表并多次引用来消除冗余代码的需求。这提高了查询的清晰度,并减少了由于代码重复而导致错误的机会。为了看到其效果,让我们看一下示例 3-41,在这里我们将继续使用我们虚构的sales表。
示例 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 中一次定义总销售额的计算,并在主查询中重复使用以计算平均值,展示了第一个聚合的可重用性用于另一个聚合。
总之,CTEs 允许我们通过将复杂问题分解为更小、更可管理的部分来解决复杂问题。通过利用 CTEs,我们可以更模块化和可读地组织和结构化我们的查询。它们为解包复杂问题提供了一个解决方案,允许我们定义中间结果集并在单个查询中多次引用它们。这消除了冗余代码的需求,促进了代码的可重用性,减少了维护工作量和由于代码重复而产生错误的机会。
窗口函数
窗口函数是分析数据集的分区或窗口时,提高效率并减少查询复杂性的有用工具。它们提供了一种替代方法来处理更复杂的 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;

图 3-16. 运行计数
使用窗口函数,您还可以使用像 COUNT() 和 AVG() 这样的聚合函数,这些函数在 “使用 GROUP BY 聚合数据” 中介绍。这些函数可以类似于常规聚合使用,但它们作用于指定的窗口。
窗口函数提供额外的功能,例如用于编号和排名行的 ROW_NUMBER()、RANK() 和 DENSE_RANK(),用于确定百分位数或四分位数的 NTILE(),以及用于访问前后行的值的 LAG() 和 LEAD()。
表 3-4 总结了多种类型的窗口函数。
表 3-4. 窗口函数
| 类型 | 函数 | 示例 |
|---|---|---|
| 聚合函数 | 在每个窗口内进行聚合并为每一行返回单个值 | MAX(), MIN(), AVG(), SUM(), COUNT() |
| 排名函数 | 根据指定的标准为窗口内的每行分配排名或位置 | ROW_NUMBER(), RANK(), DENSE_RANK(), NTILE(), PERCENT_RANK(), CUME_DIST() |
| 分析函数 | 根据窗口中的数据计算值,而不修改行数 | LEAD(), LAG(), FIRST_VALUE(), LAST_VALUE() |
为了深入了解每种类型的函数,我们将使用 publication_year 列作为基础,并尝试一系列函数。
在第一个示例中,我们希望按照升序将最新到最旧的书籍进行排名。让我们来看一下示例 3-46 中的片段。
示例 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()替代。
在示例 3-47 中,我们希望按照publication_year来分桶我们的数据。
示例 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()函数,如示例 3-48 中所示。
示例 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 所示。
示例 3-49. 一个基本的 SQL 查询
-- Retrieve top-selling O'Reilly books
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 结合使用以实现端到端流程。
示例 3-50. 一个基本的 FastAPI
from fastapi import FastAPI
import duckdb
app = FastAPI()
@app.get("/top_books")
def get_top_books():
# Establish a connection to the DuckDB database
conn = duckdb.connect()
# Execute the SQL query
query = '''
SELECT Title, UnitsSold
FROM sales
WHERE Publisher = "O'Reilly"
ORDER BY UnitsSold DESC
LIMIT 5
'''
result = conn.execute(query)
# Convert the query result to a list of dictionaries
books = []
for row in result:
book = {
"title": row[0],
"units_sold": row[1]
}
books.append(book)
# Return the result as JSON
return {"top_books": books}
我们开发了一个 FastAPI 应用程序,并设置了一个单一的 GET 端点,可通过 /top_books 路径访问。简单来说,端点 是一个特定的网络地址(URL),我们可以用它来从我们的应用程序中检索信息。当某人在其网络浏览器或应用程序中访问此 URL 时,将触发我们定义的特定函数 get_top_books 的执行。这个函数包含了当某人从 /top_books 端点检索信息时该执行的指令。本质上,这就像我们有一个特定的按钮,当按下时,会导致我们的应用程序执行特定的操作,比如提供畅销书列表。
在函数内部,我们通过使用 duckdb.connect() 建立到 DuckDB 数据库的连接。然后使用连接对象的 execute() 方法执行 SQL 查询。该查询从 sales 表中选择由出版商 O'Reilly 发布的书籍的标题和销量。结果按销量降序排序,并限制为前五本书。
查询结果然后被转换成一个字典列表;每个字典代表一本书及其标题和销量。最后,结果被包装在一个带有键top_books的字典中,以 JSON 格式返回。
通过同时利用这两种语言,我们可以通过友好的 SQL 接口创建和操作数据,并通过优秀的 FastAPI 框架将其作为 API 公开。在接下来的部分中,我们将探讨三个著名的 Python 框架,它们通过类似 SQL 的接口抽象了对分布式数据处理的访问:DuckDB、FugueSQL 和 Polars。
数据操作与 DuckDB
当涉及到数据处理库时,大多数数据科学家非常熟悉 pandas,这是 Python 中主要的数据处理库。Pandas 以其简单性、多功能性和管理多种数据格式和大小的能力而闻名。它为数据操作提供了直观的用户界面。熟悉 SQL 的人士欣赏其强大的特性,使用户可以使用简洁的语法执行复杂的数据转换。然而,在某些情况下,必须在执行速度和工具的易用性或表达能力之间做出权衡。当处理超出内存限制或需要复杂数据处理操作的大型数据集时,这种困境尤为棘手。
在这种情况下,使用 SQL 而不是 pandas 可能是更好的解决方案。这就是 DuckDB 的用武之地。DuckDB 结合了 pandas 和 SQL 的优势,提供了一个快速高效的 SQL 查询执行引擎,能够处理大数据集上的复杂查询。它与 pandas DataFrames 无缝集成,允许直接在 DataFrames 上执行查询,无需频繁数据传输。通过 DuckDB,数据科学家可以在使用 pandas 时利用 SQL 的强大功能,平衡性能与易用性。
此外,我们看到一些公司决定将 Spark 作为数据处理引擎与 dbt 结合使用 DuckDB 来替代。当然,这必须根据具体情况进行评估,但它确实为分析师支持可以在数据管道中运行的更复杂的数据转换打开了大门。
安装 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,使其能够在各种编程语言和环境中使用。它以其卓越的性能和灵活性而闻名,非常适合高效处理和查询大量数据。在分析师中运行 SQL 查询是一项有价值的技能。分析师可以利用 DuckDB 的强大功能轻松执行复杂的 SQL 查询,并从数据中获得有价值的见解。
现在我们已经更多了解了 DuckDB,让我们逐步进行一些练习,以说明一些好处。我们将使用之前使用的相同的书籍分析查询。首先,导入我们需要的库,如示例 3-53 所示,包括 pandas 和 DuckDB。
示例 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 创建了 DataFrame df_total_revenue,我们可以继续在 pandas 中进行操作。这展示了在 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. 数据可视化
# Create a bar plot
plt.bar("O'Reilly", total_revenue)
# Set the plot title and axis labels
plt.title("Total Revenue for O'Reilly Books")
plt.xlabel("Publisher")
plt.ylabel("Total Revenue")
回到 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 是一个完全用 Rust 编写的高性能 DataFrame 库,其一个关键优势是不使用 DataFrame 的索引。与经常冗余的索引依赖的 pandas 不同,Polars 消除了索引的需求,简化了 DataFrame 操作,使其更直观和高效。
此外,Polars 利用 Apache Arrow 数组进行内部数据表示。与使用 NumPy 数组的 pandas 相比(pandas 2.0 可能会修复此问题),使用 Arrow 数组在加载时间、内存使用和计算方面提供了显著的优势。Polars 利用这种高效的数据表示轻松处理大数据集,并更有效地执行计算。
Polars 的另一个优势是其支持并行操作。使用 Rust 编写,这是一种以性能和并发性为重点的语言,Polars 可以利用多线程并行运行多个操作。这种增强的并行能力允许更快速和可扩展的数据处理任务。最后,它还引入了一种强大的优化技术称为惰性评估。在 Polars 中执行查询时,库会检查和优化查询,并寻找加速执行或减少内存使用的机会。这种优化过程改善了查询的整体性能,并增强了数据处理的效率。相比之下,pandas 只支持急切评估,即遇到表达式时立即评估。
使用 Polars 进行数据操作对分析工程师非常有价值,因为它具备独特的能力。Polars 的设计强调性能和可伸缩性,非常适合高效处理大量数据。处理大数据集的分析工程师可以从其高效的内存操作和并行处理支持中受益,从而实现更快速的数据转换。Polars 与 Rust 生态系统的集成还使其成为使用 Rust 构建的数据管道的分析师的宝贵工具,提供了兼容性和易用性。查询优化能力、先进的数据操作功能以及对多数据源的支持使 Polars 成为我们工具箱中的一个宝贵补充,使其能够高效灵活地处理复杂的数据任务。
安装 Polars
要安装 Polars,我们有几个选项,这取决于我们的操作系统和我们想要进行的安装类型,但让我们看看 示例 3-60,它展示了如何使用 pip 软件包管理器安装 Polars 的简单示例。
示例 3-60. 安装 Polars
pip install polars
这将立即使 Polar 库在我们的 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 运行 SQL 查询
使用 Polars,我们可以对这个 DataFrame 执行各种操作,以深入了解 O'Reilly 的图书销售。无论是计算总收入,分析按书名或作者销售,还是识别畅销书籍,如 示例 3-62 所示,Polars 提供了一个多功能和高效的数据分析平台。
示例 3-62. Polars DataFrame—畅销书籍
# Sort the DataFrame by UnitsSold column in descending order
top_selling_books = df.sort(by="UnitsSold", reverse=True)
# Get the top-selling books' title and units sold
top_books_data = top_selling_books.select(["Title",
"UnitsSold"]).limit(5).to_pandas()
print("Top-selling O'Reilly Books:")
print(top_books_data)
如您所见,我们使用 sort 方法根据 UnitsSold 列按降序对 DataFrame df 进行排序。然后,我们使用 limit 方法选择前五本书。最后,我们使用 to_pandas() 将结果 DataFrame 转换为 pandas DataFrame,以便更轻松地打印和显示。
虽然这很有趣,并展示了与 pandas 语法的相似性,但我们提到了 Polars 具有将其功能作为 SQL 公开的能力。事实上,Polars 提供了多种在其框架内利用 SQL 功能的方法。
就像 pandas 一样,Polars 无缝集成了外部库(如 DuckDB),允许您利用它们的 SQL 功能。您可以从 DuckDB 或 pandas 导入数据到 Polars,对导入的数据执行 SQL 查询,并无缝结合 SQL 操作与 Polars DataFrame 操作。这种集成提供了全面的数据分析和操作生态系统,融合了 SQL 和 Polars 的优势。
在 Example 3-63 中,我们通过 duckdb.connect() 创建了一个 DuckDB 连接。然后,我们创建了一个 Polars DataFrame df,包含 Title、Author、Publisher、Price 和 UnitsSold 列,表示 O’Reilly 图书的数据。我们使用 con.register() 将这个 DataFrame 注册为名为 books 的表。接下来,我们使用 con.execute() 在 books 表上执行 SQL 查询,选择 Title 和 UnitsSold 列,并按 Publisher = "O'Reilly" 进行筛选。结果以元组列表形式返回。我们将结果转换为一个指定列名的 Polars DataFrame result_df。
Example 3-63. 使用 DuckDB 的 Polars DataFrame
import polars as pl
import duckdb
# Create a DuckDB connection
con = duckdb.connect()
# Create a Polars DataFrame with O'Reilly books data
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]
})
# Register the DataFrame as a table in DuckDB
con.register('books', df)
# Execute a SQL query on the DuckDB table using Polars
result = con.execute("SELECT Title, UnitsSold FROM books WHERE Publisher =
'O''Reilly'")
# Convert the result to a Polars DataFrame
result_df = pl.DataFrame(result, columns=['Title', 'UnitsSold'])
# Print the result
print(result_df)
# Close the DuckDB connection
con.close()
Polars 还提供了原生支持,可以在不依赖外部库的情况下执行 SQL 查询。使用 Polars,您可以直接在代码中编写 SQL 查询,利用 SQL 语法进行数据转换、聚合和过滤操作。这使您可以在 Polars 框架内充分利用 SQL 的强大功能,从而提供了处理结构化数据的便捷高效方法。
在 Polars 中使用 SQL 是一个简单而直接的过程。您可以按照以下步骤在 Polars DataFrame 上执行 SQL 操作。首先,创建一个 SQL 上下文,用于设置执行 SQL 查询的环境。这个上下文使您能够在 Polars 框架内无缝使用 SQL,如在 Example 3-64 中所示。
Example 3-64. 创建 SQL 上下文
# Create a Polars DataFrame with O'Reilly books data
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]
})
# Create the SQL Context
sql = pl.SQLContext()
Example 3-65 展示了下一步操作:注册您要查询的 DataFrame。
Example 3-65. 注册 DataFrame
# Register the DataFrame in the context
sql.register('df', df)
为 DataFrame 提供一个名称,可以为您的 SQL 查询建立一个参考点。此注册步骤确保 DataFrame 与一个可识别的标识符关联起来。
一旦 DataFrame 被注册,您可以使用 Polars 提供的query()函数在其上执行 SQL 查询。该函数将 SQL 查询作为输入,并返回一个 Polars DataFrame 作为结果。这个 DataFrame 包含符合 SQL 查询指定条件的数据。让我们看一下示例 3-66。
示例 3-66. 运行分析查询
# Run your SQL query
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
安装此额外功能将 Spark 作为 Fugue 中的 ExecutionEngine。使用此额外功能,用户可以利用 Spark 的能力来执行其 Fugue 工作流。要添加此额外功能,请运行 Example 3-69 中的代码。
Example 3-69. 安装 FugueSpark
pip install "fugue[spark]"
dask
此额外功能启用了对 Dask 作为 Fugue 中的 ExecutionEngine 的支持。通过安装此额外功能,用户可以在 Fugue 框架内利用 Dask 的分布式计算能力。
ray
安装此额外功能将 Ray 作为 Fugue 中的 ExecutionEngine。使用此额外功能,用户可以利用 Ray 的高效任务调度和并行执行能力来执行其 Fugue 工作流。
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 命令中安装几个之前的额外功能。在 Example 3-70 中,我们使用 Fugue 一次性安装了 duckdb、polars 和 spark 的额外功能。
Example 3-70. 安装多个 Fugue 额外功能
pip install "fugue[duckdb,spark,polars]"
另一个有趣的额外功能与笔记本有关。FugueSQL 在 Jupyter Notebooks 和 JupyterLab 中都有一个笔记本扩展。此扩展提供语法高亮。我们可以运行另一个 pip install 命令来安装扩展 (Example 3-71).
Example 3-71. 安装笔记本扩展
pip install fugue-jupyter
fugue-jupyter install startup
第二个命令 fugue-jupyter install startup 将 Fugue 注册到 Jupyter 的启动脚本中,以便您在每次打开 Jupyter Notebooks 或 JupyterLab 时都可以使用它。
如果您已安装 Fugue 并使用 JupyterLab,默认情况下会自动注册 %%fsql 单元格魔术。这意味着您可以在 JupyterLab 环境中直接使用单元格魔术,无需任何额外步骤。但是,如果您使用的是 Classic Jupyter Notebooks 或者 %%fsql 单元格魔术未注册,则可以通过在笔记本中使用 Example 3-72 中的命令来启用它。
Example 3-72. 启用笔记本扩展
from fugue_notebook import setup
setup(is_lab=True)
使用 FugueSQL 运行 SQL 查询
FugueSQL 专为希望使用 Python 数据框架(如 pandas、Spark 和 Dask)的 SQL 用户设计。FugueSQL 提供一个 SQL 接口,可以解析和运行在您选择的底层引擎上。这对于那些更喜欢专注于定义逻辑和数据转换而不是处理执行复杂性的数据科学家和分析师尤为有益。
但它也为 SQL 爱好者量身定制,使他们能够在流行的数据处理引擎(如 pandas、Spark 和 Dask)上定义端到端工作流程。这样,SQL 爱好者可以利用他们的 SQL 技能,轻松编排复杂的数据流水线,而无需在不同工具或语言之间切换。
Fugue 为主要使用 pandas 并希望利用 Spark 或 Dask 处理大数据集的数据科学家提供了实用的解决方案。使用 Fugue,他们可以轻松地扩展其 pandas 代码,并顺利过渡到 Spark 或 Dask,实现分布式计算的潜力,几乎不费力气。例如,如果有人在 Spark 中使用 FugueSQL,框架将使用 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
}
]
# Save the data as parquet
df = pd.DataFrame(data)
df.to_parquet("/tmp/df.parquet")
# Fugue with pandas Engine
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 with Spark Engine
import fugue.api as fa
query = """
LOAD "/tmp/df.parquet"
SELECT Author, COUNT(Title) AS NbBooks
GROUP BY Author
PRINT
"""
spark_df = fa.fugue_sql(query, engine="spark")
# Fugue with DuckDB
import fugue.api as fa
import 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")
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 是否是 pandas 的替代品或进化版本,而 pandasql 有 pandas 支持。我们认为,虽然 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 DataFrame (df) 注册为 dask-sql Context 中名为 iris 的表。使用 Context 类的 create_table 方法来注册表。完成此步骤后,我们可以使用 SQL 语法查询数据。
示例 3-76. 将数据加载为 Dask DataFrame 并注册为表
# Load data: Download the iris dataset
df = dd.read_csv('https://datahub.io/machine-learning/iris/r/iris.csv')
# Register a Dask table
c.create_table("iris", df)
让我们运行一个简单的选择,使用我们的 dask-sql Context 对象的 sql() 函数,在 示例 3-77 中,并将我们的 SQL 查询写为一个参数。
示例 3-77. 访问 dask-sql 表
# Test accessing the data
c.sql("""
select * from iris
""")
数据准备好后,我们可以使用训练组件来训练机器学习模型。为此,我们首先使用 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. 创建我们的聚类模型
# Train: Create our clustering model using sklearn.cluster.KMeans algorithm
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. 显示模型列表
# Show the list of models which are trained and stored in the context.
c.sql("""
SHOW MODELS
""")
另一个有趣的命令是 DESCRIBE MODEL *MODEL_NAME*(示例 3-80),它展示了用于训练该模型的所有超参数。
示例 3-80. 获取某个模型的所有超参数
# To get the hyperparameters of the trained MODEL
c.sql("""
DESCRIBE MODEL clustering
""")
在 示例 3-81 中,我们展示了 dask-sql 中最引人注目的命令之一——PREDICT 命令。它使用最近创建的聚类模型来预测 df DataFrame 的行的集群类。PREDICT 中的 SELECT 语句在 SQL 上下文中将训练好的机器学习模型应用于来自某个表的新数据点。
在这种情况下,PREDICT 命令用于将 clustering 模型应用于 iris 表的前 100 行数据。MODEL 子句指定要使用的模型名称为 clustering。PREDICT 命令中的 SELECT 语句指定用于预测的特征,这些特征与模型训练步骤中使用的相同特征相同,正如 示例 3-81 中演示的那样。
示例 3-81. 进行预测
''' Predict: Test the recently created model by applying
the predictions to the rows of the df—
in this case assign each observation to a cluster'''
c.sql("""
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. 超参数调优
# Hyperparameter tuning: Run an experiment to try different parameters
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 中所见。在这种情况下,模型以 pickle 格式导出,使用的格式参数设置为 pickle。Pickle 是一种 Python 特定的二进制序列化格式,允许您保存和加载 Python 对象。
location 参数指定应保存导出模型文件的路径和文件名。在此示例中,模型保存在当前目录,并命名为 clustering.pkl。
示例 3-83. 将模型导出为 pickle 文件
# Export the model: Export as a pickle file to be used in other contexts
c.sql("""
-- for pickle model serialization
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、数据库管理和数据分析领域不断发展。要取得成功,我们必须保持更新,保持好奇心,并自信面对挑战。
随着数据景观的迅速扩展,数据工程、分析和数据科学角色之间的区别变得更加显著。虽然这些角色确实存在重叠和融合的领域,但数据的庞大量和复杂性推动了对专业技能和专业知识的需求。本章的结论提醒我们,分析工程领域既广泛又迷人。在每一个查询和数据库中,都存在着探索和创新的新机会,这些机会是由对今天数据景观复杂性的导航需要的专业角色驱动的。
第四章:使用 dbt 进行数据转换
dbt 的主要目的是通过简单地编写 SQL 语句,帮助您在数据平台上轻松和集成地转换数据。将 dbt 置于 ELT 工作流中时,它与转换阶段的活动相匹配,为您提供额外的组件——如版本控制、文档、测试或自动部署——以简化数据专家的整体工作。这是否让您想起了分析工程师的实际活动?那是因为 dbt 是定义分析工程师工作的现代工具之一,为他们提供与平台集成的工具,减少了设置额外服务来解决特定问题并降低了整体系统复杂性的需求。
dbt 支持为分析工程师描述的任务,赋予他们在其数据平台上协作运行代码的能力,为指标和业务定义提供单一真实来源。它促进了中心化和模块化的分析代码,利用 Jinja 模板语言、宏或包来实现 DRY 代码。同时,dbt 还提供了我们通常在软件工程最佳实践中找到的安全性,例如协作在数据模型上,版本它们,并在部署到生产之前测试和文档您的查询,配合监控和可见性。
我们已经为您提供了 dbt 的全面介绍。然而,在本章中,我们将更深入地探讨 dbt 的具体内容,澄清其在数据分析世界中的重要性。我们将讨论 dbt 的设计理念,这种转换工具背后的原则,以及以 dbt 为核心的数据生命周期,展示 dbt 如何将原始数据转换为易于消费的结构化模型。我们将通过概述其各种功能,如构建模型、文档和测试,以及详细说明其他 dbt 工件,如 YAML 文件,来探索 dbt 项目结构。通过本章的学习,您将全面了解 dbt 及其能力,从而能够在您的数据分析工作流中有效实施它。
dbt 设计理念
随着数据工程和分析工作流程变得日益复杂,重视数据质量和可靠性的流程简化工具至关重要。dbt 作为一个集中的解决方案,通过其对数据建模和分析工程的方法基础的设计理念,显现出其重要性。
简言之,dbt 的设计理念依赖于以下几点:
以代码为中心的方法
dbt 设计理念的核心是基于代码的数据建模和转换方法。dbt 鼓励用户使用代码定义数据转换,而不是依赖于基于 GUI 的界面或手动 SQL 脚本。这种转向代码驱动的开发促进了协作、版本控制和自动化。
可重用性的模块化
dbt 推广模块化,允许数据从业者创建可重用的代码组件。模型、宏和测试可以组织成包,从而促进代码维护和可扩展性。这种模块化方法符合最佳实践,并增强了代码的可重用性。
转换为 SQL SELECT语句
dbt 模型被定义为 SQL SELECT语句,使得具备 SQL 技能的分析师和工程师可以轻松访问。这种设计选择简化了开发,并确保数据建模紧密遵循 SQL 最佳实践。
声明性语言
dbt 使用声明性语言定义数据转换。分析师指定所需的结果,dbt 处理底层实现。这种抽象减少了编写复杂 SQL 代码的复杂性,并增强了可读性。
增量构建
效率是 dbt 设计的重点。它支持增量构建,这使得数据工程师可以仅更新受影响的数据管道部分,而不是重新处理整个数据集。这加速了开发并减少了处理时间。
文档即代码
dbt 倡导将数据模型和转换视为代码进行文档化。描述、解释和元数据存储在项目代码旁边,使团队成员更容易理解并有效协作。
数据质量、测试和验证
dbt 非常重视数据测试。它提供了一个测试框架,使分析师能够定义数据质量检查和验证规则。这包括数据可靠性和质量在整个管道中的保证,从而确保数据符合预定义的标准并遵循业务规则。
版本控制集成
与像 Git 这样的版本控制系统的无缝集成是 dbt 的一个基本特性。这个功能支持协同开发、变更追踪和回滚功能,确保数据管道始终处于版本控制之下。
与数据平台的本地集成
dbt 被设计成与 Snowflake、BigQuery 和 Redshift 等流行数据平台无缝集成。它利用这些平台的本地功能进行扩展和性能优化。
开源且可扩展
dbt 是一个开源工具,拥有一个充满活力的社区。用户可以通过创建自定义宏和包来扩展其功能。这种可扩展性允许组织根据其特定的数据需求定制 dbt。
转换与加载的分离
dbt 将数据管道中的转换和加载步骤分开。数据在 dbt 内部进行转换,然后加载到数据平台。
实质上,dbt 的设计理念根植于为数据工程师、分析师和数据科学家创建一个协作、代码中心化和模块化的环境,以便有效地转换数据、确保数据质量并生成有价值的洞察。dbt 通过简化数据建模和分析工程的复杂性,赋予组织充分利用数据的潜力。
dbt 数据流
图 4-1 显示了数据流的大局观。它指出了 dbt 及其功能在整体数据景观中的位置。

图 4-1. 使用 dbt 的典型数据流,帮助您从 BigQuery、Snowflake、Databricks 和 Redshift 等数据平台转换您的数据(请参阅 dbt 文档支持的数据平台)
正如提到的,dbt 的主要目的是帮助您 转换 数据平台的数据,并为此,dbt 提供了两个工具来实现这一目标:
-
dbt 云
-
dbt Core 是由 dbt Labs 维护的开源 CLI 工具,您可以在托管环境中设置或在本地运行
让我们看一个例子,了解 dbt 在实际生活中的工作原理及其作用。假设我们正在处理一个周期性从数据平台(如 BigQuery)提取数据的管道。然后,通过组合表格对数据进行转换(见图 4-2)。
我们将前两个表合并为一个,应用多种转换技术,如数据清洗或合并。这一阶段在 dbt 中进行,因此我们需要创建一个 dbt 项目来完成此合并。我们会逐步学习 dbt Cloud 及如何设置我们的工作环境。
注意
本书中,我们将使用 dbt Cloud 编写我们的代码,因为这是从开发到编写测试、调度、部署和调查数据模型的最快最可靠的方式。此外,dbt Cloud 在 dbt Core 的 CLI 工具之上运行,因此在使用 dbt Cloud 时,我们将熟悉与 dbt Core 相同的命令。

图 4-2. 使用 dbt 的数据管道用例
dbt 云
dbt Cloud 是 dbt 的云版本,提供广泛的功能和服务来编写和产品化您的分析代码。dbt Cloud 允许您调度 dbt 作业,监视其进展,并实时查看日志和指标。dbt Cloud 还提供高级协作功能,包括版本控制、测试和文档编制。此外,dbt Cloud 还与各种云数据仓库集成,如 Snowflake、BigQuery 和 Redshift,可以轻松地转换您的数据。
您可以使用 dbt Core 的大多数功能,但它需要在您的基础设施上进行配置和设置,类似于在自己的服务器或类似 Airflow 的工具上运行 Amazon 弹性计算云(EC2)实例。这意味着您需要自主维护和管理它,类似于在 EC2 上管理虚拟机(VM)。
相比之下,dbt Cloud 的运作方式类似于托管服务,类似于 Amazon 管理的 Apache Airflow 工作流服务(MWAA)。它提供了便利和易用性,因为许多操作方面都已为您处理,使您能够更专注于分析任务,而不是基础设施管理。
使用 BigQuery 和 GitHub 设置 dbt Cloud 的第一步
没有比通过实践学习特定技术更好的方法,所以让我们设置我们将用来应用知识的环境。首先,让我们注册一个dbt 账户。
注册后,我们将进入完整的项目设置页面(图 4-3)。

图 4-3. dbt 项目设置的起始页
此页面包含多个部分,用于正确配置我们的 dbt 项目,包括连接到我们期望的数据平台和代码库。我们将使用 BigQuery 作为数据平台,并使用 GitHub 存储我们的代码。
在 BigQuery 中设置新项目的第一步是在GCP中搜索“创建项目”并点击搜索栏中的该选项(图 4-4)。

图 4-4. BigQuery 项目设置,第 1 步
展示了类似于图 4-5 的屏幕,您可以在此设置项目。我们将其命名为dbt-analytics-engineer。

图 4-5. BigQuery 项目设置,第 2 步
配置完成后,进入您的 BigQuery IDE —— 您可以再次使用搜索栏。它应该看起来类似于图 4-6。

图 4-6. BigQuery IDE
最后,测试 dbt 的公共数据集,以确保 BigQuery 正常工作。为此,请将代码复制到示例 4-1 中,并点击“运行”。
示例 4-1. BigQuery 中的 dbt 公共数据集
select * from `dbt-tutorial.jaffle_shop.customers`;
select * from `dbt-tutorial.jaffle_shop.orders`;
select * from `dbt-tutorial.stripe.payment`;
如果您看到图 4-7 的页面,那么您做得很好!
注意
由于我们同时执行了三个查询,我们看不到输出结果。为此,请单击“查看结果”以逐个检查查询输出。

图 4-7. BigQuery 数据集输出
现在让我们将 dbt 与 BigQuery 连接,并在 dbt IDE 中执行这些查询。为了让 dbt 连接到您的数据平台,您需要生成一个密钥文件,类似于在大多数其他数据平台中使用数据库用户名和密码。
前往BigQuery 控制台。在继续下一步之前,请确保您在页眉中选择了新项目。如果您看不到您的帐户或项目,请点击右侧的个人资料图片,并验证您是否在使用正确的电子邮件帐户:
-
前往 IAM & Admin 并选择服务帐户。
-
点击创建服务帐户。
-
在名称字段中输入
dbt-user,然后点击创建和继续。 -
在“授予此服务帐户对项目的访问权限”中,在角色字段中选择 BigQuery 管理员。然后点击继续。
-
在“授予用户访问此服务帐户”部分留空字段,然后点击完成。
屏幕应该看起来像图 4-8。

图 4-8. BigQuery 服务帐户屏幕
接下来继续进行剩余的步骤:
-
点击刚刚创建的服务帐户。
-
选择 Keys。
-
点击添加密钥;然后选择“创建新密钥”。
-
选择 JSON 作为密钥类型,然后点击创建。
-
应提示您下载 JSON 文件。将其保存在一个易于记忆的位置,并使用清晰的文件名,例如dbt-analytics-engineer-keys.json。
现在让我们返回到 dbt Cloud 进行最后的设置:
-
在项目设置屏幕上,为您的项目指定一个更详细的名称。在我们的案例中,我们选择了dbt-analytics-engineer。
-
在“选择仓库”屏幕上,点击 BigQuery 图标然后点击下一步。
-
上传先前生成的 JSON 文件。为此,请点击“上传服务帐户 JSON 文件”按钮,见图 4-9。
最后但同样重要的是,在上传文件之后,应用剩余的步骤:
- 转到底部并点击“测试”。如果看到“您的测试成功完成”,就像图 4-10 所示,那么您就可以继续进行了!现在点击下一步。另一方面,如果测试失败,很有可能是您的 BigQuery 凭据出现了问题。尝试重新生成它们。

图 4-9. dbt Cloud,提交 BigQuery 服务帐户屏幕

图 4-10. dbt 和 BigQuery 连接测试
最后一步是设置 GitHub,但首先,让我们先了解我们在这里讨论的内容。GitHub 是一个流行的版本控制平台,托管 Git 仓库,允许您跟踪代码的更改并有效地与他人协作。正确使用 Git,遵循这些原则和最佳实践是至关重要的:
经常提交,早点提交
经常进行提交,即使是小的更改也要如此。这有助于跟踪您的进度并简化调试。每个提交应代表一个逻辑变更或功能。
使用有意义的提交消息
写简洁而描述性的提交消息。一个好的提交消息应该解释了什么被改变以及为什么被改变。
遵循分支策略
使用不同的分支来处理不同的功能、错误修复或开发任务。
先拉取再推送
在推送您的更改之前,始终从远程存储库拉取最新更改(例如,git pull)。这样可以减少冲突,并确保您的更改基于最新的代码。
在提交之前审查代码
如果您的团队进行代码审查,请确保在提交之前审查和测试您的更改。这有助于维护代码质量。
使用.gitignore
创建一个.gitignore文件,指定应从版本控制中排除的文件和目录(例如构建产物、临时文件)。
使用原子提交
保持提交集中于单一特定更改。避免在同一提交中混合不相关的更改。
使用 rebase 而非 merge
使用git rebase来将功能分支的更改集成到主分支中,而不是传统的合并。这将产生更清晰的提交历史。
保持提交历史干净
避免提交“正在进行中”的或调试语句。使用git stash等工具暂时保存未完成的工作。
使用标签
创建标签,例如版本标签,以标记项目历史中的重要节点,如发布或主要里程碑。
协作和沟通
与团队沟通关于 Git 工作流程和约定。建立处理问题、提交请求和冲突解决的指南。
知道如何撤销更改
学习如何撤销提交(git revert)、重置分支(git reset)以及在需要时恢复丢失的工作(git reflog)。
文档
在README或贡献指南中记录项目的 Git 工作流程和约定,以有效引导新团队成员。
使用备份和远程存储库
定期备份您的 Git 存储库,并使用 GitHub 等远程存储库进行协作和冗余备份。
持续学习
Git 是一个功能强大的工具,具有许多特性。继续学习和探索高级 Git 概念,如挑选、交互式重置和自定义挂钩,以改进您的工作流程。
为了更好地实践一些常见的 Git 术语和命令,请参考表 4-1。
表 4-1. Git 术语和命令
| 术语/命令 | 定义 | Git 命令(如果适用) |
|---|---|---|
| 存储库(repo) | 这类似于项目文件夹,包含项目的所有文件、历史记录和分支。 | - |
| 分支 | 分支是开发的一个独立线路。它允许您在不影响主代码库的情况下工作新功能或修复。 | git branch *<branch_name>* |
| 提交请求(PR) | 提交请求是您希望合并到主分支的建议更改。这是与团队协作和审查代码更改的一种方式。 | - |
| 暂存 | git stash 是一个命令,暂时保存您在工作目录中进行的更改,但尚未提交。 |
git stash save *"在此添加您的暂存消息"* |
| 提交 | 提交是您代码在特定时间点的快照。它表示您对文件所做更改的集合。 | git commit -m *"Commit message here"* |
| 添加 | git add 用于将更改暂存到下一次提交。当您修改文件时,Git 不会自动包含它们在下一次提交中。您需要明确告诉 Git 要包含哪些更改。 |
要暂存所有更改,git 命令是 git add .,但您也可以指定文件或目录:git add *<path/to/directory/>* |
| 分叉 | 分叉存储库意味着在 GitHub 上创建别人项目的副本。您可以对分叉存储库进行更改,而不影响原始存储库。 | - |
| 克隆 | 克隆存储库意味着在本地创建远程存储库的副本。您可以在本地修改代码,并将更改推送到远程存储库。 | git clone *<repository_url>* |
| 推送 | git push 将您的本地更改上传到远程存储库。 |
git push *<origin branch_name>* |
| 拉取 | git pull 将远程存储库的更改更新到您的本地存储库。 |
git pull |
| 状态 | git status 显示您工作目录和暂存区的当前状态。 |
git status |
| 记录 | git log 显示存储库中提交的时间顺序列表以及提交消息、作者和提交 ID。 |
git log |
| 差异 | gitdiff 命令显示两组代码之间的差异。 |
git diff |
| 合并 | git merge 命令将一个分支的更改与另一个分支合并。 |
git checkout *<target_branch>* 或 git merge *<source_branch>* |
| 变基 | 变基允许您将一系列提交移动或合并到一个新的基础提交上。 | git rebase base_branch |
| 检出 | checkout 命令用于在不同分支或提交之间切换。 |
git checkout *<branch_name>* |
这些 Git 命令和术语为项目中的版本控制奠定了基础。尽管如此,Git 命令通常具有许多额外的参数和选项,允许对版本控制任务进行精细调整。虽然我们在这里介绍了一些基本命令,但需要注意的是,Git 的灵活性远远超出了我们所概述的范围。
要获取更全面的 Git 命令列表及其多样的参数,请参阅官方的 Git 文档。
现在您了解了 Git 和 GitHub 在项目中的角色及其作用,让我们建立到 GitHub 的连接。为此,您需要执行以下操作:
-
如果您还没有 GitHub 账户,请注册一个。
-
单击“新建”以创建一个新存储库,这是您将版本化您的分析代码的地方。在“创建新存储库”屏幕上,为您的存储库命名;然后单击“创建存储库”。
-
创建存储库后,让我们回到 dbt。在“设置存储库”部分,选择 GitHub,然后连接 GitHub 账户。
-
点击“配置 GitHub 集成”以打开一个新窗口,您可以选择安装 dbt Cloud 的位置。然后选择要安装的仓库。
现在点击“在 IDE 中开始开发”。图 4-11 就是您应该看到的样子。

图 4-11. dbt IDE
我们将在“使用 dbt Cloud IDE”中概述 dbt Cloud 集成开发环境(IDE),并在“dbt 项目结构”中详细介绍。
点击左上角的“初始化 dbt 项目”。现在,您应该能够看到屏幕的样子,就像图 4-12 中显示的一样。

图 4-12. 项目初始化后的 dbt
我们将在“dbt 项目结构”中详细描述每个文件夹和文件。现在,让我们看看查询是否有效。通过复制示例 4-2 的代码并点击预览来再次运行它们。
示例 4-2. BigQuery 中的 dbt 公共数据集,dbt 测试
--select * from `dbt-tutorial.jaffle_shop.customers`;
--select * from `dbt-tutorial.jaffle_shop.orders`;
select * from `dbt-tutorial.stripe.payment`;
如果输出看起来类似于图 4-13,那意味着您的连接正常。然后,您可以向您的数据平台提交查询,这在我们的情况下是 BigQuery。
注意
这里提供的步骤是 dbt 中 BigQuery 适配器文档的一部分。随着技术的发展和改进,这些步骤和配置也可能发生变化。为确保您拥有最新的信息,请参阅最新的dbt BigQuery 文档。这个资源将为您提供与 dbt 和 BigQuery 一起工作的最新指导和说明。

图 4-13. BigQuery 公共数据集的 dbt 输出
最后,让我们测试您的 GitHub 集成是否按预期工作,通过进行您的第一次“提交和推送”。点击左侧具有相同描述的按钮,在图 4-14 中可见。一个弹出屏幕,如图 4-14 右侧的图像,将弹出,您可以在其中编写您的提交消息。点击“提交更改”。

图 4-14. 提交和推送到 GitHub
由于我们没有创建 Git 分支,它将在主分支中版本化我们的代码。进入您在此设置期间创建的 GitHub 仓库,并查看您的 dbt 项目是否存在。图 4-15 应该与您在 GitHub 仓库上看到的类似。

图 4-15. dbt GitHub 仓库,第一个提交检查
使用 dbt Cloud UI
当您登录到 dbt Cloud 时,初始页面将显示欢迎消息和您作业执行历史的摘要。正如 图 4-16 所示,初始页面是空的,但一旦我们创建并运行第一个作业,我们将开始看到信息。在 “作业和部署” 中,我们将详细介绍作业的执行。

图 4-16. dbt 登陆页面
在顶部菜单栏,您会看到几个选项。从左边开始,您可以进入开发页面,在这里您将开发所有分析代码并创建模型、测试和文档。这是 dbt 开发的核心,我们将在 “使用 dbt Cloud IDE” 中为您提供更多见解,并深入研究每个组件在 “dbt 项目结构” 中。
紧挨着开发选项的右侧是 Deploy 菜单,如 图 4-17 所示。从这个菜单中,您可以配置作业并通过运行历史监视其执行情况,配置开发环境,并验证快照的源数据新鲜度。

图 4-17. dbt Deploy 菜单
Deploy 菜单的第一个选项是运行历史,打开的页面显示在 图 4-18 中。在这里,您可以查看作业的运行历史。在 dbt 的上下文中,作业 是您配置的自动化任务或流程,用于执行特定操作,如运行模型、测试或生成文档。这些作业是协调 dbt 的重要组成部分,涉及管理和自动化各种数据转换和分析任务。

图 4-18. dbt 运行历史页面
假设您已经配置了在此部分中已执行的作业。在那种情况下,您可以检查每个作业的调用和状态。作业的运行历史提供了丰富的信息,包括其状态、持续时间、作业执行的环境以及其他有用的详细信息。您可以访问作业经历的步骤信息,包括每个步骤的日志。此外,您还可以找到作业生成的文档、模型或测试等生成物。
Deploy 菜单的下一个选项是作业。这将打开一个页面,用于配置所有自动化内容,包括 CI/CD 管道、运行测试和其他有趣的行为,而无需手动从命令行运行 dbt 命令。
图 4-19 显示了空的作业登陆页面。我们在 “作业和部署” 中有一个完整的作业部分。

图 4-19. dbt 作业页面
第三个部署菜单选项是环境。在 dbt 内部,我们有两种主要类型的环境:开发和部署。开箱即用,dbt 为您配置开发环境,这在您设置完 dbt 项目后就可以看到。图 4-20 展示了环境的着陆页面,如果您按照“使用 BigQuery 和 GitHub 设置 dbt Cloud”中的步骤进行操作,您的页面应该与此类似。

图 4-20. dbt 环境页面
最后,我们有数据源选项。这一页显示在 图 4-21,由 dbt Cloud 自动填充,一旦您配置了一个作业来快照源数据的新鲜度。在这里,您将看到最新快照的状态,可以分析您的源数据新鲜度是否符合您与组织定义的服务水平协议(SLAs)。我们将在“源新鲜度”中为您提供有关数据新鲜度的更好理解,以及在“测试数据源新鲜度”中如何进行测试。

图 4-21. dbt 数据源页面
接下来是文档选项,只要您和您的团队创建了确保您的 dbt 项目正确文档化的常规,这一步将具有特定的重要性。适当的文档可以回答以下问题:
-
这些数据意味着什么?
-
这些数据来自哪里?
-
这些指标是如何计算的?
图 4-22 展示了您项目的文档页面。我们将解释如何在编写代码的同时利用和编写您的 dbt 项目内的文档在“文档”章节。

图 4-22. dbt 文档页面
右上角菜单允许您选择您的 dbt 项目(图 4-23)。这个简短的菜单使得在 dbt 项目之间移动变得简单。

图 4-23. dbt 选择账户菜单
dbt 帮助菜单(图 4-24)可以通过点击问号符号找到。在这里,您可以直接与 dbt 团队通过聊天联系,提供反馈,并访问 dbt 文档。最后,通过帮助菜单,您可以加入 Slack dbt 社区或 GitHub dbt 讨论。

图 4-24. dbt 帮助菜单
设置菜单,图 4-25,是您可以配置与您的帐户、配置文件甚至通知相关的一切内容的地方。

图 4-25. dbt 设置菜单
一旦您点击三个选项中的任何一个,您将进入设置页面,类似于图 4-26。在第一个页面上,帐户设置中,您可以编辑和创建新的 dbt 项目,管理用户及其访问控制级别(如果您是所有者),以及管理计费。

图 4-26. dbt 帐户设置页面
第二个菜单选项,配置文件设置,访问了您的个人资料页面(图 4-27)。在这个页面上,您可以查看所有个人信息,并管理关联帐户,如 GitHub 或 GitLab、Slack 和单点登录(SSO)工具。您还可以查看和编辑为数据平台和 API 访问密钥定义的凭据。

图 4-27. dbt 个人资料页面
最后,通知设置选项访问了通知中心(图 4-28),在这里您可以配置在作业运行成功、失败或取消时在选择的 Slack 频道或电子邮件中接收到的警报。

图 4-28. dbt 通知中心
使用 dbt Cloud IDE
dbt Cloud 的重要部分之一是 IDE,您可以在其中编写所有分析代码,包括测试和文档。图 4-29 显示了 dbt IDE 的主要部分。

图 4-29. dbt IDE—注释
接下来,您可以找到每个部分代表的详细解释及其在集成开发环境中的重要性:
-
Git 控制和文档
这个菜单是您与 Git 交互的地方。在这里,您可以查看自上次提交以来的更改以及新增内容。IDE 中的所有 Git 命令都在这里,您可以决定是否提交和推送或还原您的代码。此外,在窗口右上角,您可以看到文档图标。一旦生成文档,您可以单击此快捷方式访问项目文档。
-
文件资源管理器
文件资源管理器为您提供了 dbt 项目的主要概述。在这里,您可以检查您的 dbt 项目是如何构建的——通常是.sql、.yml和其他兼容的文件类型。
-
文本编辑器
这部分 IDE 是您编写和成熟分析代码的地方。在这里,您还可以编辑和创建项目的其他相关文件,如 YAML 文件。如果您从文件资源管理器中选择这些文件,它们将在这里弹出。可以同时打开多个文件。
-
信息窗口和代码预览、编译和构建
一旦单击“预览”或“编译”按钮,此菜单将显示您的结果。预览将编译并运行您的查询以及您的数据平台,并在屏幕底部的“结果”选项卡中显示结果。另一方面,“编译”将把任何 Jinja 转换成纯 SQL。这将显示在屏幕底部的“编译代码”选项卡中的信息窗口中。预览或编译按钮适用于语句和 SQL 文件。
“构建”是仅在特定文件中弹出的特殊按钮。根据您选择的构建类型,运行结果将包括有关所有模型、测试、种子和快照的信息,这些信息被合并到一个文件中。
信息窗口在开发过程中排查错误或使用“谱系”选项卡来检查当前在文本编辑器中打开的模型及其祖先和依赖项的数据谱系也很有帮助。
-
命令行
命令行是您可以执行特定 dbt 命令(如
dbt run或dbt test)的地方。在命令执行期间或之后,它还会显示一个弹出屏幕,以显示正在处理的结果—为此,请单击命令行开头的箭头。日志也可以在这里查看。图 4-30 显示了扩展的命令行;要执行的命令位于顶部,并跟随执行的日志。

图 4-30. dbt 命令行扩展
dbt 项目的结构
一个 dbt 项目 是一个由文件夹和文件、编程模式和命名约定组成的目录。所有分析代码、测试、文档和参数化都将放置在这些文件和文件夹中。它将使用这些命名约定和编程模式。您如何组织文件夹和文件目录就是您的 dbt 项目结构。
建立一个合适的 dbt 项目需要付出努力。为了良好实施,它需要汇集公司的领域和部门,利用他们的专业知识来映射整个公司的目标和需求。因此,定义一套明确、全面和一致的惯例和模式是相关的。实现这一点将确保项目在公司扩展时保持可访问和可维护,同时利用 dbt 来赋能和惠及尽可能多的人。
您如何组织您的 dbt 项目可能会有所不同,并可能受到您或公司指南定义的变化的影响。这并不是问题。重要的是,您要明确声明这些变化,并以一种严格和易于访问的方式为所有贡献者保持一致。出于本书的目的,我们将保持您初始化时遇到的 dbt 项目的基本结构(示例 4-3)。
示例 4-3. dbt 项目的初始结构
root/
├─ analyses/
├─ dbt_packages/
├─ logs/
├─ macros/
├─ models/
│ ├─ example/
│ │ ├─ schema.yml
│ │ ├─ my_second_dbt_model.sql
│ │ ├─ my_first_dbt_model.sql
├─ seeds/
├─ snapshots/
├─ target/
├─ tests/
├─ .gitignore
├─ dbt_project.yml
├─ README.md
每个文件夹和文件将在本章节和第五章中的后续部分详细解释。有些比其他更加重要和经常使用。然而,理解它们的目的是非常重要的:
分析文件夹
在“分析”中详细介绍,这个文件夹通常用于存储审计目的的查询。例如,您可能希望在从其他系统迁移逻辑到 dbt 时查找不一致之处,并利用 dbt 的能力(如使用 Jinja 和版本控制),而无需将其包含在内置模型中。
dbt_packages 文件夹
您将在此处安装 dbt 包。我们将在“dbt 包”中详细介绍包的概念。总体来说,包是独立的 dbt 项目,解决特定问题,并可以在组织间共享和重用。这促进了更干净的代码,因为您不需要一遍又一遍地实现相同的逻辑。
日志文件夹
默认情况下,所有项目日志将写入此处,除非您在dbt_project.yml中进行了不同配置。
宏文件夹
您将在此处存储 DRY 转换代码。宏,类似于其他编程语言中的函数,是可以多次重用的 Jinja 代码片段。我们将在“使用 SQL 宏”中专门讨论它们。
模型文件夹
dbt 中的强制文件夹之一。一般来说,模型是一个包含SELECT语句的 SQL 文件,其中包含将原始数据转换为最终转换数据的模块化逻辑。在 dbt 中,模型的名称表示将来的表或视图的名称,或者如果配置为临时模型,则表示不包含。此主题将在“模型”中详细说明。
种子文件夹
我们将在“种子”中讨论,这里是我们存储查找表的地方。总体思路是,种子是 CSV 文件,变化不频繁,用于建模源系统中不存在的数据。一些有用的用例可能是将邮政编码映射到州或需要从分析中排除的测试电子邮件列表。
快照文件夹
包含项目的所有快照模型,必须与模型文件夹分开。dbt 快照功能记录了可变表格随时间的变化。它应用了类型 2 慢变化维度(SCD),用于标识表中行在时间内的变化。这在“快照”中有详细介绍。
目标文件夹
包含编译后的 SQL 文件,当您运行dbt run、dbt compile或dbt test命令时将被写入。您可以选择在dbt_project.yml中配置写入到另一个文件夹。
测试文件夹
用于同时测试多个特定表的目的。这不会是您编写测试的唯一文件夹。大部分还是会在您模型文件夹内的 YAML 文件中,或通过宏。然而,测试文件夹更适合单独的测试,报告多个特定模型之间如何互动或相关的结果。我们将在 “测试” 章节深入讨论此主题。
dbt_project.yml
是每个 dbt 项目的核心。这是 dbt 知道一个目录是一个 dbt 项目的方式,并且它包含重要信息,告诉 dbt 如何在您的项目上操作。我们将在本书的整个过程中介绍这个文件。它也在 “dbt_project.yml” 中有所涵盖。
.gitignore 和 README.md
这些文件通常用于您的 Git 项目。gitignore 指定了在提交和推送期间 Git 应该忽略的有意的文件,而 README 文件是一个重要的指南,为其他开发人员详细描述了您的 Git 项目。
我们将在本章和 第五章 中更详细地介绍这些文件夹,深入探讨 dbt 项目和其特性。
Jaffle 商店数据库
在本书中,我们将提供一组实际示例,展示如何使用 dbt 的组件和功能。在大多数情况下,我们将需要开发 SQL 查询,以便更好地向您展示我们想要展示的内容。因此,有一个我们可以操作的数据库是非常重要的。这个数据库就是 Jaffle 商店。
Jaffle 商店数据库 是一个由两个表组成的简单数据库,用于存储客户和订单。为了提供更多背景信息,我们将有一个来自 Stripe 的附加数据库,其中包含与订单相关联的支付。这三个表将是我们的原始数据。
我们使用这个数据库的原因是它已经在 dbt Labs 的 BigQuery 中公开可用。它是他们文档和课程中主要使用的数据库之一,因此我们希望它能简化本书阶段 dbt 平台的整体学习曲线。
图 4-31 展示了我们的原始数据 ERD,显示了客户、订单和支付。

图 4-31. Jaffle 商店原始数据 ERD,我们如下阅读:单个客户(1)可以有多个订单(N),单个订单(1)可以有多个处理支付(N)
YAML 文件
YAML 是一种人类可读的数据序列化语言,通常用于配置文件和应用程序中存储或传输数据的地方。在 dbt 中,YAML 用于定义你的 dbt 项目的组件的属性和一些配置:模型、快照、种子、测试、源,甚至实际的 dbt 项目,dbt_project.yml。
除了顶级的 YAML 文件,例如 dbt_project.yml 和 packages.yml,需要明确命名并放置在特定位置外,您组织 dbt 项目中的其他 YAML 文件的方式由您决定。请记住,与组织 dbt 项目的其他方面一样,最重要的指导原则是保持一致性,明确您的意图,并记录组织的方式及其原因。重要的是要在集中性和文件大小之间取得平衡,以便尽可能地方便查找特定配置。以下是关于如何组织、结构化和命名您的 YAML 文件的一套建议:
-
如前所述,平衡配置的集中性和文件大小尤为重要。将所有配置放在单个文件中可能会使得随着项目规模的扩展变得更难找到特定的配置(尽管在技术上可以使用一个文件)。由于文件的重复性质,使用 Git 进行变更管理也会变得复杂。
-
如前所述,如果我们采用每个文件夹的配置方法,在长期运行中,最好保持所有配置。换句话说,在每个模型的文件夹目录中,建议有一个 YAML 文件,以便简化该目录中所有模型的配置。通过在同一目录内部分离模型配置文件,扩展此规则,有一个特定文件用于您在同一目录内部的源配置(见 示例 4-4)。
在这种结构中,我们使用了分期模型来代表所讨论的内容,因为它涵盖了大多数情况,比如源、YAML 文件。在这里,您可以看到每个文件夹系统的配置,其中源和模型配置被分开。它还介绍了文档的 Markdown 文件,我们将在 “文档” 中更详细地讨论。最后,文件名开头的下划线将所有这些文件放置在各自目录的顶部,以便更容易找到。
示例 4-4. 模型目录中的 dbt YAML 文件
root/ ├─ models/ │ ├─ staging/ │ │ ├─ jaffle_shop/ │ │ │ ├─ _jaffle_shop_docs.md │ │ │ ├─ _jaffle_shop_models.yml │ │ │ ├─ _jaffle_shop_sources.yml │ │ │ ├─ stg_jaffle_shop_customers.sql │ │ │ ├─ stg_jaffle_shop_orders.sql │ │ ├─ stripe/ │ │ │ ├─ _stripe_docs.md │ │ │ ├─ _stripe_models.yml │ │ │ ├─ _stripe_sources.yml │ │ │ ├─ stg_stripe_order_payments.sql ├─ dbt_project.yml -
当使用文档块时,也要按照相同的方法创建每个模型目录的一个 Markdown 文件(
.md)。在 “文档” 中,我们将更详细地了解这种类型的文件。
建议您在目录级别的dbt_project.yml文件中设置默认配置,使用级联的范围优先级来定义这些配置的变化。这可以帮助您简化 dbt 项目管理,确保您的配置一致且易于维护。例如,利用示例 4-4,想象一下我们所有的暂存模型默认配置为视图。这将在您的dbt_project.yml文件中进行配置。但是,如果您有特定的用例需要更改jaffle_shop暂存模型的物化配置,您可以通过修改_jaffle_shop_models.yml文件来实现。这样,您可以为这组特定模型定制物化配置,同时保持项目其余配置不变。
使用在 dbt 项目构建中使用的级联范围优先级,可以覆盖特定模型的默认配置。虽然所有暂存模型默认情况下都会作为视图物化,但暂存jaffle_shop模型将作为表物化,因为我们通过更新特定的_jaffle_shop_models.yml YAML 文件来覆盖了默认设置。
dbt_project.yml
在 dbt 中最关键的文件之一是dbt_project.yml。此文件必须位于项目的根目录,并且它是您的项目的主要配置文件,包含 dbt 正常运行所需的相关信息。
dbt_project.yml文件在编写更加 DRY 的分析代码时也具有一定的相关性。一般来说,您的项目默认配置将存储在此处,并且所有对象都将从中继承,除非在模型级别进行覆盖。
这里是您将在此文件中遇到的一些最重要的字段之一:
name
(必须)。dbt 项目的名称。我们建议将此配置更改为您的项目名称。还要记得在模型部分和dbt_project.yml文件中做出相应更改。在我们的案例中,我们将其命名为dbt_analytics_engineer_book。
version
(必须)。项目的核心版本。与dbt version不同。
config-version
(必须)。版本 2 是目前可用的版本。
profile
(必须)。在 dbt 中,配置文件用于连接到您的数据平台。
[folder]-paths
(可选)。其中[folder]是 dbt 项目中的文件夹列表。它可以是模型、种子、测试、分析、宏、快照、日志等。例如,model-paths将说明您的模型和源文件的目录。macro-paths是您的宏代码所在位置,依此类推。
target-path
(可选)。此路径将存储编译后的 SQL 文件。
clean-targets
(可选)。包含通过dbt clean命令要删除的工件的目录列表。
models
(可选。)模型的默认配置。在 Example 4-5 中,我们希望将 staging 文件夹中的所有模型物化为视图。
Example 4-5. dbt_project.yml,模型配置
models:
dbt_analytics_engineer_book:
staging:
materialized: view
packages.yml
Packages 是独立的 dbt 项目,解决特定问题并可以在组织之间重用和共享。它们是包含模型和宏的项目;通过将它们添加到你的项目中,这些模型和宏将成为其一部分。
要访问这些包,你首先需要在 packages.yml 文件中定义它们。详细步骤如下:
-
你必须确保 packages.yml 文件位于你的 dbt 项目中。如果没有,请在与 dbt_project.yml 文件相同的级别创建它。
-
在 packages.yml 文件中定义你希望在 dbt 项目中可用的包。你可以从 dbt Hub、GitHub 或 GitLab 等源安装包,甚至是本地存储的包。Example 4-6 展示了每种情况所需的语法。
-
运行
dbt deps命令来安装定义的包。除非你进行了不同的配置,否则默认情况下这些包会安装在 dbt_packages 目录中。
Example 4-6. 从 dbt hub、Git 或本地安装包的语法
packages:
- package: dbt-labs/dbt_utils
version: 1.1.1
- git: "https://github.com/dbt-labs/dbt-utils.git"
revision: 1.1.1
- local: /opt/dbt/bigquery
profiles.yml
如果你决定使用 dbt CLI 并在本地运行 dbt 项目,则需要设置 profiles.yml,但如果使用 dbt Cloud 则不需要。此文件包含了 dbt 将用于连接数据平台的数据库连接信息。由于其敏感内容,此文件保存在项目之外,以避免将凭据版本化到代码库中。如果你的凭据存储在环境变量下,你可以安全地使用代码版本控制。
当你在本地环境中调用 dbt 时,dbt 会解析你的 dbt_project.yml 文件并获取配置文件名,这是 dbt 连接到你的数据平台所需的。根据需要,你可以拥有多个配置文件,但通常每个 dbt 项目或数据平台都有一个配置文件。即使在本书中使用 dbt Cloud,并且不需要配置文件。我们展示了一个 profiles.yml 的示例,如果你好奇或者更喜欢使用 dbt CLI 连接 BigQuery。
profiles.yml 的典型 YAML 模式文件如 Example 4-7 所示。我们在本书中使用 dbt Cloud,这意味着不需要配置文件。然而,我们展示了一个 profiles.yml 的示例,如果你好奇或者更喜欢使用 dbt CLI 连接 BigQuery。
Example 4-7. profiles.yml
dbt_analytics_engineer_book:
target: dev
outputs:
dev:
type: bigquery
method: service-account
project: [GCP project id]
dataset: [the name of your dbt dataset]
threads: [1 or more]
keyfile: [/path/to/bigquery/keyfile.json]
<optional_config>: <value>
profiles.yaml 的最常见结构包含以下组件:
profile_name
配置文件的名称必须与 dbt_project.yml 中找到的名称相同。在我们的情况下,我们将其命名为 dbt_analytics_engineer_book。
target
这是在不同环境中拥有不同配置的方法。例如,当在本地开发时,您可能希望使用单独的数据集/数据库进行工作。但是在部署到生产环境时,最好将所有表放在单个数据集/数据库中。默认情况下,目标设置为dev。
类型
您希望连接的数据平台类型:BigQuery、Snowflake、Redshift 等。
特定于数据库的连接细节
示例 4-7 包括属性,如method、project、dataset 和 keyfile,这些属性在使用这种方法连接到 BigQuery 时是必需的。
线程
dbt 项目将运行的线程数。它创建了模型之间的链接 DAG。线程数代表 dbt 可以并行处理的图中路径的最大数量。例如,如果指定 threads: 1,dbt 将仅开始构建一个资源(模型、测试等),并在继续下一个之前完成它。另一方面,如果设置为 threads: 4,dbt 将同时处理多达四个模型,而不会违反依赖关系。
注意
profiles.yml 文件的整体概念在这里介绍。我们不会进一步介绍,也不会提供有关如何配置您的 dbt 本地项目与 BigQuery 的详细设置指南。大部分任务已经描述过,比如在“使用 BigQuery 和 GitHub 设置 dbt 云” 中生成 keyfile,但可能还有一些细微差别。如果您想了解更多信息,dbt 提供了全面的指南。
模型
模型 是您作为数据专家将在 dbt 生态系统内花费大部分时间的地方。它们通常以 select 语句的形式编写,保存为 .sql 文件,并且是帮助您在数据平台内转换数据的 dbt 中最重要的组成部分之一。
要正确构建您的模型并创建清晰一致的项目结构,您需要熟悉数据建模的概念和技术。如果您的目标是成为分析工程师或者说是希望与数据一起工作的人,这是核心知识。
正如我们在第 2 章中所看到的,数据建模 是通过分析和定义数据需求来创建支持组织业务流程的数据模型的过程。它塑造了您的源数据,即公司收集和生成的数据,将其转换为变换后的数据,以满足公司领域和部门的数据需求,并产生附加值。
遵循数据建模的原则,正如在第二章中介绍的,模块化是另一个对正确构建您的 dbt 项目和组织模型至关重要的概念。从概念上讲,模块化 是将问题分解为一组可以分离和重组的模块的过程,这降低了系统的整体复杂性,并常常带来灵活性和多样性的好处。在分析学中,情况也是如此。在构建数据产品时,我们不会一次性编写所有代码,而是逐步完成直到最终数据产品。
由于我们将从一开始就采用模块化方法,我们的初始模型也将考虑到模块化,并根据我们在第二章中讨论过的内容进行构建。遵循典型的 dbt 数据转换流程,我们模型目录中将有三层:
Staging layer
我们的初始模块化构建块位于我们的 dbt 项目的中间层(staging layer)内。在这一层,我们与源系统建立接口,类似于 API 与外部数据源的交互方式。在这里,数据被重新排序、清理并准备进行下游处理。这包括数据标准化和轻微转换的任务,为进一步的高级数据处理奠定基础。
Intermediate layer
此层包含介于中间层和数据仓库层之间的模型。这些模型是在我们的中间层模型基础上构建的,用于进行广泛的数据转换以及来自多个来源的数据整合,从而创建多样化的中间表,这些表将服务于不同的目的。
Marts layer
根据你的数据建模技术,数据仓库中的中间层(staging layer)将所有模块化的部分汇集在一起,以更广泛地展示公司关心的实体。例如,如果我们选择维度建模技术,数据仓库中的中间层包含事实表和维度表。在这个上下文中,事实 是随时间持续发生的事件,如订单、页面点击或库存变化,其相应的度量。维度 是描述这些事实的属性,如客户、产品和地理位置。数据仓库(marts) 可以被描述为数据平台内部特定领域或部门的子集,如财务、市场营销、物流、客户服务等。也可以养成一个名为“核心”的仓库,它不针对特定领域,而是核心业务事实和维度的集合。
With the introductions made, let’s now build our first models, initially only on our staging layer. Create a new folder inside your models folder, named staging, and the respective folders per source, jaffle_shop and stripe, inside the staging folder. Then create the necessary SQL files, one for stg_stripe_order_payments.sql (示例 4-8), another for stg_jaffle_shop_customers.sql (示例 4-9), and finally one for stg_jaffle_shop_orders.sql (示例 4-10). In the end, delete the example folder inside your models. It is unnecessary, so it would create unneeded visual noise while coding. The folder structure should be similar to 示例 4-11.
示例 4-8. stg_stripe_order_payments.sql
select
id as payment_id,
orderid as order_id,
paymentmethod as payment_method,
case
when paymentmethod in ('stripe'
, 'paypal'
, 'credit_card'
, 'gift_card')
then 'credit'
else 'cash'
end as payment_type,
status,
amount,
case
when status = 'success'
then true
else false
end as is_completed_payment,
created as created_date
from `dbt-tutorial.stripe.payment`
示例 4-9. stg_jaffle_shop_customers.sql
select
id as customer_id,
first_name,
last_name
from `dbt-tutorial.jaffle_shop.customers`
示例 4-10. stg_jaffle_shop_orders.sql
select
id as order_id,
user_id as customer_id,
order_date,
status,
_etl_loaded_at
from `dbt-tutorial.jaffle_shop.orders`
示例 4-11. Staging models’ 文件夹结构
root/
├─ models/
│ ├─ staging/
│ │ ├─ jaffle_shop/
│ │ │ ├─ stg_jaffle_shop_customers.sql
│ │ │ ├─ stg_jaffle_shop_orders.sql
│ │ ├─ stripe/
│ │ │ ├─ stg_stripe_order_payments.sql
├─ dbt_project.yml
Now let’s execute and validate what we did. Typically, typing dbt run in your command line is enough, but at BigQuery, you may need to type dbt run --full-refresh. After, look at your logs by using the arrow to the left of your command line. The logs should look similar to 图 4-32.

图 4-32. dbt 系统日志
Tip
Your logs should also give you a good idea of the issue if something goes wrong. In 图 4-32, we present a logs summary, but you can also check the detailed logs for more verbosity.
Expecting that you have received the “Completed successfully” message, let’s now take a look at BigQuery, where you should see all three models materialized, as 图 4-33 shows.

图 4-33. dbt BigQuery 模型
By default, dbt materializes your models inside your data platform as views. Still, you can easily configure this in the configuration block at the top of the model file (示例 4-12).
示例 4-12. 模型文件中的材化配置
{{
config(
materialized='table'
)
}}
SELECT
id as customer_id,
first_name,
last_name
FROM `dbt-tutorial.jaffle_shop.customers`
Now that we have created our first models, let’s move to the next steps. Rearrange the code using the YAML files, and follow the best practices recommended in “YAML Files”. Let’s take the code block from there and configure our materializations inside our YAML files (示例 4-12). The first file we will change is dbt_project.yml. This should be the core YAML file for default configurations. As such, let’s change the model’s configuration inside with the code presented in 示例 4-13 and then execute dbt run again.
示例 4-13. 将模型材化为视图和表
models:
dbt_analytics_engineer_book:
staging:
jaffle_shop:
+materialized: view
stripe:
+materialized: table
Note
前缀 + 是 dbt 语法增强功能,引入于 dbt v0.17.0,旨在澄清 dbt_project.yml 文件中的资源路径和配置。
自 Example 4-13 强制所有暂存 Stripe 模型作为表格材料化后,BigQuery 应该看起来像 Figure 4-34。

Figure 4-34. dbt BigQuery 模型与材料化表格
Example 4-13 显示了如何在 dbt_project.yml 中配置每个文件夹内特定的期望材料化方式。您的暂存模型默认将保留为视图,因此可以在模型文件夹级别覆盖此配置,利用项目构建中的级联作用域优先级。首先,让我们修改我们的 dbt_project.yml,将所有暂存模型设置为视图,如 Example 4-14 所示。
Example 4-14. 将暂存模型材料化为视图
models:
dbt_analytics_engineer_book:
staging:
+materialized: view
现在让我们为 stg_jaffle_shop_customers 创建单独的 YAML 文件,声明其需要被材料化为表格。为此,在 staging/jaffle_shop 目录内创建名为 _jaffle_shop_models.yml 的 YAML 文件,并复制 Example 4-15 中的代码。
Example 4-15. 定义模型将被材料化为表格
version: 2
models:
- name: stg_jaffle_shop_customers
config:
materialized: table
在重新运行 dbt 后,查看 BigQuery。它应该类似于 Figure 4-35。

Figure 4-35. dbt BigQuery 客户模型材料化为表格
这是使用 YAML 文件的简单示例,玩弄表格材料化,并看到级联作用域优先级在实践中的意义。还有很多要做和看到的内容,我们讨论的一些内容随着我们的前进将会有更多的适用性。目前,我们只需请您更改 _jaffle_shop_models.yml 中的模型,将其材料化为视图。这将是您的默认配置。
希望在这个阶段,您已经开发了您的第一个模型,并大致了解了 YAML 文件的总体目的和级联作用域优先级。接下来的步骤将是创建我们的中间和 Mart 模型,同时学习 ref() 函数。这将是我们首次使用 Jinja,我们将在 “使用 Jinja 进行动态 SQL” 中详细讨论它。
首先要明确我们的用例。在我们的模型位于我们的暂存区内时,我们需要知道我们想要做什么。正如我们在本节开头提到的,您需要定义支持组织业务流程的数据需求。作为业务用户,我们的数据可以从多个流中获取。其中之一,将成为我们的用例,是分析我们的每个客户订单,显示每个成功订单的总付款金额以及每个成功订单类型(现金和信用卡)的总金额。
由于我们在此处有一些转换,需要从付款类型级别改变到订单粒度,这正好解释了为什么在我们到达数据集市层之前需要隔离这个复杂操作。这就是中间层的落地点。在您的模型文件夹中,创建一个名为intermediate的新文件夹。在里面,创建一个名为int_payment_type_amount_per_order.sql的新 SQL 文件,并复制 Example 4-16 中的代码。
示例 4-16. int_payment_type_amount_per_order.sql
with order_payments as (
select * from {{ ref('stg_stripe_order_payments') }}
)
select
order_id,
sum(
case
when payment_type = 'cash' and
status = 'success'
then amount
else 0
end
) as cash_amount,
sum(
case
when payment_type = 'credit' and
status = 'success'
then amount
else 0
end
) as credit_amount,
sum(case
when status = 'success'
then amount
end
) as total_amount
from order_payments
group by 1
正如您在创建order_payments CTE 时可以看到,我们使用ref()函数从stg_stripe_order_payments中收集数据。此函数引用了构建您数据平台的上游表和视图。由于其带来的好处,如在我们实施分析代码时,我们将使用此函数作为标准:
-
它允许您以灵活的方式构建模型之间的依赖关系,这些依赖关系可以在共同的代码库中共享,因为它在
dbt run期间编译数据库对象的名称,并从环境配置中收集该名称,当您创建项目时。这意味着在您的环境中,代码将根据您的环境配置编译,这些配置在您特定的开发环境中可用,但与您的队友的开发环境不同,但共享同一代码库。 -
您可以构建血统图,以可视化特定模型的数据流和依赖关系。我们将在本章后面讨论此问题,它还涵盖在“文档”中。
最后,虽然承认前面的代码可能看起来像是一种反模式,因为CASE WHEN条件的重复感,但需要澄清整个数据集包括所有订单,无论其付款状态如何。然而,对于本例子,我们选择仅对已达到“成功”状态的订单关联的支付进行财务分析。
中间表创建完成后,让我们进入最终层。考虑到所描述的用例,我们需要从客户角度分析订单。这意味着我们必须创建一个与事实表连接的客户维度。由于当前用例可以满足多个部门,我们不会创建一个特定的部门文件夹,而是一个名为 core 的文件夹。因此,首先在我们的模型文件夹中创建 marts/core 目录。然后将 示例 4-17 复制到名为 dim_customers.sql 的新文件中,将 示例 4-18 复制到名为 fct_orders.sql 的新文件中。
示例 4-17. dim_customers.sql
with customers as (
select * from {{ ref('stg_jaffle_shop_customers')}}
)
select
customers.customer_id,
customers.first_name,
customers.last_name
from customers
示例 4-18. fct_orders.sql
with orders as (
select * from {{ ref('stg_jaffle_shop_orders' )}}
),
payment_type_orders as (
select * from {{ ref('int_payment_type_amount_per_order' )}}
)
select
ord.order_id,
ord.customer_id,
ord.order_date,
pto.cash_amount,
pto.credit_amount,
pto.total_amount,
case
when status = 'completed'
then 1
else 0
end as is_order_completed
from orders as ord
left join payment_type_orders as pto ON ord.order_id = pto.order_id
所有文件创建完成后,让我们在 dbt_project.yml 中设置默认配置,如 示例 4-19 所示,然后在 BigQuery 上执行 dbt run,或者可能是 dbt run --full-refresh。
示例 4-19. 在 dbt_project.yml 中,每层的模型配置
models:
dbt_analytics_engineer_book:
staging:
+materialized: view
intermediate:
+materialized: view
marts:
+materialized: table
Tip
如果您收到类似于“rpc 请求中的编译错误...依赖于一个名为 int_payment_type_amount_per_order 的节点,但该节点尚未在您的数据平台中——在我们的情况下是 int_payment_type_amount_per_order。要解决此问题,请转到该特定模型并执行 dbt run --select *MODEL_NAME* 命令,将 *MODEL_NAME* 替换为相应的模型名称。
如果一切顺利,您的数据平台应已完全更新所有 dbt 模型。只需查看 BigQuery,其应类似于 图 4-36。

图 4-36. 带有所有模型的 dbt BigQuery
最后,打开 fct_orders.sql 并查看信息窗口中的 Lineage 选项(参见 图 4-37)。这是我们将在 “文档” 中介绍的众多功能之一,它为我们提供了关于数据流向特定模型及其上游和下游依赖关系的良好概念。

图 4-37. dbt fct_orders 数据血统
源
在 dbt 中,sources 是您数据平台上可用的原始数据,使用通用的抽取和加载(EL)工具捕获。区分 dbt 源与传统数据源至关重要。传统数据源可以是内部或外部的。内部数据源 提供支持组织日常业务操作的交易数据。客户、销售和产品数据是内部数据源潜在内容的示例。另一方面,外部数据源 提供源自组织外部的数据,例如从商业伙伴、互联网和市场研究等处收集的数据。这通常是与竞争对手、经济学、客户人口统计等相关的数据。
dbt sources 依赖于业务需求中的内部和外部数据,但其定义有所不同。正如前文所述,dbt sources 是您数据平台内的原始数据。这些原始数据通常由数据工程团队通过 EL 工具带入您的数据平台,并且将是支持您的分析平台运行的基础。
在我们的模型中,从“模型”中,我们通过使用硬编码字符串如dbt-tutorial.stripe.payment或dbt-tutorial.jaffle_shop.customers引用我们的源。即使这样也能工作,但请考虑,如果您的原始数据发生变化,如位置或表名按照特定命名约定进行更改,则在多个文件中进行更改将会很困难且耗时。这就是 dbt sources 的用武之地。它们允许您在一个 YAML 文件中记录这些源表格,您可以在其中引用源数据库、模式和表格。
让我们把这些付诸实践。通过遵循“YAML 文件”中推荐的最佳实践,现在让我们在 models/staging/jaffle_shop 目录下创建一个名为 _jaffle_shop_sources.yml 的新 YAML 文件,并复制示例 4-20 中的代码。然后,在 models/staging/stripe 目录中创建另一个 YAML 文件,命名为 _stripe_sources.yml,并复制示例 4-21 中的代码。
示例 4-20. _jaffle_shop_sources.yml—Jaffle Shop 模式下所有表格的源参数化文件
version: 2
sources:
- name: jaffle_shop
database: dbt-tutorial
schema: jaffle_shop
tables:
- name: customers
- name: orders
示例 4-21. _stripe_sources.yml—stripe 模式下所有表格的源参数化文件
version: 2
sources:
- name: stripe
database: dbt-tutorial
schema: stripe
tables:
- name: payment
配置好我们的 YAML 文件后,我们需要在模型中进行最后一次更改。不再使用硬编码的数据源,而是使用一个名为source()的新函数。这类似于我们在“引用数据模型”中介绍的ref()函数,但现在配置源时我们传递类似于{{ source("stripe", "payment") }}的内容,这在特定情况下将引用我们在示例 4-21 中创建的 YAML 文件。
现在让我们动手吧。拿出你之前创建的所有 SQL 分期模型代码,并用示例 4-22 中相应的代码替换它。
示例 4-22. 使用source()函数的付款、订单和客户分期模型
-- REPLACE IT IN stg_stripe_order_payments.sql
select
id as payment_id,
orderid as order_id,
paymentmethod as payment_method,
case
when paymentmethod in ('stripe'
,'paypal'
, 'credit_card'
, 'gift_card')
then 'credit'
else 'cash'
end as payment_type,
status,
amount,
case
when status = 'success'
then true
else false
end as is_completed_payment,
created as created_date
from {{ source('stripe', 'payment') }}
-- REPLACE IT IN stg_jaffle_shop_customers.sql file
select
id as customer_id,
first_name,
last_name
from {{ source('jaffle_shop', 'customers') }}
-- REPLACE IT IN stg_jaffle_shop_orders.sql
select
id as order_id,
user_id as customer_id,
order_date,
status,
_etl_loaded_at
from {{ source('jaffle_shop', 'orders') }}
切换到使用我们的source()函数后,您可以通过运行dbt compile或单击 IDE 中的编译按钮来检查您的代码在数据平台中的执行情况。在后台,dbt 将查找引用的 YAML 文件,并将source()函数替换为直接的表格引用,如图 4-38 所示。

图 4-38. dbt 客户分期模型与source()函数及相应编译的代码。编译后的代码将在您的数据平台内运行。
使用source()函数的另一个好处是现在您可以在血统图中看到来源。例如,只需看一下fct_orders.sql的血统。现在,与图 4-37 中显示的相同的血统应该看起来像图 4-39。

图 4-39. dbt fct_orders 数据源血统与来源
数据源新鲜度
数据的新鲜度是数据质量的一个重要方面。如果数据不是最新的,它就是过时的,这可能会导致公司决策过程中的重大问题,因为这可能导致不准确的洞察。
dbt 允许您通过数据源新鲜度测试来缓解这种情况。为此,我们需要一个审计字段,该字段声明了您的数据平台中特定数据工件的加载时间戳。有了它,dbt 将能够测试数据的年龄,并根据指定的条件触发警告或错误。
为了实现这一点,让我们回到我们的源 YAML 文件。对于这个特定的例子,我们将使用我们数据平台中的订单数据,因此,通过推理,我们将用示例 4-23 中的代码替换_jaffle_shop_sources.yml中的代码。
示例 4-23. _jaffle_shop_sources.yml—Jaffle Shop 模式下所有表的源参数化文件,包含源新鲜度测试
version: 2
sources:
- name: jaffle_shop
database: dbt-tutorial
schema: jaffle_shop
tables:
- name: customers
- name: orders
loaded_at_field: _etl_loaded_at
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}
正如您所看到的,我们在我们的数据平台中使用了_etl_loaded_at字段。我们不需要将其带入我们的转换过程中,因为它对于前向模型没有附加值。这并不是问题,因为我们正在测试我们的上游数据,而在我们的情况下是原始数据。在 YAML 文件中,我们创建了两个额外的属性:loaded_at_field,它代表在源新鲜度测试下要监视的字段,以及freshness,其中包含监视源新鲜度的实际规则。在freshness属性内部,我们配置它在数据过时 12 小时后使用warn_after属性发出警告,并且在过去 24 小时内未刷新数据时使用error_after属性发出实际错误。
最后,让我们看看如果执行命令dbt source freshness会发生什么。在我们的情况下,我们收到了一个警告,正如您可以在图 4-40 中看到的那样。

图 4-40. dbt 订单原始数据和数据源新鲜度测试日志
如果您检查日志详细信息,可以看到在您的数据平台中执行的查询并进行故障排除。这个特定的警告是预期的。_etl_loaded_at预计将从当前时间开始花费 16 小时,所以任何低于这个时间的值都会引发警告。如果您想继续测试,请将您的warn_after更改为更高的值,如 17 小时。所有测试都应该通过。
希望现在源数据新鲜度的概念已经清晰了。我们将在本书的后面回到这个概念,并向您展示如何自动化和快照源数据新鲜度测试。与此同时,了解其在整体测试环境中的目的、如何配置以及这种测试在减少数据质量问题中的重要性非常关键。
测试
作为分析工程师,您必须确保数据准确可靠,以建立对您提供的分析的信任,并为您的组织提供客观的见解。每个人都同意这一点,但即使您遵循所有工程最佳实践,当您必须处理工作中的数据波动、类型、结构等时,总会有例外情况。
有许多方法可以捕获这些异常。然而,当您处理大量数据时,您需要考虑一种可扩展的方法来分析大型数据集并快速识别这些异常。这就是 dbt 发挥作用的地方。
dbt 允许您快速且轻松地扩展数据工作流中的测试,以便您可以在其他人之前识别出问题。在开发环境中,您可以使用测试来确保您的分析代码产生所需的输出。在部署/生产环境中,您可以自动化测试并设置警报,以便在特定测试失败时通知您,以便您可以迅速做出反应并修复问题,以免产生更严重的后果。
作为数据从业者,重要的是要理解 dbt 中的测试可以总结为关于数据的断言。当您在数据模型之上运行测试时,您断言这些数据模型产生了预期的输出,这是确保数据质量和可靠性的关键步骤。这些测试是一种验证形式,类似于确认您的数据遵循特定模式并符合预定义标准。
然而,需要注意的是,在数据测试的更广泛环境中,dbt 测试只是测试的一种类型。在软件测试中,测试通常区分为验证和验证两种类型。dbt 测试主要侧重于验证,通过确认数据是否符合已建立的模式和结构来进行验证。它们并不设计用于测试数据转换中逻辑的细节,这类似于软件开发中单元测试的作用。
此外,dbt 测试可以在某种程度上协助数据组件的集成,特别是当多个组件一起运行时。尽管如此,需要认识到 dbt 测试有其局限性,可能无法涵盖所有测试用例。在数据项目的全面测试中,您可能需要使用其他针对特定验证和验证需求定制的测试方法和工具。
有了这个理解,让我们专注于可以在 dbt 中使用哪些测试。在 dbt 中,测试分为两类:单一测试和通用测试。让我们更多地了解一下这两种类型,它们的目的以及我们如何利用它们。
通用测试
dbt 中最简单但高度可扩展的测试是通用测试。通过这些测试,通常无需编写任何新逻辑,但也可以选择自定义通用测试。尽管如此,您通常只需编写几行 YAML 代码,然后根据测试来测试特定模型或列。dbt 提供了四种内置的通用测试:
unique 测试
验证特定列中的每个值是否唯一
not_null 测试
验证特定列中的每个值是否不为 null
accepted_values 测试
确保特定列中的每个值存在于给定的预定义列表中
relationships 测试
确保特定列中的每个值存在于另一个模型中的列中,因此我们保证了参照完整性
现在我们对通用测试有了一些背景了解,让我们尝试一下。我们可以选择想要的模型,但为了简化,让我们选择一个模型,可以对其应用所有测试。为此,我们选择了stg_jaffle_shop_orders.sql模型。在这里,我们将能够测试customer_id和order_id等字段的unique和not_null。我们可以使用accepted_values检查所有订单的status是否在预定义列表中。最后,我们将使用relationships测试检查customer_id的所有值是否在stg_jaffle_shop_customers.sql模型中。让我们从用示例 4-24 中的代码替换我们的_jaffle_shop_models.yml开始。
示例 4-24. 带有通用测试的 _jaffle_shop_models.yml 参数化
version: 2
models:
- name: stg_jaffle_shop_customers
config:
materialized: view
columns:
- name: customer_id
tests:
- unique
- not_null
- name: stg_jaffle_shop_orders
config:
materialized: view
columns:
- name: order_id
tests:
- unique
- not_null
- name: status
tests:
- accepted_values:
values:
- completed
- shipped
- returned
- placed
- name: customer_id
tests:
- relationships:
to: ref('stg_jaffle_shop_customers')
field: customer_id
现在,在命令行中键入dbt test并查看日志。如果accepted_values测试失败,那么您做得很好。它应该失败的。让我们进行调试,了解失败的潜在根本原因。打开日志并展开失败的测试。然后点击详细信息。您将看到执行测试数据的查询,正如 Figure 4-41 所示。

Figure 4-41. 通用测试,使用accepted_values失败的 dbt 日志
让我们将此查询复制到您的文本编辑器中——仅保留内部查询然后执行它。您应该会得到与 Figure 4-42 中类似的输出。

Figure 4-42. 通用测试调试
天啊。我们发现了问题。附加状态return_pending在我们的测试列表中缺失。让我们添加它并重新运行我们的dbt test命令。现在所有测试都应该通过,如图 4-43 所示。

图 4-43. 所有测试成功执行的通用测试
注意
除了 dbt Core 中的通用测试之外,dbt 生态系统中还有更多测试。这些测试位于 dbt 包中,因为它们是内置于 dbt 中的通用测试的扩展。“dbt Packages”将详细介绍包的概念及其安装方法,但是对于扩展测试功能,如来自 dbt 团队的dbt_utils(https://oreil.ly/MwqgC)或来自 Python 库 Great Expectations 的dbt_expectations(https://oreil.ly/bmrqJ)等包,都是出色的使用示例,并且是任何 dbt 项目中必不可少的。最后,自定义通用测试是 dbt 的另一个功能,它允许您定义适合特定项目需求的数据验证规则和检查。
单数测试
与通用测试不同,单数测试定义在tests目录下的.sql文件中。通常,在您想要测试特定模型内特定属性时很有帮助,但内置于 dbt 中的传统测试不符合您的需求。
在查看我们的数据模型时,一个很好的测试是检查没有订单的总金额为负数。我们可以在三个层次之一——分段、中间或市场中执行此测试。我们选择了中间层,因为我们做了一些可能影响数据的转换。首先,在tests目录中创建名为assert_total_payment_amount_is_positive.sql的文件,并复制 Example 4-25 中的代码。
示例 4-25. assert_total_payment_amount_is_positive.sql,用于检查int_payment_type_amount_per_order内的total_amount属性是否仅具有非负值的单数测试
select
order_id,
sum(total_amount) as total_amount
from {{ ref('int_payment_type_amount_per_order') }}
group by 1
having total_amount < 0
现在,您可以执行以下命令之一来运行您的测试,这些测试应该通过:
dbt test
执行您的所有测试
dbt test --select test_type:singular
仅执行单数测试
dbt test --select int_payment_type_amount_per_order
执行int_payment_type_amount_per_order模型的所有测试
dbt test --select assert_total_payment_amount_is_positive
执行我们创建的特定测试
这些命令提供了根据您的需求选择运行测试的能力。无论您需要运行所有测试、特定类型的测试、特定模型的测试,甚至是单独的特定测试,dbt 都允许您在命令中利用各种选择语法选项。这种多样的选择确保您可以精确地定位您希望执行的测试,以及其他 dbt 资源。
在“dbt 命令和选择语法”中,我们将提供对可用 dbt 命令的全面概述,并探讨如何有效地使用选择语法来指定资源。
要在您的 dbt 项目中测试您的模型,您还可以将这些测试扩展到您的数据源。您已经在“数据源新鲜度”中使用数据源新鲜度测试做过了这一点。此外,您还可以为此目的加强通用和单一测试。在您的数据源中使用测试功能将使我们确信原始数据构建得符合我们的期望。
与您在模型中配置测试的方式相同,您也可以为您的数据源进行配置。无论是通用测试的 YAML 文件,还是单一测试的.sql文件,规范始终如一。让我们分别看一下每种测试类型的一个示例。
从通用测试开始,您需要编辑数据源的特定 YAML 文件。让我们保留与顾客和订单分期表相同的unique、not_null和accepted_values测试,但现在您将测试它们的数据源。因此,为了实现这一目标,请将_jaffle_shop_sources.yml中的代码替换为示例 4-26 中的代码。
执行dbt test或dbt test --select source:stripe,因为我们在这种情况下查看了 Stripe 数据源。一切也应该通过。
version: 2
sources:
- name: jaffle_shop
database: dbt-tutorial
schema: jaffle_shop
tables:
- name: customers
columns:
- name: id
tests:
- unique
- not_null
- name: orders
loaded_at_field: _etl_loaded_at
freshness:
warn_after: {count: 17, period: hour}
error_after: {count: 24, period: hour}
columns:
- name: id
tests:
- unique
- not_null
- name: status
tests:
- accepted_values:
values:
- completed
- shipped
- returned
- placed
- return_pending
一旦您在 YAML 文件中有了新的代码,您可以运行dbt test或更确切地执行命令,仅测试我们创建了这些测试的数据源,dbt test --select source:jaffle_shop。您的所有测试应该通过。
最后,您也可以像之前一样实施单一测试。让我们复制我们之前在示例 4-25 中执行的单一测试。在您的tests目录中创建一个名为assert_source_total_payment_amount_is_positive.sql的新文件,并从示例 4-27 复制代码。该测试检查付款来源表中订单的amount属性的总和是否仅为非负值。
测试数据源
select
orderid as order_id,
sum(amount) as total_amount
from {{ source('stripe', 'payment') }}
group by 1
having total_amount < 0
示例 4-27. assert_source_total_payment_amount_is_positive.sql 单一测试
analyses文件夹可以存储您的特定查询、审计查询、培训查询或重构查询,例如,在影响模型之前检查代码外观的用途。
示例 4-26. _jaffle_shop_sources.yml—使用通用测试的参数化
分析是模板化的 SQL 文件,您不能在 dbt run 中执行,但由于您可以在分析中使用 Jinja,因此您仍然可以使用 dbt compile 来查看您的代码的外观,同时保留您的代码在版本控制下。考虑到其目的,让我们看看可以利用 analyses 文件夹的一个用例。
想象一下,你不想建立一个全新的模型,但仍然想通过利用代码版本管理来保留未来需求的一部分信息。通过分析,你可以做到这一点。对于我们的用例,让我们分析总支付金额最高的前 10 名客户,仅考虑订单“完成”状态。要查看此信息,在 analyses 目录中,创建一个名为 most_valuable_customers.sql 的新文件,并复制来自 示例 4-28 的代码。
示例 4-28. most_valuable_customers.sql 分析,基于已完成订单输出前 10 名最有价值的客户
with fct_orders as (
select * from {{ ref('fct_orders')}}
),
dim_customers as (
select * from {{ ref('dim_customers' )}}
)
select
cust.customer_id,
cust.first_name,
SUM(total_amount) as global_paid_amount
from fct_orders as ord
left join dim_customers as cust ON ord.customer_id = cust.customer_id
where ord.is_order_completed = 1
group by cust.customer_id, first_name
order by 3 desc
limit 10
现在执行代码并检查结果。如果一切顺利,它将给出最有价值的前 10 名客户,就像 图 4-44 所示的那样。

图 4-44. 基于已完成订单支付的全球总金额最有价值的前 10 名客户
种子
Seeds 是您的 dbt 平台中的 CSV 文件,其中包含少量非易失性数据,可作为表在您的数据平台中实现。只需在命令行中输入 dbt seed,种子就可以像所有其他模型一样在您的模型中使用 ref() 函数。
我们可以找到种子的多个应用场景,从映射国家代码(例如,PT 表示葡萄牙或 US 表示美国)、邮政编码到州、需要排除在我们分析之外的虚拟电子邮件地址,甚至其他复杂的分析,比如价格范围分类。重要的是记住,种子不应该有大量或频繁变动的数据。如果情况是这样的话,重新考虑你的数据捕捉方法,例如使用 SFTP(SSH 文件传输协议)或 API。
为了更好地理解如何使用种子,请按照下一个用例继续操作。考虑到我们在 “分析” 中所做的工作,我们不仅希望看到根据已支付的订单确定的最有价值的前 10 名客户,还要根据支付的 total_amount 将所有客户分类为 regular、bronze、silver 或 gold。作为一个开始,让我们创建我们的种子。为此,在您的 seeds 文件夹中创建一个名为 customer_range_per_paid_amount.csv 的新文件,并复制 示例 4-29 的数据。
示例 4-29. seed_customer_range_per_paid_amount.csv,其中包含映射范围的数据
min_range,max_range,classification
0,9.999,Regular
10,29.999,Bronze
30,49.999,Silver
50,9999999,Gold
完成后,请执行dbt seed。它将把你的 CSV 文件转换成数据平台中的表格。最后,在分析目录下,让我们创建一个名为customer_range_based_on_total_paid_amount.sql的新文件,并从示例 4-30 复制代码。
示例 4-30. customer_range_based_on_total_paid_amount.sql 根据已完成订单和支付总额,显示了客户分类范围。
with fct_orders as (
select * from {{ ref('fct_orders')}}
),
dim_customers as (
select * from {{ ref('dim_customers' )}}
),
total_amount_per_customer_on_orders_complete as (
select
cust.customer_id,
cust.first_name,
SUM(total_amount) as global_paid_amount
from fct_orders as ord
left join dim_customers as cust ON ord.customer_id = cust.customer_id
where ord.is_order_completed = 1
group by cust.customer_id, first_name
),
customer_range_per_paid_amount as (
select * from {{ ref('seed_customer_range_per_paid_amount' )}}
)
select
tac.customer_id,
tac.first_name,
tac.global_paid_amount,
crp.classification
from total_amount_per_customer_on_orders_complete as tac
left join customer_range_per_paid_amount as crp
on tac.global_paid_amount >= crp.min_range
and tac.global_paid_amount <= crp.max_range
现在让我们执行我们的代码并查看结果。它将为每位客户提供总支付金额及其对应的范围(图 4-45)。

图 4-45. 基于完成订单和支付总额的客户范围。
文档
在全球软件工程领域,文档至关重要,但似乎又像个禁忌。一些团队做了,而其他团队则没有,或者做得不完整。文档可能会变得过于官僚主义或复杂,或者被视为开发者待办事项中的负担,因此不惜一切代价避免。你可能听到一长串理由来证明不创建文档或将其推迟到较不紧迫的时间。没有人会说文档是不重要的。只是“我们不会做”,“现在不做”,或者“我们没有时间”。
以下是几个理由,来证明创建和使用文档的必要性:
-
促进了入职、移交和招聘流程。有了适当的文档,任何新的团队成员都可以确保他们不是“被抛向狼群”。新同事将拥有书面的入职流程和技术文档,从而减少其对当前团队流程、概念、标准和技术发展的学习曲线。员工流失和知识分享过渡也适用同样的原则。
-
它将赋予真理的单一来源。从业务定义、流程和操作文章,到让用户自助回答问题,有文档将节省团队寻找信息的时间和精力。
-
通过文档分享知识,可以减少重复或冗余工作。如果文档已经完成,可以重新使用,而无需从头开始。
-
它促进了共同责任感,确保关键知识不局限于单个个体。在关键团队成员不可用时,这种共享所有权对防止中断至关重要。
-
当你希望建立质量、过程控制并遵守合规法规时,文档至关重要。有文档将使你的团队能够在公司各部门之间实现协调和一致性。
一个理由证明缺乏创建文档的动机是,它与实际开发流程并行,就像使用一个工具进行开发,另一个工具进行文档编写一样。但是,使用 dbt 不同。您在开发分析代码、测试和连接到源等任务时,同时构建项目文档。一切都在 dbt 内部进行,而不是在一个单独的界面中。
dbt 处理文档的方式使您能够在构建代码的同时创建文档。通常情况下,文档的很大一部分已经是动态生成的,例如我们之前介绍过的血统图,只需要您适当配置ref()和source()函数即可。另一部分是部分自动化的,需要您手动输入特定模型或列代表的内容。然而,再次强调,所有操作都是在 dbt 内部进行的,直接在 YAML 或 Markdown 文件中进行。
让我们开始我们的文档工作。我们想要实现的用例是记录我们的模型及其对应的fct_orders和dim_customers列。我们将使用模型的 YAML 文件,并为了更丰富的文档,我们将在 Markdown 文件中使用文档块。由于我们仍然需要在marts目录中为核心模型创建一个 YAML 文件,让我们使用名称为_core_models.yml。
复制示例 4-31。然后,在同一目录文件夹中创建一个名为_code_docs.md的 Markdown 文件,复制示例 4-32。
示例 4-31. _core_models.yml—带有description参数的 YAML 文件
version: 2
models:
- name: fct_orders
description: Analytical orders data.
columns:
- name: order_id
description: Primary key of the orders.
- name: customer_id
description: Foreign key of customers_id at dim_customers.
- name: order_date
description: Date that order was placed by the customer.
- name: cash_amount
description: Total amount paid in cash by the customer with "success" payment
status.
- name: credit_amount
description: Total amount paid in credit by the customer with "success"
payment status.
- name: total_amount
description: Total amount paid by the customer with "success" payment status.
- name: is_order_completed
description: "{{ doc('is_order_completed_docblock') }}"
- name: dim_customers
description: Customer data. It allows you to analyze customers perspective linked
facts.
columns:
- name: customer_id
description: Primary key of the customers.
- name: first_name
description: Customer first name.
- name: last_name
description: Customer last name.
示例 4-32. _core_doc.md—带有文档块的 Markdown 文件
{% docs is_order_completed_docblock %}
Binary data which states if the order is completed or not, considering the order
status. It can contain one of the following values:
| is_order_completed | definition |
|--------------------|-----------------------------------------------------------|
| 0 | An order that is not completed yet, based on its status |
| 1 | An order which was completed already, based on its status |
{% enddocs %}
在生成文档之前,让我们尝试理解我们做了什么。通过分析 YAML 文件_core_models.yml,您可以看到我们添加了一个新属性:description。这个基本属性允许您用手动输入来补充文档。这些手动输入可以是文本,就像我们在大多数情况下使用的那样,也可以是引用 Markdown 文件中的文档块,就像我们在fct_orders列is_order_completed中所做的那样。我们首先在 Markdown 文件_code_docs.md中创建了文档块,并命名为is_order_completed_docblock。这个名称是我们用来引用描述字段中文档块的名称:"{{ doc('is_order_completed_docblock') }}"。
让我们通过在命令行中键入dbt docs generate来生成我们的文档。当它成功完成后,您可以浏览文档页面。
要查看文档页面很简单。在您成功执行dbt docs generate之后,在 IDE 中,在屏幕左上角,在 Git 分支信息的旁边,您可以点击文档站点书本图标,即 图 4-46。

图 4-46. 查看文档
一旦进入文档页面,您将看到类似 Figure 4-47 的概述页面。目前,您将看到 dbt 提供的默认信息,但此页面也是完全可定制的。

Figure 4-47. 文档着陆页
查看概述页面,您可以看到左侧的项目结构(Figure 4-48),其中包括测试、种子和模型等,您可以自由导航。

Figure 4-48. dbt 项目结构在文档中的展示
现在选择我们开发的一个模型并查看其相应的文档。我们选择了fct_orders模型。一旦我们点击其文件,屏幕将显示有关模型的多层信息,如 Figure 4-49 所示。

Figure 4-49. fct_orders文档页面
在顶部,详细信息部分提供了关于表元数据的信息,例如表类型(也称为物化)。可用的其他详细信息包括使用的语言、行数以及表的大致大小。
紧接着,我们有模型的描述。您可能记得,这是我们在_core_models.yml文件中为fct_orders表配置的模型。
最后,我们有与fct_orders相关的列信息。该文档部分自动化(例如列类型),但也接受手动输入(例如列描述)。我们已经提供了这些输入,并使用文档块为is_order_completed属性提供了全面信息。要查看文档页上的文档块,请点击is_order_completed字段,该字段应扩展并呈现所需信息(Figure 4-50)。

Figure 4-50. is_order_completed列显示配置的文档块
在列信息之后,我们有模型的下游和上游依赖关系,包括引用和依赖部分。这些依赖关系也显示在 Figure 4-51 中。

Figure 4-51. fct_orders 在文档中的依赖关系
在fct_orders文档页面底部是生成特定模型的代码。您可以以原始格式、Jinja 或编译代码来可视化源代码。Figure 4-52 展示了其原始形式。

Figure 4-52. fct_orders 源代码
最后,如果您查看文档页面右下角,会看到一个蓝色按钮。点击该按钮将访问您正在可视化的相应模型的血统图。我们已选择了fct_orders血统图,您可以在其中看到上游依赖项,如源表或中间表,以及下游依赖项,如图 4-53 中显示的分析文件。血统图非常强大,因为它提供了数据从您消耗它的那一刻起直到转换和提供的整体视图。
dbt 文档的另一个有趣的方面是通过使用persist_docs配置直接将列级和表级描述持久化到数据库的能力。此功能对于您数据仓库的所有用户都非常有价值,包括那些可能无法访问 dbt Cloud 的用户。它确保关键的元数据和描述对数据消费者是随时可用的,有助于更好地理解和利用您的数据资产。

图 4-53. fct_orders血统图
dbt 命令和选择语法
我们已经介绍了几个 dbt 命令,例如dbt run和dbt test,以及我们如何与 CLI 交互来执行它们。在本节中,我们将探讨使您能够执行、管理和控制 dbt 项目各个方面的基本 dbt 命令和选择语法。无论是运行转换、执行测试还是生成文档,这些命令都是您有效项目管理的工具包。
让我们从头开始。在其核心,dbt 是一个旨在简化数据转换工作流程的命令行工具。它提供了一组命令,使您能够高效地与 dbt 项目交互。让我们更详细地探讨每一个这些命令。
dbt run
dbt run命令是执行您 dbt 模型中定义的数据转换的首选工具。它与项目的配置文件(如dbt_project.yml)一起工作,了解需要运行哪些模型以及以何种顺序运行它们。该命令将根据其依赖关系识别必须执行的模型,并以适当的顺序运行它们。
dbt test
确保数据的质量和可靠性至关重要。dbt test命令允许您定义和执行对数据模型的测试,验证它们是否符合您的业务规则和期望。
dbt docs
充分的文档对于协作数据项目至关重要。dbt docs自动化生成您 dbt 项目的文档,包括模型描述、列描述和模型之间的关系。要生成文档,您需要执行dbt docs generate。
dbt build
在运行 dbt 项目之前,通常需要编译它。dbt build 命令执行此任务,为执行创建所需的工件。此步骤对于优化执行过程并确保一切就位至关重要。一旦项目成功编译,您可以更有信心地进行像 dbt run 这样的其他命令。
其他命令
尽管上述命令可能是最常用的,您还应该了解其他 dbt 命令,例如以下命令:
dbt seed
将原始数据或参考数据加载到您的项目中
dbt clean
删除由 dbt build 生成的工件
dbt snapshot
对您的数据进行版本控制快照
dbt archive
存档表或模型至冷存储
dbt deps
安装在 packages.yml 中定义的项目依赖项
dbt run-operation
运行在项目中定义的自定义操作
dbt source snapshot-freshness
检查您的源数据的新鲜度
dbt ls
列出在 dbt 项目中定义的资源
dbt retry
从失败点重新运行最后一次运行的 dbt 命令
dbt debug
以调试模式运行 dbt,提供详细的调试信息
dbt parse
解析 dbt 模型而不运行它们,这对于语法检查非常有用
dbt clone
克隆从指定状态选择的模型
dbt init
在当前目录中创建一个新的 dbt 项目
选择语法
随着您的 dbt 项目的增长,您将需要针对特定模型、测试或其他资源进行执行、测试或文档生成,而不是每次运行所有资源。这就是选择语法发挥作用的地方。
选择语法允许您在运行 dbt 命令时精确指定要包括或排除的资源。选择语法包括各种元素和技术,例如以下内容。
通配符 *
星号 (*) 表示任何字符或字符序列。让我们看看 示例 4-33。
示例 4-33. 使用 * 通配符的选择语法
dbt run --select models/marts/core/*
在此,我们将 * 通配符与 --select 标志一起使用,以定位 core 目录中的所有资源或模型。此命令将执行该目录中的所有模型、测试或其他资源。
标签
标签 是您可以分配给 dbt 项目中模型、宏或其他资源的标签,特别是在 YAML 文件中。您可以使用选择语法来定位具有特定标签的资源。例如,示例 4-34 展示了如何基于 marketing 标签选择资源。
示例 4-34. 使用标签的选择语法
dbt run --select tag:marketing
模型名称
您可以通过选择语法中的模型名称精确选择单个模型,如 示例 4-35 所示。
示例 4-35. 使用模型的选择语法
dbt run --select fct_orders
依赖关系
使用+和-符号来选择依赖于或被其他模型依赖的模型。例如,fct_orders+选择依赖于fct_orders的模型,而+fct_orders选择fct_orders依赖的模型(示例 4-36)。
示例 4-36. 使用依赖关系的选择语法
# run fct_orders upstream dependencies
dbt run --select +fct_orders
# run fct_orders downstream dependencies
dbt run --select fct_orders+
# run fct_orders both up and downstream dependencies
dbt run --select +fct_orders+
包
如果将您的 dbt 项目组织为包,您可以使用包语法来选择特定包中的所有资源,如示例 4-37 所示。
示例 4-37. 使用包的选择语法
dbt run --select my_package.some_model
多重选择
您可以组合选择语法的元素来创建复杂的选择,如示例 4-38 所示。
示例 4-38. 使用多个元素的选择语法
dbt run --select tag:marketing fct_orders
在本示例中,我们结合了标记和模型选择等元素。它将仅在 dbt 模型名称为fct_orders且具有标签marketing时运行。
选择语法允许您基于各种标准(包括模型名称、标签和依赖关系)控制运行哪些 dbt 资源。您可以使用--select标志以选择语法定制您的 dbt 操作,以适应项目的特定子集。
此外,dbt 还提供了几个其他与选择相关的标志和选项,如--selector、--exclude、--defer等,这些选项提供了更精细的控制,以便您与 dbt 项目的交互方式与项目的需求和工作流程保持一致。
作业和部署
到目前为止,我们一直在讨论如何使用 dbt 进行开发。我们学习了关于模型的知识,并且如何实施测试和编写文档,以及 dbt 提供的其他相关组件。通过利用我们的开发环境并手动执行 dbt 命令,我们完成并测试了所有这些工作。
使用开发环境不应该被忽视。它允许您在准备好之前继续构建 dbt 项目,而不会影响部署/生产环境。但现在我们已经达到了需要将我们的代码投入生产并自动化的阶段。为此,我们需要将我们的分析代码部署到生产分支,通常命名为主分支,并且部署到专用的生产模式中,例如 BigQuery 中的dbt_analytics_engineering.core,或者您数据平台中等效的生产目标。
最后,我们需要配置并安排一个作业来自动化我们希望投入生产的内容。配置作业是 CI/CD 过程的重要部分。它允许您根据业务需求的节奏自动执行命令。
首先,让我们将目前为止的所有工作提交并同步到我们的开发分支,然后与主分支合并。点击“提交和同步”按钮(图 4-54)。不要忘记写一条全面的消息。

图 4-54. “提交并同步”按钮
您可能需要发起一个拉取请求。如 “在 BigQuery 和 GitHub 中设置 dbt Cloud” 中简要说明的那样,拉取请求(PR)在协作开发中发挥着重要作用。它们作为将您提议的更改传达给团队的基本机制。然而,理解 PR 并不仅仅是通知同事您的工作进展;它们是审查和集成过程中的关键步骤。
当您创建 PR 时,实质上是邀请您的团队审查您的代码、提供反馈,并共同决定这些更改是否与项目的目标和质量标准一致。
回到我们的代码,在 PR 后,将其与 GitHub 上的主分支合并。您在 GitHub 上的最终屏幕应与图 4-55. 类似。

图 4-55. 主分支合并后的拉取请求屏幕
在这个阶段,您的主分支应该与您的开发分支相同。现在是将其部署到数据平台的时候了。在创建作业之前,您需要设置您的部署环境:
-
从部署菜单中,点击“环境”选项,然后点击“创建环境”按钮。会弹出一个屏幕,您可以在其中配置您的部署环境。
-
保留最新的 dbt 版本,并且不勾选在自定义分支上运行的选项,因为我们已将代码合并到了主分支。
-
将环境命名为“部署”。
-
在“部署凭证”部分,编写将连接您的部署/生产环境的数据集。我们将其命名为
dbt_analytics_engineer_prod,但您可以根据需要选择最合适的名称。
如果一切顺利,您应该已经设置了一个与图 4-56. 类似的部署环境配置。

图 4-56. 部署环境设置
现在是配置作业的时候了。在 dbt Cloud UI 中,点击部署菜单中的“作业”选项,然后点击“创建新作业”按钮。创建作业可以涵盖从简单概念到更复杂的概念。让我们设置一个涵盖我们讨论过的主要思想的作业:
- 给作业命名(参见图 4-57.)。

图 4-57. 定义作业名称
- 在环境部分,我们将指向部署环境。将 dbt 版本配置为继承在部署环境中定义的版本。然后将目标名称设置为默认。如果您希望根据工作环境定义条件(例如:如果在部署环境中,则执行此操作;如果在开发环境中,则执行那个操作),这将非常有帮助。最后,我们在“profiles.yml”中覆盖了 Threads。让我们将其保持为默认配置。我们没有创建任何环境变量,因此该部分将保持为空。图 4-58 展示了整体环境部分的配置。

图 4-58. 定义作业环境
- 图 4-59 显示了执行设置的全局配置。我们将运行超时设置为 0,因此如果作业运行时间超过一定时间,dbt 将不会终止作业。然后我们还选择了“不推迟到另一个运行”。最后,我们选中了“在运行时生成文档”和“运行源新鲜度”框。这个配置将减少您在 Commands 部分需要编写的命令数量。对于这个用例,我们仅保留了默认的
dbt build。

图 4-59. 定义作业设置
-
最后的配置设置是Triggers,在这里您可以配置如何启动作业。有三种选项可以触发作业:
-
在 dbt 中配置的计划
-
通过 Webhooks
-
通过 API 调用
-
对于这个用例,我们选择了Schedule选项,并将计划设置为每小时运行一次,如图 4-60 所示。

图 4-60. 定义作业触发器
现在是执行并查看结果的时候了。保存您的作业;然后选择立即运行或者等待作业按照配置的计划自动触发。
在作业运行时或运行结束后,您可以随时检查状态和执行内容。从部署菜单中,选择运行历史选项。您将看到作业的执行情况。选择其中一个并查看运行概览。图 4-61 是您应该看到的内容。

图 4-61. 作业运行概览屏幕
一旦进入运行概览,您将获得有关特定作业执行的相关信息,这对于潜在的故障排除问题可能会有帮助。顶部显示作业执行状态的摘要,触发作业的人或系统,与此作业执行相关的 Git 提交,生成的文档,源和作业运行的环境。
在作业摘要之后,您可以找到执行细节,如执行作业所需的时间以及开始和结束的时间。最后,运行概述提供给您的其中一个关键信息是运行步骤,它详细列出了作业执行期间执行的所有命令,并允许您检查每个独立步骤及其日志,如 图 4-62 所示。查看每个步骤的日志将帮助您了解每个步骤中运行的内容,并在执行过程中查找问题。

图 4-62. 作业的运行步骤详细信息
通过使用 dbt 作业,您可以轻松自动化您的转换并以高效和可扩展的方式将项目部署到生产环境中。无论您是数据分析师、数据工程师还是分析工程师,dbt 都可以帮助您解决数据转换的复杂性,并确保您的数据模型始终准确和更新。
摘要
本章表明,分析工程是一个不断受创新影响的不断发展的领域。dbt 不仅仅是这个故事的一个方面;它是该领域中的一个关键工具。
分析工程的主要目标是将原始数据转化为有价值的见解,而此工具在简化数据转换的复杂性和促进各种利益相关者之间的合作中发挥着至关重要的作用。dbt 确保数据转换不仅是技术变革,还非常重视开放性、包容性和知识共享。
dbt 以其无缝集成大型数据仓库的能力而闻名。它还通过确保最佳可追溯性和准确性,促进数据转换的协作方法。此外,它强调彻底测试数据流程的重要性,以保证可靠性。其用户友好的界面强化了分析工程是一个包容性领域的观念,欢迎各种能力水平的个人贡献。
总之,我们强烈建议希望保持行业前沿的分析工程师深入了解这个变革性工具。由于 dbt 的重要性日益增加且显而易见的好处,精通这个工具不仅可以提升您的技能,还可以促进未来更顺畅和更协作的数据转换。
第五章:dbt 高级主题
dbt 是 ELT 过程中专注于转换部分的工具。只需具备 SQL 经验,我们便能利用这一工具开发所有的分析代码。与此同时,我们还可以将其封装在一套通常在软件工程中找到的最佳实践和标准下,例如测试开发、自动部署,甚至是在开发过程中并行构建文档。
在本章中,我们的 dbt 之旅将更加深入和微妙。我们将探索 dbt 中各种类型的模型物化。除了传统的视图和表之外,我们还将探索瞬时模型的潜力,利用物化视图,精确捕获数据快照,甚至使用增量模型,这些都可以使您摆脱重复的、资源密集型的完整数据加载。
但这还不是全部。我们将通过 Jinja、宏和包将您的分析代码提升到新的高度。我们的使命是转变您的代码库,使其更高效、更 DRY。在本章结束时,您将掌握提升分析工作流的知识和工具,从而更快、更精准地提供洞察力。
模型物化
物化 是在数据平台上持久化 dbt 模型的策略。在 dbt 中,物化可以通过减少即时计算查询和视图的需求,来提升数据模型的性能和可扩展性。
在 dbt 中,可以根据项目的需求和要求使用各种类型的物化。例如,您可以使用增量物化来存储仅需要增量更新的查询结果。此外,您还可以使用快照,它们与 dbt 中的物化类似,但具有独特的特征。快照用于在特定时间点存储查询或视图的结果,但快照不是 dbt 中的模型。它们被特意设计为非幂等,这使它们与 dbt 的大多数其他方面有所区别。
我们已经在 第四章 中使用了诸如视图和表之类的物化策略。然而,了解所有可用的物化类型非常重要,从瞬时模型到增量数据加载或甚至物化视图,这样您就可以在优化分析代码的同时充分利用它们,并为公司的数据消费者提供准确及时的响应。
表、视图和瞬时模型
我们一直在使用视图或表的物化来实现我们的模型。本章旨在深入探讨这两种物化类型并介绍临时模型。但让我们首先看一下 图 5-1,它展示了之前已经构建的 dim_customers 的当前谱系。在这个用例中,我们将通过更改 stg_jaffle_shop_customers.sql 模型的物化类型来测试各种物化策略。

图 5-1. dim_customers 数据谱系
让我们从表的物化类型开始。在 dbt 中,表是用于存储和组织数据的结构,由行和列组成;每一行代表一个记录或数据片段,而每一列则代表数据的特定属性或字段。当您选择这种类型的物化时,内部上,您正在参数化 dbt 以在您的数据平台中物理创建引用模型,并将数据存储在磁盘上;因此,构建速度会比较慢。通常,这些物化在 marts 层下游使用,并且在处理大量数据和需要快速响应的多个查询到我们的模型时推荐使用。
要测试表的物化,让我们再次修改您的 YAML 文件 _jaffle_shop_models.yml,将 stg_jaffle_shop_customers 模型的物化设置为table。如果您运行代码,应该与 BigQuery 中的 图 5-2 类似。

图 5-2. stg_jaffle_shop_customers 作为表物化
视图 是通过选择和组合来自一个或多个上游模型的数据而创建的虚拟表。视图本身不存储任何数据,而是在访问时从底层表中检索数据。通常,我们会在 dbt 中使用视图来简化复杂查询并促进整体转换过程,或者通过隐藏特定列或行的数据来提供安全性。当我们将一个模型设置为视图时,它将作为视图在您的数据平台中构建。存储在磁盘上的是查询本身,因此只有在运行时才捕获数据并执行转换,这可能导致查询响应时间较慢。
要测试视图的使用情况,请修改您的 YAML 文件 _jaffle_shop_models.yml,将 stg_jaffle_shop_customers 模型的物化设置为view。然后再次运行代码。这与 BigQuery 中的 图 5-3 应该是类似的。

图 5-3. stg_jaffle_shop_customers 作为视图物化
最后,我们有临时模型。dbt 在运行时构建这些临时数据模型,不在数据库中持久化存储它们。最好用临时模型进行轻量级数据操作或分析任务,这些任务不需要数据永久存储。选择这种策略时,要记住 dbt 将其解释为下游模型中的 CTEs,这可能会增加那些模型的构建时间。此外,如果滥用临时模型,调试代码时可能会带来一些复杂性,因为在数据平台中无法直接查询它们,因为它们在那里不存在。
要测试临时模型,并且遵循之前的示例,修改你的 YAML 文件 _jaffle_shop_models.yml,将stg_jaffle_shop_customers模型的实现设置为ephemeral。由于在这种情况下,你的数据平台中不会有实际的实现,看一下 dim_product 的编译代码。Figure 5-4 显示了使用视图和临时模型作为 stg_jaffle_shop_customers 模型编译的代码之间的区别。

Figure 5-4. dim_customer 代码编译,使用视图作为 stg_jaffle_shop_customers 模型的情况(顶部)和使用临时模型的情况(底部)
增量模型
在一个 dbt 项目中,增量模型 旨在处理源中仅有的新数据或更改的数据,而不是所有的数据。这些模型可以用来提高数据流水线的效率和速度,特别是在处理频繁更新的大型数据集时。
要测试增量模型,首先需要配置模型的 YAML 文件,将所需的模型设置为增量。我们将使用已创建的模型,stg_jaffle_shop_orders,作为我们的测试案例。查看其 YAML 文件 _jaffle_shop_models,我们看到它被实现为view,如早期配置的那样。由于我们希望将其改为增量,改动是直接的,但我们还将其嵌入额外的能力,如incremental_type。因此,让我们使用 Example 5-1 的代码更新我们模型的 YAML 文件。
Example 5-1. 增量模型,YAML 文件配置
version: 2
models:
- name: stg_jaffle_shop_orders
config:
materialized: incremental
incremental_strategy: merge
unique_key: order_id
首先,我们将模型物化类型更改为 incremental。这是增量模型的核心和必选配置,使增量模型能够工作。与此同时,我们还包括了两个额外的配置项:incremental_strategy: merge 和 unique_key: order_id。这些配置帮助您优化和增强增量加载过程。增量策略被定义为 merge(在 dbt 中,您还有更多选项,如 append 或 insert_overwrite),每次增量运行将根据识别的唯一键将新行与现有行合并。在这种情况下,如果存在 order_id 的匹配项,则现有行将使用新信息更新。否则,如果没有匹配项,则创建新行。在标准的增量加载中,这两种情况同时发生。
最后一步是将模型代码排列,以使其与增量加载兼容。在 示例 5-2 中,您可以看到我们如何在我们的 stg_jaffle_shop_orders 模型中实现这一点。
示例 5-2. 增量模型,示例代码
select
id as order_id,
user_id as customer_id,
order_date,
status,
_etl_loaded_at
from {{ source('jaffle_shop', 'orders') }}
{% if is_incremental() %}
where _etl_loaded_at >= (select max(_etl_loaded_at) from {{ this }} )
{% endif %}
通过分析查询,我们利用 Jinja 制作我们的增量模型。直接进入 if 语句,我们看到了 is_incremental() 宏的使用。如果正在运行的模型配置为 materialized='incremental',则此宏将返回 true,而 dbt 模型已存在且不在完全刷新模式下运行。is_incremental() 返回 true,在 if 代码块内部,我们有基于时间戳列 _etl_loaded_at 进行行过滤的 where 条件。它将此时间戳与当前表 ({{ this }}) 的最大 _etl_loaded_at 时间戳进行比较,有效地检查行的加载时间戳是否大于或等于当前表中的最大加载时间戳。
增量模型在优化 dbt 项目中的数据管道中扮演着至关重要的角色。它们的一大优势是成本效率。通过采用增量模型,您可以显著减少处理数据所需的计算资源。这种效率不仅加快了数据转换过程,还节省了成本,因为您无需重做不必要的工作。
此外,增量模型确保您的 dbt 项目始终使用最新的数据运行。这种与数据源的同步方式增强了分析的可靠性和准确性。无论您处理的是流数据还是周期性更新,增量模型都可以使您的分析洞察与数据变化同步。
物化视图
物化视图 本质上是专门设计用来将查询结果作为物理化表存储的数据库对象。它们的动态性质使其与普通表格有所区别;物化视图中的数据定期刷新,以反映底层数据集中的最新变化。这种刷新过程确保物化视图保持最新状态,无需重新处理,这使其在对低延迟数据访问至关重要时成为理想选择。
有趣的是,物化视图与 dbt 的增量模型分享了一些共同点,这种相似性并非巧合。在许多方面,物化视图可以被视为增量模型的继任者,提供了数据优化的另一种方法。根据项目的要求和所选的数据平台,您甚至可以考虑用物化视图模型替换所有增量 dbt 模型。这种转变简化了您的工作流程,消除了手动增量策略的需要,详细说明 dbt 应如何更新基表——数据平台无缝处理这些任务。
然而,认识到这一过渡带来的权衡是至关重要的。虽然物化视图提供了效率,但可能会导致对增量逻辑和编排的精细控制减少。通过委托数据平台定义更新逻辑和执行,您获得了便利性,但可能会失去增量模型可以提供的某些具体控制。
测试 dbt 物化视图的方法可能因您所用的数据平台而异。如果您使用的是 Postgres、Redshift、Databricks 或 BigQuery(在 dbt 1.7 中),并且假设您想保持对 stg_jaffle_shop_customers 模型的测试。在 _jaffle_shop_models.yml 文件中,将物化设置为 materialized_view,如示例 5-3 所示。
示例 5-3. 物化视图,YAML 文件配置
models:
- name: stg_jaffle_shop_customers
config:
materialized: materialized_view
然而,如果您使用的是 Snowflake,概念稍有不同。Snowflake 没有物化视图的概念,而是有一个独特的概念:动态表。使用动态表的基本配置如示例 5-4 所示。
示例 5-4. 动态表,YAML 文件配置
models:
- name: stg_jaffle_shop_customers
config:
materialized: dynamic_table
总之,物化视图是数据优化的一个重要组成部分,提供了性能改进和数据实时性的好处。它们的作用与 dbt 中的增量模型相交,为希望简化工作流程的数据工程师提供了选择,同时考虑控制和定制化的权衡。
快照
快照是在特定时间点保存的数据集的副本。通常,当我们的分析需要查看不断更新的表中的先前数据状态时,我们使用这些快照。例如,您可以使用快照跟踪数据集的历史,从而了解其随时间如何演变。此外,快照对于测试和调试也很有帮助,因为它们使您可以将数据集的当前状态与先前状态进行比较,以识别任何变化或差异。
dbt 快照通过在可变源表上应用类型 2 的慢速变化维度(SCDs)来实现。这些 SCD 标识表中行随时间变化的方式。让我们来看一个例子。使用 jaffle_shop 数据库,假设您想要记录订单状态的转换,以便监控和检查交货时间,并识别特定状态下的潜在瓶颈。通过查看 stg_jaffle_shop_orders 模型,我们可以看到在 Figure 5-5 中,我们已经有了订单状态,但我们需要查看订单在达到当前状态之前经历的所有状态的可见性。

Figure 5-5. stg_jaffle_shop_orders 事务数据集
为了允许我们跟踪状态转换,我们首先需要保留快照,在 dbt 中,这些是在快照文件夹中的 .sql 文件中快照块内定义的 select 语句。因此,首先让我们在 snapshots 目录中创建一个名为 snap_order_status_transition.sql 的文件,并从 Example 5-5 中复制代码。
示例 5-5. snap_order_status_transition.sql 快照创建
{% snapshot orders_status_snapshot %}
{{
config(
target_schema='snapshots',
unique_key='id',
strategy='timestamp',
updated_at='_etl_loaded_at',
)
}}
select * from {{ source('jaffle_shop', 'orders') }}
{% endsnapshot %}
在执行代码之前,让我们先概述这些配置的含义:
target_schema
这是 dbt 应该将快照表渲染到的模式。换句话说,dbt 允许您将快照存储在数据平台中与实际生产环境分开的不同模式中。这使您可以灵活地将它们取出并备份到另一个位置。您还可以利用这个字段与 target_database 的补充一起使用,将这些快照不仅存储在不同的模式中,还存储在不同的数据库中。
unique_key
典型情况下,这是记录的主键列或表达式。它必须指向一个唯一的键。
strategy
这指示使用的快照策略,可以是 timestamp 或 check。在前面的示例中,我们使用了 timestamp 策略。这是推荐的策略,因为它对于新列的添加是可扩展的。有时候时间戳不可靠,在这种情况下,我们可以使用 check 策略来比较一组列的当前值和历史值。
updated_at
当使用 timestamp 策略时,我们需要声明数据集中需要查看的列。
check_cols
仅在使用 check 策略时使用,这是 dbt 将需要检查以生成快照的列。
现在我们理解了这些配置代表什么,让我们执行快照并查看其输出。为此,在 CLI 中运行 dbt snapshot。成功完成后,查看 BigQuery。已创建一个名为 snapshots 的新模式,并实现了实际的快照材料化,如 图 5-6 所示。

图 5-6. BigQuery 内的 snap_order_status_transition 快照表
正如您所见,dbt 在您的数据平台内创建了名为 orders_status_snapshot 的快照,生成了四个额外的列:
dbt_scd_id
dbt 的内部使用,为每个记录快照生成一个唯一键。
dbt_updated_at
也在 dbt 的内部使用,这个字段是在插入这个快照行时源记录的updated_at时间戳。
dbt_valid_from
这个快照行首次插入的时间戳。可用于对记录的不同“版本”进行排序。
dbt_valid_to
当此行变为无效时的时间戳。如果记录仍然是最新/有效的记录,则会显示 null。
注意
假设您希望继续探索快照的概念。在这种情况下,dbt 提供了详尽的文档,涵盖了我们在这里提到的内容以及额外的内容,如最佳实践和如何处理源系统中的硬删除。只需在 dbt Developer Hub 中搜索 快照。
使用 Jinja 的动态 SQL
Jinja 是一个广泛用于 Python 的模板语言,在 Web 开发中被广泛使用。它允许您通过使用变量和表达式创建动态的 HTML 页面,并轻松定制您网站的外观和行为。您还可以利用它来优化您的 SQL 代码,与 dbt 一起提升数据操作技能。
Jinja 的一个关键特性是能够将变量和表达式插入模板中,允许您为不同的用户或上下文创建定制模板,而无需将值硬编码到模板中。例如,您可能希望根据工作环境定义一些行为,比如在开发环境中限制数据量。对于这种情况,我们使用 dbt 中的 target name 属性,然后在我们的 SQL 代码中,通过利用 Jinja,我们可以定义处理它的规则,如 示例 5-6 所示。
示例 5-6. 带有 target name 属性的 Jinja 示例
select *
from {{ ref('fct_orders' )}}
-- limit the amount of data queried in dev
{% if target.name != 'prod' %}
where order_date > DATE_SUB(CURRENT_DATE(), INTERVAL 3 MONTH)
{% endif %}
请注意,我们正在使用 BigQuery 和 BigQuery 语法。如果您使用不同的数据平台,某些函数和语法可能会有所不同。现在,看看前面的代码,我们可以看到已经使用了一些 Jinja 标记:
{% … %}
用于语句,执行任何功能编程,如设置变量或开始for循环。在这个特定的例子中,我们使用了一个if语句,检查属性名是否与prod字段不同(!=)。
Jinja 还提供了一系列控制结构,如循环和条件语句,允许您创建更复杂的模板,以适应不同的数据和上下文。您可以使用循环来迭代项目列表,并动态生成 SQL 代码,而不是手动逐字段执行。
您可以使用您之前在示例 4-16 中创建的int_payment_type_amount_per_order.sql模型来演示一个理想的例子。与手动编写每种类型的金额度量相比,您可以自动生成它们,并使其可扩展,处理当前和未来的支付类型。看看示例 5-7,看看我们如何利用 Jinja 来做到这一点。
示例 5-7. 使用动态 Jinja 的*int_payment_type_amount_per_order 模型
{# declaration of payment_type variable. Add here if a new one appears #}
{%- set payment_types= ['cash','credit'] -%}
with
payments as (
select * from {{ ref('stg_stripe_order_payments') }}
),
pivot_and_aggregate_payments_to_order_grain as (
select
order_id,
{% for payment_type in payment_types -%}
sum(
case
when payment_type = '{{ payment_type }}' and
status = 'success'
then amount
else 0
end
) as {{ payment_type }}_amount,
{%- endfor %}
sum(case when status = 'success' then amount end) as total_amount
from payments
group by 1
)
select * from pivot_and_aggregate_payments_to_order_grain
前面的代码是 Jinja 的更复杂用法,包括循环和变量声明,但一旦编译完成,它看起来与示例 5-6 中的代码结果非常相似。现在,如果我们想考虑一个新的支付类型,而不是手动创建一个新的度量标准,我们需要将其添加到代码顶部声明的payment_types列表中。
让我们讨论我们可以在示例 5-7 中找到的 Jinja 细微差别:
{% … %}
总结一下,这用于语句。在这种情况下,我们在两个不同的地方使用了它,不同于示例 5-6:
set payment_types= ['cash','credit']
这声明了后面代码中将要使用的payment_types变量。在这种情况下,它是一个包含两个元素(现金和信用卡)的列表。
for payment_type in payment_types
在这里,我们迭代在代码顶部声明的不同payment_types。逐行,我们将开始动态构建我们的代码。
{{ … }}
这用于表达式以打印到模板输出。在我们的示例中,我们使用它为{{ payment_type }},即与amount字符串连接以生成每种支付类型的最终度量名称。此外,我们还在表达式上使用了实际的度量计算:when payment_type = '{{ payment_type }}' and status = 'success'。
{# … #}
用于注释,这使您可以内联文档化您的代码。
空格
这是代码中另一个小但重要的细节。您可以通过在 Jinja 分隔符的两侧使用连字符{%- … -%}来控制它们,这将修剪该表达式的两侧之间的空白。
提示
我们建议探索专门的课程或参考官方Jinja 模板设计文档,以全面了解 Jinja。这些资源可以提供有价值的见解,并帮助你深入了解 Jinja 的功能。
使用 SQL 宏
宏是在 dbt 项目中用于自动化任务和流程的可重用代码片段。它们通过允许你自动化重复或复杂的任务,如查询、数据操作和数据可视化,提高了生产效率。开发完成宏后,你可以通过手动、自动或响应用户输入的方式调用和触发它们。
在 dbt 项目中,宏通常在单独的文件中定义,放在macros目录中,并使用 Jinja 语法编写。将宏与模型分开允许在项目中的多个模型和其他文件中使用它们。它还允许使用变量和表达式定制宏,使宏能够根据来自特定模型的参数进行调整。
要在 dbt 项目中使用宏,通常你会使用 Jinja 语法调用宏并传递任何必要的参数或选项。宏还可以与其他 dbt 功能(如视图)和其他宏交互,以创建更复杂和健壮的解决方案。例如,你可以使用宏自动刷新数据模型、以特定方式过滤或转换数据,或基于数据生成报告或图表。
让我们尝试创建我们的第一个宏。我们最初的用例很简单:创建一个可以对两个数字求和的宏。记住,你的宏需要使用 Jinja 语法。首先,在你的 dbt 项目中的macros目录中创建一个名为macro_sum_two_values.sql的宏文件。示例 5-8 展示了你的代码应该是什么样子的。
示例 5-8. macro_sum_two_values.sql
{% macro sum(x, y) -%}
select {{ x + y }}
{%- endmacro %}
现在我们来测试它。然后,在你的 dbt 项目中的新文件中,你可以通过调用宏并传入所需的x和y值来使用宏,如示例 5-9 所示。
示例 5-9. 在 macro_sum_two_values.sql 内触发宏
{{ sum(13, 89) }}
示例 5-9 将在触发宏时将宏的结果(102)显示在输出窗口中。你也可以将变量或表达式作为参数传递给宏,而不是硬编码的值。看看示例 5-10,它必须产生与前一个示例相同的输出。
示例 5-10. 在 macro_sum_two_values.sql 内触发宏,并在顶部定义变量
{% set x = 13 %}
{% set y = 89 %}
{{ sum(x, y) }}
在 dbt 项目中使用 Jinja 的宏允许您灵活和强大地重复使用代码并自定义您的模型。但现在,让我们举个例子来使用宏。使用jaffle_shop数据库,我们想要处理的第一个用例是一个宏,用于集中支付类型配置,以避免在每个模型中重新定义它,就像我们在int_payment_type_amount_per_order.sql的新版本中所做的那样。为此,在macros目录中,创建一个名为get_payment_types.sql的新文件,并复制示例 5-11 的代码。
示例 5-11. get_payment_types.sql 宏
{% macro get_payment_types() %}
{{ return(["cash", "credit"]) }}
{% endmacro %}
然后,在您的int_payment_type_amount_per_order.sql模型中,用来自示例 5-12 的代码替换顶部声明的payment_types变量。
示例 5-12. int_payment_type_amount_per_order.sql 中的*payment_types*变量声明调用了*get_payment_types()*宏。
{%- set payment_types= get_payment_types() -%}
现在您可以为其他用例使用您的宏,但请考虑以下内容:
-
通常,宏带有参数,因此尽管示例 5-11 是一个宏,但它并不代表您将构建的典型宏。参数指调用或执行宏时传递的值。这些参数可用于修改宏的行为,例如指定输入数据源、定义自定义配置设置或设置某些参数或标志。
-
在示例 5-11 中,我们使用了
return函数来返回一个列表——没有这个函数,宏将返回一个字符串。
查看您的操作,示例 5-11 中的宏似乎并不是非常强大或静态。我们如何优化它以避免依赖手动输入?我们可以采取以下措施来解决这些问题:
-
理解源数据,我们可以动态提取支付类型。
-
重新思考具有模块化思维方式的宏。
我们可以使用从示例 5-13 获取的以下查询来解决第一个问题。
示例 5-13. 获取不同支付类型的查询
select
distinct payment_type
from {{ ref('stg_stripe_order_payments') }}
order by 1
正如您所见,如果运行查询,它具有不同的付款类型,因此为了从中制作宏,请将示例 5-14 中的代码复制到您的get_payment_types.sql文件中。
示例 5-14. 使*get_payment_types*更具动态性和可扩展性的新版本
{% macro get_payment_types() %}
{% set payment_type_query %}
select
distinct payment_type
from {{ ref('stg_stripe_order_payments') }}
order by 1
{% endset %}
{% set results = run_query(payment_type_query) %}
{% if execute %}
{# Return the first column #}
{% set results_list = results.columns[0].values() %}
{% else %}
{% set results_list = [] %}
{% endif %}
{{ return(results_list) }}
{% endmacro %}
让我们看看我们做了什么:
-
在顶部,我们声明了查询
payment_type_query。 -
就在那之后,我们使用
run_query函数执行并将输出存储在results变量中。 -
然后,我们检查了 Jinja 是否处于
execute模式——这意味着正在执行 SQL——如果是这样,我们将数据集的第一列结果存储在results_list中。这个第一列将具有不同的值。 -
最后,我们返回
results_list变量以在我们的模型中使用。
现在,如果我们再次编译 int_payment_type_amount_per_order.sql 模型,不应有任何变化。然而,您已实现了更可扩展的代码。此时,不需要手动输入一旦新的支付类型出现。但是,我们可以通过模块化做得更多。想象一下,您希望在 dbt 项目的其他地方使用类似的模式(例如支付方法)。在那种情况下,我们可以做一些类似 Example 5-15 的事情。
Example 5-15. 为不同情景重复使用我们的代码
{# Generic macro to give a column name and table, outputs
the distinct fields of the given column name #}
{% macro get_column_values(column_name, table_name) %}
{% set relation_query %}
select distinct
{{ column_name }}
from {{ table_name }}
order by 1
{% endset %}
{% set results = run_query(relation_query) %}
{% if execute %}
{# Return the first column #}
{% set results_list = results.columns[0].values() %}
{% else %}
{% set results_list = [] %}
{% endif %}
{{ return(results_list) }}
{% endmacro %}
{# Macro to get the distinct payment_types #}
{% macro get_payment_types() %}
{{ return(get_column_values('payment_type', ref('stg_stripe_order_payments'))) }}
{% endmacro %}
{# Macro to get the distinct payment_methods #}
{% macro get_payment_methods() %}
{{ return(get_column_values('payment_method', ref('stg_stripe_order_payments'))) }}
{% endmacro %}
通过分析这段代码,我们可以看到三个宏。第一个 get_column_values() 接收 column_name 和 table_name 作为参数,并动态生成一个执行查询的查询语句,返回提供的 column_name 的不同值。接下来,我们分别调用该宏两次,使用 get_payment_types() 宏检索不同的 payment_types,并使用 get_payment_methods() 宏检索不同的 payment_methods。还请注意,宏文件名已更改为 get_distinct_by_column.sql,以使其更加透明,考虑到其目的。
上述示例展示了如何使用宏的有趣演示,但它们在许多其他情况下也很有用。另一个很好的例子是有一个宏,动态验证我们是否处于开发或部署环境中,然后自动过滤我们的数据集。为此,在 macros 目录中,创建一个名为 limit_dataset_if_not_deploy_env.sql 的新宏,并复制来自 Example 5-16 的代码。
Example 5-16. *limit_dataset_if_not_deploy_env* 宏
{# Macro that considering the target name,
limits the amount of data queried for the nbr_months_of_data defined #}
{% macro limit_dataset_if_not_deploy_env(column_name, nbr_months_of_data) %}
-- limit the amount of data queried if not in the deploy environment.
{% if target.name != 'deploy' %}
where {{ column_name }} > DATE_SUB(CURRENT_DATE(), INTERVAL {{ nbr_months_of_data }}
MONTH)
{% endif %}
{% endmacro %}
然后,在 fct_orders 模型中,在左连接之后的底部包含来自 Example 5-17 的代码。
Example 5-17. 从 *fct_orders* 调用 *limit_dataset_if_not_deploy_env* 宏
with orders as (
select * from {{ ref('stg_jaffle_shop_orders' )}}
),
payment_type_orders as (
select * from {{ ref('int_payment_type_amount_per_order' )}}
)
select
ord.order_id,
ord.customer_id,
ord.order_date,
pto.cash_amount,
pto.credit_amount,
pto.total_amount,
case
when status = 'completed'
then 1
else 0
end as is_order_completed
from orders as ord
left join payment_type_orders as pto ON ord.order_id = pto.order_id
-- Add macro here
{{- limit_dataset_if_not_deploy_env('order_date', 3) }}
现在编译代码。如果您处于开发环境,则您的 fct_orders.sql 模型应显示一个新的过滤器,where order_date > DATE_SUB(CURRENT_DATE(), INTERVAL 3 MONTH)。换句话说,在开发环境中,该过滤器仅允许您处理过去三个月的数据;否则,收集整个数据集。在开发环境中仅查看 N 个月的数据将大幅减少数据平台的开销,同时,您仍然可以使用一个良好的数据子集来开发和测试代码。如果不是这种情况,您可以增加为 12、24 或甚至 36 个月。
最后,重要的是提到 dbt 的适应性,能够定制其核心宏的能力。这些宏提供了一些 dbt 的核心功能,为常见任务提供了预定义模板。一个突出的例子是 generate_schema_name 宏。该宏负责为您的 dbt 模型制定模式名称。真正了不起的是,您可以调整它,使其与项目独特的命名约定无缝对接。想象一下,轻松生成与您组织的数据结构相匹配的模式名称。
定制这些核心宏不仅仅是技术上的壮举。它是改变游戏规则的方法,使您能够发挥 dbt 的能力,释放出根据项目需求精确制定数据转换流程的潜力。
总之,在 dbt 项目中使用宏可以是自动化和定制数据模型和流程的强大高效方式。宏使您可以轻松重用代码,并使您的模型适应不同的上下文和需求。使用 Jinja 语法,您可以创建灵活且易于维护的宏,可以以各种方式调用和触发。总体而言,宏可以帮助您提高生产力,创建更强大和可扩展的数据模型。
dbt 包
包是组织和共享代码和资源的一种方式,例如已编写的模型和宏,在 dbt 项目中。它们允许您将项目结构化为逻辑单元,并在多个模型和文件中重复使用代码和资源。
在 dbt 项目中,包定义在 packages.yml 文件中,安装在 dbt_packages 目录中,并使用目录和文件的层次结构进行组织。每个包可以包含与特定主题或功能相关的模型、测试、宏和其他资源。
在 dbt 项目中,包可以用多种方式使用。例如,您可以使用包来执行以下操作:
将项目组织成逻辑单元
包可以帮助您以直观且易于理解的方式结构化项目,将相关模型、测试和其他资源组合在一起。
重复使用代码和资源
包允许您在多个模型和文件之间重复使用代码和资源,节省时间并减少维护开销。
封装功能
包可以帮助您封装特定功能,并隐藏项目其他部分的实现细节,使您的项目更模块化且易于理解。
与他人分享代码和资源
包可以与其他用户或项目共享,这使您可以利用他人的工作,并通过贡献自己的代码成为社区的一部分。
总体而言,包是 dbt 的一个有价值的功能,可以帮助你在项目中组织、重用和共享代码和资源。你可以从三个不同的地方安装 dbt 包:公共包集、Git 或本地目录。在本节中,我们将介绍如何安装包,并展示它们的使用示例。我们将使用一个最常见的包 dbt_utils,但请注意,还有许多优秀的包可以使用。你可以在 dbt Hub 找到大量的包或直接从 GitHub 导入它们。
安装包
安装一个包在一个 dbt 项目中是一个直接的过程,可以帮助你利用他人的工作并为你的项目添加新功能。我们之前已经概述了,但让我们进一步讨论安装包。
安装包的逐步指南如下:
-
如果你还没有创建 packages.yml 文件,请创建一个。这个文件是你将要在你的 dbt 项目中配置需要安装的 dbt 包的地方。
-
将包添加到你的 packages.yml 文件中:在你的 dbt 项目中打开文件,并为你想要安装的包添加一个条目。请注意,在安装包之前,确保你的 dbt 项目符合包可能具有的任何要求或依赖关系是非常重要的。这些可能包括特定版本的 dbt 或其他包,以及系统或软件要求。
-
在终端中运行
dbt deps命令来安装包及其依赖项到你的 dbt 项目中。 -
测试包以确保它正常工作。你可以运行
dbt test命令并验证包的模型和测试是否通过。
让我们尝试安装一个最常见的可用包:dbt_utils,你可以在 dbt Hub 找到它。通常,使用公共包集中将为你提供必须放在你的 packages.yml 文件中特定 dbt 包的所有配置,从而实现更顺利的安装。因此,要安装 dbt_utils,请将配置从 示例 5-18 复制到你的 packages.yml 文件中。
示例 5-18. *dbt_utils* 包配置
packages:
- package: dbt-labs/dbt_utils
version: 1.1.1
保存你的 YAML 文件,并在你的 CLI 中运行 dbt deps。如果你收到像 图 5-7 中显示的成功消息,一切应该没问题。稍后在本章中,我们将使用 dbt_utils 来查看一切是否运行如预期。

图 5-7. *dbt_utils* 安装后日志中的成功消息
注意
如果您在使用 dbt 版本时遇到包不兼容问题,请确保您正在运行与您想要使用的包兼容的 dbt 版本。查看包的文档或存储库,了解支持的 dbt 版本信息。您也可以更新包版本以与您的 dbt 版本兼容。最后,您还可以选择其他提供类似功能并与您的 dbt 版本兼容的包作为解决方案。
探索 dbt_utils 包
首先,让我们介绍一下我们将用于示例的包:dbt_utils。这个包由 dbt 的创建者 dbt Labs 开发和维护。它包含一系列实用函数、宏和其他资源,可以扩展和增强 dbt 项目的功能。
这里有一些包含在 dbt_utils 包中的资源类型示例:
-
用于执行常见任务的辅助函数,如生成列列表、格式化日期和时间戳以及处理空值
-
自定义数据类型用于更具表现力和灵活性地表示数据,例如数组、范围和间隔
-
用于您的 dbt 项目的调试和测试工具,如日志记录函数和测试框架
-
用于执行各种任务的宏和模型,如数据操作、可视化和测试
总之,dbt_utils 是一个有助于 dbt 用户通过各种方式扩展和定制其项目的有用包。它正在不断更新和扩展,以包含新功能和资源。
在宏和模型内部使用包
在 dbt 项目中,您可以在宏中使用包来访问其他宏、模型、测试和包中定义的其他资源。这使您可以在多个模型和文件中重用代码和资源,模块化您的项目,并因此编写更干净的代码(DRY)。
一旦安装了包,在我们“安装包”中概述的步骤后,您可以通过在包名称前加上前缀并按照特定语法使用其宏来访问其宏,如示例 5-19 所示。
示例 5-19. 示例宏调用
{{ package.my_macro() }}
使用 dbt_utils,我们可以在数据库中生成一系列数字或日期,这对于各种用例非常有用。但让我们看一个实际的例子。让我们尝试一下 date_spine() 宏。复制来自示例 5-20 的代码并执行它。
示例 5-20. *date_spine* 宏位于 *dbt_utils* 内部。
{{ dbt_utils.date_spine(
datepart="day",
start_date="cast('2023-01-01' as date)",
end_date="cast('2023-02-01' as date)"
)
}}
期望输出是从 2023 年 1 月 1 日到 2023 年 2 月 1 日之间的日期列表,但不包括 2 月 1 日。date_spine 宏是一个有效且灵活的函数,可以帮助您处理日期、生成日期序列或执行涉及日期的其他任务,例如在分析模型中创建 dim_date 维度。
另一个用例是在您开发的模型中直接使用已安装的包。例如,假设您想计算 cash_amount 在特定订单中所占的百分比,但需要确保在 total_amount 为 0 的订单中,您的代码不会因除以零而报错。您当然可以自行实现这个逻辑,但 dbt_utils 已经有一个内置函数可以处理此问题。让我们来看看在 Example 5-21 中的代码。
Example 5-21. *safe_divide* 宏在 *dbt_utils* 内部
select
order_id,
customer_id,
cash_amount,
total_amount,
{{ dbt_utils.safe_divide('cash_amount', 'total_amount') }}
from {{ ref('fct_orders') }}
此代码使用 safe_divide 宏将分子 cash_amount 除以分母 total_amount,并将结果存储在名为 result 的变量中。如果分母为 0 或 null,则 safe_divide 宏将返回 null 而不是引发错误。
safe_divide 宏非常适合在 dbt 项目中执行除法操作,特别是在处理可能包含 null 或 0 值的数据时。它可以通过消除手动检查 null 或 0 值的需要,节省时间并减少维护开销。
dbt 包是一种多功能工具,可帮助您构建更好、更高效的数据转换管道。在本章中,我们介绍了 dbt_utils,它提供了一组有用的宏和函数,简化了常见的数据建模任务,是您工具包的宝贵补充。另一个有趣的包是 dbt_expectations,它使您能够定义、记录和测试数据期望,确保数据质量和可靠性。此外,dbt_date 简化了数据模型中与日期相关的计算和操作。通过利用这些包和其他工具,您可以简化代码共享和协作,减少重复劳动,并创建更可扩展和可维护的数据模型。
dbt 语义层
在数据分析中,语义层 发挥着关键作用,充当原始数据与有意义洞察之间的桥梁。这种逻辑抽象是一个决策性的翻译器,简化复杂的数据结构,并促进组织内对数据的共同理解。这样做将复杂的数据库设置转化为用户友好的语言,赋予数据分析师到业务领导层等广泛观众轻松访问和理解数据的能力。除了简化外,语义层还提供数据的完整性和可靠性,确保数据可理解和可信任。
dbt 语义层的精髓在于,它从根本上区别于传统的语义层。在许多语义层中,用户通过明确指定左右连接键来描述数据中的连接关系。然而,dbt 语义层规范采用了一种独特的方法,引入了实体的概念。这些实体使我们能够在该层内自动推断数据连接,即图中的边。例如,考虑一个顾客表,其主键为customer_id,以及一个带有customer_id实体作为外键的订单表——这可以形成一个关系,或者更准确地说,是我们数据图中的一条边。这种创新显著减少了手动逻辑维护的需求,因为一个图通常具有比节点更少的边。
这种方法的美妙之处在于其简洁性和效率。它以极其 DRY 的方式封装语义逻辑,促进更广泛的度量和维度组合,以及更清晰的 SQL。这些优势使得数据团队更容易监督、演化和利用他们的数据模型。
在 dbt 语义层的核心是两个基本组件:语义模型和度量。语义模型是构建基础,包括三个关键元素:实体、维度和度量,用于创建度量标准。这些组件赋予了 MetricFlow 框架,即支持我们语义层的框架,构建查询以定义度量标准的能力。
度量则是我们用来衡量和分析数据的工具。它们位于语义模型之上,能够基于可重用组件创建复杂和精细的定义。
正如先前提到的,语义层依赖于三个基本概念来创建度量标准:实体、维度和度量。
实体指的是特定上下文中独立且可识别的对象。在数据库的术语中,实体通常对应于表,作为我们数据收集工作的核心主体。实体代表业务中的现实概念,例如顾客或订单。在我们的语义模型中,实体使用 ID 列表示,这些列作为连接键与语义图中的其他语义模型连接。
实体在帮助语义引擎理解表或数据集之间关系中至关重要。这使引擎能够理解数据如何相互连接,确保在查询特定实体相关信息时,引擎知道从哪里检索相关信息。
另一方面,维度通过作为分类属性来为度量提供上下文,允许在分析过程中以不同方式对数据进行分解。维度通常描述模型中其他元素相关的特征。
维度被配置为增强用户从不同角度探索和分析数据的能力。语义引擎利用这些维度根据用户的偏好定制查询。
最后,度量 是分析的主要关注点的可量化数据点,代表我们打算检查的度量。度量通常会被聚合,而在许多情况下,BI 工具的一个基本作用就是跨多个维度聚合这些度量。度量的定义确保计算在所有查询和报告中保持一致,消除任何语义模糊。
让我们来说明如何构建一个 dbt 语义层。我们将继续使用客户和订单实体的示例。我们希望衡量支付的总金额(total_amount),并分别列出现金支付金额(cash_amount)和信用支付金额(credit_amount)。最后,我们还希望知道订单总数(order_count)以及有订单的客户数量(customers_with_orders)。我们还希望了解每天的滑动能力(order_date)以及订单是否完成(is_order_completed)。
考虑到这些要求,完整的语义模型显示在 示例 5-22 中。您可以将其添加到先前创建的相应 YAML 文件 _core_models.yml 中,或者为语义模型创建一个新文件。
示例 5-22. 用于语义模型的 YAML 文件配置
semantic_models:
- name: orders
description: |
Order fact table. This table is at the order grain with one row per order.
model: ref('fct_orders')
entities:
- name: order_id
type: primary
- name: customer
type: foreign
expr: customer_id
dimensions:
- name: order_date
type: time
type_params:
time_granularity: day
- name: is_order_completed
type: categorical
measures:
- name: total_amount
description: Total amount paid by the customer with successful payment
status.
agg: sum
- name: order_count
expr: 1
agg: sum
- name: cash_amount
description: Total amount paid in cash by the customer with successful
payment status.
agg: sum
- name: credit_amount
description: Total amount paid in credit by the customer with successful
payment status.
agg: sum
- name: customers_with_orders
description: Distinct count of customers placing orders
agg: count_distinct
expr: customer_id
转向最后阶段,所有先前涵盖的组件都需要语义引擎的参与来使它们操作化。这个引擎在解释提供的数据并根据这些定义构建分析查询方面发挥了基础作用。例如,即使在详细指定客户订单的所有方面之后,我们仍然依赖于一个引擎来解析语义模型并生成计算所需度量的查询。在 dbt 的领域内,MetricFlow 实现了这一功能。
语义引擎的概念类似于 dbt 文档引擎。当您为一个模型创建一个 YAML 文件时,它本身是惰性的,缺乏显著的功能。然而,dbt 文档引擎将这些数据转化为实用工具,包括文档网站、dbt 测试、警报系统、数据合同等等。类似地,MetricFlow 作为 dbt 语义引擎运作,利用其解释语义数据和生成有价值结果的能力,特别是标准化和可重用的分析查询。
要使用 MetricFlow 生成分析查询,初始步骤包括基于您精心构建的语义模型建立度量。您可以在与语义模型相同的 YAML 文件中定义度量,也可以创建一个新文件。
为了说明指标创建的过程,让我们首先澄清我们打算开发的具体指标。为了保持简单同时保持趣味性,包括计算订单总金额 (order_total) 的指标是值得的。此外,我们可以创建另一个跟踪下订单数量 (order_count) 的指标。最后,我们将探索一个基于下订单数量的指标,过滤指标本身,显示完成的订单占比。示例 5-23 提供了一个 YAML 文件,演示了这些指标的正确配置。
示例 5-23. 指标 YAML 文件配置
metrics:
- name: order_total
description: Sum of total order amount.
type: simple
label: Order Total
type_params:
measure: total_amount
- name: order_count
description: Count of orders.
label: Orders
type: simple
type_params:
measure: order_count
- name: completed_orders
description: Count of orders that were delivered
label: Delivered Orders
type: simple
type_params:
measure: order_count
filter: |
{{ Dimension('order_id__is_order_completed') }} = true
举个例子,要让 MetricFlow 在 order_total 上工作,使用 CLI 命令 mf query --metric order_total。MetricFlow 将根据这个定义以及度量的定义(在语义模型中概述)生成查询,详见 示例 5-24。
示例 5-24. 订单总额查询
SELECT SUM(total_amount) as order_total
FROM fct_orders
注意
虽然本章旨在展示 dbt 内的语义层的工作原理,但请注意,在 mf query 方面,组织范围内可能有更好的选择。为了在整个组织中更广泛且更强大地使用,考虑使用 dbt 提供的 API。此外,我们建议参考 “设置 dbt 语义层” 页面 获取语义层和 MetricFlow 的最新安装指南,因为它定期更新以反映最新信息和发展。
在建立了 dbt 语义层之后,您有效地创建了一个数据的抽象层。无论对订单数据集进行了任何修改,任何想要获取订单总金额的人都可以轻松访问 order_total 指标。这使用户能够根据他们的具体需求分析订单数据。
概要
在本章中,我们深入探讨了 dbt 世界中的高级主题,扩展了我们对这个变革性工具的理解。我们探讨了 dbt 模型和材料化的威力,揭示了它们如何使我们能够管理复杂的数据转换,同时确保高效的性能优化。使用 Jinja 动态 SQL 允许我们创建动态和可重用的查询,适应不断变化的需求,从而增强了我们数据处理的灵活性。
超越基础知识,我们介绍了 SQL 宏,解锁了我们代码库中自动化和可重用性的新水平。通过深入的示例,我们看到了 SQL 宏如何极大地简化我们的代码并为数据转换带来一致性。
此外,dbt 包的概念作为我们数据生态系统中协作和知识共享的基石出现。我们讨论了 dbt 包如何允许我们封装逻辑、最佳实践和可重复使用的代码,促进协作文化并加速开发周期。
最后,我们展示了 dbt 语义层如何通过为数据提供抽象层来增强您的分析解决方案。这一层确保了所有报告和分析的一致性和准确性,因为业务逻辑在语义层中是集中和验证的,从而最小化了差异或错误的风险。此外,随着数据库的扩展或经历修改,拥有语义层使您可以在单一位置进行调整,无需单独更新多个报告或查询。
在结束本章时,我们已经深入探讨了各种高级 dbt 主题,装备了优化数据流程所需的知识和工具。这些高级概念提升了我们的数据转换能力,并使我们能够将数据分析提升到前所未有的高度。凭借这些见解,我们在处理数据挑战的复杂性同时促进了数据驱动的创新。
然而,需要注意的是,尽管本指南全面介绍了 dbt,但 dbt 的宇宙是广阔且不断发展的。还有一些值得探索的附加主题,例如高级部署技术,比如蓝/绿、金丝雀或阴影部署。此外,深入研究 Write-Audit-Process(WAP)模式的使用可以为团队提供对数据质量和可追溯性更大的控制。同时,探索 dbt 如何与数据生态系统中的其他工具交互将非常有价值,以及如何与多项目组织合作。确实,dbt 是一个充满活力和广阔的世界;在这个激动人心的数据旅程中,总是有更多可以学习和发现的内容。
第六章:构建端到端的分析工程用例
欢迎来到我们关于使用 dbt 和 SQL 进行分析工程的书的最后一章。在前面的章节中,我们涵盖了将原始数据转化为可操作见解的各种概念、技术和最佳实践。现在是时候把这些主题整合起来,开始一个实用的旅程,构建一个端到端的分析工程用例。
在本章中,我们将探讨从头开始设计、实施和部署全面的分析解决方案。我们将充分利用 dbt 和 SQL 的潜力,构建一个强大和可扩展的分析基础设施,并且在操作和分析目的上使用数据建模。
我们的主要目标是展示本书涵盖的原则和方法如何实际应用于解决现实世界的数据问题。通过结合在前几章中获得的知识,我们将构建一个涵盖数据生命周期所有阶段的分析引擎,从数据摄入和转换到建模和报告。在整章中,我们将解决实施过程中常见的挑战,并提供如何有效克服这些挑战的指导。
问题定义:全渠道分析案例
在这个挑战中,我们的目标是通过在多个渠道上提供无缝和个性化的互动来增强客户体验。为了实现这一目标,我们需要一个全面的数据集,捕捉有价值的客户洞察。我们需要客户信息,包括姓名、电子邮件地址和电话号码,以构建一个强大的客户档案。跟踪客户在网站、移动应用和客户支持等渠道上的互动至关重要,以了解他们的偏好和需求。
我们还需要收集订单详细信息,包括订单日期、总金额和付款方式,以分析订单模式,并识别跨销售或交叉销售的机会。此外,包括产品信息如产品名称、类别和价格,将使我们能够有效地定制我们的营销活动和促销活动。通过分析这些数据集,我们可以发现有价值的见解,优化我们的全渠道策略,增强客户满意度,并推动业务增长。
操作数据建模
在我们追求整体方法的过程中,我们从操作步骤开始我们的旅程。通过这种方式,我们旨在为后续步骤打下坚实的基础。我们的方法涉及使用精心记录的管理数据库需求,这将指导我们。根据行业最佳实践,我们将认真遵循三个关键步骤——概念建模、逻辑建模和物理建模——来精心构建我们的数据库。
请记住,我们选择了广度优先策略,涵盖了所有步骤,但在细节方面并不深入。因此,请将其视为一种学术练习,具有简化的要求,旨在让您更好地理解构建操作数据库过程,而不是全面的过程。
概念模型
正如我们之前描述的,第一步,即概念建模阶段,使我们能够对数据库内部结构及其关系进行概念化和定义。这包括识别关键实体、它们的属性及其关联。通过仔细分析和与利益相关者的合作,我们将捕捉管理系统的本质,并将其转化为简明和全面的概念模型(图 6-1)。

图 6-1. 我们操作数据库的概念图
在图 6-1 的概念模型中,我们可以观察到三个实体:客户、渠道和产品,以及两个关键关系:购买和访问。第一个关系使我们能够追踪客户在某些渠道购买特定产品的情况。(请注意,我们需要渠道来了解其跨渠道的表现。)第二个关系允许我们跨渠道跟踪互动。对于每个实体和关系,我们定义了一些属性,使其成为一个更丰富的数据库。
逻辑模型
正如我们之前提到的,为了将概念性 ERD 练习转换为逻辑模式,我们创建了实体、属性及其关系的结构化表示。此模式充当在特定系统中实施数据库的基础。我们将实体转换为表,其属性成为表列。根据关系类型不同处理关系:对于 N:1 关系,我们使用外键连接表,对于 M:N 关系,我们创建一个单独的表来表示连接。通过这些步骤,我们确保数据完整性和高效的数据库管理,几乎在隐式地规范化我们的概念模型。
如果我们将上述规则应用于我们的概念,我们应该能够得出类似于图 6-2 的结果。

图 6-2. 我们操作数据库的逻辑模式
如您所见,现在我们有五个表:其中三个代表主要实体(客户、产品和渠道),而剩下的两个表则代表关系。但是为了简化起见,我们将这两个关系表从购买和访问改名为购买历史和访问历史。
物理模型
逻辑模型主要处理数据库的概念表示,而物理模型则深入探讨数据管理的实际方面,假设我们选择了特定的数据库引擎。在我们的情况下,将是 MySQL。因此,我们需要按照 MySQL 的最佳实践和限制将逻辑模型转换为具体的存储配置。
图 6-3 显示了我们的 ERD 图表,表示 MySQL 的数据类型和约束。

图 6-3. 我们在 MySQL 中操作数据库的物理图表。
现在我们可以将之前的模型翻译为一组 DDL 脚本,首先创建一个新的 MySQL 数据库以存储我们的表结构(示例 6-1)。
示例 6-1. 创建主表
CREATE DATABASE IF NOT EXISTS OMNI_MANAGEMENT;
USE OMNI_MANAGEMENT;
在 示例 6-2 中,我们现在处理创建三个主要表 customers、products 和 channels 的 DDL 代码。
示例 6-2. 创建我们的操作数据库
CREATE TABLE IF NOT EXISTS customers (
customer_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(150),
date_birth DATE,
email_address VARCHAR(150),
phone_number VARCHAR(30),
country VARCHAR(100),
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UPDATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS products (
product_sku INTEGER PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(150),
unit_price DOUBLE,
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UPDATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS channels (
channel_id INTEGER PRIMARY KEY AUTO_INCREMENT,
channel_name VARCHAR(150),
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UPDATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
customers 表格包括诸如 customer_id、name、date_birth、email_address、phone_number 和 country 的列。customer_id 列充当主键,唯一标识每个客户。它被设置为每添加一个新客户时自动递增其值。其他列存储客户的相关信息。
products 和 channels 表格采用类似的方法。然而,products 包括列如 product_sku、product_name 和 unit_price,而 channels 只包含 channel_id 和 channel_name。
所有表格的创建代码都包括 MySQL 中的 IF NOT EXISTS 子句,这确保只有在数据库中不存在这些表时才创建它们。这有助于在多次执行代码时防止任何错误或冲突。
我们在所有表格中使用 CREATED_AT 和 UPDATED_AT 列,因为这是最佳实践。通过添加这些通常称为 审计列 的列,我们使我们的数据库准备好在将来进行增量数据提取。这也是许多处理此增量提取的 CDC 工具所需的。
现在我们可以创建关系表,如 示例 6-3 所示。
示例 6-3. 创建关系表
CREATE TABLE IF NOT EXISTS purchaseHistory (
customer_id INTEGER,
product_sku INTEGER,
channel_id INTEGER,
quantity INT,
discount DOUBLE DEFAULT 0,
order_date DATETIME NOT NULL,
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UPDATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES channels(channel_id),
FOREIGN KEY (product_sku) REFERENCES products(product_sku),
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
CREATE TABLE IF NOT EXISTS visitHistory (
customer_id INTEGER,
channel_id INTEGER,
visit_timestamp TIMESTAMP NOT NULL,
bounce_timestamp TIMESTAMP NULL,
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UPDATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES channels(channel_id),
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
purchaseHistory 表可能是我们购买关系的核心,包含诸如 customer_id、product_sku、channel_id、quantity、discount 和 order_date 等列。customer_id、product_sku 和 channel_id 列分别表示引用 customers、products 和 channels 表中对应主键的外键。这些外键建立了表之间的关系。quantity 列存储购买的产品数量,而 discount 列存储购买时应用的折扣(如果没有指定,默认值为 0,假定这是标准)。order_date 列记录购买的日期和时间,并标记为 NOT NULL,意味着它必须始终有值。
visitHistory 表类似于 PurchaseHistory,包含诸如 customer_id、channel_id、visit_timestamp 和 bounce_timestamp 等列。customer_id 和 channel_id 列作为外键,引用 customers 和 channels 表的主键。visit_timestamp 列记录客户访问特定渠道的时间戳,而 bounce_timestamp 记录如果访问导致跳出(在没有进一步操作的情况下离开渠道)的时间戳。
FOREIGN KEY 约束确保了外键列中的值与参考表(customers、products 和 channels)中的现有值相对应,从而维护了数据库内数据的完整性和一致性。
建模运营数据库是分析师技能中宝贵的一部分,即使他们并非总是直接负责设计原始数据库结构。理解运营数据库建模背后的原则和考虑因素,使分析师能够全面了解整个数据管道。这种知识帮助他们理解他们正在处理的数据的起源和结构,从而使他们能够更有效地与负责运营层的数据工程师合作。
尽管分析师和分析工程师的工作并非从零开始设计这些数据库,但了解运营建模可以帮助他们解决数据源的复杂性,并确保数据按照他们的分析需求进行结构化和组织。此外,这种理解有助于故障排除和优化数据管道,因为分析工程师可以在运营数据库层面识别潜在的问题或改进机会。总之,熟悉运营数据库建模可以提升分析技能,促进更高效和协作的数据驱动工作流程。
高级数据架构
我们设计了一个精简的数据架构来支持我们全渠道使用案例的初始需求。我们将从开发一个 Python 脚本开始,从 MySQL 提取数据,清理一些数据类型,然后将数据发送到我们的 BigQuery 项目。图 6-4 说明了我们的目标解决方案。

图 6-4. 用于支持我们用例的精简数据架构图
一旦数据进入原始环境,我们将利用 dbt 来转换数据并构建所需的模型,这些模型将组成我们的星型模式。最后但同样重要的是,我们将通过对我们的星型模式数据模型运行 SQL 查询来分析 BigQuery 中的数据。
为了从 MySQL 提取数据并将其加载到 BigQuery,我们决定模拟一个 ETL 作业(示例 6-4)。包含单个函数data_pipeline_mysql_to_bq的编排器代码块执行几个步骤:从 MySQL 数据库中提取数据,转换数据,并将其加载到 BigQuery 的目标数据集中。代码从导入必要的模块开始,包括用于 MySQL 数据库连接的mysql.connector和用于数据操作的pandas。另一个关键库pandas_bq也在我们的代码结构中稍后使用。
data_pipeline_mysql_to_bq函数采用关键字参数(**kwargs)来接收流水线所需的配置详细信息。在 Python 中,**kwargs是一种特殊的语法,用于将可变数量的关键字参数传递给函数,形式上类似于字典。在函数内部,使用提供的连接详细信息与 MySQL 数据库建立连接。
为了自动化表提取,考虑到我们想要源数据库中的所有表,我们使用 MySQL 的information_schema创建了一个简单的例程。这是一个虚拟数据库,提供了关于数据库服务器、数据库、表、列、索引、权限和其他重要信息的元数据访问。information_schema是 MySQL 自动创建和维护的系统模式。我们利用information_schema获取我们数据库中所有表的表名,并将结果存储在名为df_tables的 DataFrame 中。
完成此步骤后,我们启动我们流水线的核心,调用提取、转换和加载函数来模拟 ETL 作业中的三个步骤。代码片段在示例 6-4 中展示了我们如何创建这些函数。
示例 6-4. 将数据加载到 BigQuery
import mysql.connector as connection
import pandas as pd
def data_pipeline_mysql_to_bq(**kwargs):
mysql_host = kwargs.get('mysql_host')
mysql_database = kwargs.get('mysql_database')
mysql_user = kwargs.get('mysql_user')
mysql_password = kwargs.get('mysql_password')
bq_project_id = kwargs.get('bq_project_id')
dataset = kwargs.get('dataset')
try:
mydb = connection.connect(host=mysql_host\
, database = mysql_database\
, user=mysql_user\
, passwd=mysql_password\
,use_pure=True)
all_tables = "Select table_name from information_schema.tables
where table_schema = '{}'".format(mysql_database)
df_tables = pd.read_sql(all_tables,mydb,
parse_dates={'Date': {'format': '%Y-%m-%d'}})
for table in df_tables.TABLE_NAME:
table_name = table
# Extract table data from MySQL
df_table_data = extract_table_from_mysql(table_name, mydb)
# Transform table data from MySQL
df_table_data = transform_data_from_table(df_table_data)
# Load data to BigQuery
load_data_into_bigquery(bq_project_id,
dataset,table_name,df_table_data)
# Show confirmation message
print("Ingested table {}".format(table_name))
mydb.close() #close the connection
except Exception as e:
mydb.close()
print(str(e))
在示例 6-5 中,我们定义了extract_table_from_mysql函数,模拟了 ETL 作业中的提取步骤。该函数负责从 MySQL 数据库中指定的表中检索数据。它接受两个参数:table_name,表示要提取的表的名称,以及my_sql_connection,表示 MySQL 数据库的连接对象或连接详细信息。
为了执行提取操作,该函数通过将表名与 select * from 语句连接起来构造 SQL 查询。这是一种非常简单的提取所有行的方法,在我们的示例中运行良好;然而,您可能希望通过筛选 updated_at 或 created_at 大于最后提取日期的记录(可以存储在元数据表中)来逐步提取这些数据。
接下来,该函数利用 pandas 库中的 pd.read_sql 函数执行提取查询。它将查询和 MySQL 连接对象 (my_sql_connection) 作为参数。该函数从指定表中读取数据,并将其加载到名为 df_table_data 的 pandas DataFrame 中。最后,它返回提取的包含从 MySQL 表中检索到的数据的 DataFrame。
示例 6-5. 加载数据到 BigQuery—提取
'''
Simulate the extraction step in an ETL job
'''
def extract_table_from_mysql(table_name, my_sql_connection):
# Extract data from mysql table
extraction_query = 'select * from ' + table_name
df_table_data = pd.read_sql(extraction_query,my_sql_connection)
return df_table_data
在 示例 6-6,我们定义了 transform_data_from_table 函数,该函数代表了 ETL 作业中的转换步骤。该函数负责对名为 df_table_data 的 DataFrame 执行特定的转换。在这种情况下,我们做了一些简单的事情:通过将日期转换为字符串来清理 DataFrame 中的日期,以避免与 pandas_bq 库发生冲突。为了实现这一目标,该函数使用 select_dtypes 方法识别具有对象数据类型(字符串列)的列。然后,它迭代这些列,并通过将第一个值转换为字符串表示来检查每列的数据类型。
如果数据类型被识别为 <class *datetime.date*>,表明该列包含日期值,则函数继续将每个日期值转换为字符串格式。这是通过使用 lambda 函数将每个值映射到其字符串表示来完成的。在执行转换后,函数返回具有清理后日期的修改后的 DataFrame。
示例 6-6. 加载数据到 BigQuery—转换
'''
Simulate the transformation step in an ETL job
'''
def transform_data_from_table(df_table_data):
# Clean dates - convert to string
object_cols = df_table_data.select_dtypes(include=['object']).columns
for column in object_cols:
dtype = str(type(df_table_data[column].values[0]))
if dtype == "<class 'datetime.date'>":
df_table_data[column] = df_table_data[column].map(lambda x: str(x))
return df_table_data
在 示例 6-7,我们定义了 load_data_into_bigquery 方法,它提供了一种方便的方法,通过 pandas_gbq 库将数据从 pandas DataFrame 加载到指定的 BigQuery 表中。它确保现有表格被新数据替换,允许在 BigQuery 环境中进行无缝数据传输和更新。
该函数接受四个参数:bq_project_id 表示 BigQuery 项目的项目 ID,dataset 和 table_name 分别指定 BigQuery 中的目标数据集和表。df_table_data 参数是一个包含要加载的数据的 pandas DataFrame。
示例 6-7. 加载数据到 BigQuery—加载
'''
Simulate the load step in an ETL job
'''
def load_data_into_bigquery(bq_project_id, dataset,table_name,df_table_data):
import pandas_gbq as pdbq
full_table_name_bg = "{}.{}".format(dataset,table_name)
pdbq.to_gbq(df_table_data,full_table_name_bg,project_id=bq_project_id,
if_exists='replace')
在示例 6-8 中,我们通过调用data_pipeline_mysql_to_bq函数并提供指定的关键字参数来执行数据管道。该代码创建了一个名为kwargs的字典,其中包含函数所需的关键字参数。这是在 Python 中传递多个参数的便捷方式,而无需将它们全部添加到方法签名中。kwargs字典包括 BigQuery 项目 ID、数据集名称、MySQL 连接详细信息(主机、用户名、密码)以及包含要传输数据的 MySQL 数据库的名称。但是,实际的 BigQuery 项目 ID、MySQL 主机信息、用户名和密码的值需要替换为适当的值。
通过提供kwargs字典内容作为关键字参数调用函数data_pipeline_mysql_to_bq。这会触发数据管道,将数据从指定的 MySQL 数据库移动到目标 BigQuery 表中。
示例 6-8. 加载数据到 BigQuery — 调用编排器
# Call main function
kwargs = {
# BigQuery connection details
'bq_project_id': <ADD_YOUR_BQ_PROJECT_ID>,
'dataset': 'omnichannel_raw',
# MySQL connection details
'mysql_host': <ADD_YOUR_HOST_INFO>,
'mysql_user': <ADD_YOUR_MYSQL_USER>,
'mysql_password': <ADD_YOUR_MYSQL_PASSWORD>,
'mysql_database': 'OMNI_MANAGEMENT'
}
data_pipeline_mysql_to_bq(**kwargs)
现在我们应该已经将原始数据加载到 BigQuery 的目标数据集中,准备使用 dbt 工具将其转换为维度模型。
分析数据建模
正如我们在本书前面所看到的,分析数据建模使用系统化的方法,包括若干关键步骤,以创建您业务流程的引人入胜且有意义的表示形式。第一步是确定和理解推动您组织的业务流程。这涉及映射关键运营活动、数据流和部门之间的相互依赖关系。通过充分理解您的业务流程,您可以确定生成、转换和利用数据的关键接触点。
一旦您清楚了解了业务流程,下一步是确定维度数据模型中的事实和维度。事实代表您想要分析的可衡量和可量化的数据点,例如销售数字、客户订单或网站流量。另一方面,维度为这些事实提供必要的上下文。它们定义了描述事实的各种属性和特征。有效地构建数据模型的关键在于确定这些事实和维度。
一旦确定了事实和维度,下一步是确定每个维度的属性。属性提供额外的细节,并且能够更深入地分析数据。它们描述与每个维度相关的特定特征或属性。以产品维度为例,属性可能包括产品的颜色、尺寸、重量和价格。同样,如果我们想要一个客户维度,属性可能涵盖诸如年龄、性别和位置等人口统计信息。通过确定相关属性,您可以增强数据模型的丰富性和深度,从而实现更深入的分析。
定义商业事实的粒度是分析数据建模的最后一步。粒度指的是捕获和分析业务事实的详细级别。在平衡捕获足够详细的数据以进行有意义分析和避免不必要的数据复杂性方面至关重要。例如,在零售销售分析中,粒度可以定义为交易级别,捕获每位客户的购买情况。另一方面,我们也可以选择其他更高层次的粒度,如每日、每周或每月的汇总。粒度的选择取决于您的分析目标、数据可用性以及从中获得有价值洞见所需的详细程度。
通过在分析数据建模中遵循这些步骤,您将为创建一个准确代表您的业务、捕获关键事实和维度、包含相关属性并定义适当粒度的数据模型奠定坚实的基础。一个设计良好的数据模型能让您释放数据的潜力,获取宝贵的洞见,并做出明智的决策,推动业务的增长和成功。
识别业务流程
在寻求开发有效的分析数据模型过程中,首要阶段是识别组织内的业务流程。经过与关键利益相关者的讨论和深入面试后,明确了主要目标之一是跟踪各渠道的销售表现。这一关键信息将为收入生成和各种销售渠道的有效性提供宝贵的洞见。
探索组织的目标时,另一个重要需求也显现出来:跟踪各渠道的访问量和跳出率。这一目标旨在揭示跨渠道的客户参与和网站性能。通过测量访问量和跳出率,组织可以确定哪些渠道带来了流量,以及改进的空间,以减少跳出率并增加用户参与度。
理解这些指标的重要性,我们意识到需要专注于两个不同的业务流程:销售跟踪和网站性能分析。销售跟踪流程将捕获并分析通过各种渠道产生的销售数据,例如移动端、移动应用或 Instagram。这一过程将提供销售表现的全面视角,使组织能够基于数据做出关于渠道优化和销售预测的决策。
与此同时,网站性能分析过程将收集关于网站访问和跳出率的数据,跨多个渠道进行分析。这将需要实施强大的跟踪机制,如网络分析工具,以监测和衡量用户在组织网站上的行为。通过检查特定渠道的访问模式和跳出率,组织可以识别趋势,优化用户体验,并提升网站性能,从而提高整体客户参与度。
因此,在分析数据建模过程中,识别这两个关键的业务流程——销售追踪和网站性能分析——成为一个重要的里程碑。有了这些知识,我们将能够充分准备进入下一阶段,在那里我们将深入了解与这些流程相关的数据流、相互依赖关系和特定数据点,形成一个与组织目标和要求相一致的全面维度数据模型。
在维度数据模型中识别事实和维度
基于销售追踪和网站性能分析的业务流程,我们推断需要四个维度和两个相应的事实表。让我们详细描述每一个。
第一个维度是渠道(dim_channels)。这个维度代表组织经营的各种销售和营销渠道。确定的常见渠道包括网站、Instagram 和移动应用渠道。通过跨渠道分析销售数据,组织可以洞察每个渠道在产生收入方面的表现和效果。
第二个维度是产品(dim_products)。这个维度关注组织的产品提供。通过包含产品维度,组织能够分析产品类别中的销售模式,并识别畅销产品或需要改进的领域。
第三个维度是客户(dim_customers)。该维度捕获了关于组织客户群体的信息。通过分析基于客户属性的销售数据,组织可以洞察客户偏好、行为和购买模式。
第四个也是最后一个维度是日期(dim_date)。这个维度允许对销售和网站性能进行时间分析。基于日期维度的数据分析允许组织识别趋势、季节性以及可能影响销售或网站性能的任何时间模式。
现在让我们转向事实表。第一个确定的事实表是购买历史(fct_purchase_history)。这张表是捕获购买交易并与相关维度——渠道、产品、客户和日期——关联的中心点。它允许详细的销售数据分析,使组织能够了解销售与维度之间的相关性。通过购买历史事实表,组织可以深入了解跨渠道、产品类别、客户细分和时间段的销售表现。
第二个事实表是访问历史表(fct_visit_history)。与购买历史不同,这张表专注于网站性能分析。它捕获与网站访问相关的数据,主要与渠道、客户和日期等维度相关联。通过分析访问历史,组织可以了解客户参与度,跟踪跨渠道的流量模式,以及衡量不同营销活动或网站功能的有效性。
有了这些确定的维度和事实表,我们已经为您的维度数据模型奠定了基础。这个模型使您能够有效分析和从各个维度的销售数据中获取洞察,以及跟踪与不同渠道、客户和时间段相关的网站性能指标。随着数据建模过程的进行,我们将进一步完善和定义每个维度内的属性,并建立必要的关系和层次结构,以进行全面分析,但目前,我们已经具备条件来设计我们的星型模式(图 6-5)。

图 6-5. 使用案例星型模式
星型模式包含四个主要维度:渠道、产品、客户和日期。这些维度作为分析的支柱,为数据提供了宝贵的背景信息。
注意
三个维度——dim_channels、dim_customers 和 dim_date——是符合维度。符合维度 被多个事实表共享,确保一致性,并促进在多种分析视角之间的无缝集成。
识别维度的属性
确定了维度后,现在是详细说明每个维度内识别的属性的时候了。通过将这些属性与各自的维度结合起来,数据模型变得更加深入和完整。这些属性丰富了分析,使决策者能够从数据中获得更加精细的洞察。
在考虑渠道维度(dim_channels)时,识别了多个属性。首先,渠道代理键(sk_channel)为数据模型内每个渠道提供了唯一标识符。与之配套的渠道自然键(nk_channel_id)代表了源系统的键,确保与外部数据源的无缝集成。此外,渠道名称属性(dsc_channel_name)捕获了每个渠道的描述性名称,在数据模型内易于识别和理解。这可能是最令人感兴趣的分析部分。
转向产品维度(dim_products),识别了多个关键属性。产品代理键(sk_product)作为数据模型内每个产品的唯一标识符。类似地,产品自然键(nk_product_sku)捕获了来自源系统的键,确保产品相关数据的一致链接。产品名称属性(dsc_product_name)为每个产品提供了描述性名称,有助于清晰理解。最后,产品单价属性(mtr_unit_price)记录了每个产品的价格,促进了价格分析和收入计算。
在客户维度(dim_customers)中,不同的属性帮助提供了客户相关信息的广泛视角。客户代理键(sk_customer)是数据模型内每位客户的唯一标识符。客户自然键(nk_customer_id)保留了来自源系统的键,允许与外部数据源无缝集成。此外,客户姓名(dsc_name)、出生日期(dt_date_birth)、电子邮件地址(dsc_email_address)、电话号码(dsc_phone_number)和国家(dsc_country)等属性捕获了与个体客户相关的重要细节。这些属性使得客户分割、个性化营销以及深入的客户行为和人口统计分析成为可能。
最后,日期维度(dim_date)包括一系列与日期相关的属性。这些属性增强了对时间数据的理解和分析。日期属性本身捕获特定的日期。诸如月份、季度和年份等属性提供了更高级别的时间信息,促进了聚合分析。通过包含这些属性,数据模型实现了全面的基于时间的分析和模式识别。
提示
代理键 是分配给数据库表中记录的人工标识符。它们提供了唯一性、稳定性和数据操作性能的提升。代理键独立于数据本身,确保每条记录都有一个唯一标识符,即使自然键值发生变化,也能保持稳定。它们简化了表之间的连接,增强了数据集成能力,并促进了高效的索引和查询。
为业务事实定义粒度
在完成了分析数据建模的早期阶段后,我们现在转向最后一个关键步骤,即确定未来业务事实的细粒度。细粒度指的是在维度数据模型内捕获和分析数据的详细程度。确定适当的细粒度对于确保数据模型有效地支持组织的分析需求和目标至关重要。
要定义我们业务事实的细粒度,需要考虑分析的具体需求,并在捕获足够详细信息和避免过度复杂化之间取得平衡。选择的细粒度应提供足够的信息进行有意义的分析,同时保持数据的可管理性和性能。
在销售数据的背景下,确定的细粒度是在交易级别确定的,捕获个别客户的购买情况:fct_purchase_history。这种细粒度允许对销售模式进行详细分析,如检查个别交易、识别客户行为趋势和进行产品级分析。
对于其他需求,网站性能分析,细粒度选定为访问级别,收集个别客户的访问以及他们进入平台的渠道:fct_visit_history。通过这些细节,组织可以了解客户参与度,跟踪跨渠道的流量模式,并衡量独特营销活动或网站功能的有效性。
或者,可以确定其他较少细粒度的分析单位,例如日常、每周或每月的聚合。对数据进行聚合可以提供更简洁的表达方式,同时提供有价值的见解。这种方法可以减少数据量并简化分析,使跨多个维度识别更广泛趋势、季节性模式或整体绩效更容易。
通过精确定义我们业务事实的细粒度,组织可以确保维度数据模型在捕获足够详细信息的同时保持数据的可管理性和性能。这一步骤为有意义的分析奠定了基础,使利益相关者能够从数据中获取有价值的见解,并基于数据做出明智的决策。
在完成这一阶段时,我们已成功地完成了分析数据建模的关键阶段,包括识别业务流程、事实、维度和属性,并定义了业务事实的细粒度。这些基础步骤为开发全面有效的维度数据模型提供了坚实的框架,支持组织内基于数据的决策。
通过 dbt 创建我们的数据仓库
在完成分析数据建模阶段后,现在是时候开始开发我们的数据仓库了。数据仓库作为结构化和集成数据的中心存储库,支持组织内强大的报告、分析和决策过程。
数据仓库开发的整体目标是通过建立必要的基础设施来开始。简而言之,在“高级数据架构”中,我们已经通过建立我们的 BigQuery 完成了这一点。在这个阶段,我们只需要设置我们的 dbt 项目并将其连接到 BigQuery 和 GitHub。在“使用 BigQuery 和 GitHub 设置 dbt Cloud”中,我们提供了一个全面的逐步指南,说明如何进行所有初始设置,因此我们将在本节中跳过此阶段。
在本节中,我们的主要目标是开发在分析数据建模阶段制定的所有 dbt 模型,这些模型作为设计和构建数据仓库的蓝图。与模型同时进行的是,我们还将开发所有参数化的 YAML 文件,以确保我们利用ref()和source()函数,最终使我们的代码更加 DRY。与开发 YAML 文件的目标一致,还需要执行另一个步骤:构建我们的分阶段模型区域。这些将是我们维度和事实的种子。
除了开发数据模型之外,建立数据仓库内部的命名约定也是至关重要的。这些命名约定提供了一种标准化的方法来命名表、列和其他数据库对象,确保数据基础设施的清晰度和一致性。表 6-1 展示了用于构建数据仓库的命名约定。
表 6-1. 命名约定
| 约定 | 字段类型 | 描述 |
|---|---|---|
| stg | Table/CTE | 分阶段表或 CTE |
| dim | Table | 维度表 |
| fct | Table | 事实表 |
| nk | Column | 自然键 |
| sk | Column | 代理键 |
| mtr | Column | 指标列(数值) |
| dsc | Column | 描述列(文本值) |
| dt | Column | 日期和时间列 |
要构建我们的第一个模型,我们必须确保我们的 dbt 项目已设置好,并且具备适当的文件夹结构。在此使用案例的这一部分中,我们将保持简单,仅构建分阶段和marts目录。因此,一旦初始化您的 dbt 项目,创建指定的文件夹。模型文件夹的目录应如示例 6-9。
示例 6-9. 全渠道数据仓库,模型目录
root/
├─ models/
│ ├─ staging/
│ ├─ marts/
├─ dbt_project.yml
现在我们已经建立了初始项目和文件夹,下一步是创建我们的分期 YAML 文件。按照我们在 “YAML Files” 中讨论的 YAML 文件最佳实践的分离,我们将为源和模型各有一个 YAML 文件。为了构建我们的分期层,我们现在只关注我们的 source YAML 文件。该文件必须位于 staging 目录内,看起来应该像 示例 6-10。
示例 6-10. _omnichannel_raw_sources.yml 文件配置
version: 2
sources:
- name: omnichannel
database: analytics-engineering-book
schema: omnichannel_raw
tables:
- name: Channels
- name: Customers
- name: Products
- name: VisitHistory
- name: PurchaseHistory
使用这个文件将允许你利用 source() 函数来处理你数据平台中的原始数据。在 omnichannel_raw 模式下指定了五张表:Channels、Customers、Products、VisitHistory 和 PurchaseHistory。这些表对应于用来构建我们分期层的相关源表,dbt 将与这些表进行交互以构建分期数据模型。
让我们开始构建我们的分期模型。这里的主要想法是为每个源表创建一个分期模型 — Channels、Customers、Products、VisitHistory 和 PurchaseHistory。请记住,每个新的分期模型都需要在 staging 目录内创建。
示例 6-11 至 6-15 展示了构建每个分期模型的代码片段。
示例 6-11. stg_channels
with raw_channels AS
(
SELECT
channel_id,
channel_name,
CREATED_AT,
UPDATED_AT
FROM {{ source("omnichannel","Channels")}}
)
SELECT
*
FROM raw_channels
示例 6-12. stg_customers
with raw_customers AS
(
SELECT
customer_id,
name,
date_birth,
email_address,
phone_number,
country,
CREATED_AT,
UPDATED_AT
FROM {{ source("omnichannel","Customers")}}
)
SELECT
*
FROM raw_customers
示例 6-13. stg_products
with raw_products AS
(
SELECT
product_sku,
product_name,
unit_price,
CREATED_AT,
UPDATED_AT
FROM {{ source("omnichannel","Products")}}
)
SELECT
*
FROM raw_products
示例 6-14. stg_purchase_history
with raw_purchase_history AS
(
SELECT
customer_id,
product_sku,
channel_id,
quantity,
discount,
order_date
FROM {{ source("omnichannel","PurchaseHistory")}}
)
SELECT
*
FROM raw_purchase_history
示例 6-15. stg_visit_history
with raw_visit_history AS
(
SELECT
customer_id,
channel_id,
visit_timestamp,
bounce_timestamp,
created_at,
updated_at
FROM {{ source("omnichannel","VisitHistory")}}
)
SELECT
*
FROM raw_visit_history
总结一下,每个 dbt 模型从相应的源表中提取数据,并在单独的 CTE 中进行分期存储。这些分期表在加载数据到数据仓库的最终目标表之前,作为进一步数据转换的中间存储。
成功创建分期模型后,下一阶段是为分期层设置 YAML 文件。分期层的 YAML 文件将作为一个配置文件,引用分期模型并指定它们的执行顺序和依赖关系。该文件清晰地展示了分期层设置的结构化视图,允许在整体数据建模过程中一致地集成和管理分期模型。示例 6-16 展示了分期层中 YAML 文件的样式。
示例 6-16. _omnichannel_raw_models.yml 文件配置
version: 2
models:
- name: stg_customers
- name: stg_channels
- name: stg_products
- name: stg_purchase_history
- name: stg_visit_history
一旦分段层 YAML 文件就位,就是前进构建维度模型的时候了。维度模型是数据仓库的一个重要组成部分,代表业务实体及其属性。这些模型捕捉提供上下文给事实数据并允许深入分析的描述性信息。维度表,如渠道、产品、客户和日期,将根据之前定义的维度和它们的属性构建,这些信息来自于“在维度数据模型中识别事实和维度”和“识别维度的属性”。这些表将使用来自分段层的相关数据填充,以确保一致性和准确性。
让我们继续我们的维度模型创建。在marts目录中创建示例 6-17 到 6-20 中的相应模型。
示例 6-17. dim_channels
with stg_dim_channels AS
(
SELECT
channel_id AS nk_channel_id,
channel_name AS dsc_channel_name,
created_at AS dt_created_at,
updated_at AS dt_updated_at
FROM {{ ref("stg_channels")}}
)
SELECT
{{ dbt_utils.generate_surrogate_key( ["nk_channel_id"] )}} AS sk_channel,
*
FROM stg_dim_channels
示例 6-18. dim_customers
with stg_dim_customers AS
(
SELECT
customer_id AS nk_customer_id,
name AS dsc_name,
date_birth AS dt_date_birth,
email_address AS dsc_email_address,
phone_number AS dsc_phone_number,
country AS dsc_country,
created_at AS dt_created_at,
updated_at AS dt_updated_at
FROM {{ ref("stg_customers")}}
)
SELECT
{{ dbt_utils.generate_surrogate_key( ["nk_customer_id"] )}} AS sk_customer,
*
FROM stg_dim_customers
示例 6-19. dim_products
with stg_dim_products AS
(
SELECT
product_sku AS nk_product_sku,
product_name AS dsc_product_name,
unit_price AS mtr_unit_price,
created_at AS dt_created_at,
updated_at AS dt_updated_at
FROM {{ ref("stg_products")}}
)
SELECT
{{ dbt_utils.generate_surrogate_key( ["nk_product_sku"] )}} AS sk_product,
*
FROM stg_dim_products
示例 6-20. dim_date
{{ dbt_date.get_date_dimension("2022-01-01", "2024-12-31") }}
总而言之,每个代码块定义了一个特定维度表的 dbt 模型。前三个模型,dim_channels、dim_customers 和 dim_products,从对应的分段表中检索数据,并将其转换为维度表的所需结构。在每一个模型中,我们都包括了从自然键生成伪码的步骤。为此,我们借助了 dbt_utils 包,具体来说是 generate_surrogate_key() 函数。该函数接受一个列名数组作为参数,表示维度表的自然键(或业务键),并基于这些列生成一个伪码列。
最后一个维度,dim_date,与其他不同,因为它并不来自分段层。相反,它完全使用 dbt_date 包中的 get_date_dimension() 函数生成。get_date_dimension() 函数负责生成日期维度表,包括创建所有必要列和根据指定日期范围为每个列填充数据。在我们的案例中,我们选择的日期范围是从 2022-01-01 到 2024-12-31。
最后,请记住我们现在在使用包。为了顺利在这个阶段构建项目,我们需要安装它们,所以将示例 6-21 配置添加到你的dbt_packages.yml文件中。然后执行dbt deps和dbt build命令,查看你的数据平台,确认我们已经创建了新的维度。
示例 6-21. packages.yml 文件配置
packages:
- package: dbt-labs/dbt_utils
version: 1.1.1
- package: calogica/dbt_date
version: [">=0.7.0", "<0.8.0"]
最后一步是根据早前确定需要分析新业务流程的事实表创建模型。这些表是数据仓库的一个组成部分,代表捕捉业务事件或交易的可度量、数值数据。
示例 6-22 和 6-23 代表了需要开发的新事实表。请在marts目录内创建它们。
示例 6-22. fct_purchase_history
with stg_fct_purchase_history AS
(
SELECT
customer_id AS nk_customer_id,
product_sku AS nk_product_sku,
channel_id AS nk_channel_id,
quantity AS mtr_quantity,
discount AS mtr_discount,
CAST(order_date AS DATE) AS dt_order_date
FROM {{ ref("stg_purchase_history")}}
)
SELECT
COALESCE(dcust.sk_customer, '-1') AS sk_customer,
COALESCE(dchan.sk_channel, '-1') AS sk_channel,
COALESCE(dprod.sk_product, '-1') AS sk_product,
fct.dt_order_date AS sk_order_date,
fct.mtr_quantity,
fct.mtr_discount,
dprod.mtr_unit_price,
ROUND(fct.mtr_quantity * dprod.mtr_unit_price,2) AS mtr_total_amount_gross,
ROUND(fct.mtr_quantity *
dprod.mtr_unit_price *
(1 - fct.mtr_discount),2) AS mtr_total_amount_net
FROM stg_fct_purchase_history AS fct
LEFT JOIN {{ ref("dim_customers")}} AS dcust
ON fct.nk_customer_id = dcust.nk_customer_id
LEFT JOIN {{ ref("dim_channels")}} AS dchan
ON fct.nk_channel_id = dchan.nk_channel_id
LEFT JOIN {{ ref("dim_products")}} AS dprod
ON fct.nk_product_sku = dprod.nk_product_sku
fct_purchase_history旨在回答首个识别的业务流程,即跟踪跨渠道的销售业绩。接下来,我们从stg_purchase_history模型中收集销售数据,并将其与相应的渠道、客户和产品维度结合,以捕获相应的代理键,使用COALESCE()函数处理自然键与维度表条目不匹配的情况。通过包括此事实与相应维度之间的关系,组织将能够从收入生成和各销售渠道在客户和产品方面的效果中获得有价值的见解。
要完全满足要求,还需计算两个额外的计算指标,即mtr_total_amount_gross和mtr_total_amount_net,它们基于购买的产品数量(mtr_quantity)、每个产品的单价(mtr_unit_price)以及应用的折扣(mtr_discount)计算而得。
总结一下,Example 6-22 展示了将分期数据转换为捕获相关购买历史信息的结构化事实表的过程。通过联接维度表并执行计算,事实表提供了购买数据的综合视图,从而实现有价值的见解和分析。
转向最后一个事实表,让我们看看 Example 6-23。
示例 6-23. fct_visit_history
with stg_fct_visit_history AS
(
SELECT
customer_id AS nk_customer_id,
channel_id AS nk_channel_id,
CAST(visit_timestamp AS DATE) AS sk_date_visit,
CAST(bounce_timestamp AS DATE) AS sk_date_bounce,
CAST(visit_timestamp AS DATETIME) AS dt_visit_timestamp,
CAST(bounce_timestamp AS DATETIME) AS dt_bounce_timestamp
FROM {{ ref("stg_visit_history")}}
)
SELECT
COALESCE(dcust.sk_customer, '-1') AS sk_customer,
COALESCE(dchan.sk_channel, '-1') AS sk_channel,
fct.sk_date_visit,
fct.sk_date_bounce,
fct.dt_visit_timestamp,
fct.dt_bounce_timestamp,
DATE_DIFF(dt_bounce_timestamp,dt_visit_timestamp
, MINUTE) AS mtr_length_of_stay_minutes
FROM stg_fct_visit_history AS fct
LEFT JOIN {{ ref("dim_customers")}} AS dcust
ON fct.nk_customer_id = dcust.nk_customer_id
LEFT JOIN {{ ref("dim_channels")}} AS dchan
ON fct.nk_channel_id = dchan.nk_channel_id
fct_visit_history回答了另一个识别的业务流程:跟踪每个渠道的访问和跳出率,以揭示客户参与和网站性能的情况。为了创建它,我们从stg_visit_history模型中收集访问数据,并将其与客户和渠道维度结合,以获取相应的代理键,使用COALESCE()函数处理自然键与维度表条目不匹配的情况。通过建立这种事实与维度的关系,组织将能够确定推动更多流量的渠道。还添加了一个额外的计算指标mtr_length_of_stay_minutes,以了解特定访问的停留时间。此计算指标利用DATE_DIFF()函数计算跳出和访问日期之间的差异,旨在支持组织识别改进的领域,以减少跳出率并增加用户参与度。
总之,fct_visit_history 事实表将分期数据转换为一个结构化的事实表,记录了相关的访问历史信息。通过与维度表的连接和计算,就像我们为两个事实表所做的那样,fct_visit_history 表提供了访问数据的紧凑视图,能够提供有价值的洞察和分析。
在接下来的章节中,我们将继续我们的旅程,开发测试和文档,并最终部署到生产环境。这些将是 dbt 内的最后步骤,旨在确保数据模型的可靠性、可用性,并支持组织内基于数据的决策。
与 dbt 进行测试、文档编制和部署
随着我们数据仓库开发的完成,确保实施的数据模型的准确性、可靠性和可用性至关重要。本节侧重于测试、文档编制以及数据仓库的投入使用。
正如前文所述,在使用 dbt 时,应在开发模型的同时创建测试和文档。我们采取了这种方法,但为了清晰起见,选择将其分为两部分。这种分割允许更清晰地了解我们在 dbt 中为模型开发所取得的成果,以及我们在测试和文档编制方面的流程。
简而言之,测试在验证数据模型功能和完整性方面至关重要。测试验证维度表和事实表之间的关系,检查数据一致性,并验证计算指标的准确性。通过进行测试,您可以识别和纠正数据中的任何问题或不一致性,从而确保分析输出的可靠性。
执行单一和通用测试都很重要。单一测试针对数据模型的特定方面,例如验证特定指标计算的准确性或验证特定维度与事实表之间的关系。这些测试提供了对数据模型各个组成部分的集中洞察。
另一方面,通用测试涵盖更广泛的场景,并监视数据模型的整体行为。这些测试旨在确保数据模型在各种维度、时间段和用户交互中正确运行。通用测试有助于发现在实际使用过程中可能出现的潜在问题,并提供对数据模型处理各种场景能力的信心。
同时,为了知识转移、协作和未来维护的需要,记录数据模型及其相关流程至关重要。记录数据模型涉及捕获关于模型目的、结构、关系和基础假设的信息。这包括源系统的细节、转换逻辑、应用的业务规则以及任何其他相关信息。
为了记录数据模型,建议更新相应的 YAML 文件,包括详细的说明和元数据。YAML 文件作为配置和 dbt 模型文档的集中位置,使跟踪更改和理解每个模型的目的和用法变得更容易。记录 YAML 文件确保未来的团队成员和利益相关者清楚地了解数据模型,并能够有效地使用它们。
一旦测试和文档编写完成,本节的最后一步是准备您的数据仓库进行上线。这涉及将数据模型部署到生产环境,确保建立数据管道,并设置定期数据更新。在此阶段监视数据仓库的性能、可用性和数据质量非常重要。在数据仓库完全运行之前,通过在类似生产的环境中进行彻底的测试并获得最终用户的反馈,可以帮助识别任何剩余的问题。
让我们开始测试。我们的第一批测试将专注于通用测试。第一个用例是确保所有维度的代理键是唯一且不为空。对于第二个用例,我们还必须确保事实表中的每个代理键存在于指定的维度中。让我们首先在 marts 层创建相应的 YAML 文件,使用 Example 6-24 代码块来实现我们所述的所有内容。
Example 6-24. _omnichannel_marts.yml 文件配置
version: 2
models:
- name: dim_customers
columns:
- name: sk_customer
tests:
- unique
- not_null
- name: dim_channels
columns:
- name: sk_channel
tests:
- unique
- not_null
- name: dim_date
columns:
- name: date_day
tests:
- unique
- not_null
- name: dim_products
columns:
- name: sk_product
tests:
- unique
- not_null
- name: fct_purchase_history
columns:
- name: sk_customer
tests:
- relationships:
to: ref('dim_customers')
field: sk_customer
- name: sk_channel
tests:
- relationships:
to: ref('dim_channels')
field: sk_channel
- name: sk_product
tests:
- relationships:
to: ref('dim_products')
field: sk_product
- name: fct_visit_history
columns:
- name: sk_customer
tests:
- relationships:
to: ref('dim_customers')
field: sk_customer
- name: sk_channel
tests:
- relationships:
to: ref('dim_channels')
field: sk_channel
现在让我们执行我们的 dbt test 命令,看看是否所有测试都成功执行。如果日志如 图 6-6 所示,一切顺利。

图 6-6. 通用测试的日志
现在让我们进行第二轮测试,并开发一些单独的测试。在这里,我们将专注于事实表的度量。第一个单独用例是确保我们来自 fct_purchase_history 的 mtr_total_amount_gross 度量仅具有正值。为此,让我们在 tests 文件夹中创建一个新的测试,assert_mtr_total_amount_gross_is_positive.sql,其中包含 Example 6-25 中的代码。
Example 6-25. assert_mtr_total_amount_gross_is_positive.sql
select
sk_customer,
sk_channel,
sk_product,
sum(mtr_total_amount_gross) as mtr_total_amount_gross
from {{ ref('fct_purchase_history') }}
group by 1, 2, 3
having mtr_total_amount_gross < 0
我们接下来要进行的测试是确认 mtr_unit_price 始终低于或等于 mtr_total_amount_gross。请注意,同样的测试无法应用于 mtr_total_amount_net,因为还应用了折扣。为了开发这个测试,首先创建文件 assert_mtr_unit_price_is_equal_or_lower_than_mtr_total_amount_gross.sql,并粘贴 Example 6-26 中的代码。
Example 6-26. assert_mtr_unit_price_is_equal_or_lower_than_mtr_total_amount_gross.sql
select
sk_customer,
sk_channel,
sk_product,
sum(mtr_total_amount_gross) AS mtr_total_amount_gross,
sum(mtr_unit_price) AS mtr_unit_price
from {{ ref('fct_purchase_history') }}
group by 1, 2, 3
having mtr_unit_price > mtr_total_amount_gross
所有独立测试创建完成后,我们现在可以执行它们并检查输出。为了避免执行所有测试,包括通用测试,请执行dbt test --select test_type:singular命令。此命令将执行类型为singular的测试,忽略任何generic测试。图 6-7 显示了预期的日志输出。

图 6-7. 独立测试的日志
我们希望进行的最后一个独立测试是确认在fct_visit_history中,mtr_length_of_stay_minutes指标始终为正。这个测试将告诉我们是否有记录的弹跳日期早于访问日期,这是不可能发生的。为了执行这个测试,请创建assert_mtr_length_of_stay_is_positive.sql文件,其中包含示例 6-27 中的代码。
示例 6-27. assert_mtr_length_of_stay_is_positive.sql
select
sk_customer,
sk_channel,
sum(mtr_length_of_stay_minutes) as mtr_length_of_stay_minutes
from {{ ref('fct_visit_history') }}
group by 1, 2
having mtr_length_of_stay_minutes < 0
通过实施测试,您可以验证数据的完整性,验证计算和转换,并确保符合定义的业务规则。dbt 提供了一个完整的测试框架,允许您进行单独和通用测试,涵盖数据模型的各个方面。
所有测试成功执行后,我们现在转向数据仓库开发过程的下一个重要方面:文档编制。在 dbt 中,大部分文档工作都是使用我们用来配置模型或执行测试的相同 YAML 文件完成的。让我们使用 marts 层来记录所有表和列。让我们参考_omnichannel_marts.yml文件,并将其替换为示例 6-28 中的代码。需要注意的是,我们只记录用于通用测试的列,以使示例更加清晰,但基本原理对所有其他列也适用。
示例 6-28. _omnichannel_marts.yml 文件配置及文档
version: 2
models:
- name: dim_customers
description: All customers' details. Includes anonymous users who used guest
checkout.
columns:
- name: sk_customer
description: Surrogate key of the customer dimension.
tests:
- unique
- not_null
- name: dim_channels
description: Channels data. Allows you to analyze linked facts from the channels
perspective.
columns:
- name: sk_channel
description: Surrogate key of the channel dimension.
tests:
- unique
- not_null
- name: dim_date
description: Date data. Allows you to analyze linked facts from the date
perspective.
columns:
- name: date_day
description: Surrogate key of the date dimension. The naming convention
wasn't added here.
tests:
- unique
- not_null
- name: dim_products
description: Products data. Allows you to analyze linked facts from the products
perspective.
columns:
- name: sk_product
description: Surrogate key of the product dimension.
tests:
- unique
- not_null
- name: fct_purchase_history
description: Customer orders history.
columns:
- name: sk_customer
description: Surrogate key for the customer dimension.
tests:
- relationships:
to: ref('dim_customers')
field: sk_customer
- name: sk_channel
description: Surrogate key for the channel dimension.
tests:
- relationships:
to: ref('dim_channels')
field: sk_channel
- name: sk_product
description: Surrogate key for the product dimension.
tests:
- relationships:
to: ref('dim_products')
field: sk_product
- name: fct_visit_history
description: Customer visits history.
columns:
- name: sk_customer
description: Surrogate key for the customer dimension.
tests:
- relationships:
to: ref('dim_customers')
field: sk_customer
- name: sk_channel
description: Surrogate key for the channel dimension.
tests:
- relationships:
to: ref('dim_channels')
field: sk_channel
更新 YAML 文件后,执行dbt docs generate,我们来看看可用的新文档。例如,如果您的fct_purchase_history页面类似于图 6-8,那就准备好了。

图 6-8. fct_purchase_history文档页面
就这样。最后一步是将我们所做的部署到生产环境中。为此,我们需要在 dbt 中创建一个类似于图 6-9 中展示的环境。

图 6-9. 创建生产环境
请注意,我们将生产数据集命名为omnichannel_analytics。我们将在“使用 SQL 进行数据分析”中使用此数据集。创建环境后,现在是配置作业的时候。为了简化操作,在创建作业时,请提供作业名称,将环境设置为生产环境(刚刚创建的环境),勾选“运行时生成文档”选项,并最后,在命令部分,包含dbt build命令下方的dbt test命令。其余选项保持默认。
在作业创建后,手动执行该作业,然后检查日志。如果它们类似于图 6-10,这是一个良好的指标。

图 6-10. 作业执行日志
让我们来看看我们的数据平台,这里我们使用的是 BigQuery,并检查一下是否一切运行成功。BigQuery 中的模型应该与图 6-11 中展示的模型一致。

图 6-11. BigQuery 中的模型
总之,数据仓库建设的最后一部分专注于测试、文档编制和上线过程。通过进行广泛测试、文档化数据模型,并为生产部署做准备,您可以确保数据仓库的准确性、可靠性和可用性。在下一节中,我们将深入探讨使用 SQL 进行数据分析,将我们的数据仓库推向新的高度。
使用 SQL 进行数据分析
完成我们的星型模式模型后,我们现在可以开始分析发现阶段,并开发查询来回答具体的业务问题。正如先前提到的,这种数据建模技术使得可以轻松从事实表中选择特定指标,并丰富来自维度的属性。
在示例 6-29 中,我们首先创建了一个查询,用于“每个季度销售总额(包含折扣)”。在这个查询中,我们从两个表fct_purchase_history和dim_date中获取数据,并对检索到的数据进行计算。此查询旨在获取每年每个季度的总金额信息。
Example 6-29. 每个季度销售总额(包含折扣)
SELECT dd.year_number,
dd.quarter_of_year,
ROUND(SUM(fct.mtr_total_amount_net),2) as sum_total_amount_with_discount
FROM `omnichannel_analytics`.`fct_purchase_history` fct
LEFT JOIN `omnichannel_analytics`.`dim_date` dd
on dd.date_day = fct.sk_order_date
GROUP BY dd.year_number,dd.quarter_of_year
通过分析运行此查询的结果(图 6-12),我们可以得出结论:2023 年第二季度是最好的,而 2024 年第一季度是最差的。

图 6-12. 获取每个季度销售总额(包含折扣)的分析查询
在示例 6-30 中,我们通过星型模式模型计算每个频道的平均停留时间(以分钟计)。它选择频道名称(dc.dsc_channel_name)和以函数ROUND(AVG(mtr_length_of_stay_minutes),2)计算的平均停留时间(分钟)。dc.dsc_channel_name指的是来自dim_channels维度表的channel_name属性。
ROUND(AVG(mtr_length_of_stay_minutes),2)通过在fct_visit_history事实表的mtr_length_of_stay_minutes列上使用AVG函数计算平均逗留时间(以分钟为单位)。ROUND()函数用于将结果四舍五入为两位小数。给计算的平均值分配别名avg_length_of_stay_minutes。
Example 6-30. 每个渠道访问时间的平均值
SELECT dc.dsc_channel_name,
ROUND(AVG(mtr_length_of_stay_minutes),2) as avg_length_of_stay_minutes
FROM `omnichannel_analytics.fct_visit_history` fct
LEFT JOIN `omnichannel_analytics.dim_channels` dc
on fct.sk_channel = dc.sk_channel
GROUP BY dc.dsc_channel_name
通过分析运行此查询的结果(Figure 6-13),我们可以得出结论,用户在网站上花费的时间比在移动应用或公司的 Instagram 帐户上的时间更多。

Figure 6-13. 分析查询以获取每个渠道的访问时间平均值
在 Example 6-31 中,我们将模型带到了一个高级用例。我们现在对每个渠道获取前三个产品感兴趣。由于我们有三个不同的渠道,即移动应用程序、网站和 Instagram,我们有兴趣获取九行数据,每个渠道的前三个畅销产品各三行。
为此,我们利用 CTE 的结构优势,并从将返回每个产品和渠道的sum_total_amount的基本查询开始。现在,我们可以创建第二个 CTE,从上一个 CTE 开始,并按渠道降序排列总金额,这意味着每个产品在各个渠道中的性能顺序。为了获得这个排名,我们默认使用窗口函数,特别是RANK()函数,它将根据先前提到的规则对行进行评分。
Example 6-31. 每个渠道的前三个产品
WITH base_cte AS (
SELECT dp.dsc_product_name,
dc.dsc_channel_name,
ROUND(SUM(fct.mtr_total_amount_net),2) as sum_total_amount
FROM `omnichannel_analytics`.`fct_purchase_history` fct
LEFT JOIN `omnichannel_analytics`.`dim_products` dp
on dp.sk_product = fct.sk_product
LEFT JOIN `omnichannel_analytics`.`dim_channels` dc
on dc.sk_channel = fct.sk_channel
GROUP BY dc.dsc_channel_name, dp.dsc_product_name
),
ranked_cte AS(
SELECT base_cte.dsc_product_name,
base_cte.dsc_channel_name,
base_cte.sum_total_amount,
RANK() OVER(PARTITION BY dsc_channel_name
ORDER BY sum_total_amount DESC) AS rank_total_amount
FROM base_cte
)
SELECT *
FROM ranked_cte
WHERE rank_total_amount <= 3
通过分析运行此查询的结果(Figure 6-14),我们得出结论,我们移动应用程序的表现最佳产品是带可拆卸兜帽的男式轰炸机夹克,销售额为€389.97;网站的表现最佳产品是皮质斜挎手提包,销售额为€449.97;Instagram 的表现最佳产品是男女款跑步鞋,销售额为€271.97。

Figure 6-14. 分析查询以获取每个渠道的前三个产品
在 Example 6-32 中,我们通过利用最近创建的星型模式数据模型来分析我们 2023 年移动应用程序上的顶级客户,以支持业务问题。再次利用 CTE 正确构造查询,但这次我们结合ORDER BY子句和LIMIT修饰符,以获取在购买支出方面排名前三的买家。
Example 6-32. 2023 年移动应用程序上的前三名客户
WITH base_cte AS (
SELECT dcu.dsc_name,
dcu.dsc_email_address,
dc.dsc_channel_name,
ROUND(SUM(fct.mtr_total_amount_net),2) as sum_total_amount
FROM `omnichannel_analytics`.`fct_purchase_history` fct
LEFT JOIN `omnichannel_analytics`.`dim_customers` dcu
on dcu.sk_customer = fct.sk_customer
LEFT JOIN `omnichannel_analytics`.`dim_channels` dc
on dc.sk_channel = fct.sk_channel
WHERE dc.dsc_channel_name = 'Mobile App'
GROUP BY dc.dsc_channel_name, dcu.dsc_name, dcu.dsc_email_address
ORDER BY sum_total_amount DESC
)
SELECT *
FROM base_cte
LIMIT 3
通过分析运行此查询的结果(Figure 6-15),我们得出结论,我们的顶级买家是 Sophia Garcia,花费了€389.97。我们可以通过sophia.garcia@emailaddress.com向她发送电子邮件,感谢她成为如此特别的客户。

Figure 6-15. 分析查询以获取移动应用程序的前三名客户
通过展示这些查询,我们旨在突显使用星型模式来回答复杂业务问题的固有简单性和有效性。通过使用这种模式设计,组织可以获得宝贵的洞见,并更高效地做出基于数据的决策。
虽然前面的查询展示了每个查询的简单性,但真正的力量在于能够将它们与公共表达式(CTE)结合起来。这种策略性使用 CTE 使得查询可以在结构化和易于理解的方式下进行优化和组织。通过使用 CTE,分析师可以简化他们的工作流程,提高代码的可读性,并促进中间结果的重复使用。
此外,实现窗口函数为数据分析带来了额外的效率层级。通过使用窗口函数,分析师可以在特定数据分区或窗口中高效计算聚合结果,为趋势、排名和比较分析提供宝贵见解。分析师可以通过这些函数高效地从大数据集中推导出有意义的结论,加速决策过程。
通过撰写本节,我们意在概括本书涵盖的主题的重要性。它强调了掌握 SQL、熟练的数据建模技能以及全面理解围绕数据技术如 dbt 的技术环境的重要性,以提升您的分析工程技能。掌握这些能力使专业人士能够有效地在企业或个人项目中处理产生的大量数据。
结论
分析工程的风景如同人类想象力的边界一样广阔而多样化。正如托尼·斯塔克利用尖端技术变身为钢铁侠一样,我们受到数据库、SQL 和 dbt 的赋能,使我们不仅仅是观众,而是数据驱动时代中的积极英雄。就像斯塔克的一系列盔甲一样,这些工具为我们提供了灵活性、力量和精度,使我们能够直面最复杂的挑战。
数据库和 SQL 一直是我们在数据驱动策略中的基础支柱,提供稳定性和可靠性。然而,随着需求和复杂性的增长,分析已扩展到整合复杂的数据建模实践。这种转变不仅仅是技术上的,还强调了制定业务叙事、预见分析趋势和为未来需求进行前瞻规划的重要性。
dbt 在这个充满活力的领域中崭露头角。它不仅仅是 SQL 的补充,而是重新定义了我们在协作、测试和文档化方面的方法。借助 dbt,原始和碎片化数据的处理变得更加精细化,从而形成支持决策的可操作模型。
分析工程既融合了传统实践又结合了创新进展。虽然数据建模、严格测试和透明文档等原则已被确立,但像 dbt 这样的工具引入了新的方法和可能性。每一个面临的挑战都是新人和老手们的学习机会。每个数据库和查询都提供了独特的视角和潜在的解决方案。
正如福尔摩斯从零碎的证据中编织复杂的故事一样,分析工程师可以从高度分散的数据点中创建引人入胜的数据模型。他们手中的工具不仅仅是机械的,而是赋予他们预测分析趋势、将数据模型与业务需求对齐,并且像福尔摩斯一样,成为数据驱动时代的讲故事者的力量。在这个背景下,dbt 就像他们的沃森,促进协作和效率,就像那位著名侦探可靠的伙伴一样。与福尔摩斯的相似之处令人震撼,因为两者都以揭示隐藏在复杂数据集中的秘密为使命。我们希望通过本书的章节,您能够深入了解这一不断发展的学科,并获得洞见和更清晰的理解。


浙公网安备 33010602011771号