无法在区分大小写的 Athena 中加载分区
Unable to load partitions in Athena with case sensitivity ON
我在 S3 中有数据分区在 YYYY/MM/DD/HH/
结构中(不是 year=YYYY/month=MM/day=DD/hour=HH
)
我为此设置了一个 Glue 爬虫,它在 Athena 中创建了一个 table,但是当我在 Athena 中查询数据时,它给出了一个错误,因为一个字段有重复的名称(URL
和url
,SerDe 将其转换为小写,导致名称冲突)。
为了解决这个问题,我手动创建了另一个 table(使用上面的 table 定义 SHOW CREATE TABLE),将 'case.insensitive'= FALSE
添加到 SERDEPROPERTIES
WITH SERDEPROPERTIES ('paths'='deviceType,emailId,inactiveDuration,pageData,platform,timeStamp,totalTime,userId','case.insensitive'= FALSE)
我将 s3 目录结构更改为与配置单元兼容的命名 year=/month=/day=/hour=
,然后用 'case.insensitive'= FALSE
创建了 table,然后 运行 MSCK REPAIR TABLE
新 table 的命令,它加载所有分区。
(Complete CREATE TABLE QUERY)
但是一查询,只找到1个数据列(platform
)和分区列,其余列都没有解析。但我实际上已经复制了 Glue 生成的 CREATE TABLE 查询,条件是 case_insensitive=false
。
我该如何解决这个问题?
我认为您有多个不同的问题:一个与爬虫有关,一个与 serde 有关,还有一个与重复键有关:
粘胶爬虫
如果 Glue Crawler 兑现了他们的承诺,那么在大多数情况下它们将是一个相当不错的解决方案,并且可以避免我们一遍又一遍地编写相同的代码。不幸的是,如果您偏离了 Glue Crawler 设计的(未记录的)用例,您通常会遇到各种问题,从奇怪到完全损坏(参见示例 this question, this question, this question, this question, , or this question)。
我建议您跳过 Glue Crawler,而是手动编写 table DDL(您在爬虫创建的内容中有一个很好的模板,但它还不够好)。然后,您编写一个 Lambda 函数(或 shell 脚本),您 运行 按计划添加新分区。
因为你的分区只是按时的,所以这是一个相当简单的脚本:它只需要每隔一段时间运行并为下一个时期添加分区。
您的数据似乎来自 Kinesis Data Firehose,它以小时为粒度生成分区结构。除非您每小时都有大量数据,否则我建议您创建一个仅按日期分区的 table,并且每天 运行 Lambda 函数或脚本一次以添加第二天的分区。
不使用 Glue Crawler 的一个好处是您不必在路径组件和分区键之间建立一对一的对应关系。您可以有一个键入为 date
的分区键,并像这样添加分区:ALTER TABLE foo ADD PARTITION (dt = '2020-05-13') LOCATION 's3://some-bucket/data/2020/05/13/'
。这很方便,因为在完整日期上进行范围查询比在组件分开时要容易得多。
如果你真的需要每小时的粒度,你可以有两个分区键,一个是日期,一个是小时,或者只有一个带有完整时间戳的分区键,例如ALTER TABLE foo ADD PARTITION (ts = '2020-05-13 10:00:00') LOCATION 's3://some-bucket/data/2020/05/13/10/'
。然后 运行 每小时 Lambda 函数或脚本,添加下一个小时的分区。
过于精细的分区对性能没有帮助,反而会损害性能(尽管性能损失主要来自小文件和目录)。
SerDe 配置
至于为什么你只看到 platform
列的值,这是因为这是唯一的列名和 属性 具有相同大小写的情况。
您 link 使用的 DDL 不起作用有点令人惊讶,但我可以确认它确实不起作用。我尝试从该 DDL 创建一个 table,但没有 pagedata
列(我也跳过了分区,但这对测试应该没有影响),实际上只有 platform
列在我查询 table.
时有任何值
但是,当我删除 case.insensitive
serde 属性 时,它按预期工作,这让我想到它可能不会像您认为的那样工作。我尝试将其设置为 TRUE
而不是 FALSE
,这使得 table 再次按预期工作。我认为我们可以由此得出结论,Athena 文档中所说的 "By default, Athena requires that all keys in your JSON dataset use lowercase" 是错误的。事实上,Athena 将列名称小写,但在读取 JSON.
时也会将 属性 名称小写
通过进一步的实验,结果证明 path
属性 也是多余的。这是对我有用的table:
CREATE EXTERNAL TABLE `json_case_test` (
`devicetype` string,
`timestamp` string,
`totaltime` string,
`inactiveduration` int,
`emailid` string,
`userid` string,
`platform` string
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://some-bucket/data/'
我会说 case.insensitive
似乎造成的问题多于它解决的问题。
重复键
当我添加 pagedata
列(如 struct<url:string>
)并向数据添加 "pageData":{"URL":"URL","url":"url"}
时,出现错误:
HIVE_CURSOR_ERROR: Row is not a valid JSON Object - JSONException: Duplicate key "url"
并且无论查询中是否涉及 pagedata
列,我都得到了错误(例如 SELECT userid FROM json_case_test
也出错了)。我用 TRUE
和 FALSE
尝试了 case.insensitive
serde 属性,但没有效果。
接下来我看了一下the source documentation for the serde,首先写得好多了,其次有一个关键的信息:关闭时还需要为列提供映射不区分大小写。
通过以下 serde 属性,我能够解决重复密钥问题:
WITH SERDEPROPERTIES (
"case.insensitive" = "false",
"mapping.pagedata" = "pageData",
"mapping.pagedata.url" = "pagedata.url",
"mapping.pagedata.url2"= "pagedata.URL"
)
您还必须为除 platform
之外的所有列提供映射。
备选方案:使用 JSON 函数
您在对此答案的评论中提到 pageData
属性 的架构不是恒定的。这是另一种情况,不幸的是,Glue Crawlers 并没有真正起作用。如果你运气不好,你最终会得到一个包含一些属性的不稳定模式(例如 this question)。
当我看到您的评论时,我意识到您的问题还有另一种解决方案:手动设置 table(如上所述)并使用 string
作为 pagedata
栏目。然后你可以使用像 JSON_EXTRACT_SCALAR
这样的函数在查询期间提取你想要的属性。
此解决方案以增加查询的复杂性为代价,以减少试图跟上不断发展的架构的麻烦。
我在 S3 中有数据分区在 YYYY/MM/DD/HH/
结构中(不是 year=YYYY/month=MM/day=DD/hour=HH
)
我为此设置了一个 Glue 爬虫,它在 Athena 中创建了一个 table,但是当我在 Athena 中查询数据时,它给出了一个错误,因为一个字段有重复的名称(URL
和url
,SerDe 将其转换为小写,导致名称冲突)。
为了解决这个问题,我手动创建了另一个 table(使用上面的 table 定义 SHOW CREATE TABLE),将 'case.insensitive'= FALSE
添加到 SERDEPROPERTIES
WITH SERDEPROPERTIES ('paths'='deviceType,emailId,inactiveDuration,pageData,platform,timeStamp,totalTime,userId','case.insensitive'= FALSE)
我将 s3 目录结构更改为与配置单元兼容的命名 year=/month=/day=/hour=
,然后用 'case.insensitive'= FALSE
创建了 table,然后 运行 MSCK REPAIR TABLE
新 table 的命令,它加载所有分区。
(Complete CREATE TABLE QUERY)
但是一查询,只找到1个数据列(platform
)和分区列,其余列都没有解析。但我实际上已经复制了 Glue 生成的 CREATE TABLE 查询,条件是 case_insensitive=false
。
我该如何解决这个问题?
我认为您有多个不同的问题:一个与爬虫有关,一个与 serde 有关,还有一个与重复键有关:
粘胶爬虫
如果 Glue Crawler 兑现了他们的承诺,那么在大多数情况下它们将是一个相当不错的解决方案,并且可以避免我们一遍又一遍地编写相同的代码。不幸的是,如果您偏离了 Glue Crawler 设计的(未记录的)用例,您通常会遇到各种问题,从奇怪到完全损坏(参见示例 this question, this question, this question, this question,
我建议您跳过 Glue Crawler,而是手动编写 table DDL(您在爬虫创建的内容中有一个很好的模板,但它还不够好)。然后,您编写一个 Lambda 函数(或 shell 脚本),您 运行 按计划添加新分区。
因为你的分区只是按时的,所以这是一个相当简单的脚本:它只需要每隔一段时间运行并为下一个时期添加分区。
您的数据似乎来自 Kinesis Data Firehose,它以小时为粒度生成分区结构。除非您每小时都有大量数据,否则我建议您创建一个仅按日期分区的 table,并且每天 运行 Lambda 函数或脚本一次以添加第二天的分区。
不使用 Glue Crawler 的一个好处是您不必在路径组件和分区键之间建立一对一的对应关系。您可以有一个键入为 date
的分区键,并像这样添加分区:ALTER TABLE foo ADD PARTITION (dt = '2020-05-13') LOCATION 's3://some-bucket/data/2020/05/13/'
。这很方便,因为在完整日期上进行范围查询比在组件分开时要容易得多。
如果你真的需要每小时的粒度,你可以有两个分区键,一个是日期,一个是小时,或者只有一个带有完整时间戳的分区键,例如ALTER TABLE foo ADD PARTITION (ts = '2020-05-13 10:00:00') LOCATION 's3://some-bucket/data/2020/05/13/10/'
。然后 运行 每小时 Lambda 函数或脚本,添加下一个小时的分区。
过于精细的分区对性能没有帮助,反而会损害性能(尽管性能损失主要来自小文件和目录)。
SerDe 配置
至于为什么你只看到 platform
列的值,这是因为这是唯一的列名和 属性 具有相同大小写的情况。
您 link 使用的 DDL 不起作用有点令人惊讶,但我可以确认它确实不起作用。我尝试从该 DDL 创建一个 table,但没有 pagedata
列(我也跳过了分区,但这对测试应该没有影响),实际上只有 platform
列在我查询 table.
但是,当我删除 case.insensitive
serde 属性 时,它按预期工作,这让我想到它可能不会像您认为的那样工作。我尝试将其设置为 TRUE
而不是 FALSE
,这使得 table 再次按预期工作。我认为我们可以由此得出结论,Athena 文档中所说的 "By default, Athena requires that all keys in your JSON dataset use lowercase" 是错误的。事实上,Athena 将列名称小写,但在读取 JSON.
通过进一步的实验,结果证明 path
属性 也是多余的。这是对我有用的table:
CREATE EXTERNAL TABLE `json_case_test` (
`devicetype` string,
`timestamp` string,
`totaltime` string,
`inactiveduration` int,
`emailid` string,
`userid` string,
`platform` string
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://some-bucket/data/'
我会说 case.insensitive
似乎造成的问题多于它解决的问题。
重复键
当我添加 pagedata
列(如 struct<url:string>
)并向数据添加 "pageData":{"URL":"URL","url":"url"}
时,出现错误:
HIVE_CURSOR_ERROR: Row is not a valid JSON Object - JSONException: Duplicate key "url"
并且无论查询中是否涉及 pagedata
列,我都得到了错误(例如 SELECT userid FROM json_case_test
也出错了)。我用 TRUE
和 FALSE
尝试了 case.insensitive
serde 属性,但没有效果。
接下来我看了一下the source documentation for the serde,首先写得好多了,其次有一个关键的信息:关闭时还需要为列提供映射不区分大小写。
通过以下 serde 属性,我能够解决重复密钥问题:
WITH SERDEPROPERTIES (
"case.insensitive" = "false",
"mapping.pagedata" = "pageData",
"mapping.pagedata.url" = "pagedata.url",
"mapping.pagedata.url2"= "pagedata.URL"
)
您还必须为除 platform
之外的所有列提供映射。
备选方案:使用 JSON 函数
您在对此答案的评论中提到 pageData
属性 的架构不是恒定的。这是另一种情况,不幸的是,Glue Crawlers 并没有真正起作用。如果你运气不好,你最终会得到一个包含一些属性的不稳定模式(例如 this question)。
当我看到您的评论时,我意识到您的问题还有另一种解决方案:手动设置 table(如上所述)并使用 string
作为 pagedata
栏目。然后你可以使用像 JSON_EXTRACT_SCALAR
这样的函数在查询期间提取你想要的属性。
此解决方案以增加查询的复杂性为代价,以减少试图跟上不断发展的架构的麻烦。