clickhouse 删除操作

OLAP 数据库设计的宗旨在于分析适合一次插入多次查询的业务场景,市面上成熟的 AP 数据库在更新和删除操作上支持的均不是很好,当然 clickhouse 也不例外。但是不友好不代表不支持,本文主要介绍在 clickhouse 中如何实现数据的删除,以及最新版本中 clickhouse 所做的一些技术突破

Click_House_Delete_Statement_6fd661d851

一、mutation

刚接触 clickhouse 的小伙伴或许对 mutation 就很熟悉了,mutation 查询可以看成 alter 语句的变种。虽然 mutation 能够最终实现修改和删除的需求,但不能完全用通常意义的 delete 和 update 来理解,我们需要清醒的认识到它的不同:

  1. mutation 是一个很重的操作,适合批量数据操作
  2. 不支持事务、一旦操作立刻生效无法回滚
  3. mutation 为异步操作

1.1 实操

创建一张表用于测试 mutation 操作

create table mutations_operate
(
    UserId     UInt64,
    Score      UInt64,
    CreateTime DateTime
) engine = MergeTree()
      partition by toYYYYMMDD(CreateTime)
      order by UserId;

接下来分别插入两批不同分区的数据

insert into mutations_operate
select number,
       abs(number - 100),
       '2023-08-08 00:00:00'
from system.numbers
limit 1000000;

insert into mutations_operate
select number,
       abs(number - 100),
       '2023-08-09 00:00:00'
from system.numbers
limit 1000000;

尝试删除 20230808 分区中 1000-10000 之间的所有数据,sql 如下

alter table mutations_operate delete where toYYYYMMDD(CreateTime) = 20230808 and UserId between 1000 and 10000;

可以统计一下该分区的数据条数来确认是否成功删除,从体验来说目前的数据规模感受不到 mutation 的“重”,感觉像是瞬间完成的。

当然我们也可以查看system.mutations表来监控 mutation 操作的进度

select table, mutation_id, `block_numbers.number` as num, is_done
from system.mutations;

Query id: 0878a0f1-a5ff-474c-8f84-518ce5dc5e1d

┌─table─────────────┬─mutation_id────┬─num─┬─is_done─┐
│ mutations_operate │ mutation_3.txt │ [3]1 │
└───────────────────┴────────────────┴─────┴─────────┘

1 row in set. Elapsed: 0.002 sec.

mutation_id 是一个日志文件,可以在表存储目录中查看,完整记录了本次操作的语句和时间,例如

format version: 1
create time: 2023-08-09 18:54:06
commands: DELETE WHERE (toYYYYMMDD(CreateTime) = 20230808) AND ((UserId >= 1000) AND (UserId <= 10000))

而其中的 3 以及block_numbers.number是 mutation 号,每执行一条 delete 或 update 语句都会对应一个唯一的编号

id_done 表示本次 mutation 操作是否执行完成,1 表示已经完成

1.2 原理

为了探寻 mutation 操作的原理和执行流程重置一下表数据(删除重建即可),在插入两批数据后查看磁盘目录

» du -h
  0B	./detached
7.7M	./20230809_2_2_0
7.7M	./20230808_1_1_0
 15M	.

可以看到两个分区目录均是 7.7M

尝试执行删除操作后,可以在日志中看到下面的查询信息

<Debug> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): Key condition: unknown, (column 0 in [1000, +Inf)), (column 0 in (-Inf, 10000]), and, and
<Debug> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): Key condition: unknown, (column 0 in [1000, +Inf)), (column 0 in (-Inf, 10000]), and, and
<Debug> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): MinMax index condition: (toYYYYMMDD(column 0) in [20230808, 20230808]), unknown, unknown, and, and
<Debug> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): MinMax index condition: (toYYYYMMDD(column 0) in [20230808, 20230808]), unknown, unknown, and, and
<Trace> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): Running binary search on index range for part 20230808_1_1_0 (124 marks)
<Trace> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): Found (LEFT) boundary mark: 0
<Debug> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): Selected 0/1 parts by partition key, 0 parts by primary key, 0/0 marks by primary key, 0 marks to read from 0 ranges
<Trace> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): Found (RIGHT) boundary mark: 2
<Trace> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): Found continuous range in 13 steps
<Debug> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): Selected 1/1 parts by partition key, 1 parts by primary key, 2/123 marks by primary key, 2 marks to read from 1 ranges
<Trace> delete_operate.mutations_operate (7f120927-b71e-4f85-a06c-21a94b7f89e3) (SelectExecutor): Spreading mark ranges among streams (default reading)
<Trace> MergeTreeInOrderSelectProcessor: Reading 1 ranges in order from part 20230808_1_1_0, approx. 16384 rows starting from 0
<Trace> Aggregator: Aggregation method: without_key
<Trace> AggregatingTransform: Aggregated. 0 to 1 rows (from 0.00 B) in 0.000570041 sec. (0.000 rows/sec., 0.00 B/sec.)
<Trace> Aggregator: Merging aggregated data
<Trace> MutateTask: Part 20230809_2_2_0 doesn't change up to mutation version 3

首先,clickhouse 会使用我们执行的删除语句中附带的 where 条件在每个分区中执行 count 查询,为了判断哪些分区有需要被删除的数据,从日志可以看出Reading 1 ranges in order from part 20230808_1_1_0, approx. 16384 rows starting from 0以及Part 20230809_2_2_0 doesn't change up to mutation version 3。注意日志中所说 20230808 的范围是 0~16384 并不是实际删除的范围,而是索引的范围。我们知道 mergeTree 引擎默认的跳数索引的间隔是 8192 而我们删除的数据范围是 1000-10000,显然作为一个整周期自然是 0-16384(2x8192)

当我们再次查看磁盘目录

» du -h
  0B	./detached
7.7M	./20230809_2_2_0
7.7M	./20230808_1_1_0
  0B	./20230809_2_2_0_3
7.6M	./20230808_1_1_0_3
 23M	.

总目录从 15M 变成了 23M,而两个分区也都各自生成了一个以 mutation version 为后缀的新分区

因此接下来的逻辑如下:

clickhouse 会创建一个 tmp_mut_ 为前缀、mutation version 为后缀的临时分区目录,例如这里的就是 tmp_mut_20230808_1_1_0_3

对于需要删除的分区,会在 tmp_mut 目录中生成全新的 .bin 和 .mrk 文件

对于无需删除的分区,clickhouse 会创建一个 tmp_clone_ 为前缀、mutation version 为后缀的临时分区目录并将原分区里面的数据以硬链接的方式拷贝过去,并修改目录名称为正确的格式

下面是执行的日志情况

<Debug> delete_operate.mutations_operate (4351d317-2cd6-4328-85fe-49d5beeff5c3): Clone part /opt/homebrew/var/lib/clickhouse/store/435/4351d317-2cd6-4328-85fe-49d5beeff5c3/20230809_2_2_0/ to /opt/homebrew/var/lib/clickhouse/store/435/4351d317-2cd6-4328-85fe-49d5beeff5c3/tmp_clone_20230809_2_2_0_3
<Trace> delete_operate.mutations_operate (4351d317-2cd6-4328-85fe-49d5beeff5c3): Renaming temporary part tmp_clone_20230809_2_2_0_3 to 20230809_2_2_0_3 with tid (1, 1, 00000000-0000-0000-0000-000000000000).
<Trace> MergedBlockOutputStream: filled checksums 20230808_1_1_0_3 (state Temporary)
<Trace> delete_operate.mutations_operate (4351d317-2cd6-4328-85fe-49d5beeff5c3): Renaming temporary part tmp_mut_20230808_1_1_0_3 to 20230808_1_1_0_3 with tid (1, 1, 00000000-0000-0000-0000-000000000000)

从磁盘目录也可以佐证这一点,首先上面的 20230809_2_2_0_3 占用空间为 0B,当然这是 mac 独有的现实方式,在其它 linux 系统不一定是这么显示,进入各个分区查看一下

wjun :: data/delete_operate/mutations_operate ‹stable› » ll 20230808_1_1_0_3
total 15632
-rw-r-----@ 1 wjun  admin    17863 Aug  9 19:36 CreateTime.bin
-rw-r-----@ 1 wjun  admin      369 Aug  9 19:36 CreateTime.cmrk2
-rw-r-----@ 1 wjun  admin  3968891 Aug  9 19:36 Score.bin
-rw-r-----@ 1 wjun  admin      409 Aug  9 19:36 Score.cmrk2
-rw-r-----@ 1 wjun  admin  3969011 Aug  9 19:36 UserId.bin
-rw-r-----@ 1 wjun  admin      409 Aug  9 19:36 UserId.cmrk2
-rw-r-----@ 1 wjun  admin      490 Aug  9 19:36 checksums.txt
-rw-r-----@ 1 wjun  admin       90 Aug  9 19:36 columns.txt
-rw-r-----@ 1 wjun  admin        6 Aug  9 19:36 count.txt
-rw-r-----@ 1 wjun  admin       10 Aug  9 19:36 default_compression_codec.txt
-rw-r-----@ 1 wjun  admin        1 Aug  9 19:36 metadata_version.txt
-rw-r-----@ 1 wjun  admin        8 Aug  9 19:36 minmax_CreateTime.idx
-rw-r-----@ 1 wjun  admin        4 Aug  9 19:36 partition.dat
-rw-r-----@ 1 wjun  admin      188 Aug  9 19:36 primary.cidx
wjun :: data/delete_operate/mutations_operate ‹stable› » ll 20230809_2_2_0_3
total 15768
-rw-r-----@ 2 wjun  admin    18042 Aug  9 19:35 CreateTime.bin
-rw-r-----@ 2 wjun  admin      375 Aug  9 19:35 CreateTime.cmrk2
-rw-r-----@ 2 wjun  admin  4004938 Aug  9 19:35 Score.bin
-rw-r-----@ 2 wjun  admin      415 Aug  9 19:35 Score.cmrk2
-rw-r-----@ 2 wjun  admin  4004915 Aug  9 19:35 UserId.bin
-rw-r-----@ 2 wjun  admin      415 Aug  9 19:35 UserId.cmrk2
-rw-r-----@ 2 wjun  admin      490 Aug  9 19:35 checksums.txt
-rw-r-----@ 2 wjun  admin       90 Aug  9 19:35 columns.txt
-rw-r-----@ 2 wjun  admin        7 Aug  9 19:35 count.txt
-rw-r-----@ 2 wjun  admin       10 Aug  9 19:35 default_compression_codec.txt
-rw-r-----@ 2 wjun  admin        8 Aug  9 19:35 minmax_CreateTime.idx
-rw-r-----@ 2 wjun  admin        4 Aug  9 19:35 partition.dat
-rw-r-----@ 2 wjun  admin      173 Aug  9 19:35 primary.cidx

20230809_2_2_0_3 分区 inode 被连接次数为 2 表示建立了硬链接。

因此 mutation 的删除逻辑如下:

  1. 每个分区执行附带删除操作的 where 条件的 count 查询,获取需要执行删除操作的分区
  2. 对于需要执行删除操作的分区会创建一个临时目录并生成全新(删除需要删除的行)的文件,随后 rename 分区
  3. 对于无需执行删除操作的分区会创建一个临时目录并以硬链接的方式拷贝文件,随后 rename 分区
  4. 原分区在system.parts中会被置为 inactive 状态
  5. 在下一次 merge 是删除原分区

而对于更新操作基本逻辑一致,需要注意的是需要执行更新操作的分区会有如下两种情况:

  1. 分区类型为 wide:只会重新生成受影响行的 bin 和 mrk 文件,不受影响的文件以硬链接的方式拷贝
  2. 分区类型为 compact:因为所有列都是一个文件,因此会重新生成 bin 和 mrk 文件

更新和删除操作流程不一致的原因是:删除影响全部列而更新只影响部分列

mergeTree 表的分区类型分为 wide 和 compact 两种受min_bytes_for_wide_partmin_rows_for_wide_part参数影响。wide 类型的分区一个列一个文件,compact 类型的分区所有列公用一个文件,当分区数据的行数和字节较小时为 compact 类型,不管是查询所有字段或某个字段相对较快;当数据量很大时就会以列式存储来追求 AP 查询性能

1.3 不足

当我们走一遍 mutation 时发现在删除任务完成后表 merge 前的这一段时间磁盘空间不减反增,这个就让用户很难接受了。因此就可能会出现因为磁盘空间不足想要删除数据,结果删除操作导致空间进一步不足的窘境。同时 mutation 会重写受影响的分区,这也是 mutation 操作重的原因所在。

二、mergeTree

对于 clickhouse 这类高性能分析型数据库而言,修改源文件是一件非常奢侈且代价昂贵的操作,相对于直接修改源文件,我们将修改和新增操作都转换为新增操作,即以增代删将是一个非常不错的选择。是不是和 Hbase 的思路十分接近。在 mergeTree 家族中有一个特殊的表引擎叫 CollapsingMergeTree,翻译过来叫折叠合并树引擎就是提供了这样的功能。它通过定义一个 sign 标记字段来记录数据行的状态。如果 sign 为 1 表示这是一行有效的数据,如果 sign 为 -1 表示这行数据被删除。当 CollapsingMergeTree 分区合并时同一分区的 +1、-1 将会被抵消,犹如一张纸折叠一般。

2.1 实操

创建 CollapsingMergeTree 表

create table collapsing_table
(
    Id         String,
    Code       Int32,
    CreateTime DateTime,
    Sign       Int8
) engine = CollapsingMergeTree(Sign)
      partition by toYYYYMMDD(CreateTime)
      order by Id;

注:和其它 mergeTree 引擎一样 CollapsingMergeTree 依然是以 order by 字段作为后续数据唯一性的依据

插入一批原始数据

insert into collapsing_table values ('A000', 100, '2023-08-09 00:00:00', 1);
insert into collapsing_table values ('A001', 100, '2023-08-09 00:00:00', 1);
insert into collapsing_table values ('A002', 100, '2023-08-09 00:00:00', 1);

修改 A000 的 Code 为 200 并删除 A002 的数据

# 修改 A000 的 Code 为 200
insert into collapsing_table values ('A000', 100, '2023-08-09 00:00:00', -1);
insert into collapsing_table values ('A000', 200, '2023-08-09 00:00:00', 1);
# 删除 A002 的数据
insert into collapsing_table values ('A002', 100, '2023-08-09 00:00:00', -1);
# 手动执行一下分区合并操作
optimize table collapsing_table final;

可以观察到数据已经被删除和修改。

CollapsingMergeTree 在分区合并折叠数据的时候,遵循下面规则

  1. 如果 sign = 1 比 sign = -1 多一行,最后保留 sign = 1 的数据
  2. 如果 sign = 1 比 sign = -1 少一行,最后保留 sign = -1 的数据
  3. 如果 sign = 1 和 sign = -1 一样多,且最后一行时 sign = 1,则保留第一行的 sign = -1 和最后一行 sign = 1
  4. 如果 sign = 1 和 sign = -1 一样多,且最后一行时 sign = -1,则什么也不保留
  5. 其余情况 clickhouse 会打印告警日志,但不会报错且查询情况不可预知

2.2 不足

当前表的数据如下

select *
from collapsing_table;

Query id: 4b1da757-d02a-4b88-92e5-1fe659ca462c

┌─Id───┬─Code─┬──────────CreateTime─┬─Sign─┐
│ A002 │  3002023-08-09 00:00:00-1 │
└──────┴──────┴─────────────────────┴──────┘
┌─Id───┬─Code─┬──────────CreateTime─┬─Sign─┐
│ A002 │  3002023-08-09 00:00:001 │
└──────┴──────┴─────────────────────┴──────┘
┌─Id───┬─Code─┬──────────CreateTime─┬─Sign─┐
│ A000 │  2002023-08-09 00:00:001 │
│ A001 │  1002023-08-09 00:00:001 │
└──────┴──────┴─────────────────────┴──────┘

4 rows in set. Elapsed: 0.003 sec.

从操作来看 A002 是要被删除的

但是如果查询sql如下

select Id, sum(Code), count(Code), avg(Code)
from collapsing_table
group by Id;

Query id: 610f6503-1344-4ba0-9564-6327277ffe95

┌─Id───┬─sum(Code)─┬─count(Code)─┬─avg(Code)─┐
│ A001 │       1001100 │
│ A000 │       2001200 │
│ A002 │       6002300 │
└──────┴───────────┴─────────────┴───────────┘

3 rows in set. Elapsed: 0.005 sec.

此时的结果是不对的,因此需要改写 sql

select Id, sum(Code * Sign), count(Code * Sign), avg(Code * Sign)
from collapsing_table
group by Id
having sum(Sign) > 0;

Query id: a3fe84d0-33a5-4287-bd02-49ab03df1852

┌─Id───┬─sum(multiply(Code, Sign))─┬─count(multiply(Code, Sign))─┬─avg(multiply(Code, Sign))─┐
│ A001 │                       1001100 │
│ A000 │                       2001200 │
└──────┴───────────────────────────┴─────────────────────────────┴───────────────────────────┘

2 rows in set. Elapsed: 0.005 sec.

当然还有一种方式就是在查询数据前执行分区合并操作optimize table collapsing_table final;,但这种方式效率极低在生产中慎用

同时 CollapsingMergeTree 还存在一些问题,例如在分区合并前用户是可以看到所有数据的。当然上面所说的问题都不是最致命的,CollapsingMergeTree 最致命点在于对于 sign 的写入顺序有严格的要求,对于一个删除操作正常的顺序应该是先写入 1 再写入 -1

insert into collapsing_table values ('A002', 300, '2023-08-09 00:00:00', 1);
insert into collapsing_table values ('A002', 300, '2023-08-09 00:00:00', -1);

但如果颠倒顺序

insert into collapsing_table values ('A002', 300, '2023-08-09 00:00:00', -1);
insert into collapsing_table values ('A002', 300, '2023-08-09 00:00:00', 1);

则不会被删除。而在生产环境一旦 CollapsingMergeTree 在多线程中处理就无法保证写入顺序了。

当然幸运的是 clickhouse 也注意到 CollapsingMergeTree 的缺点并推出了新的表引擎 VersionedCollapsingMergeTree,在 CollapsingMergeTree 的基础上将按照写入顺序折叠修改为按照版本号顺序进行折叠,而版本号交由用户来管理。VersionedCollapsingMergeTree 引擎的操作就交给读者来体验,毕竟下面还有一种更贴合 TP 数据库操作的删除操作

三、lightweight

上面介绍了通过 mutation 和 mergeTree 来实现删除操作,但是 mutation 操作太重,mergeTree 则需要修改 sql 且删除操作受分区合并时机影响。从 clickhouse v22.8 开始提供了一个轻量级删除功能且语法为标准 sql 🎉🎉🎉

3.1 实操

准备表和数据

create table lightweight_operate
(
    UserId     UInt64,
    Score      UInt64,
    CreateTime DateTime
) engine = MergeTree()
      partition by toYYYYMMDD(CreateTime)
      order by UserId;

insert into lightweight_operate
select number,
       abs(number - 100),
       '2023-08-08 00:00:00'
from system.numbers
limit 1000000;

insert into lightweight_operate
select number,
       abs(number - 100),
       '2023-08-09 00:00:00'
from system.numbers
limit 1000000;

同样删除 20230808 分区中 1000-10000 之间的所有数据,sql 如下

delete from lightweight_operate where toYYYYMMDD(CreateTime) = 20230808 and UserId between 1000 and 10000;

验证一下

select count() from lightweight_operate where toYYYYMMDD(CreateTime) = 20230808;

Query id: 0344da3b-5ea5-436d-ba29-cfb1a8e3420e

┌─count()─┐
│  990999 │
└─────────┘

1 row in set. Elapsed: 0.008 sec. Processed 1.00 million rows, 5.00 MB (128.59 million rows/s., 642.93 MB/s.)

成功删除

3.2 原理

查看磁盘目录

» ll
total 16
drwxr-x---@ 16 wjun  admin  512 Aug  9 21:09 20230808_1_1_0
drwxr-x---@ 18 wjun  admin  576 Aug  9 21:10 20230808_1_1_0_3
drwxr-x---@ 16 wjun  admin  512 Aug  9 21:09 20230809_2_2_0
drwxr-x---@ 15 wjun  admin  480 Aug  9 21:10 20230809_2_2_0_3
drwxr-x---@  2 wjun  admin   64 Aug  9 21:09 detached
-rw-r-----@  1 wjun  admin    1 Aug  9 21:09 format_version.txt
-rw-r-----@  1 wjun  admin  171 Aug  9 21:10 mutation_3.txt

» du -h
  0B	./detached
7.7M	./20230809_2_2_0
7.7M	./20230808_1_1_0
  0B	./20230809_2_2_0_3
 28K	./20230808_1_1_0_3
 15M	.

可以看出轻量删除依然是一个 mutation 操作,从system.mutations表也可以验证,但轻量删除生成的新的分区 20230808_1_1_0_3 仅 28K,那么轻量删除和 mutation 删除的区别在哪

查看 20230808_1_1_0_3 磁盘目录

wjun :: data/delete_operate/lightweight_operate ‹stable› » ll 20230808_1_1_0_3
total 15800
-rw-r-----@ 2 wjun  admin    18042 Aug  9 21:09 CreateTime.bin
-rw-r-----@ 2 wjun  admin      375 Aug  9 21:09 CreateTime.cmrk2
-rw-r-----@ 2 wjun  admin  4004938 Aug  9 21:09 Score.bin
-rw-r-----@ 2 wjun  admin      415 Aug  9 21:09 Score.cmrk2
-rw-r-----@ 2 wjun  admin  4004915 Aug  9 21:09 UserId.bin
-rw-r-----@ 2 wjun  admin      415 Aug  9 21:09 UserId.cmrk2
-rw-r-----@ 1 wjun  admin     4493 Aug  9 21:10 _row_exists.bin
-rw-r-----@ 1 wjun  admin      236 Aug  9 21:10 _row_exists.cmrk2
-rw-r-----@ 1 wjun  admin      589 Aug  9 21:10 checksums.txt
-rw-r-----@ 1 wjun  admin      110 Aug  9 21:10 columns.txt
-rw-r-----@ 2 wjun  admin        7 Aug  9 21:09 count.txt
-rw-r-----@ 1 wjun  admin       10 Aug  9 21:10 default_compression_codec.txt
-rw-r-----@ 1 wjun  admin        1 Aug  9 21:10 metadata_version.txt
-rw-r-----@ 2 wjun  admin        8 Aug  9 21:09 minmax_CreateTime.idx
-rw-r-----@ 2 wjun  admin        4 Aug  9 21:09 partition.dat
-rw-r-----@ 2 wjun  admin      173 Aug  9 21:09 primary.cidx

发现多了一组 _row_exists 文件而其余文件的 inode 连接数均为 2,也就是说轻量删除是真正的给字段添加了一个标记。

在查询的时候过滤

lightweight_deletes_v2_b891b54446

在分区合并的时候删除

lightweight_delete_merge_1_a2519ab507

比 mutation 轻的点在于轻量删除不会重构整个分区目录而是重写 _row_exists 文件这样涉及到的修改会少很多,至于分区的拷贝和不涉及删除操作的分区操作逻辑则和上面介绍的 mutation 流程一致

3.3 不足

轻量删除的设计思路相比之前的会好上很多,但 clickhouse 毕竟不是 TP 数据库,目前轻量删除依然存在一些问题和限制,如:

  1. 轻量删除是异步的,只有在分区合并的时候才会被真正删除(轻量删除执行完是逻辑上删除)
  2. 对 wide 类型分区友好,对于 compact 类型分区会产生较大的磁盘 IO
  3. 会修改分区在磁盘中的名称,可能会影响备份

对于 mutation 是否为异步操作可以通过参数进行配置,只需将mutations_sync置为 true 即可

set mutations_sync = true;

至于其它的不足需要用户结合实际场景进行取舍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/67357.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

从数据仓库到数据结构:数据架构的演变之路

在上个世纪&#xff0c;从电子商务巨头到医疗服务机构和政府部门&#xff0c;数据已成为每家组织的生命线。有效地收集和管理这些数据可以为组织提供宝贵的洞察力&#xff0c;以帮助决策&#xff0c;然而这是一项艰巨的任务。 尽管数据很重要&#xff0c;但CIOinsight声称&…

定制 ChatGPT 以满足您的需求 自定义说明

推荐&#xff1a;使用 NSDT场景编辑器 快速助你搭建可二次编辑的3D应用场景 20 月 <> 日&#xff0c;OpenAI 宣布他们正在引入带有自定义说明的新流程&#xff0c;以根据您的特定需求定制 ChatGPT。 什么是自定义说明&#xff1f; 新的测试版自定义指令功能旨在通过防止…

BM8 链表中倒数最后k个结点

/*** struct ListNode {* int val;* struct ListNode *next;* ListNode(int x) : val(x), next(nullptr) {}* };*/ class Solution { public:/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可** * param pHead ListNode类 …

leetcode 343. 整数拆分

2023.8.10 本题用dp算法来做&#xff0c;dp[i]代表的含义是&#xff1a;当前数字i 在拆分之后所能获得的最大乘积。然后由于n>2&#xff0c;所以dp[0]和dp[1]没有意义&#xff0c;不用初始化&#xff0c;直接初始化dp[2] 1。 然后再遍历给dp数组赋值&#xff1a;dp[i]的来源…

业务逻辑漏洞之支付逻辑漏洞

业务逻辑漏洞之支付逻辑漏洞 一、漏洞挖掘介绍二、Web漏洞产生的原因三、业务逻辑简述四、 常见业务逻辑漏洞的功能点五、支付逻辑漏洞5.1、漏洞背景5.2、产生原因5.3、测试方法 六、挖到这些漏洞有什么用&#xff1f; 一、漏洞挖掘介绍 漏洞定义&#xff1a; 官方定义&#…

Linux基础与拓展

文章目录 虚拟机网络连接方式VIMvi和vim常用的三种模式各种模式的相互切换快捷键 用户管理权限 基本介绍&#xff1a;添加用户指定/修改密码删除用户切换用户用户组 路径命令学习mkdir命令介绍语法注意 touch 创建文件介绍语法 cat 查看文件内容介绍语法 more 查看文件内容介绍…

pve和openwrt以及我的电脑中网络的关系和互通组网

情况1 一台主机 有4个口&#xff0c;分别eth0,eth1,eth2,eth3 pve有管理口 这个情况下 &#xff0c;没有openwrt 直接电脑和pve管理口连在一起就能进pve管理界面 情况2 假设pve 的管理口味eth0 openwrt中桥接的是eth0 eth1 eth2 那么电脑连接eth3或者pve管理口设置eth3&#xf…

mysql不用窗口函数,后面加一列序号

前言 在后端开发中最常用的数据库还是比较稳定的5.8&#xff0c;而窗口函数是只有在mysql8以上才有的&#xff0c;然后在开发中有个需要排序序号的需求&#xff0c;翻找资料&#xff0c;问AI得出结论可以实现。 列出方法 如果你使用的是MySQL 5.7版本&#xff0c;而没有窗口…

C++笔记之回调函数的演变

C笔记之回调函数的演变 code review! 文章目录 C笔记之回调函数的演变1.使用函数指针2.使用typedef加函数指针3.使用std::using加函数指针4.使用typedef加std::function5.使用std::using加std::function6.使用回调和不使用回调对比 1.使用函数指针 代码 #include <iostre…

C# WPF 开源主题 HandyControl 的使用(一)

HandyControl是一套WPF控件库&#xff0c;它几乎重写了所有原生样式&#xff0c;同时包含80余款自定义控件&#xff08;正逐步增加&#xff09;&#xff0c;下面我们开始使用。 1、准备 1.1 创建项目 C# WPF应用(.NET Framework)创建项目 1.2 添加包 1.3 在App.xaml中引用…

性能调优,看过的都说会了...

在展开今天的内容之前&#xff0c;我们先来看一下&#xff0c;是不是任何一个测试都可以学习性能测试。 如果说需求、开发、DB、运维、测试是单一一门学科&#xff0c;那么性能就是综合学科&#xff0c;它包含了需求分析、DB、开发、测试、运维的所有学科。 所以说&#xff0…

html实现商品图片放大镜,html图片放大镜预览

效果 实现 复制粘贴&#xff0c;修改图片路径即可使用 <!DOCTYPE html> <html><head><meta charset"UTF-8"><title>商品图片放大镜</title></head><style>body {margin: 0;padding: 0;}#app {padding: 10px;posit…

Java反射机制详解与使用方法大全!!!

❤ 作者主页&#xff1a;李奕赫揍小邰的博客 ❀ 个人介绍&#xff1a;大家好&#xff0c;我是李奕赫&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 记得点赞、收藏、评论⭐️⭐️⭐️ &#x1f4e3; 认真学习!!!&#x1f389;&#x1f389; 文章目录 Java反射机制…

c++中的继承

文章目录 1.继承的概念及定义1.1继承的概念1.2继承的定义1.2.1定义格式1.2.2继承关系和访问限定符1.2.3继承基类成员访问方式的变化 2.基类和派生类对象赋值转换3.继承中的作用域4.派生类的默认成员函数5.继承与友元6.继承与静态成员7.复杂的菱形继承及菱形虚拟继承 1.继承的概…

【深度学习注意力机制系列】—— SKNet注意力机制(附pytorch实现)

SKNet&#xff08;Selective Kernel Network&#xff09;是一种用于图像分类和目标检测任务的深度神经网络架构&#xff0c;其核心创新是引入了选择性的多尺度卷积核&#xff08;Selective Kernel&#xff09;以及一种新颖的注意力机制&#xff0c;从而在不增加网络复杂性的情况…

opencv基础53-图像轮廓06-判断像素点与轮廓的关系(轮廓内,轮廓上,轮廓外)cv2.pointPolygonTest()

点到轮廓的距离 在 OpenCV 中&#xff0c;函数 cv2.pointPolygonTest()被用来计算点到多边形&#xff08;轮廓&#xff09;的最短距离&#xff08;也 就是垂线距离&#xff09;&#xff0c;这个计算过程又称点和多边形的关系测试。该函数的语法格式为&#xff1a; retval cv2…

Linux —— 基础I/O

一&#xff0c;背景介绍 狭义的文件存放在磁盘上&#xff0c;广义上在Linux下一切皆文件&#xff1b;磁盘上的文件一般为永久存储的外设&#xff0c;本质上对文件的操作&#xff0c;即为对外设的输入和输出&#xff08;简称I/O&#xff09;&#xff1b;空文件并不是不占磁盘文件…

uC-OS2 V2.93 STM32L476 移植:串口打印篇

前言 前几篇已经 通过 STM32CubeMX 搭建了 NUCLEO-L476RG 的 STM32L476RG 的 裸机工程&#xff0c;下载了 uC-OS2 V2.93 的源码&#xff0c;并把 uC-OS2 的源文件加入 Keil MDK5 工程&#xff0c;通过适配 Systick 系统定时器与 PendSV 实现任务调度&#xff0c;初步让 uC-OS2 …

Facebook商城号最全解析,Facebook Marketplace运营技巧

相信许多做跨境的小伙伴都听说过Facebook商城号。其实所谓的商城并不是Facebook Shop&#xff0c;而是指Facebook Marketplace&#xff0c;它不像前者需要各类入驻条件&#xff0c;只要拥有facebook账号而且所在地区有能够使用marketplace就可以在上面进行商品售卖。这种低成本…

validator入门

validator中文文档地址和英文地址 https://docs.jboss.org/hibernate/validator/4.2/reference/zh-CN/html/validator-gettingstarted.html https://docs.jboss.org/hibernate/validator/6.0/reference/en-US/html_single/#preface自定义hibernate-validator校验 工具类Valid…