《Delta Lake Up & Running》第七章:Schema处理

传统上,数据湖一直遵循"模式在读"(schema on read)的原则,但在写入时始终存在着强制执行"模式在写"(schema on write)的挑战。这意味着在将数据写入存储时没有预定义的模式,只有在处理数据时才会调整模式。对于分析和数据平台的情况,强制执行"模式在写"的原则非常重要,以防止引入破坏更改的过程,并保持良好的数据质量和完整性。

虽然遵循"模式在写"原则至关重要,但我们也必须认识到,在当今快节奏的商业环境和不断变化的数据管理格局中,数据源、分析以及数据本身的整体结构不断发生变化。这些变化需要通过灵活演进的模式来适应时间的推移,以便捕捉新的、不断变化的信息。

传统数据湖中经常出现的模式挑战可以进一步分为两个关键的模式处理特性,无论存储层如何,任何数据平台和表格式都必须支持:

模式强制执行 这是确保添加到表中的所有数据符合特定模式的过程,其中模式通过列名列表、它们的数据类型以及任何可选约束定义表结构。强制执行数据符合定义模式的结构有助于维护数据的质量和一致性,因为不符合模式的表写入将被拒绝。从而有助于防止由于数据以不同格式存在而可能难以确保表中的数据准确和一致引起的数据质量问题。

模式演变 这允许存储在数据湖中的数据具有灵活性和适应性,以应对不断变化的业务需求和数据格局。模式演变应该以非常有意识、受控和有组织的方式进行,主要限于向模式添加列。

Delta Lake幸运地具有出色的模式处理功能,允许进行灵活的模式演变和严格的强制执行。本章将演示Delta Lake如何执行验证和强制执行,以及模式演变方案以及Delta Lake如何处理这些方案。

Schema验证

在Apache Spark中,您创建的每个DataFrame都将具有模式。最好将模式看作是定义数据形状的蓝图或结构。这包括每个列的名称、数据类型、列是否可以为NULL,以及与每个列相关联的任何元数据。

Delta Lake将Delta表的模式存储为事务日志条目的metaData操作中的schemaString。在本节中,我们将查看这些条目。接下来,我们将查看Delta Lake在模式写入操作期间应用的验证规则。我们将通过每个模式验证规则的一个用例来结束本节。

要跟随代码进行操作,请首先执行第7章的"00 - Chapter Initialization"笔记本,以创建TaxiRateCode Delta表。

查看事务日志条目中的模式

Delta Lake将以JSON格式存储模式在事务日志中。例如,初始化笔记本会像这样写入Delta表:

lua 复制代码
# Write in Delta Lake format
df.write.format("delta")   \
        .mode("overwrite") \
        .save("/mnt/datalake/book/chapter07/TaxiRateCode")

要查看模式如何保存,请打开"01 - 模式强制执行"笔记本。在这个笔记本中,当我们查看事务日志文件时,可以看到表的模式以JSON格式保存在事务日志中:

bash 复制代码
%sh
# The schemaString is part of the metaData action of the Transaction Log entry
# The schemaString contains the full schema of the Delta table file 
# at the time that the log entry was written
grep "metadata" /dbfs/mnt/datalake/.../TaxiRateCode.delta/_delta_log/...000.json 
> /tmp/commit.json
python -m json.tool < /tmp/commit.json

我们能看到如下输出:

python 复制代码
{
    "metaData": {
        "id": "8f348474-0288-440a-a76e-2358ccf45a96",
        "format": {
            "provider": "parquet",
            "options": {}
        },
        "schemaString": "{"type":"struct","fields":[{"name":\
                        "RateCodeId","type":"integer","nullable\
                        ":true,"metadata":{}},{"name":"RateCodeDesc\
                        ","type":"string","nullable":true,\
                        "metadata":{}}]}",
        "partitionColumns": [],
        "configuration": {},
        "createdTime": 1681161987269
    }
}

模式是一个结构(struct),其中包含表示列的字段列表,每个字段都有一个名称、一个类型和一个可空指示器,告诉我们该字段是否是必填的。 每列还包含一个元数据字段。元数据字段是一个JSON字符串,可以包含各种类型的信息,取决于正在执行的事务,例如:

  • 执行事务的人的用户名
  • 事务的时间戳
  • 使用的Delta Lake版本
  • 模式分区列
  • 与事务相关的任何额外的应用程序特定的元数据

写入时模式

模式验证会拒绝写入不符合表模式的表。Delta Lake在写入时执行模式验证,因此它将检查要写入表格的数据的模式。如果模式是兼容的,验证将通过,写入将成功;如果数据的模式不兼容,Delta Lake将取消事务,不会写入任何数据。

请注意,此操作始终是原子的,因此永远不会出现只有部分数据写入表的情况。当事务成功时,所有源数据都被写入,当验证失败时,不会写入任何源数据。当模式验证失败时,Delta Lake将引发异常,以通知用户存在不匹配。

要确定写入表是否兼容,Delta Lake使用以下规则:

  1. 要写入的源DataFrame:

    • 不能包含在目标表模式中不存在的任何列。
    • 请注意,新数据不得包含目标表模式中没有的每列,只要缺少的列在目标表模式中被标记为可空。如果在目标模式中没有将缺少的列标记为可空,则事务将失败。
  2. 不能具有与目标表中的列数据类型不同的列数据类型。

    • 例如,如果目标表的列包含StringType数据,但相应的源列包含IntegerType数据,则模式强制执行将引发异常,并阻止写入操作的进行。
  3. 不能包含仅通过大小写区分的列名称。

    • 例如,如果源数据包含一个名为Foo的列,而源数据有一个名为foo的列,则事务将失败。这个规则背后有一些历史:
    • Spark可以在区分大小写或不区分大小写(默认)模式下使用。
    • 另一方面,Parquet在存储和返回列信息时是区分大小写的。
    • Delta Lake在存储模式时是区分大小写的但在存储模式时是不区分大小写的。

由于上述规则的结合变得相当复杂。因此,为了避免潜在的错误、数据损坏或丢失问题,Delta Lake不允许仅在大小写方面不同的列名。

模式强制执行示例

让我们详细了解模式强制执行的细节。我们将首先追加一个具有匹配模式的DataFrame,这将顺利成功。接下来,我们将向DataFrame添加一个额外的列,并尝试将其追加到Delta表中。我们将验证这是否导致异常,并且没有写入任何数据。

匹配的模式

为了说明模式强制执行,我们首先将一个具有正确模式的DataFrame追加到TaxiRateCode表中,如在"01 - 模式强制执行"笔记本中的第2步所示:

ini 复制代码
# Define the schema for the DataFrame
# Notice that the columns match the table schema
schema = StructType([
    StructField("RateCodeId", IntegerType(), True),
    StructField("RateCodeDesc", StringType(), True)
])

# Create a list of rows for the DataFrame
data = [(10, "Rate Code 10"), (11, "Rate Code 11"), (12, "Rate Code 12")]

# Create a DataFrame, passing in the data rows
# and the schema
df = spark.createDataFrame(data, schema)

# Perform the write. This write will succeed without any
# problems
df.write           \
  .format("delta") \
  .mode("append")  \
  .save("/mnt/datalake/book/chapter07/TaxiRateCode")

由于源模式和目标模式对齐,因此成功将DataFrame追加到表中。

带有额外列的模式

在笔记本的第3步中,我们将尝试向源模式添加一个额外的列:

ini 复制代码
# Define the schema for the DataFrame
# Notice that we added an additional column
schema = StructType([
    StructField("RateCodeId", IntegerType(), True),
    StructField("RateCodeDesc", StringType(), True),
    StructField("RateCodeName", StringType(), True)
])

# Create a list of rows for the DataFrame
data = [
    (15, "Rate Code 15", "C15"),
    (16, "Rate Code 16", "C16"),
    (17, "Rate Code 17", "C17")]

# Create a DataFrame from the list of rows and the schema
df = spark.createDataFrame(data, schema)

# Attempt to append the DataFrame to the table
df.write           \
  .format("delta") \
  .mode("append")  \
  .save("/mnt/datalake/book/chapter07/TaxiRateCode")

这段代码将失败,并引发以下异常:

sql 复制代码
AnalysisException: A schema mismatch detected when writing to the Delta table 
(Table ID: 8f348474-0288-440a-a76e-2358ccf45a96).

当我们向下滚动时,我们看到Delta Lake提供了详细的解释发生了什么事情:

vbnet 复制代码
To enable schema migration using DataFrameWriter or DataStreamWriter, please set:
'.option("mergeSchema", "true")'.
For other operations, set the session configuration
spark.databricks.delta.schema.autoMerge.enabled to "true". See the documentation
specific to the operation for details.

Table schema:
root
-- RateCodeId: integer (nullable = true)
-- RateCodeDesc: string (nullable = true)


Data schema:
root
-- RateCodeId: integer (nullable = true)
-- RateCodeDesc: string (nullable = true)
-- RateCodeName: string (nullable = true)

Delta Lake通知我们,我们可以使用mergeSchema选项设置为true来演变模式,这是我们将在下一部分中详细研究的内容。然后,它向我们显示了表和源数据的模式,这对于调试非常有帮助。当我们查看事务日志条目时,我们看到以下内容:

bash 复制代码
# Create a listing of all transaction log entries.
# We notice that there are only two entries.
# The first entry represents the creation of the table
# The second entry is the append of the valid dataframe
# There is no entry for the above code since the exception
# occurred, resulting in a rollback of the transaction
ls -al /dbfs/mnt/datalake/book/chapter07/TaxiRateCode/_delta_log/*.json


-rwxrwxrwx 1 Apr 10 21:26 /dbfs/.../TaxiRateCode.delta/_delta_log/...000.json
-rwxrwxrwx 1 Apr 10 21:27 /dbfs/.../TaxiRateCode.delta/_delta_log/...001.json

第一个条目(0000...0.json)表示表的创建,第二个条目(0000...1.json)是使用有效DataFrame的追加。由于引发了模式不匹配的异常,并且根本没有写入任何数据,因此先前的代码没有条目,这说明了Delta Lake事务的原子行为。 对该表运行DESCRIBE HISTORY命令的结果证实了这一点:

sql 复制代码
%sql
-- Look at the history for the Delta table
DESCRIBE HISTORY delta.`/mnt/datalake/book/chapter07/TaxiRateCode`

输出(仅显示相关数据):

lua 复制代码
+-------+----------+-----------------------------------------+
|version|operation |    operationParameters                  |
+-------+----------+-----------------------------------------+
|1      | WRITE    | {"mode":"Append","partitionBy":"[]"}    |
|0      | WRITE    | {"mode":"Overwrite","partitionBy":"[]"} |
+-------+----------+-----------------------------------------+

在这一部分中,我们看到了模式强制执行的工作原理。它确保了除非您选择更改它,否则表的模式不会改变。模式强制执行确保了Delta Lake表的数据质量和一致性,使开发人员保持诚实,表保持清洁。 然而,如果您经过深思熟虑,确实需要在表中添加附加列以促进业务,那么您可以利用模式演变,我们将在下一部分进行介绍。

Schema Evolution

Delta Lake中的模式演变是指能够随着时间的推移演变Delta表的模式,同时保留表中的现有数据。换句话说,模式演变允许我们在现有的Delta表中添加、删除或修改列,而不会丢失任何数据或破坏依赖于该表的任何下游作业。这是因为随着时间的推移,您的数据和业务需求可能会发生变化,您可能需要在表中添加新列或修改现有列以支持新的用例。 在写操作期间,可以通过在表级别使用.option("mergeSchema", "true")来启用表级别的模式演变。您还可以通过将spark.databricks.delta.schema.autoMerge.enabled设置为true,在整个Spark集群上启用模式演变。默认情况下,此设置将被设置为false。 启用模式演变时,将应用以下规则:

  • 如果源DataFrame中存在但在Delta表中不存在的列,则会向Delta表添加一个新列,该列具有相同的名称和数据类型。所有现有行的新列都将具有null值。
  • 如果Delta表中存在但在源DataFrame中不存在的列,则该列不会更改,并保留其现有值。新记录将在源DataFrame中缺少的列上具有null值。
  • 如果Delta表中存在与源DataFrame中相同名称但数据类型不同的列,则Delta Lake将尝试将数据转换为新的数据类型。如果转换失败,将引发错误。
  • 如果向Delta表添加了NullType列,则所有现有行的该列将设置为null。

让我们看一下几种模式演变的场景,从最常见的开始:向表中添加列。

添加列

回到我们的模式强制执行示例,我们可以使用模式演变将由于模式不匹配而被拒绝的RateCodeName列添加到先前的模式中。回想一下规则: 如果源DataFrame中存在但在Delta表中不存在的列,则会向Delta表添加一个新列,该列具有相同的名称和数据类型。所有现有行的新列都将具有null值。 您可以在"02 - 模式演变"笔记本中查看代码。在笔记本的第2步中,通过将.option("mergeSchema", "true")添加到.write Spark命令来激活模式演变:

ini 复制代码
# Define the schema for the DataFrame
# Notice the additional RateCodeName column, which
# is not part of the target table schema
schema = StructType([
    StructField("RateCodeId", IntegerType(), True),
    StructField("RateCodeDesc", StringType(), True),
    StructField("RateCodeName", StringType(), True)
])

# Create a list of rows for the DataFrame
data = [
    (20, "Rate Code 20", "C20"),
    (21, "Rate Code 21", "C21"),
    (22, "Rate Code 22", "C22")
]

# Create a DataFrame from the list of rows and the schema
df = spark.createDataFrame(data, schema)

# Append the DataFrame to the Delta Table
df.write                         \
  .format("delta")               \
  .option("mergeSchema", "true") \
  .mode("append")                \
  .save("/mnt/datalake/book/chapter07/TaxiRateCode")

# Print the schema
df.printSchema()

我们将看到新的Schema:

ini 复制代码
root
 |-- RateCodeId: integer (nullable = true)
 |-- RateCodeDesc: string (nullable = true)
 |-- RateCodeName: string (nullable = true)

现在,写操作将成功完成,并且数据将被添加到Delta表中:

sql 复制代码
%sql
SELECT
      *
FROM
      delta.`/mnt/datalake/book/chapter07/TaxiRateCode`
ORDER BY
    RateCodeId

我们得到如下输出:

sql 复制代码
+-----------+---------------------+------------+
|RateCodeId |   RateCodeDes       |RateCodeName|
+-----------+---------------------+------------+
|1          |Standard Rate        | null       |
|2          |JFK                  | null       |
|3          |Newark               | null       |
|4          |Nassau or Westchester| null       |
|5          |Negotiated fare      | null       |
|6          |Group ride           | null       |
|20         |Rate Code 20         | C20        |
|21         |Rate Code 21         | C21        |
|22         |Rate Code 22         | C22        |
+-----------+---------------------+------------+

新数据已添加,对于现有行,RateCodeName已设置为null,这是预期的结果。当我们查看相应的事务日志条目时,可以看到已写入具有更新模式的新元数据条目:

swift 复制代码
{
    "metaData": {
        "id": "ac676ac9-8805-4aca-9db7-4856a3c3a55b",
        "format": {
            "provider": "parquet",
            "options": {}
        },
        "schemaString": "{\"type\":\"struct\",\"fields\":[
            {\"name\":\"RateCodeId\",\"type\":\"integer\",\"nullable\
            ":true,\"metadata\":{}},
            {\"name\":\"RateCodeDesc\",\"type\":\"string\",\"nullable\
            ":true,\"metadata\":{}},
            {\"name\":\"RateCodeName\",\"type\":\"string\",\"nullable\
            ":true,\"metadata\":{}}]}",
        "partitionColumns": [],
        "configuration": {},
        "createdTime": 1680650616156
    }
}

这验证了添加列的模式演变规则。

源DataFrame中缺少数据列

接下来,让我们看看删除列的影响。回想一下规则: 如果Delta表中存在但在正在写入的DataFrame中不存在的列,则该列不会更改,并保留其现有值。新记录将在源DataFrame中缺少的列上具有null值。 在"02 - 模式演变"笔记本中的第3步中,有一个代码示例,我们在DataFrame中遗漏了RateCodeDesc列:

ini 复制代码
# Define the schema for the DataFrame
schema = StructType([
    StructField("RateCodeId", IntegerType(), True),
    StructField("RateCodeName", StringType(), True)
])

# Create a list of rows for the DataFrame
data = [(30, "C30"), (31, "C31"), (32, "C32")]

# Create a DataFrame from the list of rows and the schema
df = spark.createDataFrame(data, schema)

# Append the DataFrame to the table
df.write                         \
  .format("delta")               \
  .option("mergeSchema", "true") \
  .mode("append")                \
  .save("/mnt/datalake/book/chapter07/TaxiRateCode")

当我们现在查看Delta表中的数据时,我们看到以下情况:

sql 复制代码
+-----------+---------------------+------------+
|RateCodeId |   RateCodeDes       |RateCodeName|
+-----------+---------------------+------------+
|1          |Standard Rate        | null       |
|2          |JFK                  | null       |
|3          |Newark               | null       |
|4          |Nassau or Westchester| null       |
|5          |Negotiated fare      | null       |
|6          |Group ride           | null       |
|20         |Rate Code 20         | C20        |
|21         |Rate Code 21         | C21        |
|22         |Rate Code 22         | C22        |
|30         |null                 | C30        |
|31         |null                 | C31        |
|32         |null                 | C32        |
+-----------+---------------------+------------+

观察以下行为:

  • Delta表的模式保持不变。
  • 现有行的RateCodeDesc列值不会更改。
  • 新DataFrame的RateCodeDesc列的值设置为NULL,因为它们在DataFrame中不存在。

当我们查看相应的事务日志条目时,可以看到commitInfo和三个add部分(每个新源记录一个),但没有新的schemaString,这意味着模式没有更改:

json 复制代码
{
    "commitInfo": {
     ...
   }
}
{
    "add": {
        ...
        "stats": "{"numRecords":1,"minValues":{"RateCodeId":30,
                 "RateCodeName":"C30"},"maxValues":
                 {"RateCodeId":30,"RateCodeName":"C30"},"nullCount"
                 :{"RateCodeId":0,"RateCodeDesc":1,
                 "RateCodeName":0}}",
        ...
        }
    }
}
{
    "add": {
        ...
        "stats": "{"numRecords":1,"minValues":{"RateCodeId":31,
                 "RateCodeName":"C31"},"maxValues":
                 {"RateCodeId":31,"RateCodeName":"C31"},"nullCount"
                 :{"RateCodeId":0,"RateCodeDesc":1,
                 "RateCodeName":0}}",
        "tags": {
             ...
        }
    }
}
{
    "add": {
        ...
        "stats": "{"numRecords":1,"minValues":{"RateCodeId":32,
                 "RateCodeName":"C32"},"maxValues":
                  {"RateCodeId":32,"RateCodeName":"C32"},"nullCount"
                  :{"RateCodeId":0,"RateCodeDesc":1,
                  "RateCodeName":0}}",
        "tags": {
            ...
        }
    }
}

这验证了我们在介绍中提到的删除列的规则。

更改列数据类型

接下来,让我们看看更改列数据类型的影响。回想一下规则: 如果Delta表中存在与源DataFrame相同名称但数据类型不同的列,则Delta Lake将尝试将数据转换为新的数据类型。如果转换失败,将引发错误。 在"02 - 模式演变"笔记本的第4步中,我们将首先通过删除目录来重置表:

ini 复制代码
dbutils.fs.rm("dbfs:/mnt/datalake/book/chapter07/TaxiRateCode", recurse=True)

然后我们可以删除表:

sql 复制代码
%sql
drop table taxidb.taxiratecode;

接下来,我们重新创建表,但这次我们使用了RateCodeId的短数据类型:

bash 复制代码
# Read our CSV data, and change the data type of
# the RateCodeId to short
df = spark.read.format("csv")      \
        .option("header", "true") \
        .load("/mnt/datalake/book/chapter07/TaxiRateCode.csv")
df = df.withColumn("RateCodeId", df["RateCodeId"].cast(ShortType()))

# Write in Delta Lake format
df.write.format("delta")   \
        .mode("overwrite") \
        .save("/mnt/datalake/book/chapter07/TaxiRateCode")
   
# Print the schema
df.printSchema()

我们可以看到新的模式,并验证RateCodeId现在确实是一个短数据类型:

ini 复制代码
root
 |-- RateCodeId: short (nullable = true)
 |-- RateCodeDesc: string (nullable = true)

接下来,我们将尝试将RateCodeId列的数据类型从ShortType更改为IntegerType,这是模式演变的支持转换之一:

ini 复制代码
# Define the schema for the DataFrame
# Note that we now define the RateCodeId to be an
# Integer type
schema = StructType([
    StructField("RateCodeId", IntegerType(), True),
    StructField("RateCodeDesc", StringType(), True)
])

# Create a list of rows for the DataFrame
data = [(20, "Rate Code 20"), (21, "Rate Code 21"), (22, "Rate Code 22")]

# Create a DataFrame from the list of rows and the schema
df = spark.createDataFrame(data, schema)

# Write the DataFrame with Schema Evolution
df.write                         \
  .format("delta")               \
  .option("mergeSchema", "true") \
  .mode("append")                \
  .save("/mnt/datalake/book/chapter07/TaxiRateCode")

# Print the schema
df.printSchema()

此代码将成功执行并打印以下模式:

ini 复制代码
root
 |-- RateCodeId: integer (nullable = true)
 |-- RateCodeName: string (nullable = true)

在相应的事务日志条目中,使用IntegerType写入了新的schemaString:

python 复制代码
{
    "metaData": {
        "id": "7af3c5b8-0742-431f-b2d5-5634aa316e94",
        "format": {
            "provider": "parquet",
            "options": {}
        },
        "schemaString": "{"type":"struct","fields":[
            {"name":"RateCodeId","type":"integer","nullable":
            true,"metadata":{}},
            {"name":"RateCodeDesc","type":"string","nullable":
            true,"metadata":{}}]}",
        "partitionColumns": [],
        "configuration": {},
        "createdTime": 1680658999999
    }
}

目前,Delta Lake仅支持有限数量的转换:

  • 您可以从NullType转换为任何其他类型。
  • 您可以从ByteType向上转换为ShortType。
  • 您可以从ShortType向上转换为IntegerType(这是我们之前的用例)。

添加NullType列

在Delta Lake中,NullType()类型是一种有效的数据类型,用于表示可以包含null值的列,如"02 - 模式演变"笔记本的第5步所示:

python 复制代码
# Define the schema for the DataFrame
schema = StructType([
    StructField("RateCodeId", IntegerType(), True),
    StructField("RateCodeDesc", StringType(), True),
    StructField("RateCodeExp", NullType(), True)
])

# Create a list of rows for the DataFrame
data = [
    (50, "Rate Code 50", None),
    (51, "Rate Code 51", None),
    (52, "Rate Code 52", None)]

# Create a DataFrame from the list of rows and the schema
df = spark.createDataFrame(data, schema)

df.write                         \
  .format("delta")               \
  .option("mergeSchema", "true") \
  .mode("append")                \
  .save("/mnt/datalake/book/chapter07/TaxiRateCode")

# Print the schema
df.printSchema()

这个DataFrame的模式是:

ini 复制代码
root
 |-- RateCodeId: integer (nullable = true)
 |-- RateCodeDesc: string (nullable = true)
 |-- RateCodeExp: void (nullable = true)

当我们查看相应事务日志条目的元数据时,我们可以看到可空类型的反映:

swift 复制代码
"schemaString": "{\"type\":\"struct\",\"fields\":[
           {\"name\":\"RateCodeId\",\"type\":\"integer\",\"nullable\"
           :true,\"metadata\":{}},
           {\"name\":\"RateCodeDesc\",\"type\":\"string\",\"nullable\"
           :true,\"metadata\":{}},
           {\"name\":\"RateCodeExp\",\"type\":\"void\",\"nullable\"
           :true,\"metadata\":{}}]}",

我们可以看到数据类型反映为空。请注意,如果尝试使用SELECT *查询此表,将会出错:

sql 复制代码
%sql
SELECT
  *
FROM
  delta.`/mnt/datalake/book/chapter07/TaxiRateCode`

我们将得到如下异常:

ini 复制代码
java.lang.IllegalStateException: Couldn't find RateCodeExp#26346 
in [RateCodeId#26344,RateCodeDesc#26345]

此错误的原因是Delta Lake中的NullType列没有定义的模式,因此Spark无法推断列的数据类型。因此,当尝试运行SELECT *查询时,Spark无法将NullType列映射到特定的数据类型,查询失败。 如果要查询表,我们可以列出不包含NullType列的列:

sql 复制代码
%sql
SELECT
  RateCodeId,
  RateCodeDesc
FROM
  delta.`/mnt/datalake/book/chapter07/TaxiRateCode`

这将成功而没有任何问题。

显式Schema更新

到目前为止,我们已经利用模式演变使模式根据一些规则演变。让我们看看如何显式操纵Delta表的模式。首先,我们将使用SQL ALTER TABLE和ADD COLUMN命令向Delta表添加列。接下来,我们将使用SQL ALTER COLUMN语句向表列添加注释。然后,我们将使用ALTER TABLE命令的变体来更改表的列顺序。我们将查看Delta Lake列映射,因为它是以下内容所需的。

向表添加列

在"03 - 显式模式更新"笔记本的第3步中,我们有一个示例,演示如何使用SQL ALTER TABLE...ADD COLUMN命令向Delta表添加列:

sql 复制代码
%sql
ALTER TABLE delta.`/mnt/datalake/book/chapter07/TaxiRateCode`
ADD COLUMN RateCodeTaxPercent INT AFTER RateCodeId

请注意,我们使用了AFTER关键字,因此该列将在RateCodeId字段之后添加,而不是在列列表的末尾,这是在没有AFTER关键字的情况下的标准做法。类似地,我们可以使用FIRST关键字将新列添加到列列表的第一位置。 使用DESCRIBE命令查看模式时,我们可以看到新列确实在RateCodeId列之后插入:

sql 复制代码
+------------------+----------+--------+
|col_name          | data_type|comment |
+------------------+----------+--------+
|RateCodeId        |int       |  null  |
|RateCodeTaxPercent|int       |  null  |
|RateCodeDesc      |string    |  null  |
+------------------+----------+--------+

默认情况下,可空性被设置为true,因此新添加列的所有值都将设置为null:

sql 复制代码
+----------+------------------+---------------------+
|RateCodeId|RateCodeTaxPercent|RateCodeDesc         |
+----------+------------------+---------------------+
|1         |      null        |Standard Rate        |
|2         |      null        |JFK                  |
|3         |      null        |Newark               |          
|4         |      null        |Nassau or Westchester|
|5         |      null        |Negotiated fare      | 
|6         |      null        |Group ride           |
+----------+------------------+---------------------+

当我们查看添加列操作的事务日志条目时,可以看到:

  • 带有ADD COLUMN运算符的commitInfo操作。
  • 带有新schemaString的metaData操作。在schemaString中,我们看到了新的RateTaxCodePercent列:
swift 复制代码
{
    "commitInfo": {
        ...
        "operation": "ADD COLUMNS",
        "operationParameters": {
            "columns": "[{\"column\":{\"name\":\"RateCodeTaxPercent\",\"type\":
                        \"integer\",\"nullable\":true,
                          \"metadata\":{}},\"position\":\"AFTER RateCodeId\"}]"
        },
        ...
    }
}
{
    "metaData": {
        ...
        "schemaString": "{\"type\":\"struct\",\"fields\":[
            {\"name\":\"RateCodeId\", \"type\":\"integer\",\"nullable\":
            true,\"metadata\":{}},
            {\"name\":\"RateCodeTaxPercent\",\"type\":\"integer\",\"nullable\":
            true,\"metadata\":{}},
            {\"name\":\"RateCodeDesc\",\"type\":\"string\",\"nullable\":
            true,\"metadata\":{}}]}",
        "partitionColumns": [],
        "configuration": {},
        "createdTime": 1681168745910
    }
}

请注意,没有add或remove操作,因此不需要重新写入数据即可成功执行ADD COLUMN;Delta Lake唯一需要执行的操作是在metaData事务日志操作中更新schemaString。

向列添加注释

在"显式模式更新"笔记本的第3步中,我们看到如何使用带有ALTER COLUMN语句的SQL向Delta表添加注释。例如,如果我们有标准的taxidb.TaxiRateCode表,我们可以向列添加注释:

sql 复制代码
%sql
--
-- Add a comment to the RateCodeId column
--
ALTER TABLE taxidb.TaxiRateCode
ALTER COLUMN RateCodeId COMMENT 'This is the id of the Ride'

在相应的事务日志条目中,我们看到一个带有CHANGE COLUMN操作的commitInfo条目,以及注释的添加:

swift 复制代码
{
    "commitInfo": {
        ...
        "userName": "bennie.haelen@insight.com",
        "operation": "CHANGE COLUMN",
        "operationParameters": {
            "column": "{\"name\":\"RateCodeId\",\"type\":\"integer\",
                \"nullable\":
                true,\"metadata\":
                {\"comment\":\"This is the id of the Ride\"}}"
        },
        ...
    }
}

在元数据条目中,我们看到列的更新元数据:

swift 复制代码
"schemaString": "{\"type\":\"struct\",\"fields\":[
            {\"name\":\"RateCodeId\",\"type\":\"integer\",\"nullable\":
            true,\"metadata\":
                {\"comment\":\"This is the id of the Ride\"}},
            {\"name\":\"RateCodeDesc\",\"type\":\"string\",\"nullable\":
            true,\"metadata\":{}}]}",

我们还可以使用DESCRIBE HISTORY命令查看列的更改:

sql 复制代码
DESCRIBE HISTORY taxidb.TaxiRateCode

更改列顺序

默认情况下,Delta Lake仅收集前32列的统计信息。因此,如果有一个特定的列我们希望包含在统计信息中,我们可能会希望移动该列的列顺序。在"03 - 显式模式更新"笔记本的第4步中,我们可以看到如何使用ALTER TABLE和ALTER COLUMN来更改表的顺序。现在,表的顺序如下:

sql 复制代码
%sql
DESCRIBE taxidb.TaxiRateCode

+------------------+----------+--------------------------+
|col_name          | data_type|comment                   |
+------------------+----------+--------------------------+
|RateCodeId        |int       |This is the id of the Ride|
|RateCodeTaxPercent|int       |  null                    |
|RateCodeDesc      |string    |  null                    |
+------------------+----------+--------------------------+

假设我们想将RateCodeDesc列向上移动,使其在RateCodeId之后显示。我们可以使用ALTER COLUMN语法:

sql 复制代码
%sql
ALTER TABLE taxidb.TaxiRateCode  ALTER COLUMN RateCodeDesc AFTER RateCodeId

执行此语句后,模式将如下所示:

sql 复制代码
+------------------+----------+--------------------------+
|col_name          |    data_type|comment                |
+------------------+----------+--------------------------+
|RateCodeId        |int       |This is the id of the Ride|
|RateCodeDesc      |string    | null                     |
|RateCodeTaxPercent|int       | null                     |
+------------------+----------+--------------------------+

您可以在单个ALTER COLUMN语句中组合列排序和添加注释。此操作将保留表中的所有数据。

Delta Lake列映射

列映射允许Delta Lake表和底层Parquet文件列使用不同的名称。这使得Delta Lake表可以进行诸如RENAME COLUMN和DROP COLUMN之类的模式演变,而无需重写底层的Parquet文件。

Delta Lake支持Delta Lake表的列映射,从而使得可以进行元数据的唯一更改,标记列为删除或重命名,而无需重新编写数据文件。它还允许用户使用Parquet不允许的字符来命名Delta表列,例如空格,以便用户可以直接将CSV或JSON数据摄入到Delta Lake中,而无需因为先前的字符约束而重新命名列。

列映射需要以下Delta Lake协议:

  • 读取器版本2或以上
  • 写入器版本5或以上

一旦Delta表具有所需的协议版本,您可以通过将delta.columnmapping.mode设置为name来启用列映射。

在"03 - 显式模式更新"笔记本的第4步中,我们可以看到要检查表的读取器和写入器协议版本,我们可以使用DESCRIBE EXTENDED命令:

sql 复制代码
%sql
DESCRIBE EXTENDED taxidb.TaxiRateCode
+-----------------+-----------------------------------------------------+
|col_name         |data_type                                            |
+-----------------+-----------------------------------------------------+
|RateCodeId       |int                                                  |
|  .........      |                                                     |
|Table Properties |[delta.minReaderVersion=1,delta.minWriterVersion=2]  |
+-----------------+-----------------------------------------------------+

我们看到表不符合列映射所需的协议版本。我们可以使用以下SQL语句更新版本和delta.columnmapping.mode:

sql 复制代码
%sql
 ALTER TABLE taxidb.TaxiRateCode  SET TBLPROPERTIES (
    'delta.minReaderVersion' = '2',
    'delta.minWriterVersion' = '5',
    'delta.columnMapping.mode' = 'name'
  )

当我们查看SET TBLPROPERTIES语句的相应日志条目时,我们看到了相当多的更改。 首先,我们看到了一个带有SET TBLPROPERTIES条目的commitInfo操作:

swift 复制代码
{
    "commitInfo": {
        ...
        "operation": "SET TBLPROPERTIES",
        "operationParameters": {
            "properties": "{\"delta.minReaderVersion\":\"2\",
                            \"delta.minWriterVersion\":\"5\",
                            \"delta.columnMapping.mode\":\"name\"}"
        },
        ...
    }
}

接下来,我们看到一个协议操作,通知我们minReader和minWriter版本已经更新:

json 复制代码
{
    "protocol": {
        "minReaderVersion": 2,
        "minWriterVersion": 5
    }
}

最后,我们看到一个带有schemaString的metaData条目。但现在,列映射已添加到schemaString中:

python 复制代码
{
    "metaData": {
        ...,
        "schemaString": "{"type":"struct","fields":[
            {"name":"RateCodeId","type":"integer","nullable":true,
                "metadata":{"comment":"This is the id of the Ride",
                "delta.columnMapping.id":1,"delta.columnMapping.physicalName\
                ":"RateCodeId"}},
            {"name":"RateCodeDesc","type":"string","nullable":true,
                "metadata":{"delta.columnMapping.id":2,
                "delta.columnMapping.physicalName":"RateCodeDesc"}},
            {"name":"RateCodeTaxPercent","type":"integer","nullable":
            true,
                "metadata":{"delta.columnMapping.id":3,
                "delta.columnMapping.physicalName":"RateCodeTaxPercent"}}]}",
        ..,
        "configuration": {
            "delta.columnMapping.mode": "name",
            "delta.columnMapping.maxColumnId": "3"
        },
        ..
    }
}

对于每一列,您有:

  • 名称,即Delta Lake的官方列名(例如,RateCodeId)。
  • delta.columnMapping.id,即列的ID。此ID将保持稳定。
  • delta.columnMapping.physicalName,即Parquet文件中的物理名称。

重命名列

您可以使用ALTER TABLE...RENAME COLUMN来重命名列,而无需重新编写该列的任何现有数据。请注意,为了启用此功能,需要有列映射。假设我们要将RateCodeDesc列重命名为更具描述性的RateCodeDescription:

sql 复制代码
%sql
-- Perform our column rename
ALTER TABLE taxidb.taxiratecode RENAME COLUMN RateCodeDesc to RateCodeDescription

当我们查看相应的日志条目时,我们可以看到在schemaString中反映了重命名:

swift 复制代码
"schemaString": "{\"type\":\"struct\",\"fields\":[
            ...
            {\"name\":\"RateCodeDescription\",\"type\":\"string\",\"nullable\"
            :true,
                  \"metadata\":{\"delta.columnMapping.id\":
                  2,\"delta.columnMapping.physicalName\":\"RateCodeDesc\"}},
            ...

我们看到Delta Lake列名已更改为RateCodeDescription,但Parquet文件中的physicalName仍为RateCodeDesc。这就是Delta Lake如何执行复杂的DDL操作(例如RENAME COLUMN)而无需重写任何文件,作为一个简单的元数据操作。

替换表列

在Delta Lake中,可以使用ALTER TABLE REPLACE COLUMNS命令将现有Delta表的所有列替换为一组新列。请注意,为了执行此操作,您需要启用Delta Lake列映射,如前一部分所述。 一旦启用了列映射,我们可以使用REPLACE COLUMNS命令:

sql 复制代码
%sql
ALTER TABLE taxidb.TaxiRateCode
REPLACE COLUMNS (
  Rate_Code_Identifier  INT    COMMENT 'Identifies the code',
  Rate_Code_Description STRING COMMENT 'Describes the code',
  Rate_Code_Percentage  INT    COMMENT 'Tax percentage applied'
)

当我们查看模式时,我们看到以下内容:

sql 复制代码
%sql
DESCRIBE EXTENDED taxidb.TaxiRateCode

+---------------------+-----------------------------------------------------+
|col_name             |data_type                                            |
+---------------------+-----------------------------------------------------+
|Rate_Code_Identifier | int                                                 |
|Rate_Code_Description| string                                              |
|Rate_Code_Percentage | int                                                 |
|  .........          |   ......                                            |
|Table Properties     |[delta.columnMapping.maxColumnId=6,                  |
|                     |  delta.columnMapping.mode=name,                     |
|                     |delta.minReaderVersion=2,delta.minWriterVersion=5]   |
+---------------------+-----------------------------------------------------+

在DESCRIBE输出中,我们可以看到新的模式,还可以看到最小的读取器和写入器版本。 当我们查看相应的事务日志条目时,我们看到了带有REPLACE COLUMNS操作的commitInfo:

swift 复制代码
"commitInfo": {
        ...
        "operation": "REPLACE COLUMNS",
        "operationParameters": {
            "columns": "[
                {\"name\":\"Rate_Code_Identifier\",\"type\":\"integer\",\
                "nullable\":true,
                       \"metadata\":{\"comment\":\"Identifies the code\"}},
                {\"name\":\"Rate_Code_Description\",\"type\":\"string\",\
                "nullable\":true,
                       \"metadata\":{\"comment\":\"Describes the code\"}},
                {\"name\":\"Rate_Code_Percentage\",\"type\":\"integer\",\
                "nullable\":true,
                        \"metadata\":{\"comment\":\"Tax percentage applied\"}}]"
        },
        ...
    }
 }

在metaData部分,我们看到了带有一些有趣信息的新schemaString。新的Delta Lake列现在映射到基于guide的列名,具有新的ID(从4开始):

python 复制代码
{
    "metaData": {
    ...,
    "schemaString": "{"type":"struct","fields":[
        {"name":"Rate_Code_Identifier","type":"integer",
        "nullable":true,
            "metadata":{"comment":"Identifies the code", 
            "delta.columnMapping.id":4,
                "delta.columnMapping.physicalName":
                "col-72397feb-3cb0-4613-baad-aa78fff64a40"}},
        {"name":"Rate_Code_Description","type":
        "string","nullable":true,
    "metadata":{"comment":"Describes the code",
    "delta.columnMapping.id":5,
    "delta.columnMapping.physicalName":
    "col-67d47d0c-5d25-45d8-8d0e-c9b13f5f2c6e"}},
    {"name":"Rate_Code_Percentage","type":"integer","nullable":true,
    "metadata":{"comment":"Tax percentage applied",
    "delta.columnMapping.id":"delta.columnMapping.physicalName":
    "col-3b8f9847-71df-4e64-a921-64c918de328d"}}]}", ...
    "configuration": {
    "delta.columnMapping.mode": "name",
    "delta.columnMapping.maxColumnId": "6"
    },
    ...
    }
    }

当我们查看数据时,我们看到所有六行,但所有列都设置为null:

sql 复制代码
+--------------------+---------------------+--------------------+
|Rate_Code_Identifier|Rate_Code_Description|Rate_Code_Percentage|
+--------------------+---------------------+--------------------+
| null               |  null               | null               |
| null               |  null               | null               |
| null               |  null               | null               |
| null               |  null               | null               |
| null               |  null               | null               |
| null               |  null               | null               |
+--------------------+---------------------+--------------------+

REPLACE COLUMNS操作将所有列值设置为null,因为新的模式可能具有与旧模式不同的数据类型或不同顺序的列。因此,表中的现有数据可能不符合新模式。因此,Delta Lake将所有列的值设置为null,以确保新模式一致应用于表中的所有记录。

删除列

Delta Lake现在支持删除列作为仅元数据操作,而无需重写任何数据文件。请注意,此操作必须启用列映射。 重要的是要注意,从元数据中删除列不会删除文件中该列的底层数据。要清除已删除列的数据,您可以使用REORG TABLE重写文件。然后,您可以使用VACUUM命令物理删除包含已删除列数据的文件。 让我们从taxidb.TaxiRateCode表中的标准模式开始:

ini 复制代码
root
 |-- RateCodeId: integer (nullable = true)
 |-- RateCodeDesc: string (nullable = true)

假设我们想要删除RateCodeDesc列。我们可以使用ALTER TABLE与DROP COLUMN SQL命令来执行此操作:

sql 复制代码
%sql
-- Use the ALTER TABLE... DROP COLUMN command
-- to drop the RateCodeDesc column
ALTER TABLE taxidb.TaxiRateCode DROP COLUMN RateCodeDesc

当我们使用DESCRIBE命令查看模式时,我们看到只剩下RateCodeId列:

sql 复制代码
+----------+---------+---------+        
|col_name  |data_type|  comment|
+----------+---------+---------+        
|RateCodeId|int      |  null   |
+----------+---------+---------+
相关推荐
lucky_syq1 分钟前
Spark和Hadoop之间的区别
大数据·hadoop·spark
小白学大数据1 小时前
高级技术文章:使用 Kotlin 和 Unirest 构建高效的 Facebook 图像爬虫
爬虫·数据分析·kotlin
小蜗牛慢慢爬行1 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
小扳3 小时前
微服务篇-深入了解 MinIO 文件服务器(你还在使用阿里云 0SS 对象存储图片服务?教你使用 MinIO 文件服务器:实现从部署到具体使用)
java·服务器·分布式·微服务·云原生·架构
WTT001111 小时前
2024楚慧杯WP
大数据·运维·网络·安全·web安全·ctf
盛派网络小助手11 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
云云32115 小时前
怎么通过亚矩阵云手机实现营销?
大数据·服务器·安全·智能手机·矩阵
新加坡内哥谈技术15 小时前
苏黎世联邦理工学院与加州大学伯克利分校推出MaxInfoRL:平衡内在与外在探索的全新强化学习框架
大数据·人工智能·语言模型
快乐非自愿16 小时前
分布式系统架构2:服务发现
架构·服务发现