跳至主要内容

JupySQL 和 chDB

JupySQL 是一个 Python 库,允许你在 Jupyter notebook 和 iPython shell 中运行 SQL。在本指南中,我们将学习如何使用 chDB 和 JupySQL 查询数据。

设置

首先,让我们创建一个虚拟环境

python -m venv .venv
source .venv/bin/activate

然后,我们将安装 JupySQL、iPython 和 Jupyter Lab

pip install jupysql ipython jupyterlab

我们可以在 iPython 中使用 JupySQL,可以通过运行以下命令启动 iPython:

ipython

或者在 Jupyter Lab 中运行:

jupyter lab
注意

如果你使用的是 Jupyter Lab,你需要在继续本指南的其余部分之前创建一个 notebook。

下载数据集

我们将使用 Jeff Sackmann 的 tennis_atp 数据集之一,其中包含有关球员及其排名随时间变化的元数据。让我们从下载排名文件开始

from urllib.request import urlretrieve
files = ['00s', '10s', '20s', '70s', '80s', '90s', 'current']
base = "https://raw.githubusercontent.com/JeffSackmann/tennis_atp/master"
for file in files:
_ = urlretrieve(
f"{base}/atp_rankings_{file}.csv",
f"atp_rankings_{file}.csv",
)

配置 chDB 和 JupySQL

接下来,让我们导入 chDB 的 dbapi 模块

from chdb import dbapi

我们将创建一个 chDB 连接。我们持久化的任何数据都将保存到 atp.chdb 目录中

conn = dbapi.connect(path="atp.chdb")

现在,让我们加载 sql 魔法并创建到 chDB 的连接

%load_ext sql
%sql conn --alias chdb

接下来,我们将显示显示限制,以便查询结果不会被截断

%config SqlMagic.displaylimit = None

## 查询 CSV 文件中的数据

我们下载了一堆以 atp_rankings 为前缀的文件。让我们使用 DESCRIBE 子句来了解架构

%%sql
DESCRIBE file('atp_rankings*.csv')
SETTINGS describe_compact_output=1,
schema_inference_make_columns_nullable=0
+--------------+-------+
| name | type |
+--------------+-------+
| ranking_date | Int64 |
| rank | Int64 |
| player | Int64 |
| points | Int64 |
+--------------+-------+

我们还可以直接针对这些文件编写 SELECT 查询,以查看数据的样子

%sql SELECT * FROM file('atp_rankings*.csv') LIMIT 1
+--------------+------+--------+--------+
| ranking_date | rank | player | points |
+--------------+------+--------+--------+
| 20000110 | 1 | 101736 | 4135 |
+--------------+------+--------+--------+

数据的格式有点奇怪。让我们清理该日期并使用 REPLACE 子句返回清理后的 ranking_date

%%sql
SELECT * REPLACE (
toDate(parseDateTime32BestEffort(toString(ranking_date))) AS ranking_date
)
FROM file('atp_rankings*.csv')
LIMIT 10
SETTINGS schema_inference_make_columns_nullable=0
+--------------+------+--------+--------+
| ranking_date | rank | player | points |
+--------------+------+--------+--------+
| 2000-01-10 | 1 | 101736 | 4135 |
| 2000-01-10 | 2 | 102338 | 2915 |
| 2000-01-10 | 3 | 101948 | 2419 |
| 2000-01-10 | 4 | 103017 | 2184 |
| 2000-01-10 | 5 | 102856 | 2169 |
| 2000-01-10 | 6 | 102358 | 2107 |
| 2000-01-10 | 7 | 102839 | 1966 |
| 2000-01-10 | 8 | 101774 | 1929 |
| 2000-01-10 | 9 | 102701 | 1846 |
| 2000-01-10 | 10 | 101990 | 1739 |
+--------------+------+--------+--------+

将 CSV 文件导入 chDB

现在我们将把这些 CSV 文件中的数据存储在一个表中。默认数据库不会将数据持久化到磁盘上,因此我们需要先创建一个其他数据库

%sql CREATE DATABASE atp

现在我们将创建一个名为 rankings 的表,其架构将从 CSV 文件中数据的结构派生而来

%%sql
CREATE TABLE atp.rankings
ENGINE=MergeTree
ORDER BY ranking_date AS
SELECT * REPLACE (
toDate(parseDateTime32BestEffort(toString(ranking_date))) AS ranking_date
)
FROM file('atp_rankings*.csv')
SETTINGS schema_inference_make_columns_nullable=0

让我们快速检查一下表中的数据

%sql SELECT * FROM atp.rankings LIMIT 10
+--------------+------+--------+--------+
| ranking_date | rank | player | points |
+--------------+------+--------+--------+
| 2000-01-10 | 1 | 101736 | 4135 |
| 2000-01-10 | 2 | 102338 | 2915 |
| 2000-01-10 | 3 | 101948 | 2419 |
| 2000-01-10 | 4 | 103017 | 2184 |
| 2000-01-10 | 5 | 102856 | 2169 |
| 2000-01-10 | 6 | 102358 | 2107 |
| 2000-01-10 | 7 | 102839 | 1966 |
| 2000-01-10 | 8 | 101774 | 1929 |
| 2000-01-10 | 9 | 102701 | 1846 |
| 2000-01-10 | 10 | 101990 | 1739 |
+--------------+------+--------+--------+

看起来不错 - 正如预期的那样,输出与直接查询 CSV 文件时相同。

我们将对球员元数据遵循相同的流程。这次数据都在一个 CSV 文件中,所以让我们下载该文件

_ = urlretrieve(
f"{base}/atp_players.csv",
"atp_players.csv",
)

然后根据 CSV 文件的内容创建一个名为 players 的表。我们还将清理 dob 字段,使其成为 Date32 类型。

在 ClickHouse 中,Date 类型仅支持 1970 年以后的日期。由于 dob 列包含 1970 年之前的日期,因此我们将使用 Date32 类型。

%%sql
CREATE TABLE atp.players
Engine=MergeTree
ORDER BY player_id AS
SELECT * REPLACE (
makeDate32(
toInt32OrNull(substring(toString(dob), 1, 4)),
toInt32OrNull(substring(toString(dob), 5, 2)),
toInt32OrNull(substring(toString(dob), 7, 2))
)::Nullable(Date32) AS dob
)
FROM file('atp_players.csv')
SETTINGS schema_inference_make_columns_nullable=0

完成后,我们可以查看我们已摄取的数据

%sql SELECT * FROM atp.players LIMIT 10
+-----------+------------+-----------+------+------------+-----+--------+-------------+
| player_id | name_first | name_last | hand | dob | ioc | height | wikidata_id |
+-----------+------------+-----------+------+------------+-----+--------+-------------+
| 100001 | Gardnar | Mulloy | R | 1913-11-22 | USA | 185 | Q54544 |
| 100002 | Pancho | Segura | R | 1921-06-20 | ECU | 168 | Q54581 |
| 100003 | Frank | Sedgman | R | 1927-10-02 | AUS | 180 | Q962049 |
| 100004 | Giuseppe | Merlo | R | 1927-10-11 | ITA | 0 | Q1258752 |
| 100005 | Richard | Gonzalez | R | 1928-05-09 | USA | 188 | Q53554 |
| 100006 | Grant | Golden | R | 1929-08-21 | USA | 175 | Q3115390 |
| 100007 | Abe | Segal | L | 1930-10-23 | RSA | 0 | Q1258527 |
| 100008 | Kurt | Nielsen | R | 1930-11-19 | DEN | 0 | Q552261 |
| 100009 | Istvan | Gulyas | R | 1931-10-14 | HUN | 0 | Q51066 |
| 100010 | Luis | Ayala | R | 1932-09-18 | CHI | 170 | Q1275397 |
+-----------+------------+-----------+------+------------+-----+--------+-------------+

查询 chDB

数据摄取已完成,现在是激动人心的部分——查询数据!

网球运动员根据他们在比赛中的表现获得积分。每个球员在 52 周滚动期间的积分。我们将编写一个查询,查找每个球员累积的最大积分以及他们当时的排名

%%sql
SELECT name_first, name_last,
max(points) as maxPoints,
argMax(rank, points) as rank,
argMax(ranking_date, points) as date
FROM atp.players
JOIN atp.rankings ON rankings.player = players.player_id
GROUP BY ALL
ORDER BY maxPoints DESC
LIMIT 10
+------------+-----------+-----------+------+------------+
| name_first | name_last | maxPoints | rank | date |
+------------+-----------+-----------+------+------------+
| Novak | Djokovic | 16950 | 1 | 2016-06-06 |
| Rafael | Nadal | 15390 | 1 | 2009-04-20 |
| Andy | Murray | 12685 | 1 | 2016-11-21 |
| Roger | Federer | 12315 | 1 | 2012-10-29 |
| Daniil | Medvedev | 10780 | 2 | 2021-09-13 |
| Carlos | Alcaraz | 9815 | 1 | 2023-08-21 |
| Dominic | Thiem | 9125 | 3 | 2021-01-18 |
| Jannik | Sinner | 8860 | 2 | 2024-05-06 |
| Stefanos | Tsitsipas | 8350 | 3 | 2021-09-20 |
| Alexander | Zverev | 8240 | 4 | 2021-08-23 |
+------------+-----------+-----------+------+------------+

很有趣的是,名单上的一些球员在获得如此多的积分时,却没有获得第一名的排名。

保存查询

我们可以在与 %%sql 魔法相同的行上使用 --save 参数保存查询。--no-execute 参数表示将跳过查询执行。

%%sql --save best_points --no-execute
SELECT name_first, name_last,
max(points) as maxPoints,
argMax(rank, points) as rank,
argMax(ranking_date, points) as date
FROM atp.players
JOIN atp.rankings ON rankings.player = players.player_id
GROUP BY ALL
ORDER BY maxPoints DESC

当我们运行保存的查询时,它将在执行前转换为公共表表达式 (CTE)。在下面的查询中,我们计算球员在排名第一时获得的最大积分

%sql select * FROM best_points WHERE rank=1
+-------------+-----------+-----------+------+------------+
| name_first | name_last | maxPoints | rank | date |
+-------------+-----------+-----------+------+------------+
| Novak | Djokovic | 16950 | 1 | 2016-06-06 |
| Rafael | Nadal | 15390 | 1 | 2009-04-20 |
| Andy | Murray | 12685 | 1 | 2016-11-21 |
| Roger | Federer | 12315 | 1 | 2012-10-29 |
| Carlos | Alcaraz | 9815 | 1 | 2023-08-21 |
| Pete | Sampras | 5792 | 1 | 1997-08-11 |
| Andre | Agassi | 5652 | 1 | 1995-08-21 |
| Lleyton | Hewitt | 5205 | 1 | 2002-08-12 |
| Gustavo | Kuerten | 4750 | 1 | 2001-09-10 |
| Juan Carlos | Ferrero | 4570 | 1 | 2003-10-20 |
| Stefan | Edberg | 3997 | 1 | 1991-02-25 |
| Jim | Courier | 3973 | 1 | 1993-08-23 |
| Ivan | Lendl | 3420 | 1 | 1990-02-26 |
| Ilie | Nastase | 0 | 1 | 1973-08-27 |
+-------------+-----------+-----------+------+------------+

使用参数查询

我们也可以在查询中使用参数。参数只是普通的变量

rank = 10

然后我们可以在查询中使用 {{variable}} 语法。以下查询查找球员在首次进入前 10 名和最后一次进入前 10 名之间天数最少的球员

%%sql
SELECT name_first, name_last,
MIN(ranking_date) AS earliest_date,
MAX(ranking_date) AS most_recent_date,
most_recent_date - earliest_date AS days,
1 + (days/7) AS weeks
FROM atp.rankings
JOIN atp.players ON players.player_id = rankings.player
WHERE rank <= {{rank}}
GROUP BY ALL
ORDER BY days
LIMIT 10
+------------+-----------+---------------+------------------+------+-------+
| name_first | name_last | earliest_date | most_recent_date | days | weeks |
+------------+-----------+---------------+------------------+------+-------+
| Alex | Metreveli | 1974-06-03 | 1974-06-03 | 0 | 1 |
| Mikael | Pernfors | 1986-09-22 | 1986-09-22 | 0 | 1 |
| Felix | Mantilla | 1998-06-08 | 1998-06-08 | 0 | 1 |
| Wojtek | Fibak | 1977-07-25 | 1977-07-25 | 0 | 1 |
| Thierry | Tulasne | 1986-08-04 | 1986-08-04 | 0 | 1 |
| Lucas | Pouille | 2018-03-19 | 2018-03-19 | 0 | 1 |
| John | Alexander | 1975-12-15 | 1975-12-15 | 0 | 1 |
| Nicolas | Massu | 2004-09-13 | 2004-09-20 | 7 | 2 |
| Arnaud | Clement | 2001-04-02 | 2001-04-09 | 7 | 2 |
| Ernests | Gulbis | 2014-06-09 | 2014-06-23 | 14 | 3 |
+------------+-----------+---------------+------------------+------+-------+

绘制直方图

JupySQL 还具有有限的图表功能。我们可以创建箱线图或直方图。

我们将创建一个直方图,但首先让我们编写(并保存)一个查询,计算每个球员在排名前 100 名中取得的排名。我们将能够使用它来创建一个直方图,统计有多少球员取得了每个排名

%%sql --save players_per_rank --no-execute
select distinct player, rank
FROM atp.rankings
WHERE rank <= 100

然后,我们可以通过运行以下命令创建直方图:

from sql.ggplot import ggplot, geom_histogram, aes

plot = (
ggplot(
table="players_per_rank",
with_="players_per_rank",
mapping=aes(x="rank", fill="#69f0ae", color="#fff"),
) + geom_histogram(bins=100)
)
Migrating Self-managed ClickHouse