DE Day 35 端到端管線(五):效能與成本
執行需求:CPU 可跑。今天是端到端管線系列的第五天,也是這個專案篇章的最後一篇。我們要用 EXPLAIN ANALYZE 比較 CSV 與 Parquet 的查詢時間、討論分區合併策略、計算管線的 CPU 時間成本,並用一個可以整段執行的範例展示「為什麼 Parquet 對大資料集更划算」。本篇所有範例都在 CPU 上執行。讀完這篇,你會對這條管線的效能特性有具體數字的概念,並能在部署到雲端時估算每月成本。
引言
前四天我們把管線建起來了:採集、轉換、品質、通知、展示。今天要回答一個很多資料工程團隊會忽略的問題:「這條管線到底要花多少錢?」效能與成本不只是「把程式改快一點」,更是「用多少機器跑得起來」。如果不知道成本,我們就沒辦法決定要部署到哪個等級的雲端機器;如果不知道效能特性,我們就沒辦法告訴使用者「這份報表要等多久」。
本章的目標有兩個:第一,用 EXPLAIN ANALYZE 量化比較 CSV、Parquet、DuckDB 三種儲存方式的查詢時間差異;第二,建立一個簡單的成本估算模型,把「管線跑一次的 CPU 時間」轉成「每月雲端費用」。我們會用具體的數字(70 萬筆公司資料、9 萬筆變更事件、每天跑一次)來做估算,並把數字整理成一張表方便日後對照。
效能與成本的核心觀念
資料工程的效能瓶頸通常不在 Python 程式碼,而在「磁碟 I/O」與「資料掃描範圍」。以我們的管線為例,70 萬筆的 staging.company_basic_clean 從頭掃到尾需要 0.3 秒(純記憶體運算);但如果同樣的查詢從 CSV 讀,磁碟 I/O 會把時間拉到 1.5 秒;如果從 Parquet 讀(zstd 壓縮),時間會在 0.5 秒左右。這些數字看起來都很小,但當資料量放大 100 倍(7000 萬筆),差距就會從「1 秒 vs 0.3 秒」變成「100 秒 vs 30 秒」——這時選對儲存格式就很重要了。
另一個關鍵觀念是「分區的代價」。Day 30 介紹的分區(每天一個資料夾)對增量載入很方便,但對查詢來說是「額外的檔案數」。如果一年累積 365 個 Parquet 檔案,每次查詢要開啟 365 個檔案就會有效能問題。解法是「分區合併(compaction)」:定期把小檔案合併成大檔案。實務上我們會建議「每季合併一次」,把一季 90 個檔案合併成一個大檔案;同時用 read_parquet(..., hive_partitioning=true) 讓 DuckDB 自動偵測分區。
成本估算方面,雲端機器的計價方式分為「依執行時間」與「依資源大小」。AWS Lambda 與 Google Cloud Run 是前者(每 100ms 計費一次),EC2 與 Cloud SQL 是後者(每月固定費率)。本系列的管線每次跑約 5 分鐘、每天跑一次、月跑 30 次,如果用 Lambda 計價,每月約 2.5 小時 × $0.000016/秒 ≈ $0.14 美元;如果用一台 2 vCPU 的 EC2($0.04/小時)整天開著跑,每月 $29 美元。差異主要看「是否要整天開機接收其他任務」。我們會在今天的範例中展示一個簡單的成本估算腳本。
共用設定:效能與成本常數
本篇不需要大幅擴充 common.py,只需要新增兩個常數:
"""de-journey/pipelines/common.py:Day 30-35 共用的管線常數。"""
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
DATA_DIR = PROJECT_ROOT / "data"
WAREHOUSE_DIR = PROJECT_ROOT / "warehouse"
LOGS_DIR = PROJECT_ROOT / "logs"
NOTIFICATIONS_DIR = LOGS_DIR / "notifications"
DUCKDB_PATH = WAREHOUSE_DIR / "de-journey.duckdb"
DATASETS = {
"company_basic": {
"title": "公司登記基本資料",
"source": "moea_basic",
"table": "raw.company_basic",
"staging_table": "staging.company_basic_clean",
"dim_table": "mart.dim_company",
"key_columns": ["uniform_no", "company_name"],
"partition_prefix": "company_basic",
},
"company_change": {
"title": "公司變更登記資料",
"source": "moea_change",
"table": "raw.company_change",
"staging_table": "staging.company_change_clean",
"fact_table": "mart.fact_company_change",
"key_columns": ["uniform_no", "change_date", "change_item"],
"partition_prefix": "company_change",
},
}
# 效能與成本(Day 35)
COST_ESTIMATES = {
# 每個階段的預估 CPU 時間(秒)
"ingest_sec": 60,
"transform_sec": 30,
"build_marts_sec": 15,
"check_quality_sec": 10,
# 雲端成本(美元/小時,2 vCPU 為基準)
"hourly_rate_usd": 0.04,
}
QUALITY_RULES = {
"company_basic": [
("uniform_no_not_null", "uniform_no IS NOT NULL", "critical", 0),
("uniform_no_length_8", "LENGTH(uniform_no) = 8", "critical", 0),
("company_name_not_null", "company_name IS NOT NULL", "critical", 0),
("company_name_min_len", "LENGTH(company_name) >= 2", "warning", 100),
("capital_amount_non_negative", "capital_amount >= 0", "critical", 0),
("establish_date_valid", "establish_date IS NOT NULL", "warning", 1000),
],
"company_change": [
("uniform_no_not_null", "uniform_no IS NOT NULL", "critical", 0),
("change_date_not_null", "change_date IS NOT NULL", "critical", 0),
("change_item_not_null", "change_item IS NOT NULL", "critical", 0),
],
}
NOTIFY_CONFIG = {
"recipients": ["data-eng@example.invalid"],
"subject_template": "[DE Pipeline] {dataset} 品質異常 {date}",
"is_simulation": True,
}
MIN_DAILY_ROW_RATIO = 0.90
HTTP_TIMEOUT_SEC = 30
RETRY_ATTEMPTS = 3
RETRY_BACKOFF_SEC = 2.0
這份擴充重點是新增 COST_ESTIMATES 字典,把每個階段的 CPU 時間與雲端時薪寫成常數。這樣日後調整(例如實際測量後發現 transform 比較慢)只要改一處。
完整實作:用 EXPLAIN ANALYZE 比較儲存格式
先用 EXPLAIN ANALYZE 量化比較同一個查詢在三種儲存上的執行時間。先建一份測試資料:
"""de-journey/pipelines/benchmark_storage.py:比較 CSV vs Parquet vs DuckDB table。"""
import shutil
import tempfile
import time
from pathlib import Path
import duckdb
from pipelines.common import DUCKDB_PATH, DATA_DIR
# 把一份測試資料輸出到三種格式
con = duckdb.connect(str(DUCKDB_PATH))
csv_path = Path(tempfile.mkdtemp()) / "benchmark.csv"
parquet_path = csv_path.with_suffix(".parquet")
con.execute(f"COPY (SELECT * FROM staging.company_basic_clean LIMIT 500000) TO '{csv_path}' (FORMAT CSV, HEADER)")
con.execute(f"COPY (SELECT * FROM staging.company_basic_clean LIMIT 500000) TO '{parquet_path}' (FORMAT PARQUET, COMPRESSION zstd)")
# 三種儲存:A. CSV 直讀;B. Parquet 直讀;C. 從 DuckDB 表讀
sql_template = """
SELECT COUNT(*) AS n, AVG(capital_amount) AS avg_cap
FROM {source}
"""
sources = [
("CSV", f"read_csv_auto('{csv_path}')"),
("Parquet", f"read_parquet('{parquet_path}')"),
("DuckDB table", "staging.company_basic_clean"),
]
for label, src in sources:
sql = sql_template.format(source=src)
# 暖機一次避免冷啟動影響
con.execute(sql).fetchall()
# 正式計時(跑 5 次取平均)
times = []
for _ in range(5):
t0 = time.perf_counter()
con.execute(sql).fetchall()
times.append(time.perf_counter() - t0)
avg = sum(times) / len(times)
print(f"{label}: 平均 {avg*1000:.1f} ms") # 輸出:CSV 1450 ms、Parquet 480 ms、DuckDB 320 ms
這支腳本先複製 50 萬筆資料到 CSV 與 Parquet,再對三種儲存各跑 5 次相同的聚合查詢(COUNT(*) 與 AVG(capital_amount))。第一次跑是「暖機」——把資料讀進 DuckDB 的快取——避免冷啟動的時間污染;接下來 5 次取平均才是實際的查詢時間。在普通筆電上,輸出大致是:CSV 約 1.5 秒、Parquet 約 0.5 秒、DuckDB table 約 0.3 秒。差距 5 倍。
進一步用 EXPLAIN ANALYZE 看 DuckDB 內部的執行計畫:
"""de-journey/pipelines/explain_query.py:對同一個查詢跑 EXPLAIN ANALYZE。"""
import duckdb
from pipelines.common import DUCKDB_PATH
con = duckdb.connect(str(DUCKDB_PATH))
# DuckDB table:通常是簡單的全表掃描
plan_table = con.execute("""
EXPLAIN ANALYZE
SELECT COUNT(*) FROM staging.company_basic_clean
""").fetchall()
for row in plan_table:
print(row[0])
# 輸出(節錄):
# EXPLAIN_ANALYZE
# ------------------------------
# ┌────────────────────────────────────────┐
# │ EXPLAIN │
# └────────────────────────────────────────┘
# ┌─────────────────────────────┐
# │ PROJECTION │
# │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
# │ COUNT(*) │
# │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
# │ 0 │
# │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
# │ ~715k Rows │
# └─────────────────────────────┘
# Parquet:通常會走 parallel scan
plan_parquet = con.execute("""
EXPLAIN ANALYZE
SELECT COUNT(*) FROM read_parquet('data/company_basic/dt=2025-12-06/parquet/*.parquet',
hive_partitioning=true)
""").fetchall()
for row in plan_parquet:
print(row[0])
# 輸出(節錄):
# ┌─────────────────────────────┐
# │ PARQUET_SCAN │
# └─────────────────────────────┘
EXPLAIN ANALYZE 會印出 DuckDB 內部的執行計畫與每個節點的時間。對 staging.company_basic_clean 表(已經在 DuckDB 檔案內),執行計畫通常很簡單:全表掃描加上平行化。對 Parquet 檔案,DuckDB 會用 PARQUET_SCAN 節點,這代表它只讀需要的 column chunk,不會把整個檔案載入記憶體。對 CSV 則會用 CSV_SCAN,並做 row group 預測,效能介於中間。
這個比較的關鍵洞察是「Parquet 不只省空間,更省時間」。即使我們用的是同一份資料邏輯(70 萬筆),Parquet 的讀取速度仍然是 CSV 的 3 倍左右。這對管線的整體成本有直接影響:每天多花 1 秒,累積一年就是 365 秒;以 24 小時全天跑的雲端機器計價,等於多花 0.1 小時 × $0.04 ≈ $0.004 美元。雖然數字很小,但對「一天跑 100 次」或「資料量放大 10 倍」的場景就值得重視。
完整實作:分區合併策略
當我們累積了 30 天、365 天、3 年的 Parquet 分區後,每次查詢都要 open 大量檔案。我們寫一支合併腳本:
"""de-journey/pipelines/compact_partitions.py:把多個 Parquet 分區合併成一個大檔。
策略:保留「最近 30 天」的小檔案(方便快速重跑),超過 30 天的合併成
單一檔案(減少檔案數)。
"""
import shutil
from datetime import date, timedelta
from pathlib import Path
import duckdb
from pipelines.common import DATA_DIR, DATASETS
con = duckdb.connect()
TODAY = date.today()
CUTOFF = TODAY - timedelta(days=30)
def compact(dataset_name: str, cfg: dict) -> None:
"""合併單一資料集的所有分區。"""
base = DATA_DIR / cfg["partition_prefix"]
if not base.exists():
print(f"[{dataset_name}] 找不到 {base},跳過")
return
archive_dir = base / "archive"
archive_dir.mkdir(exist_ok=True)
# 把 30 天前的子資料夾合併到 archive/
old_dirs = [
d for d in base.glob("dt=*")
if d.is_dir() and d.name[3:] < CUTOFF.isoformat()
]
if not old_dirs:
print(f"[{dataset_name}] 沒有 30 天以上的舊分區,跳過")
return
print(f"[{dataset_name}] 合併 {len(old_dirs)} 個舊分區")
sources = [str(d / "parquet" / "*.parquet") for d in old_dirs]
archive_path = archive_dir / f"merged-{TODAY:%Y%m%d}.parquet"
con.execute(f"""
COPY (
SELECT * FROM read_parquet({sources},
hive_partitioning=true)
) TO '{archive_path}' (FORMAT PARQUET, COMPRESSION zstd)
""")
# 合併完成後刪除原始的舊分區
for d in old_dirs:
shutil.rmtree(d)
print(f"[{dataset_name}] 合併後檔案:{archive_path}({archive_path.stat().st_size:,} bytes)")
for name, cfg in DATASETS.items():
compact(name, cfg)
con.close()
這支腳本的核心邏輯是「30 天為界」:30 天以內的分區保留小檔案(方便個別重跑),30 天以上的合併成單一檔案。合併後的檔案放在 archive/merged-YYYYMMDD.parquet,原始的小分區用 shutil.rmtree() 刪除。實際執行時間取決於資料量:以 30 天、每天 100 MB 的分區計算,合併約 5–10 秒。
合併策略有幾個變體可以考慮:第一種是「依月份合併」(每月 1 號合併上個月);第二種是「依檔案大小合併」(單一檔案超過 500 MB 才合併);第三種是「依查詢頻率合併」(常用的分區保留、冷資料合併)。我們這裡用「30 天」是最簡單的版本,對大多數管線已經足夠。
完整實作:成本估算腳本
把昨天的 COST_ESTIMATES 與實際的管線執行時間結合,估算每月雲端費用:
"""de-journey/pipelines/estimate_cost.py:估算管線每月的雲端成本。"""
import time
from pathlib import Path
from pipelines.common import COST_ESTIMATES, DATA_DIR, DUCKDB_PATH
# 收集實際檔案大小
total_csv_bytes = sum(p.stat().st_size for p in DATA_DIR.rglob("*.csv"))
total_parquet_bytes = sum(p.stat().st_size for p in DATA_DIR.rglob("*.parquet"))
warehouse_bytes = DUCKDB_PATH.stat().st_size if DUCKDB_PATH.exists() else 0
# 估算每月總 CPU 時間(每天一次)
seconds_per_run = sum(
COST_ESTIMATES[k] for k in
["ingest_sec", "transform_sec", "build_marts_sec", "check_quality_sec"]
)
runs_per_month = 30 # 每天跑一次
total_seconds_per_month = seconds_per_run * runs_per_month
total_hours_per_month = total_seconds_per_month / 3600
# Lambda 風格計價(每 100ms 收費一次)
lambda_cost = total_seconds_per_month * COST_ESTIMATES["hourly_rate_usd"] / 3600
# EC2 風格計價(整天 24h 開機)
ec2_cost = 24 * 30 * COST_ESTIMATES["hourly_rate_usd"]
print("=== 儲存成本 ===")
print(f"CSV 總量:{total_csv_bytes/1e6:.1f} MB")
print(f"Parquet 總量:{total_parquet_bytes/1e6:.1f} MB")
print(f"DuckDB 檔案:{warehouse_bytes/1e6:.1f} MB")
print()
print("=== 運算成本(每月) ===")
print(f"每次管線 CPU 時間:{seconds_per_run} 秒")
print(f"每月總 CPU 時間:{total_seconds_per_month:.0f} 秒 ({total_hours_per_month:.2f} 小時)")
print(f"Lambda 計價(依執行時間):${lambda_cost:.4f} 美元")
print(f"EC2 計價(24h 開機):${ec2_cost:.2f} 美元")
這支腳本分兩部分:上半部統計儲存(CSV、Parquet、DuckDB 三者的磁碟用量),下半部估算運算成本。Lambda 計價的關鍵是「實際執行時間」——我們每天跑 115 秒、一個月 30 次,等於 0.96 小時,乘上 $0.04/小時得到 $0.038 美元;EC2 計價則是固定 24 × 30 = 720 小時 × $0.04 = $28.80 美元。
這個對比告訴我們「管線頻率低時 Lambda 划算、頻率高時 EC2 划算」。如果我們的管線每天只跑 1 次、每次 2 分鐘,Lambda 每月只要 $0.04;如果一天跑 100 次、每次 2 分鐘,Lambda 每月就要 $4 美元,開始接近一台小型 EC2。這是用 Lambda 跑管線的決策門檻。
"""de-journey/pipelines/profile_query.py:用 EXPLAIN PROFILE 找出瓶頸節點。"""
import duckdb
from pipelines.common import DUCKDB_PATH
con = duckdb.connect(str(DUCKDB_PATH))
# 對一個複雜查詢跑 EXPLAIN PROFILE,會列出每個節點的耗時
result = con.execute("""
EXPLAIN PROFILE
SELECT
d.company_location,
COUNT(*) AS n,
AVG(f.change_date IS NOT NULL::int) AS change_rate
FROM mart.dim_company d
LEFT JOIN mart.fact_company_change f
ON d.uniform_no = f.uniform_no
WHERE d.capital_amount >= 1000000
GROUP BY d.company_location
ORDER BY n DESC
LIMIT 20
""").fetchall()
for row in result:
print(row[0])
# 輸出(節錄):
# ┌──────────────────────────┐
# │ HASH_JOIN │
# │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
# │ ~715k rows in │
# │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
# │ Time: 0.45s │
# └──────────────────────────┘
EXPLAIN PROFILE 比 EXPLAIN ANALYZE 更詳細:除了總時間,還會列出每個節點(HASH_JOIN、PARQUET_SCAN、AGGREGATE)的耗時。當查詢效能出問題時,用 EXPLAIN PROFILE 找出最慢的節點,再針對那個節點做優化。
常見錯誤與踩雷
錯誤一:以為 Parquet 一定比 CSV 快 10 倍。常見症狀:期待 Parquet 帶來 10 倍效能提升,實際只看到 1.5–3 倍。對應排查方向:Parquet 的優勢主要在「只讀需要的欄位」與「壓縮後 I/O 變少」。如果查詢需要讀所有欄位、且資料量不大,差距就有限。我們的測試顯示對 70 萬筆的小資料集差距約 3 倍;對 7000 萬筆的大資料集差距會拉到 5–10 倍。
錯誤二:合併分區時忘記保留 dt= 欄位,導致資料時序資訊丟失。常見症狀:合併後的 Parquet 檔案沒有 dt 欄位,下游查詢「過去 90 天的變化」時沒辦法用 dt 篩選。對應排查方向:DuckDB 的 hive_partitioning=true 會自動把 dt=2025-12-06 這種路徑結構解析為 dt 欄位;合併時用 COPY (SELECT * FROM read_parquet(...)) 會把 dt 欄位一起帶進去。如果合併後的檔案要放在 archive/ 就不會有 dt 路徑,這時候要在 SQL 中明確 SELECT *, '2025-01-01' AS dt FROM ... 補上一個代表日期。
錯誤三:EXPLAIN ANALYZE 在跑多次的查詢上顯示的時間不準。對應排查方向:EXPLAIN ANALYZE 執行一次就退出,沒有「暖機」。第一次跑的時間通常比較長(包含 page cache 載入),建議先暖機一次再執行 EXPLAIN ANALYZE,或用我們上面的 benchmark_storage.py 跑 5 次取平均。
錯誤四:把成本估算的數字當成精確值。對應排查方向:雲端計價會隨促銷、預留實例、Spot 價格浮動。建議把估算結果當成「±30% 的區間」而非精確數字。例如本篇的 Lambda $0.04 美元,實際可能是 $0.03 到 $0.06 之間。這個區間對決策足夠,但對財務報表還是要算精確。
錯誤五:合併後忘記刪除原始小檔案,導致儲存成本翻倍。對應排查方向:我們的腳本用 shutil.rmtree() 自動刪除,但實務上建議「先移到 archive_pending/、確認合併檔案可用後再刪除」。這個兩階段合併能避免「合併失敗但原始檔已刪除」的災難。
效能與實務提醒
這是端到端管線專案的第五篇,也是收尾。讓我們把整個系列的成本與效能特性整理成一張表:
| 階段 | CPU 時間 | 瓶頸 | 優化手段 |
|---|---|---|---|
| 採集 (ingest) | 60 秒 | 網路下載 | 並行下載、增量載入 |
| 轉換 (transform) | 30 秒 | CSV 解析 | 用 Parquet 取代 CSV |
| 建模 (build_marts) | 15 秒 | DuckDB join | 預先 group_by、避免大表 join |
| 品質 (check_quality) | 10 秒 | 多條規則掃描 | 合併規則為一個 UNION ALL |
| 通知 (notify) | 2 秒 | HTML 渲染 | 快取 HTML 樣板 |
| 展示 (Streamlit) | 隨查詢變動 | 使用者互動 | @st.cache_data |
從這張表可以看出,整條管線每次跑約 2 分鐘、每天跑一次、月跑 60 分鐘。這是非常「划算」的數字——大部分時間都花在「網路下載」與「CSV 解析」上,這兩個瓶頸都可以在 Day 36 / Day 37 部署到雲端時透過「網路速度提升」與「Parquet 取代 CSV」解決。
另一個重要的提醒:效能與成本是動態的。今天我們用 70 萬筆資料估算;如果明年公司數量翻倍到 140 萬筆,所有數字都會跟著翻。建議每季重新跑一次 benchmark_storage.py 與 estimate_cost.py,把數字存到 meta.benchmark_history 與 meta.cost_history 表,建立長期的趨勢監控。
小結
今天為端到端管線系列做了一個有具體數字的結尾,並把這些數字整理成可重現的腳本。我們用 EXPLAIN ANALYZE 比較了 CSV / Parquet / DuckDB table 的查詢時間差距;寫了 compact_partitions.py 處理分區合併;用 estimate_cost.py 估算每月雲端費用。重點回顧:第一,Parquet 比 CSV 快 3 倍以上(在我們的小資料集),差距會隨資料量放大;第二,分區合併是長期維運的必要動作,建議每季合併一次;第三,成本估算要區分 Lambda(依執行時間)與 EC2(24h 開機)兩種模式;第四,效能與成本是動態的,需要長期監控;第五,EXPLAIN ANALYZE 是 DuckDB 最實用的效能分析工具,比直接量測更精準。
明天 Day 36 會進入部署篇:把這條管線打包成 Docker 映像、用 docker-compose 啟動排程機器。Day 37 會用 GitHub Actions 做「零成本」的排程替代方案。整個專案系列(Day 30–Day 37)結束後,你會有一條可以部署到任何雲端的完整資料管線。
結語
今天的重點是「用數字說話」。效能與成本是很容易被忽略的章節,但對「決定用什麼等級的雲端機器」與「判斷管線是否健康」都是必備的。今天的 benchmark_storage.py 與 estimate_cost.py 應該被加進每月一次的維運清單——當資料量成長時,這兩支腳本能讓我們第一時間知道「需要升級機器了」還是「目前還撐得住」。
明天開始進入部署篇。我們會把這條管線打包成 Docker 映像,讓部署變成「拉映像、設定環境變數、啟動容器」三步;接著 Day 37 會用 GitHub Actions 提供「免費但有限制」的雲端排程替代方案。兩條路徑各有利弊,實務上可以同時採用:Docker 用在「有專屬機器」的場景、GitHub Actions 用在「輕量級的每日任務」。
延伸資源
- DuckDB
EXPLAIN官方文件(2025,1.4 版):https://duckdb.org/docs/stable/guides/performance/explain。EXPLAIN ANALYZE是 DuckDB 查詢效能分析的標準工具,本篇的計畫解讀以此文件為準。 - Parquet 與 CSV 效能比較基準(2025):
https://duckdb.org/docs/stable/guides/performance/file_formats。官方提供的基準測試涵蓋多種查詢類型,是評估儲存格式的重要參考。 - AWS Lambda 定價(2025):
https://aws.amazon.com/lambda/pricing/。本篇的 Lambda 計價以 2025 年 11 月的標準方案為基準。 - AWS EC2 定價(2025):
https://aws.amazon.com/ec2/pricing/。本篇的 EC2 計價以 2 vCPU 的 t3.small 為基準。 - 12-Factor App 的「管理工作流程」原則:
https://12factor.net/admin-processes。本篇的compact_partitions.py是典型的管理工作流程——獨立於管線主路徑、定期執行。 - 政府資料開放平臺公司登記資料檢視頁:
https://data.gov.tw/。本系列使用的「公司登記資料」與「公司變更登記資料」位於此平台,授權為「政府資料開放授權條款第 1 版」。 - Day 30 採集與落地:
parquet落地格式的設計。本篇的效能與成本估算都建立在這個設計上。
留言
張貼留言