1. Đọc và lưu file
Đọc đúng dữ liệu và lưu bảng để dùng lại.
CSV không lưu kiểu dữ liệu
Giá giả lập (USD/đêm); ? là chưa có giá.
File gốc
id,khu,gia
001,A,100
002,B,80
003,A,200
004,B,120
005,A,?
Đọc bằng pandas
suy_luan = pd.read_csv(
"data/raw/cho_o.csv"
)
print(suy_luan.to_string(
index=False))
id khu gia
1 A 100
2 B 80
3 A 200
4 B 120
5 A ?
Mã 001 thành 1; cột gia vẫn là chuỗi vì có ?.
Dùng cùng năm hàng này trong phần SQL. Cho sinh viên đoán kết quả trước khi chạy, rồi chỉ ra hai thông tin đã đổi. Notebook in kiểu từng cột: id int64, gia str. Hỏi thêm: ghép bảng này với một bảng lưu mã "001" dạng chuỗi thì sao? pandas báo lỗi vì hai cột khoá khác kiểu; nếu ép cả hai về số thì 001 và 1 bị coi là một.
Khai báo cách đọc
df = pd.read_csv(
"data/raw/cho_o.csv",
dtype={"id": "str", "gia": "float64"},
keep_default_na=False, na_values={"gia": ["?"]},
)
print(df.to_string(index=False))
id khu gia
001 A 100.0
002 B 80.0
003 A 200.0
004 B 120.0
005 A NaN
dtype đặt kiểu cột; na_values khai báo chuỗi nào là thiếu.
Mặc định pandas coi các chuỗi như NA, null, N/A và ô trống là thiếu; keep_default_na=False tắt danh sách này.
Danh sách mặc định của pandas có 19 chuỗi. Với file này, bỏ keep_default_na=False vẫn ra cùng kết quả vì không cột nào chứa các chuỗi đó; tham số cần khi một giá trị hợp lệ trùng danh sách, như mã khu NA ở bài tập 1. Khi đã tắt danh sách, ô trống cũng không còn tự thành giá trị thiếu.
Bài tập 1: Đọc bảng giá
File gia_theo_khu.csv (giả lập, USD/đêm):
khu,gia
NA,100
TB,80
NA,
TB,0
NA là mã khu hợp lệ; ô trống là chưa có giá; 0 là giá đã ghi nhận.
Viết mã tính giá trung bình từng khu từ file này.
Trọng tâm là quy ước khi đọc, không phải cú pháp groupby. Lời giải: đọc với keep_default_na=False, na_values={"gia": [""]}, rồi groupby("khu")["gia"].mean(). Kết quả: NA = 100, TB = 40. Đọc mặc định thì mã NA thành giá trị thiếu và groupby bỏ nhóm đó; thêm dropna=False vẫn chưa đúng vì nhãn nhóm thành NaN. Chỉ tắt danh sách mặc định mà không khai báo ô trống thì cột gia còn chuỗi rỗng, không đọc thành số được. Điền giá thiếu bằng 0 cho NA = 50.
Lưu bảng để dùng lại
Bảng đã đọc đúng mã, kiểu cột và giá trị thiếu; cần lưu lại để lần sau không phải khai báo lại.
Parquet là định dạng file nhị phân dành cho dữ liệu dạng bảng.
Nhu cầu Cách Parquet lưu dữ liệu
Giữ kiểu cột đã xác định Lưu kiểu cùng dữ liệu
Chỉ đọc các cột cần dùng Tổ chức dữ liệu theo cột
Giảm dung lượng lưu trữ Nén dữ liệu theo từng cột
Nếu ghi lại CSV, lần đọc sau lại phải khai báo kiểu. CSV vẫn tiện để xem và trao đổi; Parquet hợp với bảng trung gian được đọc nhiều lần. Parquet chia dữ liệu thành các nhóm hàng, trong mỗi nhóm lưu riêng từng cột, nên bộ đọc bỏ qua được cột không dùng; với CSV, usecols vẫn phải quét cả dòng văn bản. Kích thước và tốc độ tuỳ dữ liệu, không phải lúc nào cũng nhỏ hơn hay nhanh hơn CSV. Nguồn: McKinney §6.1–6.2; https://parquet.apache.org/docs/overview/.
Lưu bảng bằng Parquet
Lưu df đã đọc đúng ở đầu mục, rồi đọc lại:
df.to_parquet("data/processed/cho_o.parquet", index=False)
lai = pd.read_parquet("data/processed/cho_o.parquet")
print(lai.loc[0, "id"], lai["gia"].dtype)
001 float64
Mã 001 vẫn là chuỗi; gia vẫn là số.
Giữ file gốc trong raw/, ghi kết quả vào processed/.
Ví dụ kiểm trên pandas 3.0.3 và pyarrow. Parquet không sửa được kiểu đã đọc sai. So sánh tốc độ và kích thước file là phần tự học trong notebook; không kết luận công cụ nào luôn nhanh hơn.
Áp dụng cho dữ liệu Santiago
Bản CSV rút gọn ngày 29/06/2026 : 19 cột; price đã là số, đơn vị CLP/đêm.
listings = pd.read_csv(
"data/raw/santiago_2026-06-29.csv",
usecols=["id", "neighbourhood", "room_type", "price"],
dtype={"id": "str"},
)
print(listings.shape)
listings.to_parquet("data/processed/listings.parquet", index=False)
(18534, 4)
id là mã, không dùng để tính: đọc thành chuỗi. usecols chỉ đọc bốn cột cần dùng.
File Parquet này dùng lại ở mục 3.
Bản visualisations/listings.csv khác bản đầy đủ data/listings.csv.gz: bản đầy đủ lưu giá thành chuỗi tiền tệ, sẽ xử lý ở bài sau. Mã tải file và kiểm tra dữ liệu nằm trong notebook.
2. Lấy dữ liệu qua API
Tự động lấy dữ liệu từ dịch vụ theo yêu cầu.
Tự động lấy dữ liệu từ dịch vụ
Dữ liệu nằm trên hệ thống của nhà cung cấp và thay đổi theo thời gian, như dự báo thời tiết.
Chương trình cần cập nhật dữ liệu tự động.
Mỗi lần chỉ cần một phần dữ liệu: một địa điểm, vài ngày tới, một mã sản phẩm.
Nhà cung cấp cần quy định cách chương trình gửi yêu cầu và nhận đúng phần dữ liệu đó.
Bắt đầu từ nhu cầu trao đổi giữa các hệ thống, chưa nói đến code. Dự báo được cập nhật nhiều lần mỗi ngày; tải file bằng tay mỗi lần là không thực tế. Tải một file qua URL cũng tự động hoá được; khác biệt ở đây là dịch vụ trả đúng phần dữ liệu theo tham số đã quy định, chương trình không cần biết dữ liệu được lưu bên trong thế nào. Slide sau mới gọi tên API; ví dụ Open-Meteo bắt đầu từ slide thứ ba. Nguồn: McKinney §6.3.
Trao đổi dữ liệu qua API
API (giao diện lập trình ứng dụng) quy định cách các chương trình tương tác với nhau.
Web API trao đổi yêu cầu và phản hồi qua HTTP, giao thức truyền dữ liệu trên web.
Chương trình
Cần lấy dữ liệu
Phản hồi
Mã trạng thái, dữ liệu
API là viết tắt của Application Programming Interface; khái niệm này còn dùng cho thư viện và hệ điều hành, bài này chỉ xét web API. Chương trình gọi API có thể là ứng dụng web, ứng dụng di động hay script. Địa chỉ và tham số là hai thành phần dùng trong ví dụ GET kế tiếp, chưa phải mọi thành phần của một yêu cầu HTTP. Cách gọi và cấu trúc phản hồi do tài liệu của dịch vụ quy định. Nguồn: McKinney §6.3.
Ví dụ: lấy dự báo thời tiết
Tra tài liệu Open-Meteo để lấy endpoint (địa chỉ nhận yêu cầu):
https://api.open-meteo.com/v1/forecast
Yêu cầu Tham số Giá trị
Vị trí Santiago latitude, longitude-33.45, -70.66
Nhiệt độ tối đa ngày dailytemperature_2m_max
Ba ngày kể từ hôm nay forecast_days3
Ngày theo giờ Santiago timezoneAmerica/Santiago
Dịch yêu cầu phân tích thành tham số trước khi viết code. Toạ độ là một điểm minh hoạ trong Santiago. daily là đại lượng tổng hợp theo ngày; timezone quyết định ranh giới ngày; forecast_days=3 gồm hôm nay và hai ngày tiếp theo. Tên tham số do dịch vụ quy định: AI hay bịa tên tham số, luôn đối chiếu tài liệu. Open-Meteo mặc định trả °C, nhưng vẫn đọc daily_units khi xử lý.
Gửi yêu cầu bằng Python
requests.get() gửi yêu cầu HTTP; params là các tham số ở slide trước.
import requests
r = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": -33.45, "longitude": -70.66,
"daily": "temperature_2m_max",
"timezone": "America/Santiago", "forecast_days": 3},
timeout=20,
)
print(r.status_code)
200
r chứa phản hồi; mã 200 cho biết yêu cầu thành công.
Requests mã hoá params và ghép vào URL. Mở r.url (notebook in ra) trên trình duyệt: thấy chuỗi truy vấn và đúng JSON thô mà chương trình nhận. timeout=20 giới hạn thời gian chờ kết nối và chờ dữ liệu, không phải tổng thời gian tải; mất kết nối hay chờ quá lâu thì get báo lỗi ngay. Mã 200 chỉ nói yêu cầu HTTP thành công; vẫn phải kiểm cấu trúc, đơn vị và dữ liệu.
Đọc dữ liệu trong phản hồi
Open-Meteo trả nội dung dạng JSON : văn bản chứa các cặp tên–giá trị và mảng.
r.raise_for_status()
du_lieu = r.json()
print(type(du_lieu).__name__, len(du_lieu))
dict 9
raise_for_status() báo lỗi HTTP trước khi đọc dữ liệu.
json() chuyển đối tượng JSON thành dict, mảng thành list: ở đây là dict có 9 khoá.
JSON (JavaScript Object Notation) là định dạng văn bản để trao đổi dữ liệu. Chín khoá ở lớp ngoài: latitude, longitude, generationtime_ms, utc_offset_seconds, timezone, timezone_abbreviation, elevation, daily_units, daily; notebook in đủ danh sách. Đọc được JSON chưa chứng tỏ yêu cầu thành công, vì nội dung báo lỗi cũng có thể là JSON; vì vậy gọi raise_for_status trước (báo lỗi với mã 4xx, 5xx). Phản hồi không phải JSON hợp lệ thì r.json() báo JSONDecodeError. Nguồn: https://requests.readthedocs.io/en/latest/user/quickstart/#json-response-content.
Chọn phần JSON để tạo bảng
Bản rút gọn còn ba khoá cần dùng; số liệu giả lập:
{
"timezone": "America/Santiago",
"daily_units": {"temperature_2m_max": "°C"},
"daily": {
"time": ["2026-10-01", "2026-10-02", "2026-10-03"],
"temperature_2m_max": [22.0, 26.0, 24.0]
}
}
Hai danh sách trong daily khớp theo vị trí, mỗi vị trí một ngày.
daily_units cho biết đơn vị; timezone cho biết múi giờ.
01/10 ứng với 22°C, 02/10 với 26°C, 03/10 với 24°C. Số liệu cố định để học cấu trúc, không phải dự báo của lần gọi API trên lớp; tháng 10, nhiệt độ cao nhất ở Santiago trung bình khoảng 22°C (Open-Meteo, 2016–2025). Không đưa cả phản hồi vào DataFrame: lớp ngoài là thông tin mô tả, không phải cột của bảng theo ngày. Trước khi tạo bảng, xác định mỗi hàng biểu diễn điều gì.
Tạo bảng theo ngày
mau là dict đọc từ JSON giả lập ở slide trước.
thoi_tiet = pd.DataFrame(mau["daily"])
print(thoi_tiet.to_string(index=False))
time temperature_2m_max
2026-10-01 22.0
2026-10-02 26.0
2026-10-03 24.0
Mỗi hàng là một ngày; nhiệt độ tính bằng °C.
Giữ lại bản dự báo đã lấy
Gọi lại API có thể nhận được dự báo khác. Lưu nguyên phản hồi để phân tích lại cùng dữ liệu.
moc = pd.Timestamp.now(tz="UTC").strftime("%Y%m%d-%H%M%S")
tep_json = Path("data/raw") / f"du_bao_{moc}.json"
tep_json.write_bytes(r.content)
print(tep_json)
data/raw/du_bao_20260928-041605.json
du_lieu = json.loads(tep_json.read_text(encoding="utf-8"))
du_bao = pd.DataFrame(du_lieu["daily"])
print(du_bao.to_string(index=False))
time temperature_2m_max
2026-09-28 19.7
2026-09-29 18.8
2026-09-30 16.0
Đọc lại file này lúc nào cũng ra đúng bảng trên.
r.content là nguyên văn phản hồi, gồm cả dữ liệu và đơn vị. Tên file ghi thời điểm tải theo giờ UTC (giờ phối hợp quốc tế) nên mỗi lần tải có một file riêng. strftime đổi thời điểm thành chuỗi theo mã: %Y năm, %m tháng, %d ngày, %H giờ, %M phút, %S giây; Bài 8 dùng ba mã đầu trong format của pd.to_datetime để đọc chuỗi thành ngày. Out [8] và Out [9] là lần chạy lúc soạn slide (04:16 UTC ngày 28/09/2026); chạy trên lớp sẽ ra tên file và số liệu khác. Mở ngăn Files (biểu tượng thư mục) bên trái Colab để thấy file vừa ghi trong data/raw. json.loads chuyển văn bản trong file thành dict, như r.json(). Muốn phân tích lại cùng đầu vào thì đọc file cũ, không gọi lại get. Trong chương trình thực tế, lưu ngay sau khi nhận và kiểm phản hồi.
Bài tập 2: Lọc ngày nóng
Phản hồi giả lập:
{
"timezone": "America/Phoenix",
"daily_units": {"temperature_2m_max": "°F"},
"daily": {
"time": ["2026-10-01", "2026-10-02", "2026-10-03"],
"temperature_2m_max": [95, 100, 88]
}
}
Đổi đơn vị: °C = (°F − 32) / 1,8.
Lấy các ngày có nhiệt độ tối đa từ 35°C trở lên.
Kết quả: 01/10 và 02/10. Nhiệt độ sau đổi là 35; 37,8; 31,1°C. Ngày 01/10 đúng bằng ngưỡng nên phải dùng ≥; phép tính (95 − 32)/1,8 cho đúng 35.0, không có sai số làm tròn. Có thể đổi cả cột sang °C hoặc đổi ngưỡng 35°C thành 95°F; chấp nhận mọi cách đúng. Trọng tâm: đọc daily_units trước khi lọc; so trực tiếp với 35 sẽ chọn cả ba ngày. 35°C là ngưỡng nắng nóng của cơ quan khí tượng Việt Nam (từ 37°C là nắng nóng gay gắt). Phoenix (Arizona, Mỹ) dùng °F; nhiệt độ cao nhất ngày 01–03/10 ở đó trung bình khoảng 94°F (Open-Meteo, 2016–2025).
3. SQL và DuckDB
Truy vấn dữ liệu tại nguồn, lấy kết quả về phân tích.
Lấy kết quả từ cơ sở dữ liệu
Bảng chỗ ở nằm trong cơ sở dữ liệu (CSDL) trên máy chủ; báo cáo chỉ cần giá trung bình từng khu.
pandas
Python
Nạp vào RAM rồi groupby
SQL
CSDL
Tính theo truy vấn SQL
SQL mô tả kết quả cần lấy; hệ quản trị CSDL tính ngay tại nơi lưu dữ liệu.
Tình huống: dữ liệu nằm trong CSDL trên máy chủ, chưa phải DataFrame trong bộ nhớ. pandas chỉ tính được trên bảng đã nạp vào RAM của chương trình, nên phải tải cả bảng về: tốn thời gian truyền, và bảng phải vừa bộ nhớ. Với SQL, hệ quản trị tính tại chỗ và chỉ gửi bảng kết quả, ở đây mỗi khu một hàng. Khung cam là nơi thực hiện phép tính. Lệnh tải bảng về pandas (pd.read_sql) thật ra cũng là một câu SQL, SELECT * lấy cả bảng; khác biệt nằm ở chỗ để CSDL tính hay tải hết về rồi mới tính. SQL (Structured Query Language) mô tả kết quả cần lấy; hệ quản trị chọn cách thực hiện, người viết không viết vòng lặp qua từng hàng. Lọc, nhóm, ghép là các thao tác đã gặp ở Bài 5; cú pháp và hàm cụ thể có thể khác nhau giữa các hệ quản trị. Hỏi lớp: nếu bảng lớn hơn RAM của laptop thì cách nào còn làm được? Nguồn: McKinney §6.4.
Dùng DuckDB trong Python
Dữ liệu của bài là file và DataFrame trên máy; cần chạy SQL ngay trong Python.
DuckDB là hệ quản trị cơ sở dữ liệu dành cho phân tích:
Chạy cùng chương trình, không cần máy chủ riêng.
Truy vấn trực tiếp file CSV, Parquet hoặc DataFrame.
Lấy kết quả về pandas bằng .df().
SQL là ngôn ngữ, DuckDB là công cụ thực hiện. Chọn DuckDB vì dùng được ngay trên file và DataFrame đã có ở mục 1, không phải dựng máy chủ hay nhập dữ liệu vào một hệ quản trị khác. Bắt đầu với df năm hàng để tính tay, cuối mục truy vấn file Parquet. DuckDB được thiết kế cho tổng hợp và ghép bảng, nhưng không phải lúc nào cũng nhanh hơn pandas. Nguồn: https://duckdb.org/why_duckdb, https://duckdb.org/docs/current/clients/python/overview.
Lọc hàng bằng SQL
duckdb.sql() chạy câu SQL. FROM df là DataFrame df của mục 1: DuckDB tìm bảng theo tên biến Python.
import duckdb
ket_qua = duckdb.sql("""
SELECT id, khu, gia
FROM df
WHERE gia >= 100
ORDER BY gia DESC
""").df()
print(ket_qua.to_string(index=False))
id khu gia
003 A 200.0
004 B 120.0
001 A 100.0
WHERE lọc hàng; ORDER BY … DESC sắp xếp giảm dần.
Chỉ lần lượt: FROM là bảng nguồn, WHERE là điều kiện, SELECT là cột kết quả. Hỏi trước khi chạy: hàng 005 (giá thiếu) có trong kết quả không? Không: phép so sánh giá thiếu với 100 không trả về true, nên hàng bị loại. Thứ tự viết các mệnh đề không phải thứ tự máy thực hiện.
Tính theo nhóm bằng SQL
theo_khu = duckdb.sql("""
SELECT khu, AVG(gia) AS trung_binh,
COUNT(*) AS so_cho_o, COUNT(gia) AS so_gia
FROM df
GROUP BY khu
ORDER BY khu
""").df()
print(theo_khu.to_string(index=False))
khu trung_binh so_cho_o so_gia
A 150.0 3 2
B 100.0 2 2
Khu A có ba chỗ ở nhưng chỉ hai giá đã biết.
NaN của pandas thành NULL trong SQL; AVG và COUNT(gia) bỏ qua NULL.
Liên hệ size và count của Bài 5: COUNT(*) đếm mọi hàng, COUNT(gia) chỉ đếm giá khác NULL. Cho sinh viên tính tay trước: A = (100 + 200)/2 = 150. AS chỉ đặt tên cột kết quả. Chỉ NaN đọc từ DataFrame mới thành NULL; một giá trị NaN thật trong SQL không bị bỏ qua (AVG trả NaN).
Lọc hàng và lọc nhóm
Giá trong df: khu A có 100, 200, NaN; khu B có 80, 120.
Trung bình các chỗ ở giá từ 150
SELECT khu, AVG(gia) AS tb
FROM df
WHERE gia >= 150
GROUP BY khu
Lọc hàng: còn (A, 200); B hết hàng Tính theo khu: A 200 Các khu có giá trung bình từ 150
SELECT khu, AVG(gia) AS tb
FROM df
GROUP BY khu
HAVING AVG(gia) >= 150
Tính theo khu: A 150, B 100 Lọc nhóm: còn A 150; B bị loại WHERE lọc hàng trước khi tính; HAVING lọc nhóm sau khi tính.
Hỏi lớp trước khi chỉ vào hai bước: khu B có trong kết quả không? Bên trái: WHERE loại hàng giá 100 và hàng thiếu giá của khu A, nên trung bình còn 200; cả hai hàng khu B (80, 120) đều bị loại nên không còn nhóm B. Bên phải: khu A tính đủ 100 và 200; nhóm B có trung bình 100 < 150 nên HAVING loại. Hai truy vấn trả lời hai câu hỏi khác nhau (dòng chữ xanh trên mỗi cột), không câu nào sai. Ý bên trái dùng ngay ở bài tập 3.
Bài tập 3: Đếm giá thiếu
id khu gia (USD/đêm) 001 A 100 002 B 80 003 A 200 004 B 120 005 A NaN
Viết một truy vấn SQL đếm số chỗ ở chưa có giá của từng khu, kể cả khu không thiếu giá.
SELECT khu, COUNT(*) - COUNT(gia) AS so_thieu FROM df GROUP BY khu ORDER BY khu. A = 1, B = 0. Nếu chỉ WHERE gia IS NULL rồi GROUP BY khu sẽ mất khu B. Chấp nhận các cách đúng khác; không bắt buộc ghi nhớ CASE WHEN chưa được dạy.
Ghép bảng bằng SQL
Bảng khu_vung có một hàng: khu A thuộc vùng Bắc .
ghep = duckdb.sql("""
SELECT df.id, df.khu, khu_vung.vung
FROM df LEFT JOIN khu_vung ON df.khu = khu_vung.khu
ORDER BY df.id
""").df()
print(ghep.to_string(index=False))
id khu vung
001 A Bắc
002 B NaN
003 A Bắc
004 B NaN
005 A Bắc
LEFT JOIN giữ cả hai chỗ ở khu B dù chưa có vùng tương ứng.
Liên hệ left merge của Bài 5. khu_vung.vung là cột vung của bảng khu_vung; ON là điều kiện ghép. NULL trong SQL về pandas thành NaN. Khoá lặp ở bảng phải cũng nhân hàng như merge.
Truy vấn file Parquet
Truy vấn thẳng file đã lưu; DuckDB chỉ đọc hai cột cần dùng.
ket_qua_that = duckdb.sql("""
SELECT room_type, MEDIAN(price) AS trung_vi,
COUNT(price) AS so_gia
FROM 'data/processed/listings.parquet'
GROUP BY room_type
ORDER BY trung_vi DESC
""").df()
print(ket_qua_that.to_string(index=False))
room_type trung_vi so_gia
Hotel room 117138.5 56
Entire home/apt 64900.0 14425
Private room 34235.0 3148
Shared room 20541.0 59
Santiago 29/06/2026, CLP/đêm; khớp với groupby của pandas.
Trung vị cao nhất (Hotel room) chỉ dựa trên 56 giá.
Khép lại nhu cầu đầu mục: lấy bảng tổng hợp mà không nạp cả file thành DataFrame. DuckDB chỉ đọc hai cột room_type và price; phần tự học của notebook in kế hoạch truy vấn để kiểm. Với pandas, muốn bớt cột phải tự ghi columns=... khi đọc. .df() đưa bảng kết quả bốn hàng về pandas. Notebook đối chiếu bằng groupby trên cùng dữ liệu (assert_frame_equal). Hỏi lớp: xếp hạng theo trung vị có đáng tin khi Hotel room chỉ có 56 giá, Shared room 59 giá? Nguồn: https://duckdb.org/docs/current/data/parquet/overview.
Chọn SQL hay pandas?
Cùng lọc, nhóm, ghép được; khác ở nơi tính và cách viết.
SQL pandas
Nơi tính Tại CSDL, file; chỉ kết quả về Python Trong RAM, sau khi nạp cả bảng
Cách viết Mô tả kết quả; hệ thống tự chọn cách đọc, cách tính Từng bước; tự chọn cột cần đọc
Dùng ở Hầu hết hệ quản trị CSDL Chương trình Python
Hợp với Lấy, lọc, gộp bảng lớn tại nguồn Phân tích tiếp, hàm tuỳ ý, vẽ biểu đồ
Thường dùng cả hai: SQL lấy, gộp tại nguồn; pandas xử lý tiếp.
Khép lại mục 3. Bằng chứng ở ví dụ Parquet: kế hoạch truy vấn (phần tự học của notebook) cho thấy DuckDB chỉ đọc room_type và price. DuckDB xử lý được dữ liệu lớn hơn RAM với GROUP BY, JOIN, ORDER BY; pandas cần cả bảng trong RAM, hoặc người viết tự đọc từng khối (cũng ở phần tự học). Trên bảng nhỏ đã nằm trong RAM như df, hai cách cho cùng kết quả: chọn cách dễ đọc hơn; không khẳng định SQL luôn nhanh hơn. SQL dùng được ở hầu hết CSDL quan hệ như PostgreSQL, MySQL, SQLite, BigQuery; hàm và cú pháp chi tiết khác nhau đôi chút. Nguồn: https://duckdb.org/docs/current/guides/performance/how_to_tune_workloads.