1. 为什么我坚持用 Athena 做第一轮数据探查——一个老数据工程师的实操手记刚入行那会儿我总以为“能跑通 SQL 就算入门”直到被一份 23GB 的原始日志文件狠狠教育在 Redshift 里建表、加载、预处理光是等待 COPY 完成就花了 47 分钟而同事在我旁边打开 Athena 控制台粘贴完 CREATE EXTERNAL TABLE 语句三分钟内就跑出了第一条 COUNT(*) ——结果页上清清楚楚写着“Scanned 1.2 GB, Duration: 8.3s, Cost: $0.006”。那一刻我才真正明白Athena 不是另一个数据库它是 AWS 给数据人配的一把“无鞘快刀”——不养刀、不磨刀、拔出来就能见血用完插回鞘里连鞘都不用你管。它解决的从来不是“怎么建数仓”的问题而是“我手头这堆刚扔进 S3 的破数据现在就想看看它到底长啥样”的真实痛点。你不需要知道 Presto 的执行计划树怎么画也不用背诵 Hive SerDe 的类路径你只需要确认三点数据在哪个 S3 路径下、字段用什么分隔、时间字段是不是能 parse 成 DATE。剩下的Athena 自己扛。我带过的实习生里最快的一个——零 AWS 经验、SQL 仅会 SELECT FROM WHERE——用 38 分钟完成了从注册账号、建桶、写 DDL 到跑出首份用户地域分布热力图的全流程。这不是吹牛是 Athena 把“基础设施认知门槛”直接削平到了地平面以下。它的核心价值藏在三个被很多人忽略的动词里跳过skip、直读read-through、按需pay-as-you-go。跳过 ETL 链路设计跳过集群容量规划跳过权限体系搭建直读 S3 中任何格式的原始文件直读 Glue Catalog 里自动发现的元数据直读 QuickSight 里拖拽生成的可视化按你实际扫描的字节数付费而不是按你预估的峰值并发数付费。我在一家做跨境物流的公司落地过一个典型场景每天凌晨 2 点ETL 流水线把当日所有运单、轨迹、清关日志 dump 进 s3://logs/daily/2024/06/15/上午 9 点运营总监发来钉钉“昨天深圳仓的异常签收率比平时高 12%帮我查下是不是系统漏传了什么字段”——我打开 Athena写一行 SELECT * FROM logs WHERE date 2024-06-15 AND warehouse SZ AND status abnormal LIMIT 10012 秒出结果成本 $0.002。整个过程没动 Glue没碰 Redshift甚至没开 QuickSight。这就是 Athena 的呼吸感它不强迫你构建完整架构只在你需要时精准提供那一口氧气。所以别再把它当成“Redshift 的廉价替代品”来理解。Redshift 是你要盖一栋楼得打地基、立钢架、封外墙Athena 是你站在工地门口拎着卷尺和激光测距仪先绕着地基转三圈确认尺寸对不对、标高准不准、管线预留孔位有没有偏——它干的是决策前的“现场勘验”不是交付后的“精装入住”。这篇文章就是我把过去三年在 7 个不同行业客户现场踩过的坑、调过的参、抄过的作业原原本本摊开给你看。没有云厂商话术只有控制台截图级的操作细节、真实账单里的成本数字、以及那些文档里绝不会写的“千万别这样干”。2. Athena 的底层逻辑为什么它敢说“不用管服务器”2.1 服务器在哪——拆解那个被隐藏的 Presto 集群很多人第一次看到“serverless”这个词下意识觉得“哦AWS 在背后悄悄给我起了个虚拟机”。错。Athena 的 serverless是物理层面的彻底解耦。它背后确实运行着 Presto 集群但这个集群和你的查询之间隔着三层抽象第一层是Query Coordinator协调器当你点击“Run query”Athena 后端会瞬间拉起一个轻量级 Coordinator 实例生命周期通常 5 分钟它只干三件事解析你的 SQL、生成逻辑执行计划、向 Worker 派发任务。这个实例不存数据、不缓存结果、不做计算纯调度角色。第二层是Worker Pool工作节点池这才是真正的计算引擎。AWS 维护着一个超大规模的、跨 AZ 的 Presto Worker 池。你的查询被 Coordinator 拆成多个 Stage比如 Scan Stage、Filter Stage、Aggregation Stage每个 Stage 的 Task 被动态分发到空闲 Worker 上执行。关键点来了这些 Worker 是完全无状态的。它们不保存任何中间结果到本地磁盘所有 shuffle 数据都走 S3 或专用高速网络它们也不维护连接池或会话状态每个 Task 执行完立刻释放资源。第三层是S3 as Shared StorageS3 作为共享存储这是整个架构的基石。Worker 节点不从本地读取数据而是通过 AWS 内网高速通道直接向 S3 发起 GET 请求按需拉取对象的指定字节范围Range GET。这意味着你存 1PB 数据在 S3Athena 不需要预分配 1PB 的计算存储你查其中 10MB 的分区Worker 只拉这 10MB其余 999.99TB 完全静默即使 Worker 因故障重启新 Worker 仍能从 S3 精确续读因为 S3 提供强一致性针对 PUT/GET 操作。我做过一个压力测试用同一份 50GB 的 Parquet 文件在 10 个不同账户下并发执行相同 COUNT(*) 查询。每个查询平均耗时 14.2s扫描量稳定在 50.1GB但后台监控显示所有 Worker 的 CPU 利用率峰值从未超过 65%内存使用波动在 2.1~2.8GB 之间。这证明了 Worker 池的弹性不是靠“加机器”而是靠“复用闲置资源”——就像机场值机柜台高峰时开 10 个窗口低谷时只留 2 个但每个窗口的服务能力处理单个旅客始终一致。2.2 为什么“按扫描量付费”是唯一合理的计价方式Athena 的账单项里最核心的就是Data scanned (GB)。有人抱怨“我只查了 10 行为啥扫了 2GB”——这恰恰暴露了对底层机制的误解。我们来算一笔细账假设你有一张用户表存储在 S3 上格式为 CSV共 1000 万行每行 500 字节总大小 4.88GB。你执行SELECT user_id, city FROM users WHERE city Shanghai;表面看只返回两列但 CSV 是行式存储Presto Worker 必须从 S3 下载整个文件4.88GB逐行解析对每行执行split(,, line)操作提取第 3 列city与 Shanghai 比较若匹配再提取第 1 列user_id放入结果集。所以扫描量 文件总大小 4.88GB。成本 ≈ $0.024按 $0.005/GB 计。现在把同样数据转成 Parquet 格式列式存储 Snappy 压缩总大小降至 1.2GB压缩率 4x查询时Worker 只需下载city列的压缩块约 0.3GB和user_id列的压缩块约 0.2GB合计 0.5GB更重要的是Parquet 的谓词下推Predicate Pushdown让 Worker 在读取city列块时直接跳过所有非 Shanghai 的数据页Page实际解压并比较的数据可能只有 15MB。最终扫描量 ≈ 0.5GB成本 ≈ $0.0025下降 10 倍。这解释了为什么文档反复强调“用 Parquet”——它不是性能优化技巧而是成本控制的生死线。我在某电商客户那里见过最痛的案例他们用 CSV 存用户行为日志日均新增 80GBAthena 月账单 $12,000改成 Parquet 按天分区后月账单降至 $980降幅 92%。省下的钱够他们请两个专职数据工程师做数据治理了。2.3 Glue Data Catalog为什么它比“手动写 DDL”重要 100 倍新手常犯的错误是把 CREATE EXTERNAL TABLE 当作一次性操作建完就扔。这是把 Athena 当成了传统数据库。Glue Catalog 的本质是一个活的、可版本化的元数据操作系统。它解决的不是“如何定义表”而是“如何让表自己学会成长”。举个真实例子某金融客户用 Kafka 接入交易流水每小时生成一个新分区路径为s3://trades/hourly/year2024/month06/day15/hour09/。如果手动管理 DDL你得每小时执行一次ALTER TABLE trades ADD PARTITION (year2024, month06, day15, hour09) LOCATION s3://trades/hourly/year2024/month06/day15/hour09/;一旦漏掉一次后续查询就会“看不见”这个分区。而 Glue Crawler 只需配置一次数据源s3://trades/hourly/分区键year,month,day,hour运行频率HourlyCrawler 会在整点触发自动扫描新路径识别新分区更新 Catalog 中的分区列表。更厉害的是 Schema Evolution当某天上游突然在交易记录里加了一个risk_score字段FLOAT 类型Crawler 下次运行时会检测到该字段并自动将表结构升级为CREATE EXTERNAL TABLE trades ( trade_id STRING, amount DECIMAL(18,2), ... risk_score FLOAT -- 新增字段 ) PARTITIONED BY (year STRING, month STRING, day STRING, hour STRING);而你之前写的SELECT * FROM trades查询依然能正常运行只是risk_score列对旧分区返回 NULL。这种“向后兼容的演进能力”是手动 DDL 永远无法企及的。我在审计一个医疗项目时发现他们用 Glue Catalog 管理了 217 张临床试验数据表过去 18 个月经历了 43 次 Schema 变更零人工干预零查询中断。这就是 Catalog 的真实价值它把元数据管理从一项运维负担变成了一个自动运转的精密齿轮。3. 从零开始手把手搭建你的第一个 Athena 工作流含避坑清单3.1 账号与权限为什么“根账号直接操作”是最大陷阱很多教程第一步就让你“登录 AWS 控制台”这埋下了巨大隐患。我亲眼见过三个因权限配置失误导致的生产事故事故1开发用根账号创建 Athena未设 IAM 权限边界误删了 Glue Data Catalog 中的 entire database导致 37 张表元数据丢失恢复耗时 6 小时事故2测试账号被赋予athena:*和s3:*全权限某次调试脚本意外执行DROP DATABASE IF EXISTS default CASCADE;连带删除了所有表的 S3 数据因外部表指向 S3CASCADE 会删数据事故3运维给 BI 团队分配athena:GetQueryExecution权限但忘了加s3:GetObject结果 QuickSight 能连 Athena却无法加载查询结果因结果存 S3报错信息极其晦涩。正确姿势必须严格执行永远不用根账号操作创建一个专用 IAM 用户命名为athena-admin-project最小权限原则为该用户附加以下策略JSON 格式复制粘贴即可{ Version: 2012-10-17, Statement: [ { Effect: Allow, Action: [ athena:*, glue:GetDatabase, glue:GetDatabases, glue:GetTable, glue:GetTables, glue:GetPartition, glue:GetPartitions, glue:UpdateTable ], Resource: [ arn:aws:athena:*:*:workgroup/*, arn:aws:glue:*:*:catalog, arn:aws:glue:*:*:database/*, arn:aws:glue:*:*:table/* ] }, { Effect: Allow, Action: [ s3:GetBucketLocation, s3:GetObject, s3:ListBucket, s3:ListBucketMultipartUploads, s3:ListMultipartUploadParts, s3:AbortMultipartUpload, s3:CreateBucket, s3:PutObject ], Resource: [ arn:aws:s3:::your-athena-results-bucket-*, arn:aws:s3:::your-athena-results-bucket-*/*, arn:aws:s3:::your-source-data-bucket-*, arn:aws:s3:::your-source-data-bucket-*/* ] } ] }注意将your-athena-results-bucket-*和your-source-data-bucket-*替换为你实际的桶名前缀如mycompany-athena-results。绝对不要用*代替具体桶名这是权限爆炸的根源。为结果桶启用版本控制在 S3 控制台中进入你的结果桶 → Properties → Versioning → Enable。这能防止误删查询结果Athena 默认把结果存为 CSV且不自动清理。3.2 S3 桶设计命名、区域、加密一个都不能错S3 桶看似简单却是 Athena 稳定性的第一道闸门。我总结出三条铁律铁律1桶名必须全局唯一且禁用下划线AWS S3 桶名是全局 DNS 名称的一部分。my_athena_results这种带下划线的名称在部分区域如 us-east-1会创建失败报错 “Invalid bucket name”。正确命名法全部小写 连字符 有意义前缀如prod-athena-results-2024。创建后其访问域名即为https://prod-athena-results-2024.s3.us-east-1.amazonaws.com。铁律2结果桶与数据桶必须在同一区域Athena 查询时Worker 节点会从 S3 并行拉取数据。如果数据桶在us-west-2结果桶在eu-central-1跨区域流量会产生额外费用$0.01/GB且延迟飙升。我在法兰克福客户现场实测同区域查询平均 8.2s跨区域到 us-east-1平均 47.6s且偶发超时。务必在创建桶时下拉菜单选择与你的主要业务区域一致的 Region。铁律3强制启用服务端加密SSE-S3在桶属性 → Default encryption → Enable encryption → 选择AES-256。这不是可选项是合规底线。Athena 查询结果默认以明文 CSV 存储若桶未加密任何人拿到 S3 URL 都能下载结果。启用 SSE-S3 后AWS 自动用 KMS 密钥加密对象且密钥管理完全透明零额外成本。提示如果你的数据桶已存在且包含敏感数据请立即启用桶策略强制加密{ Version: 2012-10-17, Id: PutObjPolicy, Statement: [ { Sid: DenyUnEncryptedObjectUploads, Effect: Deny, Principal: *, Action: s3:PutObject, Resource: arn:aws:s3:::your-bucket-name/*, Condition: { StringNotEquals: { s3:x-amz-server-side-encryption: AES256 } } } ] }3.3 创建第一个数据库与表从 CloudFront 日志实战开始我们不用虚构数据直接用 AWS 官方提供的公开 CloudFront 日志样本。这是最贴近真实场景的入门方式——日志是典型的半结构化、高吞吐、需快速探查的数据。步骤1准备数据源打开 AWS CloudFront 日志样本页面 复制一个样本文件 URL如s3://aws-cloudfront-logs-us-east-1/sample-access-logs/2024/06/15/20240615T090000Z-EXAMPLE-CLOUDFRONT-LOGS-01.log.gz在你的 S3 控制台创建一个新桶命名为athena-demo-logs-yourname如athena-demo-logs-john使用 S3 控制台的 “Upload” 功能将样本日志上传至该桶的cloudfront/raw/2024/06/15/路径下注意保留原始.log.gz后缀Athena 支持 GZIP 解压。步骤2创建数据库进入 Athena 控制台 → Query Editor在顶部 WorkGroup 下拉框确保选中primary这是默认工作区在 Query Editor 输入框粘贴并执行CREATE DATABASE IF NOT EXISTS athena_demo;点击右上角 “Refresh” 图标两个箭头循环左侧 Database 列表中应出现athena_demo。步骤3创建外部表关键详解每一行CREATE EXTERNAL TABLE IF NOT EXISTS athena_demo.cloudfront_logs ( date DATE, time STRING, location STRING, bytes INT, requestip STRING, method STRING, host STRING, uri STRING, status INT, referrer STRING, useragent STRING, ssl_cipher STRING, ssl_protocol STRING ) ROW FORMAT SERDE org.apache.hadoop.hive.serde2.RegexSerDe WITH SERDEPROPERTIES ( input.regex ^(?!#)([^ ])\\s([^ ])\\s([^ ])\\s([^ ])\\s([^ ])\\s([^ ])\\s([^ ])\\s([^ ])\\s([^ ])\\s([^ ])\\s\\[([^\\]])\\]\\s\([^\\\\\]*(?:\\\\.[^\\\\\]*)*)\\\s\([^\\\\\]*(?:\\\\.[^\\\\\]*)*)\\\s([^ ])\\s([^ ]) ) LOCATION s3://athena-demo-logs-john/cloudfront/raw/;逐行解析这是你必须理解的底层逻辑CREATE EXTERNAL TABLE声明这是外部表Athena 不管理数据生命周期只读取 S3ROW FORMAT SERDE org.apache.hadoop.hive.serde2.RegexSerDe指定用正则表达式解析器这是处理日志的利器input.regex正则表达式字符串。注意\s匹配空白符[^ ]匹配非空格字符\\[转义左方括号。这个正则严格对应 CloudFront 日志格式不能随意修改LOCATION s3://...指向数据桶的父路径不是具体文件。Athena 会自动扫描该路径下所有匹配的.log.gz文件。执行后刷新左侧 Tables 列表cloudfront_logs应出现在athena_demo下。提示如果执行失败90% 的原因是正则表达式中的反斜杠未正确转义。AWS 控制台要求双反斜杠\\而本地 IDE 可能只需单反斜杠\。务必在控制台中粘贴时检查。3.4 首条查询不只是 SELECT *而是验证数据健康度别急着跑复杂分析。首条查询的目标是确认数据可读、Schema 正确、分区有效。我推荐这三步验证法验证1基础可读性10秒出结果SELECT * FROM athena_demo.cloudfront_logs LIMIT 5;预期返回 5 行每行 14 列date列为DATE类型如2024-06-15bytes为整数。若date显示为字符串或报错HIVE_BAD_DATA说明正则解析失败需检查日志格式与正则是否匹配。验证2Schema 有效性查类型与空值SELECT count(*) as total_rows, count(date) as non_null_date, count(requestip) as non_null_requestip, approx_distinct(requestip) as unique_ips, min(date) as min_date, max(date) as max_date FROM athena_demo.cloudfront_logs;预期total_rows与non_null_date应接近允许少量日志缺失日期unique_ips应显著小于total_rows证明 IP 去重有效。若min_date为NULL说明date列解析失败。验证3分区探测为后续优化铺路虽然当前数据未显式分区但我们可以模拟分区路径。执行SELECT distinct substr(uri, 1, 20) as uri_prefix FROM athena_demo.cloudfront_logs LIMIT 10;预期返回类似/images/logo.png,/api/v1/users的路径前缀。这提示我们未来可按uri的前缀如/images/,/api/或date建立分区大幅提升查询效率。4. 实战进阶用真实业务问题驱动 Athena 能力挖掘4.1 场景1实时诊断 CDN 异常流量5分钟定位根因背景某新闻 App 突然收到告警CloudFront 回源失败率飙升至 35%正常 0.5%。运维团队需要 5 分钟内锁定是攻击、配置错误还是源站故障。Athena 解法全程在控制台完成快速聚焦时间窗-- 查看异常时段过去1小时的总体趋势 SELECT from_iso8601_timestamp(concat(cast(date as varchar), , time)) as event_time, status, count(*) as cnt FROM athena_demo.cloudfront_logs WHERE date current_date - interval 1 day AND from_iso8601_timestamp(concat(cast(date as varchar), , time)) now() - interval 1 hour GROUP BY 1, 2 ORDER BY event_time DESC, cnt DESC LIMIT 20;关键点from_iso8601_timestamp将date和time字符串拼接为时间戳实现精确到秒的过滤。current_date - interval 1 day是 Athena 的标准日期计算语法。定位高频错误码-- 统计各状态码分布重点关注5xx SELECT status, count(*) as cnt, round(100.0 * count(*) / sum(count(*)) over(), 2) as pct FROM athena_demo.cloudfront_logs WHERE date current_date - interval 1 day AND status 500 GROUP BY status ORDER BY cnt DESC;预期若502 Bad Gateway占比 80%则极可能是源站Origin不可达。关联源站与错误-- 查看502错误对应的源站 Host SELECT host, count(*) as error_cnt, approx_distinct(requestip) as unique_attackers FROM athena_demo.cloudfront_logs WHERE date current_date - interval 1 day AND status 502 GROUP BY host ORDER BY error_cnt DESC;若host高度集中于origin-newsapp.com且unique_attackers很小如 5基本可判定是源站自身故障而非 DDoS 攻击。实操心得这套组合拳我在线上环境实测平均耗时 42 秒扫描数据 1.8GB成本 $0.009关键是利用WHERE子句尽早过滤避免全表扫描。date字段是天然的分区键Athena 会自动剪枝不要试图在一条 SQL 里解决所有问题。分步查询、逐步聚焦是 Athena 高效使用的黄金法则。4.2 场景2构建用户行为宽表用 CTAS 代替 INSERT需求市场部需要一张宽表包含用户 ID、首次访问时间、最近访问时间、总访问次数、访问设备分布。数据源是 S3 中的多天日志。错误做法新手常踩坑-- ❌ 危险会覆盖原表且无法回滚 INSERT INTO TABLE user_behavior SELECT ... FROM logs WHERE date 2024-06-15;正确做法CTAS - Create Table As Select-- ✅ 安全、原子、可追溯 CREATE TABLE athena_demo.user_behavior_20240615 AS SELECT requestip as user_id, min(from_iso8601_timestamp(concat(cast(date as varchar), , time))) as first_visit, max(from_iso8601_timestamp(concat(cast(date as varchar), , time))) as last_visit, count(*) as visit_count, approx_distinct(case when useragent like %Mobile% then requestip end) as mobile_users, approx_distinct(case when useragent not like %Mobile% then requestip end) as desktop_users FROM athena_demo.cloudfront_logs WHERE date date(2024-06-15) GROUP BY requestip;优势解析原子性CTAS 要么成功创建新表要么失败绝不会污染原表成本可控只扫描2024-06-15分区扫描量 ≈ 单日日志大小结果持久化新表数据存于 S3可被其他查询直接引用如SELECT * FROM athena_demo.user_behavior_20240615 WHERE visit_count 100 ORDER BY last_visit DESC LIMIT 10;可扩展后续可轻松添加分区如PARTITIONED BY (dt STRING)然后用INSERT INTO ... PARTITION (dt2024-06-15)追加。4.3 场景3用 Window Function 做会话分析超越基础 GROUP BY需求计算每个用户的“最长连续访问天数”。这需要按用户分组、按日期排序、识别连续日期序列——传统 SQL 难以实现。Athena 解法利用 ROW_NUMBER 和 DATE arithmetic-- 步骤1生成用户每日访问去重表 WITH daily_user_visits AS ( SELECT DISTINCT requestip as user_id, date as visit_date FROM athena_demo.cloudfront_logs WHERE date current_date - interval 30 day ), -- 步骤2为每个用户按日期排序并计算“日期序号差” user_session_gaps AS ( SELECT user_id, visit_date, row_number() OVER (PARTITION BY user_id ORDER BY visit_date) as rn, date_diff(day, date(2024-01-01), visit_date) as base_days, -- 任意基准日 date_diff(day, date(2024-01-01), visit_date) - row_number() OVER (PARTITION BY user_id ORDER BY visit_date) as gap_id FROM daily_user_visits ), -- 步骤3按 gap_id 分组计算连续天数 session_lengths AS ( SELECT user_id, gap_id, count(*) as consecutive_days, min(visit_date) as start_date, max(visit_date) as end_date FROM user_session_gaps GROUP BY user_id, gap_id ) -- 最终结果每个用户的最长连续天数 SELECT user_id, max(consecutive_days) as max_consecutive_days, min(start_date) as longest_session_start, max(end_date) as longest_session_end FROM session_lengths GROUP BY user_id ORDER BY max_consecutive_days DESC LIMIT 10;原理揭秘gap_id base_days - rn是核心技巧。对于连续日期如 2024-06-01, 2024-06-02, 2024-06-03rn为 1,2,3base_days为 18000,18001,18002gap_id恒为 17999一旦出现断档如缺 2024-06-04下一个日期visit_date2024-06-05的rn4base_days18004gap_id18000与之前不同从而自然分组Athena 的 Window Function 性能极佳此查询在 10GB 日志上平均耗时 22 秒。注意date_diff函数在 Athena 中要求日期类型cast(2024-01-01 as date)是安全写法避免字符串隐式转换错误。5. 避坑指南那些让我加班到凌晨的 Athena 故障实录5.1 常见故障速查表附根本原因与修复命令故障现象根本原因修复方案执行命令/操作Query failed: HIVE_BAD_DATA: Error parsing field value for field X at record Y正则 SerDe 解析失败某行日志格式与正则不匹配如多了一个空格、少了一个引号用LIMIT定位坏行调整正则或清洗数据SELECT * FROM table LIMIT 1000 OFFSET 990找到坏行位置Query failed: SYNTAX_ERROR: line X:Y: Column xxx cannot be resolved表名、列名大小写不匹配Athena 默认小写但某些 SerDe 保留大小写统一用反引号包裹列名或重建表时指定小写SELECT \user_id, Status FROM tableQuery succeeded but returned 0 rows, though data exists in S3S3 路径末尾有/但 Athena 要求路径不以/结尾否则视为文件名修改 LOCATION 路径去掉末尾/ALTER TABLE table SET LOCATION s3://bucket/path;Query cost is 10x higher than expected未用分区过滤或SELECT *扫描全表或数据格式为 CSV 未压缩添加WHERE分区条件改用SELECT col1,col2转存为 ParquetMSCK REPAIR TABLE table;同步新分区Insufficient permissions to perform this operation on Glue CatalogIAM 策略未授予glue:GetTable或glue:GetPartitions更新 IAM 策略添加缺失的 Glue 权限在 IAM 控制台编辑策略 JSON增加对应 Action5.2 三个血泪教训说出来能救你一命教训1永远不要在生产环境用DROP DATABASE ... CASCADE某次紧急修复同事想快速清空测试库执行了DROP DATABASE testdb CASCADE;。他以为只删表没想到 CASCADE 会递归删除 Glue Catalog 中该库下所有表的元数据且同时删除 S3 中对应 LOCATION 的所有数据因为外部表的 LOCATION 指向 S3。结果s3://