> 技术文档 > 利用DeepSeek测试kdb+x的tpch sf=10数据

利用DeepSeek测试kdb+x的tpch sf=10数据

提示词如下

我想给kdb+/q数据库做tpc-h测试,请提供从建表,数据导入开始的相关命令,注意分区等提高效率的手段,并提供q语言和sql查询对比测试

他的回答可以说非常详细,如下所示

KDB+/q TPC-H 测试指南

准备工作

首先需要下载TPC-H工具包并生成测试数据:

  1. 从TPC官网下载TPC-H工具包
  2. 编译生成dbgen工具
  3. 使用dbgen生成测试数据(建议比例因子SF=1开始)

建表与数据导入

非分区表方案

// 创建表结构customer:([] c_custkey:`long$(); c_name:`symbol$(); c_address:`symbol$(); c_nationkey:`long$(); c_phone:`symbol$(); c_acctbal:`float$(); c_mktsegment:`symbol$(); c_comment:`symbol$())lineitem:([] l_orderkey:`long$(); l_partkey:`long$(); l_suppkey:`long$(); l_linenumber:`long$(); l_quantity:`float$(); l_extendedprice:`float$(); l_discount:`float$(); l_tax:`float$(); l_returnflag:`symbol$(); l_linestatus:`symbol$(); l_shipdate:`date$(); l_commitdate:`date$(); l_receiptdate:`date$(); l_shipinstruct:`symbol$(); l_shipmode:`symbol$(); l_comment:`symbol$())nation:([] n_nationkey:`long$(); n_name:`symbol$(); n_regionkey:`long$(); n_comment:`symbol$())orders:([] o_orderkey:`long$(); o_custkey:`long$(); o_orderstatus:`symbol$(); o_totalprice:`float$(); o_orderdate:`date$(); o_orderpriority:`symbol$(); o_clerk:`symbol$(); o_shippriority:`long$(); o_comment:`symbol$())part:([] p_partkey:`long$(); p_name:`symbol$(); p_mfgr:`symbol$(); p_brand:`symbol$(); p_type:`symbol$(); p_size:`long$(); p_container:`symbol$(); p_retailprice:`float$(); p_comment:`symbol$())partsupp:([] ps_partkey:`long$(); ps_suppkey:`long$(); ps_availqty:`long$(); ps_supplycost:`float$(); ps_comment:`symbol$())region:([] r_regionkey:`long$(); r_name:`symbol$(); r_comment:`symbol$())supplier:([] s_suppkey:`long$(); s_name:`symbol$(); s_address:`symbol$(); s_nationkey:`long$(); s_phone:`symbol$(); s_acctbal:`float$(); s_comment:`symbol$())// 加载数据customer:(\"JIIISFSIS\"; enlist \",\") 0: `:path/to/customer.tbllineitem:(\"JIJFFFFSISDDDSSS\"; enlist \",\") 0: `:path/to/lineitem.tblnation:(\"ISIS\"; enlist \",\") 0: `:path/to/nation.tblorders:(\"JISFDSISLS\"; enlist \",\") 0: `:path/to/orders.tblpart:(\"JSSSSISFS\"; enlist \",\") 0: `:path/to/part.tblpartsupp:(\"JJIFS\"; enlist \",\") 0: `:path/to/partsupp.tblregion:(\"ISIS\"; enlist \",\") 0: `:path/to/region.tblsupplier:(\"JSSISFS\"; enlist \",\") 0: `:path/to/supplier.tbl// 添加索引`p_partkey xasc `part`ps_partkey xasc `partsupp`l_orderkey xasc `lineitem`o_orderkey xasc `orders`c_custkey xasc `customer`s_suppkey xasc `supplier`n_nationkey xasc `nation`r_regionkey xasc `region

分区表方案(按日期分区)

// 创建分区数据库结构`:tpch/db/ set .Q.en[`:tpch/db;] ([] c_custkey:`long$(); c_name:`symbol$(); c_address:`symbol$(); c_nationkey:`long$(); c_phone:`symbol$(); c_acctbal:`float$(); c_mktsegment:`symbol$(); c_comment:`symbol$())`:tpch/db/ set .Q.en[`:tpch/db;] ([] l_orderkey:`long$(); l_partkey:`long$(); l_suppkey:`long$(); l_linenumber:`long$(); l_quantity:`float$(); l_extendedprice:`float$(); l_discount:`float$(); l_tax:`float$(); l_returnflag:`symbol$(); l_linestatus:`symbol$(); l_shipdate:`date$(); l_commitdate:`date$(); l_receiptdate:`date$(); l_shipinstruct:`symbol$(); l_shipmode:`symbol$(); l_comment:`symbol$())// 按日期分区加载lineitem数据loadPartitionedLineitem:{[path] data:(\"JIJFFFFSISDDDSSS\"; enlist \",\") 0: path; dates:exec distinct l_shipdate from data; {[d;dt] `:tpch/db/lineitem/,(string dt),\"/\" upsert .Q.en[`:tpch/db] select from data where l_shipdate=dt }[data] each dates; }loadPartitionedLineitem `:path/to/lineitem.tbl// 其他表可以类似处理,orders表可按o_orderdate分区

查询对比测试

查询1 (Q1)

SQL:

SELECT l_returnflag, l_linestatus, sum(l_quantity) as sum_qty, sum(l_extendedprice) as sum_base_price, sum(l_extendedprice*(1-l_discount)) as sum_disc_price, sum(l_extendedprice*(1-l_discount)*(1+l_tax)) as sum_charge, avg(l_quantity) as avg_qty, avg(l_extendedprice) as avg_price, avg(l_discount) as avg_disc, count(*) as count_orderFROM lineitemWHERE l_shipdate <= date \'1998-12-01\' - interval \'90\' dayGROUP BY l_returnflag, l_linestatusORDER BY l_returnflag, l_linestatus;

q:

q1:{ startDate:1998.12.01-90; select sum_qty:sum l_quantity, sum_base_price:sum l_extendedprice, sum_disc_price:sum l_extendedprice*1-l_discount, sum_charge:sum l_extendedprice*1-l_discount*1+l_tax, avg_qty:avg l_quantity, avg_price:avg l_extendedprice, avg_disc:avg l_discount, count_order:count i by l_returnflag,l_linestatus from lineitem where l_shipdate<=startDate } \\t q1[] // 计时执行

查询3 (Q3)

SQL:

SELECT l_orderkey, sum(l_extendedprice*(1-l_discount)) as revenue, o_orderdate, o_shippriorityFROM customer, orders, lineitemWHERE c_mktsegment = \'BUILDING\' AND c_custkey = o_custkey AND l_orderkey = o_orderkey AND o_orderdate < date \'1995-03-15\' AND l_shipdate > date \'1995-03-15\'GROUP BY l_orderkey, o_orderdate, o_shippriorityORDER BY revenue desc, o_orderdateLIMIT 10;

q:

q3:{ select revenue:sum l_extendedprice*1-l_discount, o_orderdate, o_shippriority by l_orderkey from lineitem lj `l_orderkey xkey orders where o_orderdate1995.03.15 | 10#reverse `revenue xdesc } \\t q3[] // 计时执行

查询优化技巧

  1. 分区策略:

    • 按日期分区(如lineitem按l_shipdate)
    • 按范围分区(如orders按o_orderkey范围)
  2. 索引优化:

    // 创建索引`l_shipdate xasc `lineitem`o_orderdate xasc `orders`c_mktsegment xasc `customer
  3. 内存映射:

    // 启动时映射分区数据库\\l tpch/db
  4. 并行查询:

    // 使用peach并行处理q1_parallel:{ startDate:1998.12.01-90; {[d] select sum l_quantity, sum l_extendedprice, ... from d where l_shipdate<=startDate } peach lineitem}

性能对比建议

  1. 创建测试脚本分别执行SQL和q版本查询
  2. 使用\\t定时命令记录执行时间
  3. 多次运行取平均值
  4. 测试不同数据量下的表现
  5. 比较分区表与非分区表的性能差异

注意事项

  1. TPC-H查询需要根据q语法特点进行调整
  2. 分区策略应根据实际查询模式设计
  3. 大数据量下注意内存使用情况
  4. 考虑使用压缩技术减少存储空间
  5. 定期维护表统计信息以优化查询计划

通过以上方法,您可以全面评估kdb+/q在TPC-H基准测试中的表现,并与传统SQL数据库进行对比。

但经过实际操作,其中有一些错误, 创建表结构时,表的字段名都是对的,导入后标题行变了

region:(\"ISIS\"; enlist \"|\") 0: `:/mnt/c/d/tpch/region.tblselect from region0 AFRICA lar deposits. blithely final packages cajole. regular waters are final requests. regular accounts are a..---------------------------------------------------------------------------------------------------------------------..1 AMERICA  ..2 ASIA ..3 EUROPE  ..4 MIDDLE EAST 

继续提问

我的tbl文件没有标题行,请给出region表的插入语句,不做别的

他的回答

如果您的TPC-H region.tbl文件没有标题行,且只需要region表的插入语句,可以使用以下q命令:

region:(\"ISIS\"; enlist \"|\") 0: `:path/to/region.tbl

或者如果您想明确指定列名:

region:(\"ISIS\"; enlist \"|\") 0: `:path/to/region.tbl`r_regionkey`r_name`r_comment xcol region

说明:

  1. \"ISIS\" 指定了各列的数据类型:I(整数)/S(符号)
  2. enlist \"|\" 指定分隔符为竖线(TPC-H标准格式)
  3. 执行后会创建region表,包含r_regionkey、r_name和r_comment三列

这个命令只加载region表数据,不会影响其他表或执行任何额外操作。

我不太理解, 继续提问

请解释为何已经创建表结构region:([] r_regionkey:long$(); r_name:symbol();rcomment:‘symbol(); r_comment:`symbol();rcomment:symbol()),还需要用r_regionkeyr_name`r_comment xcol region再次指定

回答

在 q/kdb+ 中,即使您已经预先定义了表结构 region:([] r_regionkey:long$(); …),当您使用 0:文件加载函数导入数据时,仍然需要xcol` 重新指定列名的原因如下:


1. 0: 文件加载会丢失原表结构

当您用 (\"ISIS\"; enlist \"|\") 0: file 加载数据时:

  • 该操作会创建一个全新的表,完全忽略之前定义的 region 的空表结构
  • 新表的列名默认是 text 生成的(如 c0, c1, c2),而不是您预先定义的列名

2. xcol 的作用

xcol(extended column)操作会:

  • 重命名列:将默认的 c0/c1/c2 映射到您指定的列名(r_regionkey, r_name, r_comment
  • 不改变数据类型:之前 \"ISIS\" 已确保数据类型正确(longsymbol

3. 为什么不能省略预定义?

虽然预定义表结构看似冗余,但它的好处是:

  • 明确文档化:预先声明让代码更易读
  • 类型安全:如果文件数据与预期类型不匹配,后续操作会报错
  • 可维护性:其他开发者能清晰看到表结构

替代方案(一步到位)

如果不想分两步,可以直接合并:

// 加载时直接命名列(跳过预定义空表)region:`r_regionkey`r_name`r_comment xcol (\"ISIS\"; enlist \"|\") 0: `:region.tbl

总结

操作步骤 目的 region:([] r_regionkey:long$()…)` 声明表结构和类型(可选) (\"ISIS\";...) 0: file 加载数据,生成匿名列(c0/c1/c2 `x`y`z xcol `table 将匿名列重命名为正式列名

即使预定义了空表,文件加载仍会创建新表,因此需要 xcol 确保列名正确。

用了\\t 就只输出计时,不能同时显示结果,是这样的?比如:

\\t .s.e\"select l_shipmode,count(*) from lineitem group by l_shipmode\"316.s.e\"select l_shipmode,count(*) from lineitem group by l_shipmode\"l_shipmode xcol------------------AIR 8566164FOB 8569760

他的回答

是的,在 kdb+/q 中,\\t 命令默认只返回执行时间(毫秒),不会同时显示查询结果。如果你希望 同时计时并显示结果,可以使用以下方法:


方法 1:用 \\t + 单独打印结果

\\t res: .s.e \"select l_shipmode,count(*) from lineitem group by l_shipmode\";show res; // 手动显示结果

输出示例

42 // 耗时 42msl_shipmode| x ---------| ------AIR | 8566164FOB | 8569760...

方法 2:用 .Q.ts 返回时间和结果

.Q.ts[{.s.e \"select l_shipmode,count(*) from lineitem group by l_shipmode\"}]

输出示例

42 // 时间(毫秒)l_shipmode| x ---------| ------AIR | 8566164FOB | 8569760...

方法 3:自定义计时函数

// 定义一个计时并打印结果的函数timeAndShow:{[q] t:.z.p; res:value q; show \"Time: \",string .z.p-t; show res; };// 使用示例timeAndShow \".s.e \\\"select l_shipmode,count(*) from lineitem group by l_shipmode\\\"\"

输出示例

Time: 0D00:00:00.042000000l_shipmode| x ---------| ------AIR | 8566164FOB | 8569760...

方法 4:用 \\ts(时间 + 内存)

\\ts .s.e \"select l_shipmode,count(*) from lineitem group by l_shipmode\"

输出示例

42 16384 // 时间(ms) 内存(bytes)l_shipmode| x ---------| ------AIR | 8566164FOB | 8569760...

总结

方法 命令 输出内容 \\t 仅计时 只返回时间(ms) .Q.ts 计时+内存 返回时间和结果 自定义函数 完整控制 时间+结果 \\ts 计时+内存 返回时间和内存,并显示结果

如果你希望 默认同时显示时间和结果,建议使用方法 2(.Q.ts)或方法 4(\\ts)。

这个回答依然有错,方法4也不会输出查询结果,还是要用方法1的步骤。

\\ts .s.e\"select l_shipmode,count(*) from lineitem group by l_shipmode\"166 1073754928\\t res: .s.e \"select l_shipmode,count(*) from lineitem group by l_shipmode\";182show res;l_shipmode xcol------------------AIR 8566164FOB 8569760

经过测量数据导入和汇总用时,在不分区,无索引的情况下,sf=10的性能不高,导入6000万行lineitem数据(tbl文件约7.3GB)要用将近8分钟,首次单表分组查询要13秒,估计是读数据到内存的原因,但是插入阶段已经用了8分钟,难以理解,可能是我的物理内存不足,只有12GB,写交换分区导致。

\\t region:`r_regionkey`r_name`r_comment xcol (\"ISIS\"; enlist \"|\") 0: `:/mnt/c/d/tpch/region.tbl4 \\t lineitem:`l_orderkey`l_partkey`l_suppkey`l_linenumber`l_quantity`l_extendedprice`l_discount`l_tax`l_returnflag`l_linestatus`l_shipdate`l_commitdate`l_receiptdate`l_shipinstruct`l_shipmode`l_comment xcol (\"JIJFFFFSISDDDSSS\"; enlist \"|\") 0: `:/mnt/c/d/tpch/lineitem.tbl;445406\\t s)select l_shipmode,count(*) from lineitem group by l_shipmode13324\\t .s.e\"select l_shipmode,count(*) from lineitem group by l_shipmode\"316s)select count(*) from lineitemxcol--------59986051

最后问他

我已经导入了数据,怎么把它保存为数据库,以便kdb+下次直接查询表,不用重新导入

他的回答

在 kdb+ 中,您可以通过 持久化保存表到数据库 来避免每次重新导入数据。以下是具体步骤,包括 非分区数据库分区数据库 两种方式:


方法 1:保存为非分区数据库(适合小数据)

适用于数据量较小(如 regionnation 等表),直接保存整个表到磁盘。

1. 创建数据库目录

// 创建数据库目录(如果不存在)system \"mkdir -p tpch_db\";

2. 保存表到磁盘

// 保存 region 表`:tpch_db/region/ set .Q.en[`:tpch_db] region;// 保存 nation 表`:tpch_db/nation/ set .Q.en[`:tpch_db] nation;// 其他表同理...

3. 下次启动时加载

// 启动 kdb+ 并加载数据库q tpch_db

或:

// 在 q 会话中手动加载\\l tpch_db

加载后,表(如 regionnation)可以直接查询,无需重新导入。


方法 2:保存为分区数据库(适合大数据,如 lineitemorders

如果数据量较大(如 lineitemorders),可以按 日期/范围分区 存储,提高查询效率。

1. 创建分区数据库

// 创建分区数据库目录system \"mkdir -p tpch_partitioned_db\";

2. 按日期分区存储(以 orders 表为例)

// 按 o_orderdate 分区保存 orders 表`:tpch_partitioned_db/orders/ upsert .Q.en[`:tpch_partitioned_db] orders;

如果是 lineitem,可以按 l_shipdate 分区:

// 按 l_shipdate 分区保存 lineitem 表`:tpch_partitioned_db/lineitem/ upsert .Q.en[`:tpch_partitioned_db] lineitem;

3. 下次启动时加载

// 启动 kdb+ 并加载分区数据库q tpch_partitioned_db

或:

// 在 q 会话中手动加载\\l tpch_partitioned_db

加载后,所有表可以直接查询,无需重新导入。


方法 3:使用 .Q.dpft 保存带索引的分区表

如果希望表在加载时自动排序(如 orderso_orderkey 排序):

// 按 o_orderdate 分区,并按 o_orderkey 排序.Q.dpft[`:tpch_partitioned_db; `o_orderdate; `o_orderkey; `orders];

这样查询时 o_orderkey 会自动优化。


验证数据库是否正确保存

// 检查数据库目录结构system \"ls tpch_db\";system \"ls tpch_partitioned_db\";// 检查表是否可加载\\l tpch_dbselect count i from region; // 验证数据是否正确

总结

方法 适用场景 命令示例 非分区存储 小表(regionnation) ``:db/table/ set .Q.en[:db] table 分区存储 大表(lineitemorders) ``:db/table/ upsert .Q.en[:db] table 带索引的分区存储 需要排序优化 .Q.dpft[:db; partitionCol; sortCol; table]`

这样,下次启动 kdb+ 时直接加载数据库目录即可,无需重新导入数据:

q tpch_db # 非分区数据库# 或q tpch_partitioned_db # 分区数据库

还真是这样,我用第一种方法保存小表没问题,保存大表直接被系统杀掉进程,其他方法还没能验证,不知对错,下次先用sf=1测试。

system \"mkdir -p tpch_db\";`:tpch_db/region/ set .Q.en[`:tpch_db] region;\\t `:tpch_db/lineitem/ set .Q.en[`:tpch_db] lineitem;Killed