暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

67. clickhouse字典操作

大数据技能圈 2023-04-14
20

字典是一种映射(key ->属性),方便于各种类型的引用列表。

ClickHouse支持在查询中使用字典的特殊功能。在函数中使用字典比在引用表中使用JOIN更容易、更有效。

ClickHouse支持:

  • 带有一组函数的字典。

  • 带有特定函数集的嵌入式字典。

您可以从各种数据源中添加自己的字典。字典的源可以是一个ClickHouse表、一个本地文本或可执行文件、一个HTTP(s)资源或另一个DBMS。

ClickHouse:

  • 将字典全部或部分存储在RAM中。

  • 定期更新字典并动态加载缺失值。换句话说,字典可以动态加载。

  • 允许使用xml文件或DDL查询创建字典。

字典的配置可以位于一个或多个xml文件中。配置的路径在dictionaries_config参数中指定。

字典可以在服务器启动时加载,也可以在第一次使用时加载,这取决于dictionaries_lazy_load设置。

字典系统表包含服务器上配置的字典信息。对于你可以在那里找到的每一本字典:

  • 字典的状态。

  • 配置参数。

  • 诸如为字典分配的RAM数量或自字典成功加载以来的查询数量等指标。

如果您正在使用ClickHouse Cloud的字典,请使用DDL查询选项来创建您的字典,并以默认用户创建您的字典。此外,请验证云兼容性指南中支持的字典源列表。

使用DDL查询创建字典

字典可以用DDL查询创建,这是推荐的方法,因为DDL创建的字典:

  • 不向服务器配置文件添加额外的记录

  • 字典可以像表或视图一样作为一级实体使用

  • 可以直接读取数据,使用熟悉的SELECT函数而不是字典表函数

  • 字典可以很容易地重命名

创建带有配置文件的字典

字典配置文件格式如下:

<clickhouse>
<comment>An optional element with any content. Ignored by the ClickHouse server.</comment>

<!--Optional element. File name with substitutions-->
<include_from>/etc/metrika.xml</include_from>


<dictionary>
<!-- Dictionary configuration. -->
<!-- There can be any number of dictionary sections in a configuration file. -->
</dictionary>

</clickhouse>

您可以在同一个文件中配置任意数量的字典。

配置字典

如果使用xml文件配置字典,则字典配置具有以下结构:

<dictionary>
<name>dict_name</name>

<structure>
<!-- Complex key configuration -->
</structure>

<source>
<!-- Source configuration -->
</source>

<layout>
<!-- Memory layout configuration -->
</layout>

<lifetime>
<!-- Lifetime of dictionary in memory -->
</lifetime>
</dictionary>

对应的DDL-query结构如下:

CREATE DICTIONARY dict_name
(
... -- attributes
)
PRIMARY KEY ... -- complex or single key configuration
SOURCE(...) -- Source configuration
LAYOUT(...) -- Memory layout configuration
LIFETIME(...) -- Lifetime of dictionary in memory

在内存中存储字典

在内存中存储字典的方法有很多种。

我们推荐flat, hashed和complex_key_hashed,它们提供了最佳的处理速度。

不建议使用缓存,因为可能会导致性能较差,并且难以选择最佳参数。

有几种方法可以提高字典的性能:

  • 调用在GROUP BY之后使用字典的函数。

  • 将要提取的属性标记为单射。如果不同的属性值对应不同的键,则属性称为单射。因此,当GROUP BY使用一个通过键获取属性值的函数时,这个函数将自动从GROUP BY中取出。

ClickHouse为字典的错误生成一个异常。错误的例子:

  • 无法加载正在访问的字典。

  • 查询缓存的字典时出错。

您可以查看字典列表及其在系统中的状态。字典表

配置如下所示:

<clickhouse>
<dictionary>
...
<layout>
<layout_type>
<!-- layout settings -->
</layout_type>
</layout>
...
</dictionary>
</clickhouse>

相应DDL-query:

CREATE DICTIONARY (...)
...
LAYOUT(LAYOUT_TYPE(param value)) -- layout settings
...

在布局中没有单词complex-key的字典有一个UInt64类型的键,complex-key字典有一个复合键(复杂,有任意类型)。

XML字典中的UInt64键用标签定义。

配置举例(列key_column为UInt64类型):

...
<structure>
<id>
<name>key_column</name>
</id>
...

XML字典定义复合复杂键标记。

组合键的配置示例(键有一个String类型的元素):

...
<structure>
<key>
<attribute>
<name>country_code</name>
<type>String</type>
</attribute>
</key>
...

在内存中存储字典的方法

  • flat

  • hashed

  • sparse_hashed

  • complex_key_hashed

  • complex_key_sparse_hashed

  • hashed_array

  • complex_key_hashed_array

  • range_hashed

  • complex_key_range_hashed

  • cache

  • complex_key_cache

  • ssd_cache

  • complex_key_ssd_cache

  • direct

  • complex_key_direct

  • ip_trie

flat

字典以平面数组的形式完全存储在内存中。这本字典用了多少内存?数量与最大键的大小成比例(在使用的空间中)。

字典键类型为UInt64,取值限制为max_array_size(默认为 500000)。如果在创建字典时发现了一个更大的键,ClickHouse将抛出异常,并且不创建字典。字典平面数组的初始大小由initial_array_size设置控制(默认值为1024)。

支持所有类型的源。更新时,读取数据(来自文件或表)。

在所有可用的存储字典的方法中,此方法提供了最好的性能。

配置的例子:

<layout>
<flat>
<initial_array_size>50000</initial_array_size>
<max_array_size>5000000</max_array_size>
</flat>
</layout>

或者

LAYOUT(FLAT(INITIAL_ARRAY_SIZE 50000 MAX_ARRAY_SIZE 5000000))

hashed

字典以哈希表的形式完全存储在内存中。字典可以包含任意数量的具有任意标识符的元素。实际上,键的数量可以达到数千万个项。

字典键的类型为UInt64。

支持所有类型的源。更新时,读取数据(来自文件或表)的全部内容。

配置的例子:

<layout>
<hashed >
</layout>

或者

LAYOUT(HASHED())

如果shards大于1(默认为1),字典将并行加载数据,如果一个字典中有大量元素,这很有用。

配置示例:

<layout>
<hashed>
<shards>10</shards>
<!-- Size of the backlog for blocks in parallel queue.

Since the bottleneck in parallel loading is rehash, and so to avoid
stalling because of thread is doing rehash, you need to have some
backlog.

10000 is good balance between memory and speed.
Even for 10e10 elements and can handle all the load without starvation. -->
<shard_load_queue_backlog>10000</shard_load_queue_backlog>
</hashed>
</layout>

或者

LAYOUT(HASHED(SHARDS 10 [SHARD_LOAD_QUEUE_BACKLOG 10000]))

sparse_hashed

类似于散列,但使用更少的内存,支持更多的CPU使用。

字典键的类型为UInt64。

配置的例子:

<layout>
<sparse_hashed >
</layout>

或者

LAYOUT(SPARSE_HASHED())

对于这种类型的字典也可以使用shard,同样,sparse_hashed比hashed更重要,因为sparse_hashed更慢。

complex_key_hashed

这种类型的存储用于组合键。类似于散列。

配置的例子:

<layout>
<complex_key_hashed>
<shards>1</shards>
<!-- <shard_load_queue_backlog>10000</shard_load_queue_backlog> -->
</complex_key_hashed>
</layout>

或者

LAYOUT(COMPLEX_KEY_HASHED([SHARDS 1] [SHARD_LOAD_QUEUE_BACKLOG 10000]))

complex_key_sparse_hashed

这种类型的存储用于组合键。类似于sparse_hash。

配置的例子:

<layout>
<complex_key_sparse_hashed>
<shards>1</shards>
</complex_key_sparse_hashed>
</layout>
LAYOUT(COMPLEX_KEY_SPARSE_HASHED([SHARDS 1] [SHARD_LOAD_QUEUE_BACKLOG 10000]))

hashed_array

字典完全存储在内存中。每个属性存储在一个数组中。键属性以散列表的形式存储,其中value是属性数组中的索引。字典可以包含任意数量的具有任意标识符的元素。在实践中,密钥的数量可以达到数千万个项目。

字典键的类型为UInt64。

支持所有类型的源。更新时,读取数据(来自文件或表)的全部内容。

配置的例子:

<layout>
<hashed_array>
</hashed_array>
</layout>
LAYOUT(HASHED_ARRAY())

complex_key_hashed_array

这种类型的存储用于组合键。类似于hashed_array。

配置的例子:

<layout>
<complex_key_hashed_array >
</layout>
LAYOUT(COMPLEX_KEY_HASHED_ARRAY())

range_hashed

字典以哈希表的形式存储在内存中,哈希表包含一个有序的范围数组及其对应的值。

字典键的类型为UInt64。这种存储方法的工作方式与散列相同,并且允许在键之外使用日期/时间(任意数字类型)范围。

示例:该表包含每个广告主的折扣,格式为:

┌─advertiser_id─┬─discount_start_date─┬─discount_end_date─┬─amount─┐
│ 123 │ 2015-01-16 │ 2015-01-31 │ 0.25 │
│ 123 │ 2015-01-01 │ 2015-01-15 │ 0.15 │
│ 456 │ 2015-01-01 │ 2015-01-15 │ 0.05 │
└───────────────┴─────────────────────┴───────────────────┴────────┘

要使用日期范围的示例,请在结构中定义range_min和range_max元素。这些元素必须包含元素名称和类型(如果没有指定type,将使用默认类型- Date)。type为任意数值类型(Date DateTime UInt64 Int32 others)。

range_min和range_max的值应该适合Int64类型。

举例:

<layout>
<range_hashed>
<!-- Strategy for overlapping ranges (min/max). Default: min (return a matching range with the min(range_min -> range_max) value) -->
<range_lookup_strategy>min</range_lookup_strategy>
</range_hashed>
</layout>
<structure>
<id>
<name>advertiser_id</name>
</id>
<range_min>
<name>discount_start_date</name>
<type>Date</type>
</range_min>
<range_max>
<name>discount_end_date</name>
<type>Date</type>
</range_max>
...

或者

CREATE DICTIONARY discounts_dict (
advertiser_id UInt64,
discount_start_date Date,
discount_end_date Date,
amount Float64
)
PRIMARY KEY id
SOURCE(CLICKHOUSE(TABLE 'discounts'))
LIFETIME(MIN 1 MAX 1000)
LAYOUT(RANGE_HASHED(range_lookup_strategy 'max'))
RANGE(MIN discount_start_date MAX discount_end_date)

要使用这些字典,你需要向dictGet函数传递一个额外的参数,为其选择一个范围:

dictGet('dict_name', 'attr_name', id, date)

查询的例子:

SELECT dictGet('discounts_dict', 'amount', 1, '2022-10-20'::Date);

此函数返回指定id的值和包含已传递日期的日期范围。

算法细节:

  • 如果没有找到id或没有找到id的范围,则返回属性类型的默认值。

  • 如果有重叠的范围并且range_lookup_strategy=min,它会返回一个最小的range_min的匹配范围,如果找到几个范围,它会返回一个最小的range_max的范围,如果找到几个范围(几个范围有相同的range_min和range_max,它会返回一个随机的范围。

  • 如果有重叠的范围和range_lookup_strategy=max,它返回一个匹配的范围与最大的range_min,如果找到几个范围,它返回一个范围与最大的range_max,如果再次找到几个范围(几个范围有相同的range_min和range_max,它返回一个随机的范围。

  • 如果range_max为NULL,则表示该范围是开放的。NULL被视为可能的最大值。对于range_min, 1970-01-01或0 (-MAX_INT)可以用作打开值。

配置的例子:

<clickhouse>
<dictionary>
...

<layout>
<range_hashed >
</layout>

<structure>
<id>
<name>Abcdef</name>
</id>
<range_min>
<name>StartTimeStamp</name>
<type>UInt64</type>
</range_min>
<range_max>
<name>EndTimeStamp</name>
<type>UInt64</type>
</range_max>
<attribute>
<name>XXXType</name>
<type>String</type>
<null_value >
</attribute>
</structure>

</dictionary>
</clickhouse>

或者

CREATE DICTIONARY somedict(
Abcdef UInt64,
StartTimeStamp UInt64,
EndTimeStamp UInt64,
XXXType String DEFAULT ''
)
PRIMARY KEY Abcdef
RANGE(MIN StartTimeStamp MAX EndTimeStamp)

重叠范围和开放范围的配置示例:

CREATE TABLE discounts
(
advertiser_id UInt64,
discount_start_date Date,
discount_end_date Nullable(Date),
amount Float64
)
ENGINE = Memory;

INSERT INTO discounts VALUES (1, '2015-01-01', Null, 0.1);
INSERT INTO discounts VALUES (1, '2015-01-15', Null, 0.2);
INSERT INTO discounts VALUES (2, '2015-01-01', '2015-01-15', 0.3);
INSERT INTO discounts VALUES (2, '2015-01-04', '2015-01-10', 0.4);
INSERT INTO discounts VALUES (3, '1970-01-01', '2015-01-15', 0.5);
INSERT INTO discounts VALUES (3, '1970-01-01', '2015-01-10', 0.6);

SELECT * FROM discounts ORDER BY advertiser_id, discount_start_date;
┌─advertiser_id─┬─discount_start_date─┬─discount_end_date─┬─amount─┐
│ 1 │ 2015-01-01 │ ᴺᵁᴸᴸ │ 0.1 │
│ 1 │ 2015-01-15 │ ᴺᵁᴸᴸ │ 0.2 │
│ 2 │ 2015-01-01 │ 2015-01-15 │ 0.3 │
│ 2 │ 2015-01-04 │ 2015-01-10 │ 0.4 │
│ 3 │ 1970-01-01 │ 2015-01-15 │ 0.5 │
│ 3 │ 1970-01-01 │ 2015-01-10 │ 0.6 │
└───────────────┴─────────────────────┴───────────────────┴────────┘

-- RANGE_LOOKUP_STRATEGY 'max'

CREATE DICTIONARY discounts_dict
(
advertiser_id UInt64,
discount_start_date Date,
discount_end_date Nullable(Date),
amount Float64
)
PRIMARY KEY advertiser_id
SOURCE(CLICKHOUSE(TABLE discounts))
LIFETIME(MIN 600 MAX 900)
LAYOUT(RANGE_HASHED(RANGE_LOOKUP_STRATEGY 'max'))
RANGE(MIN discount_start_date MAX discount_end_date);

select dictGet('discounts_dict', 'amount', 1, toDate('2015-01-14')) res;
┌─res─┐
│ 0.1 │ -- the only one range is matching: 2015-01-01 - Null
└─────┘

select dictGet('discounts_dict', 'amount', 1, toDate('2015-01-16')) res;
┌─res─┐
│ 0.2 │ -- two ranges are matching, range_min 2015-01-15 (0.2) is bigger than 2015-01-01 (0.1)
└─────┘

select dictGet('discounts_dict', 'amount', 2, toDate('2015-01-06')) res;
┌─res─┐
│ 0.4 │ -- two ranges are matching, range_min 2015-01-04 (0.4) is bigger than 2015-01-01 (0.3)
└─────┘

select dictGet('discounts_dict', 'amount', 3, toDate('2015-01-01')) res;
┌─res─┐
│ 0.5 │ -- two ranges are matching, range_min are equal, 2015-01-15 (0.5) is bigger than 2015-01-10 (0.6)
└─────┘

DROP DICTIONARY discounts_dict;

-- RANGE_LOOKUP_STRATEGY 'min'

CREATE DICTIONARY discounts_dict
(
advertiser_id UInt64,
discount_start_date Date,
discount_end_date Nullable(Date),
amount Float64
)
PRIMARY KEY advertiser_id
SOURCE(CLICKHOUSE(TABLE discounts))
LIFETIME(MIN 600 MAX 900)
LAYOUT(RANGE_HASHED(RANGE_LOOKUP_STRATEGY 'min'))
RANGE(MIN discount_start_date MAX discount_end_date);

select dictGet('discounts_dict', 'amount', 1, toDate('2015-01-14')) res;
┌─res─┐
│ 0.1 │ -- the only one range is matching: 2015-01-01 - Null
└─────┘

select dictGet('discounts_dict', 'amount', 1, toDate('2015-01-16')) res;
┌─res─┐
│ 0.1 │ -- two ranges are matching, range_min 2015-01-01 (0.1) is less than 2015-01-15 (0.2)
└─────┘

select dictGet('discounts_dict', 'amount', 2, toDate('2015-01-06')) res;
┌─res─┐
│ 0.3 │ -- two ranges are matching, range_min 2015-01-01 (0.3) is less than 2015-01-04 (0.4)
└─────┘

select dictGet('discounts_dict', 'amount', 3, toDate('2015-01-01')) res;
┌─res─┐
│ 0.6 │ -- two ranges are matching, range_min are equal, 2015-01-10 (0.6) is less than 2015-01-15 (0.5)
└─────┘

complex_key_range_hashed

字典以哈希表的形式存储在内存中,哈希表包含一个有序的范围数组及其对应的值(参见range_hashed)。这种类型的存储用于组合键。

配置的例子:

CREATE DICTIONARY range_dictionary
(
CountryID UInt64,
CountryKey String,
StartDate Date,
EndDate Date,
Tax Float64 DEFAULT 0.2
)
PRIMARY KEY CountryID, CountryKey
SOURCE(CLICKHOUSE(TABLE 'date_table'))
LIFETIME(MIN 1 MAX 1000)
LAYOUT(COMPLEX_KEY_RANGE_HASHED())
RANGE(MIN StartDate MAX EndDate);

cache

字典存储在具有固定单元格数量的缓存中。这些单元格包含常用的元素。

字典键的类型为UInt64。

在搜索字典时,首先搜索缓存。对于每个数据块,使用SELECT attrs从源请求缓存中没有找到或过时的所有键…从db。表WHERE id IN (k1, k2,…)然后将接收到的数据写入缓存。

如果在字典中没有找到键,则创建更新缓存任务并将其添加到更新队列中。更新队列属性可以通过设置max_update_queue_size、update_queue_push_timeout_milliseconds、query_wait_timeout_milliseconds、max_threads_for_updates来控制。

对于缓存字典,可以设置缓存中数据的过期生命期。如果自加载单元格中的数据以来已经过了超过生命周期的时间,则该单元格的值将不被使用,键将过期。下次需要使用该密钥时,将重新请求该密钥。这种行为可以通过设置allow_read_expired_keys来配置。

这是所有存储字典的方法中最低效的。缓存的速度很大程度上取决于正确的设置和使用场景。缓存类型字典只有在命中率足够高(推荐99%或更高)时才能执行良好。您可以在系统中查看平均命中率。字典表。

如果allow_read_expired_keys设置为1,则默认为0。那么字典就可以支持异步更新了。如果客户端请求密钥,并且所有密钥都在缓存中,但其中一些密钥已经过期,那么dictionary将为客户端返回过期的密钥,并从源端异步请求它们。

为了提高缓存性能,可以使用带有LIMIT的子查询,并在外部使用字典调用该函数。

支持所有类型的源。

设置示例:

<layout>
<cache>
<!-- The size of the cache, in number of cells. Rounded up to a power of two. -->
<size_in_cells>1000000000</size_in_cells>
<!-- Allows to read expired keys. -->
<allow_read_expired_keys>0</allow_read_expired_keys>
<!-- Max size of update queue. -->
<max_update_queue_size>100000</max_update_queue_size>
<!-- Max timeout in milliseconds for push update task into queue. -->
<update_queue_push_timeout_milliseconds>10</update_queue_push_timeout_milliseconds>
<!-- Max wait timeout in milliseconds for update task to complete. -->
<query_wait_timeout_milliseconds>60000</query_wait_timeout_milliseconds>
<!-- Max threads for cache dictionary update. -->
<max_threads_for_updates>4</max_threads_for_updates>
</cache>
</layout>

或者

LAYOUT(CACHE(SIZE_IN_CELLS 1000000000))

设置一个足够大的缓存大小。您需要通过实验来选择单元格的数量:

  1. 设置一些值。

  2. 运行查询直到缓存完全满。

  3. 使用系统评估内存消耗。字典表。

  4. 增加或减少单元格的数量,直到达到所需的内存消耗。

不要使用ClickHouse作为源,因为它处理随机读取查询的速度很慢。

complex_key_cache

这种类型的存储用于组合键。类似于cache。

ssd_cache

类似于缓存,但将数据存储在SSD上,索引存储在RAM中。所有与更新队列相关的缓存字典设置也可以应用于SSD缓存字典。

字典键的类型为UInt64。

<layout>
<ssd_cache>
<!-- Size of elementary read block in bytes. Recommended to be equal to SSD's page size. -->
<block_size>4096</block_size>
<!-- Max cache file size in bytes. -->
<file_size>16777216</file_size>
<!-- Size of RAM buffer in bytes for reading elements from SSD. -->
<read_buffer_size>131072</read_buffer_size>
<!-- Size of RAM buffer in bytes for aggregating elements before flushing to SSD. -->
<write_buffer_size>1048576</write_buffer_size>
<!-- Path where cache file will be stored. -->
<path>/var/lib/clickhouse/user_files/test_dict</path>
</ssd_cache>
</layout>

或者

LAYOUT(SSD_CACHE(BLOCK_SIZE 4096 FILE_SIZE 16777216 READ_BUFFER_SIZE 1048576
PATH '/var/lib/clickhouse/user_files/test_dict'))

complex_key_ssd_cache

这种类型的存储用于组合键。类似于ssd_cache。

direct

字典不存储在内存中,而是在处理请求时直接访问源。

字典键的类型为UInt64。

除本地文件外,支持所有类型的源。

配置的例子:

<layout>
<direct >
</layout>

或者

LAYOUT(DIRECT())

complex_key_direct

这种类型的存储用于组合键。类似于direct。

ip_trie

这种类型的存储用于将网络前缀(IP地址)映射到元数据(如ASN)。

例子

假设我们在ClickHouse中有一个表,其中包含我们的IP前缀和映射:

CREATE TABLE my_ip_addresses (
prefix String,
asn UInt32,
cca2 String
)
ENGINE = MergeTree
PRIMARY KEY prefix;
INSERT INTO my_ip_addresses VALUES
('202.79.32.0/20', 17501, 'NP'),
('2620:0:870::/48', 3856, 'US'),
('2a02:6b8:1::/48', 13238, 'RU'),
('2001:db8::/32', 65536, 'ZZ')
;

让我们为这个表定义一个ip_trie字典。ip_trie布局需要一个复合键:

<structure>
<key>
<attribute>
<name>prefix</name>
<type>String</type>
</attribute>
</key>
<attribute>
<name>asn</name>
<type>UInt32</type>
<null_value >
</attribute>
<attribute>
<name>cca2</name>
<type>String</type>
<null_value>??</null_value>
</attribute>
...
</structure>
<layout>
<ip_trie>
<!-- Key attribute `prefix` can be retrieved via dictGetString. -->
<!-- This option increases memory usage. -->
<access_to_key_from_attributes>true</access_to_key_from_attributes>
</ip_trie>
</layout>

或者

CREATE DICTIONARY my_ip_trie_dictionary (
prefix String,
asn UInt32,
cca2 String DEFAULT '??'
)
PRIMARY KEY prefix
SOURCE(CLICKHOUSE(TABLE 'my_ip_addresses'))
LAYOUT(IP_TRIE)
LIFETIME(3600);

密钥必须只有一个包含允许的IP前缀的String类型属性。目前还不支持其他类型。

对于查询,必须使用与使用组合键的字典相同的函数(带元组的dictGetT)。语法为:

dictGetT('dict_name', 'attr_name', tuple(ip))

对于IPv4,该函数接受UInt32;对于IPv6,该函数接受FixedString(16)。例如:

select dictGet('my_ip_trie_dictionary', 'asn', tuple(IPv6StringToNum('2001:db8::1')))

目前还不支持其他类型。该函数返回与此IP地址对应的前缀的属性。如果有重叠的前缀,则返回最具体的前缀。

数据必须完全适合RAM。

字典更新

ClickHouse定期更新字典。完全下载字典的更新间隔和缓存字典的失效间隔在生命周期标记中定义,单位为秒。

字典更新(除了第一次使用的加载)不会阻塞查询。在更新过程中,使用旧版本的字典。如果在更新期间发生错误,则该错误被写入服务器日志,并且继续使用旧版本的字典进行查询。

设置示例:

<dictionary>
...
<lifetime>300</lifetime>
...
</dictionary>

或者

CREATE DICTIONARY (...)
...
LIFETIME(300)
...

设置0 (lifetime(0))阻止字典更新。

您可以设置更新的时间间隔,ClickHouse将在此范围内选择一个统一的随机时间。在大量服务器上更新时,为了在字典源上分配负载,这是必要的。

设置示例:

<dictionary>
...
<lifetime>
<min>300</min>
<max>360</max>
</lifetime>
...
</dictionary>

或者

LIFETIME(MIN 300 MAX 360)

如果0和0,则ClickHouse超时不重新加载字典。在这种情况下,如果修改了字典配置文件或执行了SYSTEM reload dictionary命令,则ClickHouse可以更早地重新加载字典。

当更新字典时,ClickHouse服务器根据源的类型应用不同的逻辑:

对于文本文件,它检查修改的时间。如果时间与之前记录的时间不一致,则更新字典。 对于MySQL源,使用SHOW TABLE STATUS查询检查修改时间(对于MySQL 8,您需要通过set global information_schema_stats_expiry=0禁用MySQL中的元信息缓存)。 默认情况下,来自其他来源的字典每次都会更新。

对于其他来源(ODBC, PostgreSQL, ClickHouse等),你可以设置一个查询,只在字典真的发生变化时才更新字典,而不是每次都更新。要做到这一点,请遵循以下步骤:

字典表必须有一个字段,该字段必须在源数据更新时始终更改。 源的设置必须指定检索更改字段的查询。ClickHouse服务器将查询结果解释为一行,如果该行相对于之前的状态发生了变化,则更新字典。在源的设置中的<invalidate_query>字段中指定查询。

<dictionary>
...
<odbc>
...
<invalidate_query>SELECT update_time FROM dictionary_source where id = 1</invalidate_query>
</odbc>
...
</dictionary>

或者

...
SOURCE(ODBC(... invalidate_query 'SELECT update_time FROM dictionary_source where id = 1'))
...

对于Cache、ComplexKeyCache、SSDCache和SSDComplexKeyCache字典,都支持同步更新和异步更新。

Flat, Hashed, ComplexKeyHashed字典也可以只请求在上次更新后更改的数据。如果update_field作为字典源配置的一部分被指定,那么上一次更新时间(以秒为单位)的值将被添加到数据请求中。根据源类型(可执行文件、HTTP、MySQL、PostgreSQL、ClickHouse或ODBC)的不同,在从外部源请求数据之前,update_field将应用不同的逻辑。

如果源是HTTP,则update_field将作为查询参数添加,最后更新时间为参数值。 如果源是可执行的,update_field将作为可执行脚本参数添加,最后更新时间作为参数值。 如果源是ClickHouse, MySQL, PostgreSQL, ODBC, WHERE将有一个额外的部分,其中update_field将与上次更新时间进行比较。

默认情况下,在SQL-Query的最高级别检查where条件。或者,可以使用{condition}-关键字在查询中的任何其他where子句中检查条件。例子:

...
SOURCE(CLICKHOUSE(...
update_field 'added_time'
QUERY '
SELECT my_arr.1 AS x, my_arr.2 AS y, creation_time
FROM (
SELECT arrayZip(x_arr, y_arr) AS my_arr, creation_time
FROM dictionary_source
WHERE {condition}
)'
))
...

如果设置了update_field选项,则可以设置附加选项update_lag。update_lag选项的值在请求更新数据之前从上一次更新时间中减去。

设置示例:

<dictionary>
...
<clickhouse>
...
<update_field>added_time</update_field>
<update_lag>15</update_lag>
</clickhouse>
...
</dictionary>

或者

...
SOURCE(CLICKHOUSE(... update_field 'added_time' update_lag 15))
...

Dictionary Sources

字典可以从许多不同的来源连接到ClickHouse。

如果使用xml文件配置字典,则配置如下:

<clickhouse>
<dictionary>
...
<source>
<source_type>
<!-- Source configuration -->
</source_type>
</source>
...
</dictionary>
...
</clickhouse>

在ddl查询的情况下,上面描述的配置看起来像:

CREATE DICTIONARY dict_name (...)
...
SOURCE(SOURCE_TYPE(param1 val1 ... paramN valN)) -- Source configuration
...

源在source部分中配置。

对于源类型本地文件,可执行文件,HTTP(s), ClickHouse可选设置可用:

<source>
<file>
<path>/opt/dictionaries/os.tsv</path>
<format>TabSeparated</format>
</file>
<settings>
<format_csv_allow_single_quotes>0</format_csv_allow_single_quotes>
</settings>
</source>

或者

SOURCE(FILE(path './user_files/os.tsv' format 'TabSeparated'))
SETTINGS(format_csv_allow_single_quotes = 0)
  • Local file

  • Executable File

  • Executable Pool

  • HTTP(s)

  • ODBC

  • MySQL

  • ClickHouse

  • MongoDB

  • Redis

  • Cassandra

  • PostgreSQL

Local File

设置示例:

<source>
<file>
<path>/opt/dictionaries/os.tsv</path>
<format>TabSeparated</format>
</file>
</source>

或者

SOURCE(FILE(path './user_files/os.tsv' format 'TabSeparated'))

设置字段:

  • path -文件的绝对路径。

  • format -文件格式。支持格式中描述的所有格式。

当通过DDL命令(CREATE dictionary…)创建带有源文件的字典时,源文件需要位于user_files目录中,以防止DB用户访问ClickHouse节点上的任意文件。

Executable File

使用可执行文件取决于字典在内存中的存储方式。如果使用缓存和complex_key_cache存储字典,则ClickHouse通过向可执行文件的STDIN发送请求来请求必要的键。否则,ClickHouse启动可执行文件,并将其输出视为字典数据。

设置示例:

<source>
<executable>
<command>cat opt/dictionaries/os.tsv</command>
<format>TabSeparated</format>
<implicit_key>false</implicit_key>
</executable>
</source>

设置字段:

  • command -可执行文件的绝对路径,或者文件名(如果命令的目录在path中)。

  • format -文件格式。支持格式中描述的所有格式。

  • command_termination_timeout -可执行脚本应该包含一个主读写循环。在字典被销毁后,管道被关闭,可执行文件将有command_termination_timeout秒来关闭,然后ClickHouse将向子进程发送SIGTERM信号。Command_termination_timeout的单位是秒。缺省值为10。可选参数。

  • command_read_timeout -从命令stdout读取数据的超时时间,单位为毫秒。默认值10000。可选参数。

  • command_write_timeout -向命令stdin写入数据的超时时间,单位为毫秒。默认值10000。可选参数。

  • implicit_key—可执行源文件只能返回值,与请求键的对应关系由结果中的行顺序隐式确定。默认值为false。

  • execute_direct—如果execute_direct = 1,则命令将在user_scripts_path指定的user_scripts文件夹中搜索。其他脚本参数可以使用空白分隔符指定。示例:script_name arg1 arg2。如果execute_direct = 0, command将作为bin/sh -c的参数传递。缺省值为0。可选参数。

  • Send_chunk_header -控制在向处理发送数据块之前是否发送行数。可选的。默认值为false。

该字典源只能通过XML配置进行配置。禁止通过DDL创建带有可执行源的字典;否则,DB用户将能够在ClickHouse节点上执行任意二进制文件。

Executable Pool

可执行池允许从进程池加载数据。此源不适用于需要从源加载所有数据的字典布局。如果字典使用cache、complex_key_cache、ssd_cache、complex_key_ssd_cache、direct或complex_key_direct布局存储,则可执行池正常工作。

可执行池将使用指定的命令生成一个进程池,并保持它们运行,直到它们退出。程序应该在STDIN可用时从STDIN读取数据,并将结果输出到STDOUT。它可以等待STDIN上的下一个数据块。在处理完一个数据块后,ClickHouse不会关闭STDIN,但会在需要时通过管道传输另一个数据块。可执行脚本应该为这种数据处理方式做好准备——它应该轮询STDIN并尽早将数据刷新到STDOUT。

设置示例:

<source>
<executable_pool>
<command><command>while read key; do printf "$key\tData for key $key\n"; done</command</command>
<format>TabSeparated</format>
<pool_size>10</pool_size>
<max_command_execution_time>10<max_command_execution_time>
<implicit_key>false</implicit_key>
</executable_pool>
</source>

设置字段:

  • command-可执行文件的绝对路径,或者文件名(如果程序目录被写入path)。

  • format -文件格式。支持“格式”中描述的所有格式。

  • pool_size -存储池大小。如果指定0为pool_size,则没有池大小限制。缺省值为16。

  • Command_termination_timeout -可执行脚本应该包含主读写循环。字典销毁后,管道关闭,可执行文件将有command_termination_timeout秒关闭,然后ClickHouse将向子进程发送SIGTERM信号。以秒为单位指定。缺省值为10。可选参数。

  • max_command_execution_time -可执行脚本命令处理数据块的最大执行时间。以秒为单位指定。缺省值为10。可选参数。

  • Command_read_timeout -从命令stdout读取数据的超时时间,单位为毫秒。默认值10000。可选参数。

  • Command_write_timeout -写入命令stdin的超时时间,单位为毫秒。默认值10000。可选参数。

  • implicit_key—可执行源文件只能返回值,与请求键的对应关系由结果中的行顺序隐式确定。默认值为false。可选参数。

  • execute_direct—如果execute_direct = 1,则命令将在user_scripts_path指定的user_scripts文件夹中搜索。可以使用空格分隔符指定其他脚本参数。示例:script_name arg1 arg2。如果execute_direct = 0, command将作为bin/sh -c的参数传递。缺省值为1。可选参数。

  • Send_chunk_header -控制在向处理发送数据块之前是否发送行数。可选的。默认值为false。

该字典源只能通过XML配置进行配置。禁止通过DDL创建带有可执行源的字典,否则,DB用户将能够在ClickHouse节点上执行任意二进制文件。

Http(s)

使用HTTP服务器取决于字典在内存中的存储方式。如果使用缓存和complex_key_cache存储字典,则ClickHouse通过POST方法发送请求来请求必要的键。

设置示例:

<source>
<http>
<url>http://[::1]/os.tsv</url>
<format>TabSeparated</format>
<credentials>
<user>user</user>
<password>password</password>
</credentials>
<headers>
<header>
<name>API-KEY</name>
<value>key</value>
</header>
</headers>
</http>
</source>

或者

SOURCE(HTTP(
url 'http://[::1]/os.tsv'
format 'TabSeparated'
credentials(user 'user' password 'password')
headers(header(name 'API-KEY' value 'key'))
))

为了让ClickHouse访问HTTPS资源,必须在服务器配置中配置openSSL。

设置字段:

  • url -源url。

  • format -文件格式。支持“格式”中描述的所有格式。

  • credentials -基本HTTP身份验证。可选参数。

  • user -鉴权所需的用户名。

  • password -鉴权时输入的密码。

  • headers -用于HTTP请求的所有自定义HTTP头条目。可选参数。

  • header -单个HTTP头条目。

  • name -用于在请求上发送消息头的标识名称。

  • value -为特定标识名称设置的值。

当使用DDL命令(CREATE dictionary…)创建一个字典时,HTTP字典的远程主机将根据配置中的remote_url_allow_hosts部分的内容进行检查,以防止数据库用户访问任意HTTP服务器。

连接Postgresql实例

Ubuntu操作系统。

为PostgreSQL安装unixODBC和ODBC驱动程序:

$ sudo apt-get install -y unixodbc odbcinst odbc-postgresql

配置/etc/odbc.ini(或者~/.odbc.ini,如果你在运行ClickHouse的用户下登录):

    [DEFAULT]
Driver = myconnection

[myconnection]
Description = PostgreSQL connection to my_db
Driver = PostgreSQL Unicode
Database = my_db
Servername = 127.0.0.1
UserName = username
Password = password
Port = 5432
Protocol = 9.3
ReadOnly = No
RowVersioning = No
ShowSystemTables = No
ConnSettings =

ClickHouse中的字典配置:

<clickhouse>
<dictionary>
<name>table_name</name>
<source>
<odbc>
<!-- You can specify the following parameters in connection_string: -->
<!-- DSN=myconnection;UID=username;PWD=password;HOST=127.0.0.1;PORT=5432;DATABASE=my_db -->
<connection_string>DSN=myconnection</connection_string>
<table>postgresql_table</table>
</odbc>
</source>
<lifetime>
<min>300</min>
<max>360</max>
</lifetime>
<layout>
<hashed/>
</layout>
<structure>
<id>
<name>id</name>
</id>
<attribute>
<name>some_column</name>
<type>UInt64</type>
<null_value>0</null_value>
</attribute>
</structure>
</dictionary>
</clickhouse>

或者

CREATE DICTIONARY table_name (
id UInt64,
some_column UInt64 DEFAULT 0
)
PRIMARY KEY id
SOURCE(ODBC(connection_string 'DSN=myconnection' table 'postgresql_table'))
LAYOUT(HASHED())
LIFETIME(MIN 300 MAX 360)

你可能需要编辑odbc.ini,用driver =/usr/local/lib/psqlodbcw.so指定库的完整路径。

连接MS SQL Server的示例

Ubuntu操作系统。

安装连接MS SQL的ODBC驱动程序:

$ sudo apt-get install tdsodbc freetds-bin sqsh

配置驱动程序:

    $ cat etc/freetds/freetds.conf
...

[MSSQL]
host = 192.168.56.101
port = 1433
tds version = 7.0
client charset = UTF-8

# test TDS connection
$ sqsh -S MSSQL -D database -U user -P password


$ cat etc/odbcinst.ini

[FreeTDS]
Description = FreeTDS
Driver = usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so
Setup = usr/lib/x86_64-linux-gnu/odbc/libtdsS.so
FileUsage = 1
UsageCount = 5

$ cat etc/odbc.ini
# $ cat ~/.odbc.ini # if you signed in under a user that runs ClickHouse

[MSSQL]
Description = FreeTDS
Driver = FreeTDS
Servername = MSSQL
Database = test
UID = test
PWD = test
Port = 1433


# (optional) test ODBC connection (to use isql-tool install the [unixodbc](https://packages.debian.org/sid/unixodbc)-package)
$ isql -v MSSQL "user" "password"

在ClickHouse中配置字典:

<clickhouse>
<dictionary>
<name>test</name>
<source>
<odbc>
<table>dict</table>
<connection_string>DSN=MSSQL;UID=test;PWD=test</connection_string>
</odbc>
</source>

<lifetime>
<min>300</min>
<max>360</max>
</lifetime>

<layout>
<flat >
</layout>

<structure>
<id>
<name>k</name>
</id>
<attribute>
<name>s</name>
<type>String</type>
<null_value></null_value>
</attribute>
</structure>
</dictionary>
</clickhouse>

或者

CREATE DICTIONARY test (
k UInt64,
s String DEFAULT ''
)
PRIMARY KEY k
SOURCE(ODBC(table 'dict' connection_string 'DSN=MSSQL;UID=test;PWD=test'))
LAYOUT(FLAT())
LIFETIME(MIN 300 MAX 360)

ODBC

您可以使用此方法连接任何具有ODBC驱动程序的数据库。

设置示例:

<source>
<odbc>
<db>DatabaseName</db>
<table>ShemaName.TableName</table>
<connection_string>DSN=some_parameters</connection_string>
<invalidate_query>SQL_QUERY</invalidate_query>
<query>SELECT id, value_1, value_2 FROM ShemaName.TableName</query>
</odbc>
</source>

或者

SOURCE(ODBC(
db 'DatabaseName'
table 'SchemaName.TableName'
connection_string 'DSN=some_parameters'
invalidate_query 'SQL_QUERY'
query 'SELECT id, value_1, value_2 FROM db_name.table_name'
))

设置字段:

  • db -数据库名称。如果在<connection_string>参数中设置了数据库名称,则省略它。

  • table -表名和模式(如果存在)。

  • connection_string -连接字符串。

  • invalidate_query—查询字典状态。可选参数。在更新字典一节中阅读更多信息。

  • query—自定义查询。可选参数。

请注意 :表和查询字段不能一起使用。必须声明表字段或查询字段中的一个。

ClickHouse从ODBC-driver接收引用符号,并将查询中的所有设置引用给驱动程序,因此需要根据数据库中的表名大小写设置表名。

Mysql

设置示例:

<source>
<mysql>
<port>3306</port>
<user>clickhouse</user>
<password>qwerty</password>
<replica>
<host>example01-1</host>
<priority>1</priority>
</replica>
<replica>
<host>example01-2</host>
<priority>1</priority>
</replica>
<db>db_name</db>
<table>table_name</table>
<where>id=10</where>
<invalidate_query>SQL_QUERY</invalidate_query>
<fail_on_connection_loss>true</fail_on_connection_loss>
<query>SELECT id, value_1, value_2 FROM db_name.table_name</query>
</mysql>
</source>

或者

SOURCE(MYSQL(
port 3306
user 'clickhouse'
password 'qwerty'
replica(host 'example01-1' priority 1)
replica(host 'example01-2' priority 1)
db 'db_name'
table 'table_name'
where 'id=10'
invalidate_query 'SQL_QUERY'
fail_on_connection_loss 'true'
query 'SELECT id, value_1, value_2 FROM db_name.table_name'
))

设置字段:

  • port - MySQL服务器的端口。可以为所有副本指定它,也可以为每个副本单独指定它(在内)。

  • user - MySQL用户名。可以为所有副本指定它,也可以为每个副本单独指定它(在内)。

  • password - MySQL用户密码。可以为所有副本指定它,也可以为每个副本单独指定它(在内)。

  • replica -副本配置的部分。可以有多个部分。

- `replica/host` – The MySQL host.
- `replica/priority` – The replica priority. When attempting to connect, ClickHouse traverses the replicas in order of priority. The lower the number, the higher the priority.
  • db -数据库名称。

  • table -表的名称。

  • where -选择标准。条件的语法与MySQL中的WHERE子句相同,例如:id > 10 AND id < 20。可选参数。

  • invalidate_query—查询字典状态。可选参数。在更新字典一节中阅读更多信息。

  • fail_on_connection_loss -控制服务器在连接丢失时的行为的配置参数。如果为真,如果客户端和服务器之间的连接丢失,则立即抛出异常。如果为false, ClickHouse服务器将在抛出异常之前重试执行查询三次。请注意,重新尝试会增加响应时间。默认值:false。

  • query—自定义查询。可选参数。

MySQL可以通过套接字连接到本地主机。为此,设置host和socket。

设置示例:

<source>
<mysql>
<host>localhost</host>
<socket>/path/to/socket/file.sock</socket>
<user>clickhouse</user>
<password>qwerty</password>
<db>db_name</db>
<table>table_name</table>
<where>id=10</where>
<invalidate_query>SQL_QUERY</invalidate_query>
<fail_on_connection_loss>true</fail_on_connection_loss>
<query>SELECT id, value_1, value_2 FROM db_name.table_name</query>
</mysql>
</source>

或者

SOURCE(MYSQL(
host 'localhost'
socket '/path/to/socket/file.sock'
user 'clickhouse'
password 'qwerty'
db 'db_name'
table 'table_name'
where 'id=10'
invalidate_query 'SQL_QUERY'
fail_on_connection_loss 'true'
query 'SELECT id, value_1, value_2 FROM db_name.table_name'
))

ClickHouse

设置示例:

<source>
<clickhouse>
<host>example01-01-1</host>
<port>9000</port>
<user>default</user>
<password></password>
<db>default</db>
<table>ids</table>
<where>id=10</where>
<secure>1</secure>
<query>SELECT id, value_1, value_2 FROM default.ids</query>
</clickhouse>
</source>

或者

SOURCE(CLICKHOUSE(
host 'example01-01-1'
port 9000
user 'default'
password ''
db 'default'
table 'ids'
where 'id=10'
secure 1
query 'SELECT id, value_1, value_2 FROM default.ids'
));
  • host - ClickHouse主机。如果是本地主机,则在不进行任何网络活动的情况下处理查询。为了提高容错性,您可以创建一个分布式表,并在后续配置中输入它。

  • port - ClickHouse服务器上的端口。

  • user - ClickHouse用户名。

  • password - ClickHouse用户密码。

  • db -数据库名称。

  • table -表的名称。

  • where -选择标准。可以省略。

  • invalidate_query—查询字典状态。可选参数。在更新字典一节中阅读更多信息。

  • secure -使用ssl连接。

  • query—自定义查询。可选参数。

Mongodb

设置示例:

<source>
<mongodb>
<host>localhost</host>
<port>27017</port>
<user></user>
<password></password>
<db>test</db>
<collection>dictionary_source</collection>
</mongodb>
</source>

或者

SOURCE(MONGODB(
host 'localhost'
port 27017
user ''
password ''
db 'test'
collection 'dictionary_source'
))

设置字段:

  • host—MongoDB主机。

  • port—MongoDB服务器的端口。

  • user - MongoDB用户名。

  • password—MongoDB用户密码。

  • db -数据库名称。

  • collection -集合的名称。

Redis

设置示例:

<source>
<redis>
<host>localhost</host>
<port>6379</port>
<storage_type>simple</storage_type>
<db_index>0</db_index>
</redis>
</source>

或者

SOURCE(REDIS(
host 'localhost'
port 6379
storage_type 'simple'
db_index 0
))

设置字段:

  • host—Redis主机。

  • port - Redis服务器端口号。

  • storage_type - Redis内部存储结构,用于使用键工作。Simple用于简单源,hash_map用于散列的单键源,hash_map用于有两个键的散列源。不支持具有复杂键的远程源和缓存源。可省略,默认值为simple。

  • db_index - Redis逻辑数据库的特定数字索引。可以省略,默认值为0。

Cassandra

设置示例:

<source>
<cassandra>
<host>localhost</host>
<port>9042</port>
<user>username</user>
<password>qwerty123</password>
<keyspase>database_name</keyspase>
<column_family>table_name</column_family>
<allow_filering>1</allow_filering>
<partition_key_prefix>1</partition_key_prefix>
<consistency>One</consistency>
<where>"SomeColumn" = 42</where>
<max_threads>8</max_threads>
<query>SELECT id, value_1, value_2 FROM database_name.table_name</query>
</cassandra>
</source>

设置字段:

  • host - Cassandra主机或以逗号分隔的主机列表。

  • port—Cassandra服务器的端口。如果不指定,则使用默认端口9042。

  • user - Cassandra用户名。

  • password—Cassandra用户密码。

  • keyspace -密钥空间(数据库)的名称。

  • column_family -列族(表)的名称。

  • allow_filering -允许或不允许集群关键列上可能昂贵的条件。缺省值为1。

  • partition_key_prefix - Cassandra表主键分区键列数。需要组成键字典。字典定义中键列的顺序必须与Cassandra中相同。默认值为1(第一个键列为分区键,其他键列为集群键)。

  • consistency—一致性级别。可选值:One、Two、Three、All、EachQuorum、Quorum、LocalQuorum、LocalOne、Serial、LocalSerial。默认值为1。

  • where -可选的选择标准。

  • max_threads -用于从组合键字典中的多个分区加载数据的最大线程数。

  • query—自定义查询。可选参数。

PostgreSQL

设置示例:

<source>
<postgresql>
<port>5432</port>
<user>clickhouse</user>
<password>qwerty</password>
<db>db_name</db>
<table>table_name</table>
<where>id=10</where>
<invalidate_query>SQL_QUERY</invalidate_query>
<query>SELECT id, value_1, value_2 FROM db_name.table_name</query>
</postgresql>
</source>

或者

SOURCE(POSTGRESQL(
port 5432
host 'postgresql-hostname'
user 'postgres_user'
password 'postgres_password'
db 'db_name'
table 'table_name'
replica(host 'example01-1' port 5432 priority 1)
replica(host 'example01-2' port 5432 priority 2)
where 'id=10'
invalidate_query 'SQL_QUERY'
query 'SELECT id, value_1, value_2 FROM db_name.table_name'
))

设置字段:

  • host - PostgreSQL服务器上的主机。可以为所有副本指定它,也可以为每个副本单独指定它(在内)。

  • port - PostgreSQL服务器端口号。可以为所有副本指定它,也可以为每个副本单独指定它(在内)。

  • user - PostgreSQL用户名。可以为所有副本指定它,也可以为每个副本单独指定它(在内)。

  • password—PostgreSQL用户密码。可以为所有副本指定它,也可以为每个副本单独指定它(在内)。

  • replica -副本配置的部分。可以有多个部分:

  • replica/host—PostgreSQL主机。

  • replica/port—PostgreSQL端口。

  • replica/priority -副本优先级。当尝试连接时,ClickHouse会按照优先级顺序遍历副本。数字越低,优先级越高。

  • db -数据库名称。

  • table -表的名称。

  • where -选择标准。条件的语法与PostgreSQL中的WHERE子句相同。例如:id > 10 AND id < 20。可选参数。

  • invalidate_query—查询字典状态。可选参数。在更新字典一节中阅读更多信息。

  • query—自定义查询。可选参数。

Null

一个特殊的源,可以用来创建虚拟(空)字典。这样的字典可以用于测试或使用分离数据的设置,以及分布式表节点上的查询节点。

CREATE DICTIONARY null_dict (
id UInt64,
val UInt8,
default_val UInt8 DEFAULT 123,
nullable_val Nullable(UInt8)
)
PRIMARY KEY id
SOURCE(NULL())
LAYOUT(FLAT())
LIFETIME(0);


文章转载自大数据技能圈,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论