DuckDB精读——基于Getting started with DuckDB

一、命令行直接运行DuckDB 命令

duckdb -markdown -s "select * from tmp"

  • 教材上是./duckdb ...,但我的电脑不行,直接用duckdb
  • -markdown 是改变输出模式
  • -s是个重要的参数,待补充

二、利用powershell性质进行过滤

duckdb --help | Select-String -pattern 'mode'


三、强大的copy命令

copy tb_name from file_name

  • 支持CSV, Parquet, and JSON 导入导出
  • 必须是向已有的表copy,主要目的可能是为了为表定义正确的schema
  • 直接导入固然简单,但在设置schema时一点也不少做
  • copy的对象是表名或查询名,后面用from或to设置方向
  • copy只适用已有表,这不影响用create table tb as select * from file

四、copy from csv

  • 文件与数据库表列名顺序不同,可以在表名后面的括号中写明列的顺序
  • 后面括号中的内容,应该是copy语法的,不是read_csv语法的
sql 复制代码
	copy 
		foods(food_name,is_healthy,color,calories) 
		from 'foods_with_heading.tsv' 
		(delim '\t',header true);

五、DuckDB自带csv嗅探器sniff_csv

copy from时,默认用DuckDB自带的嗅探器分析csv文件每列的格式

这个过程,不是猜,DuckDB有专门的函数sniff_csv(file_name) 用.mode:box`模式复制代码

sql 复制代码
	 from sniff_csv('food_prices.csv');
       Delimiter = ,
           Quote = (empty)
          Escape = (empty)
NewLineDelimiter = \n
         Comment = (empty)
        SkipRows = 0
       HasHeader = true
         Columns = [{'name': food_name, 'type': VARCHAR}, 
         {'name': price, 'type': DOUBLE}]
      DateFormat = NULL
 TimestampFormat = NULL
   UserArguments = NULL
          Prompt = FROM read_csv('food_prices.csv', 
          auto_detect=false, delim=',', quote='',
           escape='', new_line='\n', skip=0, 
           comment='', header=true, 
           columns={'food_name': 'VARCHAR', 'price': 'DOUBLE'});
  • sniff_csv是自动探测,可以傻瓜式执行
  • Prompt部分提供了自动探测最终形成的SQL语句
  • 注意,columns={'col':'type}语法
  • read_csv中的auto_detect=false后,变成手动设置

六、read_csv设置列的时间格式

  • 时间格式是应用于所有列的:dateformat='%Y%m%d',
  • 具体哪个列应用这些格式,需要明确设置:types={'order_date':'date'}
  • 原文件中有多列、多种时间格式怎么办?
  • 如果要对多列设置类型,用:`columns={a:typea,b:typeb,...}

七、read_csv同时读入多个文件并记录文件名

sql 复制代码
	from read_csv('food_collection/pizza*.csv',
      filename=true);

八、read_csv将schema不同的文件合并到一张表中

将参数union_by_name=true,可以将不同schema的文件合并到一张表中


九、方便的parquet_schema()

parquet是自描述文件,包含schema元数据,本函数直接返回数据列名、类型等信息

DuckDB导入数据时,有时会进行类型转化,相关信息保存在converted_type字段中


十、CSV文件标题行有空格

平台反馈数据,每行都带有空格,直接导入DuckDB,无法解决

目前找到的办法是

  • sniff_csv(file,encoding='zh_cn.gbk')借鉴格式
  • 参考前面信息在read_csv函数中手动设置表的schema
  • quote='"' 提示错误,可以直接删除,不删除,注意看一下里面有几个双引号
  • 导入时全部用varchar类型
  • 入库后用strptime将字符转成timestamp类型

十一、dbeaver中时间戳后面多.000问题

dbeaver连接DuckDB,字段是timestamp格式时,dbeaver显示数据时,字段值后面多了.000

这不要DuckDB的原因,是dbeaver显示精度设置的问题,解决办法:

dbeaver --> 窗口 --> 首选项 --> 编辑器 --> 数据编辑器 --> 数据格式

在右侧窗口选择时间戳,将格式中.SSS删除即可


十二、模式box和duckbox不同

  • box 使用 Unicode 框绘制字符的表格
  • duckbox 具有丰富功能的表格

十三、多种describe

无论是库中的表,还是disk上的csv,可以用多种方式获得表属性

对于disk上的csv,可以直接看在表,在describe时不必带read_csv()

  • describe 'file.csv';
  • describe table 'bike.csv';
  • describe from read_csv('bike.csv');
  • show 'bike.csv';
  • show table 'bike.csv';
  • show from read_csv('bike.csv');

不知道为什么,describe的结果不显示总列数,需要另外查询

select count(1) from( describe 'bike.csv');

注意,必须用括号括起来。


十四、create table tb as from read_csv

用硬盘上的csv创建数据库的表,特别是某列是timestamp格式,如果先建表、后导入,很难成功

最好的办法是用read_csv创建表

sql 复制代码
create or replace table bikes as
      from read_csv('bike.csv',
      timestampformat='%Y%m%d%H%M%S',
      types={'RUNDATE':'TIMESTAMP'}
      ); 

十五、关于values()

values() 可以作为 select * from或 insert into ... from 的数据来源,也可以单独使用,比如

values(1,'a'),(2,'b')

select col 与 Insert into tble,虽然一个面向列维度,一个面向表维度,但from 后面,全是行维度。可以把values() 视作行集合。

但 values() 单独用时,不能与AS连用

sql 复制代码
values(1,'a'),(2,'b') as tt(a,b);
Parser Error:
syntax error at or near "as"

LINE 1: values(1,'a'),(2,'b') as tt(a,b);
                              ^

与select 连用时可以用 AS


十六、summarize 函数

注意,关键字后面直接跟表名,没有括号。

summarize tble;

返回也是一个表,所以可以在此基础上二次查询

sql 复制代码
select * exclude(count,q25,q50,q75) from
      (summarize bikes);

十七、将表案某列的值拆分成多个文件

sql 复制代码
copy (

SELECT *,date_trunc('day',交易时间) as flg from bnk

) to 'd:/bnk'(

format parquet,

partition_by (flg),

overwrite_or_ignore true

);
  • 普通的copy..to语句,to后面是文件名,所以有后缀,但质量拆分成多个文件,是文件夹名称,所以没有后缀
  • 但输出文件格式必须规定,所以在后面的括号中,用format参数设置
  • partiton_by()参数,一是有下划线,二是有括号

十八、数据清洗

  • create table
  • copy tbl from 'file.log' (DELIM '') --隐式调用read_csv
  • 因为不相信自动分列,所以全部作为一列导入
  • 能直接利用分隔符分列固然好,如果不能,借用正则表达式
  • 全部用varchar类型导入
  • 需要改变类型的,用添加新列,而不是更改原列
sql 复制代码
alter table bnk add column 交易日期时间 timestamp;
update bnk set 交易日期时间=strptime(交易时间,'%Y-%m-%d %H:%M:%S');

十九、正则表达式的捕获组

捕获组是正则表达式中的一个重要特性,用于将匹配的子表达式保存到内存中,以便后续引用和处理。

捕获组是用圆括号 () 括起来的正则表达式的一部分。当正则表达式匹配成功时,捕获组会记录该部分匹配的内容。捕获组可以嵌套,并且每个捕获组都有一个编号,编号从 1 开始,整个正则表达式匹配的内容被视为第 0 个捕获组。

命名捕获组 :使用 (?<name\>...)的语法来定义,允许为捕获组指定一个名称,便于后续引用。例如,(?<year>\d{4}) 会捕获四位数字并将其命名为 "year"。

sql 复制代码
select regexp_extract(raw_text,'^[0-9\.]*') as ip,
      regexp_extract(raw_text,'\[(.*)\]',1) as txt,
      regexp_extract(raw_text,'"([A-Z]*) ',1) AS http,
      regexp_extract(raw_text,'([a-zA-Z\-]*)"$',1) as lang
      from web_log_text limit 4;

抽取时,可以向两端扩展一下,方便唯一性识别目标,用捕获组获得想要的内容


二十、特殊字符的英文

  • 换行符 line break
  • 连字符 hyphens

二十一、关于CTE

  • CTE 是临时性的,它只能用在定义它的查询中
  • 使用CTE的原因之一是一个大型查询中避免重复书写同一个逻辑子查询
  • 就像变量,把一个大型查询中重复出现的子查询定义成CTE,直接引用即可
  • CTE既然是子查询,不用CTE用括号也可
  • CTE似乎也起到管道符的作用,只是书写复杂些:把前面的步骤写成CTE,最后组合起来
  • 需要临时在原表中增加辅助列,用CTE
SQL 复制代码
with e_weather as(
         select *,
         lead(measurement_time,1) over(order by measurement_time) as eend
         from weather
         order by measurement_time)
         select * from e_weather
         where timestamp '2023-12-01 10:01:00'
         between measurement_time and eend;

二十二、pivot(tbl,on,using)

  • 参数On将被映射为列名
  • 参数using应该是聚合函数,因为长数据即使分组也有多行
  • 按某行分组,用On做列名

二十三、多个join

sql 复制代码
select t1.*,t2.zone as pick_up_zone,t3.zone as drop_off_zone
      from trips as t1
      left join locations as t2
      on t2.locationId=t1.PULocationID
      left join locations as t3
      on t3.locationId = t1.DOLocationID;

按照代码书写顺序,第一个join的结果是一个表,依次与后面的join逐个连接,逻辑上没有问题


二十四、CASE语句

总体来说,关键字CASE开始一段判断,根据判断结果反馈一个结果

对于判断值的计算,有两者方式,

  • 被判断的列直接放在CASE后面,WHEN直接罗列其值
  • CASE后面不放内容,判断放到每个WHEN中进行
sql 复制代码
-- 例一
select i,
	case
	  when i=1 then 'a' 
	  when i=2 then 'b' 
	  else 'c' 
	end 
	  as ll 
	from tmp;
	  
-- 例二
 select i,
	 case i 
		 when 1 then 'a' 
		 when 2 then 'b' 
		 else 'c' 
	 end 
		 as ll 
	 from tmp;

二十五、用IF代替CASE

sql 复制代码
select i,
	if(i<2,'a',
		if(i<3,'b','c')
	) as ll 
from tmp;

二十六、一个例子

sql 复制代码
SELECT time_bucket(interval '1 day',
 tpep_pickup_datetime) AS day_of,
 count(*) AS num_trips,
 min(fare_amount) AS fare_min,
 max(fare_amount) AS fare_max,
 avg(fare_amount) AS fare_avg,
 avg(tip_amount) AS tip_avg,
 avg( 
	CASE 
		WHEN Payment_type = 1 
			THEN tip_amount / fare_amount END ) * 100 AS cc_tip_avg_pct 
FROM trips_with_location 
WHERE tpep_pickup_datetime 
BETWEEN '2023-01-20 00:00:00' AND '2023-01-29 23:59:59' 
AND fare_amount > 0 
GROUP BY 1 
ORDER BY 1;
  • 聚合函数括号里面括的是一列,即有很多行的列向量
  • 可以对这些列向量进行处理,比如用CASE将其转换为其他值
  • 这种转换,是基于行的,结果还是一个列向量
  • DuckDB中,group by 和 order by 可以用列的索引号

二十七、窗口函数

也叫解析函数( analytic functions)

所谓窗口,指用来计算聚合函数的子集,用关键字over

窗口函数是阻塞运算符,即它们需要缓冲全部输入,因此是SQL中内存需求最重的运算符之一。


二十八、关键字rowid 和窗口函数 row_number()

rowid

  • 适用于所有表
  • 从0开始编号
  • 后面不带括号
  • 不是窗口函数,即不用over
    语法:select rowid from tbl;

row_number()

  • 适用所有表
  • 从1开始编号
  • 后面带括号
  • 属于窗口函数,需要与over同用
  • 如果对全表操作,over() 里面空
  • 对全表操作,可以在over()中对数据进行排序,根据排序编号
    语法:select row_number() over() from tbl
    SELECT row_number() OVER (PARTITION BY region ORDER BY time) FROM sales;

二十九、窗口函数

  • cume_dist 累积分布:cumulative distribution 不是distance
  • dense_rank 连续排名:
  • rank 不连续排名:
  • fill 填充:以 ORDER BY 为 X 轴,使用线性插值法填充缺失值。
  • first_value 窗框第一行:
  • last_value 窗框最后一行
  • lag 窗框前偏移行
  • leadch窗框后偏移行
  • nth_value 窗框第 n 行
  • ntile(num_buckets) 分块: 从 1 到 num_buckets 的整数,尽可能平均分配分区。
  • percent_rank 当前行的相对排名:
  • row_number当前行在分区中的编号,从 1 开始计数。

三十、窗框 window frame

框(frame)指定为当前行两侧(前面或后面)的行数。距离可以指定为行的数量、使用分区的排序值和距离指定的值范围,或指定为组的数量(具有相同排序值的行集)。

Framing specifies a set of rows relative to each row where the function is evaluated. The distance from the current row is given as an expression either PRECEDING or FOLLOWING the current row in the order specified by the ORDER BY clause in the OVER specification.

SQL在确定窗框时,以当前行为起点


三十一、窗口函数的使用思路

使用了窗口函数,必然在原表后面增加一列存储计算结果,而原表没有这一列,思路自然可以是创建表、视图,但这些都会写入数据库,对于即抛型计算,没有必要,所以选择CTE

CTE是命名的子查询,当然以表的形式呈现,重复利用原有列和新增列,用where条件,操作数据

sql 复制代码
 with e_window as(
      select t1.*,
      max(fare_amount) over(
      partition by
      time_bucket(interval '1 day',tpep_pickup_datetime)
      ) as max_day_fare_amount
      from trips_with_location as t1
      )
      select tpep_pickup_datetime,
      pick_up_zone,
      drop_off_zone,
      fare_amount
      from e_window
      where fare_amount = max_day_fare_amount
      and tpep_pickup_datetime between
      '2023-01-20 00:00:00' and '2023-01-29 23:59:59'
      order by tpep_pickup_datetime;

三十二、索引

DuckDB 自动设置索引,采用 block range index (BRIN)

它的工作原理是将数据划分为连续的物理块(block),为每个块范围建立索引条目,通常存储该范围内数据的最小值、最大值或其他统计信息。当查询时,系统可通过索引快速排除不包含目标值的块范围,大幅减少需要扫描的实际数据量。例如,PostgreSQL中的BRIN索引​(Block Range Index)就是典型应用,尤其适合时间序列、地理空间等有序数据------相邻块的数据取值往往连续,BRIN索引只需存储每个块范围的边界值,就能高效过滤无关数据,且索引体积远小于传统B树索引。

也支持 Adaptive Radix Tree 自适应基数树

\[Adaptive Radix Tree 自适应基数树\]


三十三、sequence & range

nextval('seq_name') 之前,需要先定义一个sequnce:

create or replace sequence seq_name

然后在查询中按行调用:

nextval('seq_name')

range(闭,开),必须闭< 开,开不在range范围中

用range生成一个有序序列:

  • 生成range
  • 根据range的值,用case映射成字符串

三十四、临时表放到库里还是硬盘上?

sql 复制代码
copy(
      select nextval('seq_book') as book_reviews_id,
      id as book_id,
      title as book_title,
      price,
      user_id,
      region,
      to_timestamp("review/time") as review_time,
      cast(datepart('year',review_time) as varchar) as review_year,
      "review/summary" as review_summary,
      "review/text" as review_text,
      "review/score" as review_score
      from read_csv('books_rating.csv')
      cross join(
      select range,
      case when range=0 then 'JP' ELSE 'US'
      end as region
      from range(0,2))
      ) to 'book_reviews.parquet';
  • 有特殊字符的列名用双引号括起来
  • 在select中,用as重命名列
  • 用select选择需要的列
  • 首先选择parquet

三十五、内存不足错误

Out of Memory Error Depending on your hardware setup, you may encounter an out-of-memory error in these exercises, such as Error: Out of Memory Error: failed to allocate data of size. This error indicates that DuckDB is unable to hold the result set within the available memory. If you do encounter an error during these steps, you can instruct DuckDB to offload to disk as required by establishing a temporary disk file: SET temp_directory = 'temp.tmp'


三十六、ASOF和CTE

当要检索的值没有完全匹配值,退而求其次,用基于某一列的范围值来代替,有两种解决方案

1、CTE法

利用CTE,增加辅助列,用between..and..

sql 复制代码
with e_weather as(
         select *,
         lead(measurement_time,1) over(order by measurement_time) as eend
         from weather
         order by measurement_time)
         select * from e_weather
         where timestamp '2023-12-01 10:01:00'
         between measurement_time and eend;

2、ASOF法

sql 复制代码
select * from weather as t1
         asof join scores as t2
         on t1.measurement_time <= t2.score_time;
  • 语法结构与其他 join 语法结构相同
  • 也是ON关键字,但用 >=|<=等
  • ASOF判断标准是只返回那个最接近的值,所以只返回一个值
  • 在JOIN中,如果没有前缀,* 代表所有表的所有列

三十七、递归和层级关系

  • 行之间的从属关系,用一种表达形式,放在表的某一列中,作为计算的依据
  • 递归,是一个函数调用它自己
  • 所以,调用的是一段代码,每个函数都会开辟一段新的内存
  • 主要算法是分而治之,把问题不断拆解
  • 既然是重复调用同一段代码,其思路与CTE吻合,CTE是解决递归的最优方案
  • CTE解决了代码复用问题,接下来需要解决分而治之的问题
  • 这便是基线条件和递归条件的问题,递归条件必然隐含终止条件
  • 在SQL中,连接各递归子查询,必然用JOIN

三十八、递归CTE的例子

sql 复制代码
with recursive e_wine(wine_id,start_with,wine_path) as (
         select wine_id,wine_name,[wine_name] as wine_path
         from wines
         where sub_class_of is null
         union all
         select wines.wine_id,wines.wine_name,
         list_prepend(
         wines.wine_name,e_wine.wine_path)
         from wines,e_wine
         where wines.sub_class_of = e_wine.wine_id
         )
         select wine_path from e_wine
         where start_with = 'Rothschild';
  • 关键字不同:with recursive,必须有recursive关键字,否则提示错误

  • 在CTE内部,引用了该CTE,必须设置终止条件,设置的位置是recursive case中where 位置

  • union all 前面部分,是base case,是递归的起点,本例是sub_class_of is null的记录

  • union all 后面部分,是recursive case,里面引用了CTE,因为是递归计算,起点是 base部分

  • wines.sub_class_of = e_wine.wine_id部分判定,是在递归部分进行的,所以,无论递归部分是否有结果,base部分已经存在,这里也是递归终止条件所在的地方

  • 如果递归部分没有结果,union all只保留base部分

  • union all 组合了每一次迭代的结果,形成一个临时表,再次进入recursive case状态,利用前面这个临时表,和新的递归表,判断wines.sub_class_of = e_wine.wine_id是否还有真值,如果有就继续迭代,直到没有真值为止。所有的临时表,都保存在内存中,所以官网说本功能最吃内存

  • CTE中引用自身,核心是在recursive case的where语句中,设置了终止条件,才不会成为死循环

  • 递归的实现,依赖于union all将过程中的所有结果组合到一张表中,但这是最后一步做的工作,计算过程中,只与上一步生成的临时表进行条件判断,这也是能够实现逐层计算的原因

  • 基于以上结论,base case中一定不能引用自身,recursive case中必须引用自身

  • 这里所谓的"引用",引用的是上一步的计算结果,不是对象本身,用函数的语言描述,就是base case的结果,是recursive case部分的input


三十九、递归CTE详述

\[Recursive SQL Expression Visually Explained\]

是保存计算的结果,还是保存一段算法?

前者是临时表的思路,后者是视图和CTE的思路

把一段代码命名,后面复用,这是一个牛叉的思路


四十、同时使用多个CTE

SQL 复制代码
WITH cte1 AS (
	SELECT column1
	FROM table1
),
cte2 AS (
	SELECT column2
	FROM table2
)
SELECT *
FROM cte1
JOIN cte2 ON cte1.column1 = cte2.column2

相关推荐
凯瑟琳.奥古斯特2 小时前
数据库原理选择题精选
数据库·python·职场和发展
曹牧2 小时前
C#:主线程能够捕获到子线程中的异常
开发语言·数据库·c#
朝阳5812 小时前
MongoDB 副本集从零搭建到生产可用
数据库·mongodb
雨辰AI3 小时前
SpringBoot3 整合达梦 DM9 超详细入门实战|从零搭建可直接上线
数据库·微服务·架构·政务
我是一颗柠檬3 小时前
【MySQL全面教学】MySQL性能优化实战Day13(2026年)
数据库·后端·sql·mysql·性能优化·database
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第84题】【Mysql篇】第14题:为什么用 InnoDB 存储引擎的表建议用整型的自增主键?
java·开发语言·数据库·mysql·面试
张彦峰ZYF4 小时前
检索增强生成(RAG)系统的基础:全面深入矢量数据库
数据库·大模型·rag
Elastic 中国社区官方博客4 小时前
我们如何在 Elasticsearch Serverless 上将向量搜索吞吐量提升一倍
大数据·数据库·人工智能·elasticsearch·搜索引擎·云原生·serverless
一 乐5 小时前
高校实习信息发布网站|基于Spring Boot的高校实习信息发布网站的设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·高校实习信息发布网站