Перейти к содержимому
TelegramGitHub

DuckDB: Up and Running, глава 4: DuckDB и Polars

Главная мысль: Polars быстро обрабатывает таблицы в памяти. DuckDB позволяет писать SQL прямо по DataFrame. Вместе они дают скорость и привычный язык.

pandas грузит всю таблицу в память. На больших файлах он медленный. Polars решает это.

СвойствоЧто даёт
Написан на RustВысокая скорость
ПараллельностьИспользует все ядра процессора
Колоночное хранениеЭффективная память
Lazy evaluationЗапрос оптимизируется до запуска
СинтаксисПохож на pandas и SQL
  • У Polars DataFrame нет индекса. Автор считает индекс ненужным.
  • Под названием колонки Polars показывает её тип: str, i64, f64, bool.
  • Квадратные скобки df['col'] работают, но не рекомендуются. Используйте select() и filter().
ЗадачаМетод
Выбрать колонкиdf.select('a', 'b')
Выбрать колонки по типуdf.select(pl.col(pl.String))
Выбрать строкиdf.filter(pl.col('year') > 1980)
Несколько условийКаждое условие в скобках, между ними & и |
Значение из спискаpl.col('x').is_in([...])
Сортировка.sort_by(...) в выражении или df.sort(...)
Первая строкаdf.row(0)

pl.col(...) называется expression. Выражения можно соединять в цепочку.

Класс SQLContext выполняет SQL по DataFrame. Имя таблицы вы задаёте сами: ctx = pl.SQLContext(cars=df).

Идея: не выполнять шаги сразу. Собрать план, оптимизировать, и только потом запустить.

РежимКак включитьЧто происходит
Eagerpl.read_csv()Файл целиком читается в память сразу
Implicit lazypl.scan_csv()Возвращает LazyFrame. Ничего не читается
Explicit lazypl.read_csv(...).lazy()Файл уже прочитан, но дальше шаги оптимизируются
  • collect() запускает план и возвращает DataFrame.
  • show_graph() рисует план. Оптимизированный план читает только нужные колонки и применяет фильтр при чтении.
  • На файле в 5,8 млн строк это экономит память: выбираются 3 колонки из 31.

DuckDB видит DataFrame через Apache Arrow. Нужна библиотека pyarrow. Имя Python-переменной с DataFrame можно использовать как имя таблицы.

import duckdb
# df — любая переменная с Polars или pandas DataFrame
result = duckdb.sql("SELECT * FROM df")

Функция duckdb.sql() возвращает объект DuckDBPyRelation. Из него можно получить результат в нужном виде.

МетодРезультат
.pl()Polars DataFrame
.df()pandas DataFrame
.describe()Статистика по колонкам
.order('year DESC')Сортировка
.apply('min', 'year')Агрегат по колонке

Автор советует: если задачу проще выразить на SQL, пишите SQL и забирайте результат через .pl().

Это второй способ строить запросы. Вместо текста SQL вы вызываете методы.

МетодАналог в SQL
conn.table('t')FROM t
.insert([...])INSERT INTO
.join(other, condition=..., how='inner')JOIN
.filter('customer_id = 1')WHERE
.aggregate('...', 'group_col')GROUP BY
.project('a, b')SELECT a, b
.limit(3, 2)LIMIT 3 OFFSET 2
  • Методы можно соединять в цепочку.
  • Для повторного запроса к результату можно вызвать conn.execute('SELECT ... FROM result').
ТерминЗначение
PolarsБиблиотека DataFrame на Rust
LazyFrameПлан запроса, который ещё не выполнен
expressionВыражение pl.col(...), из которых строят запрос
collect()Запуск ленивого плана
Apache ArrowКолоночный формат данных в памяти. Через него DuckDB читает DataFrame без копирования
DuckDBPyRelationОбъект запроса DuckDB в Python
  • DuckDB читает pandas и Polars без загрузки в базу. Это удобно в ноутбуке.
  • Для больших CSV и Parquet используйте ленивое чтение (scan_csv, scan_parquet). Вы не платите за колонки, которые не нужны.
  • Смешивайте стили. Фильтры и выражения пишите в Polars, сложные JOIN и оконные функции пишите в SQL.
  • Если команда знает SQL, DuckDB снижает порог входа в DataFrame.
  • Версия в книге: Polars 1.8.2. В новых версиях API мог поменяться. Проверяйте документацию.

Нужны Python и пакеты: pip install polars duckdb pyarrow. Блоки идут по порядку. SQL-блоки можно запустить и в терминале duckdb.

import polars as pl
import duckdb
cars = pl.DataFrame({
"model": ["Alfa", "Bravo", "Cargo", "Delta", "Echo", "Fox"],
"year": [1982, 1994, 2000, 1975, 2007, 1966],
"company": ["A", "A", "B", "B", "C", "C"],
"engine": [2.5, 2.0, 1.5, 2.7, 1.4, 1.8],
})
print(cars)
# DuckDB находит переменную cars по имени
duckdb.sql("""
SELECT company, count(*) AS models, round(avg(engine), 2) AS avg_engine
FROM cars
GROUP BY company
ORDER BY company
""").pl()

Ожидаем три строки: A, B, C. По 2 модели в каждой.

q = (
cars.lazy()
.filter(pl.col("year") > 1980)
.select("model", "year")
)
print(q.collect()) # 4 строки: Alfa, Bravo, Cargo, Echo

Этот блок работает в терминале duckdb. Он проверяет логику без Python.

CREATE TABLE cars AS
SELECT * FROM (VALUES
('Alfa', 1982, 'A', 2.5),
('Bravo', 1994, 'A', 2.0),
('Cargo', 2000, 'B', 1.5),
('Delta', 1975, 'B', 2.7),
('Echo', 2007, 'C', 1.4),
('Fox', 1966, 'C', 1.8)
) t(model, year, company, engine);
SELECT model, year FROM cars WHERE year > 1980 ORDER BY year;

1. На Polars выберите модели компании A, у которых двигатель больше 2.0. Выведите только model.

Решение
cars.filter(
(pl.col("company") == "A") & (pl.col("engine") > 2.0)
).select("model")

Результат: одна строка, Alfa. Каждое условие стоит в скобках.

2. Запросом SQL по DataFrame посчитайте самый ранний год для каждой компании.

Решение
duckdb.sql("""
SELECT company, min(year) AS first_year
FROM cars GROUP BY company ORDER BY company
""").pl()

Получим A: 1982, B: 1975, C: 1966.

3. Сделайте то же через методы DuckDBPyRelation, без текста SQL: отфильтруйте year > 1980, оставьте model и year, ограничьте до 2 строк.

Решение
rel = duckdb.sql("SELECT * FROM cars")
rel.filter("year > 1980").project("model, year").limit(2)

Методы идут по цепочке, как WHERE, SELECT, LIMIT в SQL.

  1. Чем read_csv отличается от scan_csv в Polars?
  2. Что делает collect()? Что будет, если его не вызвать?
  3. Как DuckDB читает Python-переменную с DataFrame? Какая библиотека нужна?
  4. Когда лучше писать запрос на SQL, а когда в выражениях Polars?

Предыдущая: Глава 3. Основы SQL. Следующая: Глава 5. EDA и геоанализ.