网易首页 > 网易号 > 正文 申请入驻

TiFlash 源码解读(八)TiFlash 表达式的实现与设计

0
分享至

TiFlash 是 TiDB 的分析引擎,是 TiDB HTAP 形态的关键组件,TiFlash 源码阅读系列文章将从源码层面介绍 TiFlash 的内部实现。在上一期的源码阅读中,大家应该对 TiFlash Proxy 模块的实现,即 TiFlash 副本是如何被添加以及获取数据有了一定的了解。

本文主要介绍的是 TiFlash 表达式的实现与设计,系统性地介绍了 TiFlash 表达式的基本概念,包括表达式体系,标量函数、聚合函数等,以期望读者能够对 TiFlash 的表达式计算有一个初步的了解。

本文作者:黄海升,TiFlash 研发工程师

表达式概要

表达式是承载 SQL 大部分逻辑的一个重要部分。SQL 中的表达式和编程语言中的表达式并没有差异。表达式可以大致分为函数、常量、列引用。如 select a + 1 from table 中的 a + 1 是一个表达式,其中 + 是函数, 是常量, 是列引用。

1

a

在 SQL 中,表达式会归属在不同的算子里执行,以 为例,大家可以从下图看到不同的表达式归属在哪些算子里。

select a + b from tests.t where c > 0

表达式在 SQL 中如何划分出来,并且归属在哪些算子里,是由一套语法规则决定的。下图是 MySQL 8.0 Parser 的语法规则简图,里面大号粗体的是算子标识符,后面跟着的小号字段是归属这个算子的表达式。

在了解了什么是表达式之后,我们来了解一下表达式在 TiFlash 里执行的情况。

在 TiDB HTAP 的体系里,TiFlash 的表达式是由 TiDB 下推给 TiFlash 执行的。首先我们来回顾下 TiDB 计算下推 TiFlash 的流程。

TiDB 接收 MySQL Client 发送的 sql,经由 Parser 和 Optimizer 解析成算子,在之后将算子下推到 TiFlash 里执行。与此同时,算子内部的表达式也会跟随一起下推到 TiFlash 里执行。

如下图所示,如果某个算子带有 TiFlash 不支持的函数,就会导致一连串的算子都无法下推到 TiFlash 里执行。算子内部的表达式都可以下推执行,是算子下推的必要条件。

在算子和表达式下推到 TiFlash 后,TiFlash 会用向量化执行引擎来执行这些算子和表达式。在谈到 TiFlash 的向量化执行引擎之前,我们先来讲一下执行引擎的一个经典模型 Volcano Model。

Volcano Model 源自 1994 年的论文 Volcano-An Extensible and Parallel Query Evaluation System。Volcano Model 将 SQL 分解为若干个独立的 Operator,Operator 串联成一棵 Operator Tree。

如上图所示,从最源头的 Table Scan Operator 开始,一行一行地读取数据,Operator 处理后,传给上游 Operator。最终 Operator Tree 一行一行地输出结果。

下面是对 Operator 接口的一个简单的伪代码描述。

struct Operator
{
Row next()
{
Row row = child.next();
....
return row;
}
}

Volcano Model 提供了一个非常简洁的执行模型,在工程上也非常容易实现。但是 Volcano Model 在现代编译器和硬件下运行得慢。在后续几年了诞生了对 Volcano Model 的两个改进方案,Codegen 和 Vectorized。TiFlash 就是使用的 Vectorized 即向量化执行。

向量化执行与 Volcano Model 基本一致,区别在于 Block by Block。

Block 是若干 Row 组合在一起的数据块,Block 内部按 Column 保存数据。

这样设计的好处有几个:

  • 虚函数调用的开销会被减小为 1 / (Block Size)。Volcano Model 中的 Operator 和表达式通常都是用多态来实现的,在其中就会引入虚函数调用。每次 Operator Tree 和内部的表达式被调用就是一系列的虚函数调用,在数据量大的情况下,虚函数开销甚至会成为一个不可忽视的点。Block by Block 可以让 Operator Tree 和内部的表达式的一次调用处理多行而不是一行数据,从而均摊了虚函数开销。
  • Cache Friendly。把会被连续处理的数据放在一个数据块里,提高在 Cache 上的空间局部性。

TiFlash 表达式体系 ExpressionActions

除了向量化执行外,TiFlash 在表达式执行上还有一套独立的执行体系 ExpressionActions,不同于 TiDB 源自 Volcano Model 的 ExpressionTree。

这两个表达式体系在逻辑语意上是一致的,仅仅在执行过程上有差别。

如上图所示,同样的表达式在 TiDB 和 TiFlash 里会分别在不同的表达式体系里执行。

接下来以 (a + b) * (a + b) 为例子来讲述一下两个表达式体系执行的差异。

首先从 TiDB ExpressionTree 讲起。 会被分解成一棵 Expression Tree,每一个 Expression 都是一个节点。从 Column Expression 和 Literal Expression 开始读取数据,遍历整棵 Expression Tree,最终得出表达式结果。

(a + b) * (a + b)

如下图所示,沿着图中的箭头方向,就可以从 Input 计算得出 (a + b) * (a + b) 的结果 Output。

在图中大家可以发现 (a + b) 这个子树出现了两次,也就意味着 (a + b) 本身执行了两次,那么可不可以复用 (a + b) 的计算结果,如下图所示连线?哪怕这样就不是一棵树了。

事实上是可以的,这也是 TiFlash ExpressionActions 的设计初衷: 中间计算结果复用。

在 TiFlash ExpressionActions 下,中间计算的临时结果会作为 Column 会被写到 Block 里,同时我们会通过 Column Name 获取 Block 中对应的 Column。

如上图所示,沿右图箭头方向遍历 Expression Action,即可得出 (a + b) * (a + b) 的计算结果。可以看到,同样是 (a + b) * (a + b),TiFlash ExpressionActions 里的 只计算了一次。下面是 ExpressionActions 的执行分解图,从左到右。大家可以对照一下分解图,大概了解一下 ExpressionActions 的执行过程。

(a + b)

左右滑动查看

接下来我们深入一下代码,从代码层面来了解一下 究竟做了些什么。

ExpressionActions

class ExpressionActions
{
public:
void execute(Block & block) const
{
for (const auto & action : actions)
action.execute(action);
}
void add(const ExpressionAction & action);
void finalize(const Names & output_columns);
private:
std::vector

actions;
}

以上是 ExpressionActions 接口的简化代码。有三个主要方法:

  • execute
  • 对 的包装。用于执行表达式。
  • ExpressionAction::execute
  • add
  • 用于外部组装出一个 ExpressionActions。
  • 会维护一个 Sample Block,在 Add Action 的过程中 Sample Block 会不断更新,模拟实际 Block 的变化情况。
  • ExpressionActions
  • 在 Add 的过程中,重复的 Action 会被跳过。重复 Action 的判断条件是,该 Action 的执行结果是否已经出现在 Sample Block 里了。

  • finalize
  • 分析 Block 内的 Column 引用情况,在合适的位置插入 Remove Action 来移除无用的 Column。
  • 在 Column 引用数归 0 的时候,就会插入对应 Column 的 Remove Action。
  • 下图是 (a + b) * (a + b) 的引用数分析和 Remove Action 插入情况。

是 ExpressionActions 内部的执行单元。

ExpressionAction

struct ExpressionAction
{
Type type;

void execute(Block & block) const
{
switch (type)
{
case: APPLY_FUNCTION:

case: REMOVE_COLUMN:

case: ADD_COLUMN:

}
}
}

有不同的 Type 用于对 Block 进行不同的处理

ExpressionAction

  • REMOVE_COLUMN
  • 即前文所讲的 Remove Action
  • ADD_COLUMN
  • 用于执行 Literal Expression,在 Block 插入一个 Const Column。
  • 会在 Block 中插入一个 Column。对于 Literal,插入的会是 ColumnConst,即常量 Column。
  • ADD_COLUMN
  • block.insert({added_column->cloneResized(block.rows()), result_type, result_name});

  • APPLY_FUNCTION
  • 用于执行 Function Expression,由 ExpressionAction 持有的 执行
  • IFunction
  • 会读取 Block 中的 Argument Columns 传给 做计算。计算出结果 Column 后,插入到 Block 中。
  • APPLY_FUNCTION
  • IFunction

Column Expression 没有对应的 Action Type,直接执行 Block::getPositionByName 获取 Column 在 Block 里的下标。但是从 TiDB 获得的 Column Expression 计算得出的并不是 Column 在 Block 中的 Column Name,而是 Column 在 TiDB Schema 中的下标。所以 TiFlash 会维护 TiDB Schema (std::vector) 来桥接 TiDB Column Index 和 TiFlash Column Name,如下图所示。

标量函数在 TiFlash 中的编译与执行

当 的 Type 为 时,内部会持有。对的调用都会转发给执行。是 TiFlash 向量化标量函数的基类。

ExpressionAction

APPLY_FUNCTION

ExpressionAction

IFunction

ExpressionAction::execute

IFunction

IFunction

首先我们从函数在 TiFlash 中的编译讲起。是 TiDB 与 TiFlash 之间的序列化协议,下图的等同是 TiDB 里的 Expression。

tipb

tipb::Expr

对于传入的一个 ,首先分门别类,按照 Column,Literal,Function 分别处理。

tipb::Expr

如果是 Function,首先处理 Function 的所有参数。参数本身也是 ,所以也会按照对应的处理流程处理。在处理完 Function 的所有参数后,就可以去构建本身,然后塞入,返回处理结果。

tipb::Expr

tipb::Expr

IFunction

ExpressionAction

TiDB 对函数的标识是 tipb::ScalarFuncSig,而 TiFlash 使用 Function Name 作为函数的标识。在 TiFlash 里,我们会用映射表的形式将 tipb::ScalarFuncSig 映射成 Function Name。再根据 Function Name 找到对应的 TiFlash Function Builder。

对于窗口函数、聚合函数、distinct 聚合函数、标量函数都有各自的映射表。

const std::unordered_map;
const std::unordered_map;
const std::unordered_map;
const std::unordered_map;

window_func_map({
{tipb::ExprType::Rank, "rank"},
...
})

agg_func_map({
{tipb::ExprType::Count, "count"},
...
})

distinct_agg_func_map({
{tipb::ExprType::Count, "countDistinct"},
...
})

scalar_func_map({
{tipb::ScalarFuncSig::CastIntAsInt, "tidb_cast"},
...
})

拿到 Function Name 之后,我们就可以去找到 Function 对应的 Function Builder 去 Build 出 实现。TiFlash 有两类 Function Builder: Default Function Builder 和 Special Function Builder。前者是用于处理大多数 Function,后者处理某些特殊情况。

IFunction

String DAGExpressionAnalyzerHelper::buildFunction(
DAGExpressionAnalyzer * analyzer,
const tipb::Expr & expr,
const ExpressionActionsPtr & actions)
{
const String & func_name = getFunctionName(expr);
if (function_builder_map.count(func_name) != 0)
{
return function_builder_map[func_name](analyzer, expr, actions);
}
else
{
return buildDefaultFunction(analyzer, expr, actions);
}
}

Default Function Builder 的处理本身很简单,就是先处理所有的函数入参,然后调用 applyFunction 生成对应的 IFunction 实现。

String DAGExpressionAnalyzerHelper::buildDefaultFunction(
DAGExpressionAnalyzer * analyzer,
const tipb::Expr & expr,
const ExpressionActionsPtr & actions)
{
const String & func_name = getFunctionName(expr);
Names argument_names;
for (const auto & child : expr.children())
{
String name = analyzer->getActions(child, actions);
argument_names.push_back(name);
}
return analyzer->applyFunction(func_name, argument_names, actions, getCollatorFromExpr(expr));
}

对某些函数,有些特殊处理,这时就会使用到 Special Function Builder。

特殊处理的 Function Builder 会放在表里,遇到对应的函数,会转发过来。

下面是一些函数的特殊处理映射。

FunctionBuilderMap DAGExpressionAnalyzerHelper::function_builder_map(
{...
{"ifNull", DAGExpressionAnalyzerHelper::buildIfNullFunction},
{"multiIf", DAGExpressionAnalyzerHelper::buildMultiIfFunction},
...
{"bitAnd", DAGExpressionAnalyzerHelper::buildBitwiseFunction},
{"bitOr", DAGExpressionAnalyzerHelper::buildBitwiseFunction},
{"bitXor", DAGExpressionAnalyzerHelper::buildBitwiseFunction},
{"bitNot", DAGExpressionAnalyzerHelper::buildBitwiseFunction},
{"bitShiftLeft", DAGExpressionAnalyzerHelper::buildBitwiseFunction},
{"bitShiftRight", DAGExpressionAnalyzerHelper::buildBitwiseFunction},
...
});

一个特殊的处理的情况是复用函数实现。某些函数可以由另一个函数来代理执行,比如

。如果让 来代理执行 ,那么就可以省掉本身的开发实现工作。下面是的代理实现代码。为生成第二个参数后,将的两个参数传入,就可以代理执行。

leftUTF8(str, len) = substrUTF8(str, 1, len)

substrUTF8

leftUTF8

leftUTF8

leftUTF8(str, len) = substrUTF8(str, 1, len)

substrUTF8

1

leftUTF8

substrUTF8

substrUTF8

leftUTF8

String DAGExpressionAnalyzerHelper::buildLeftUTF8Function(
DAGExpressionAnalyzer * analyzer,
const tipb::Expr & expr,
const ExpressionActionsPtr & actions)
{
const String & func_name = "substringUTF8";
Names argument_names;
// the first parameter: str
String str = analyzer->getActions(expr.children()[0], actions, false);
argument_names.push_back(str);
// the second parameter: const(1)
auto const_one = constructInt64LiteralTiExpr(1);
auto col_const_one = analyzer->getActions(const_one, actions, false);
argument_names.push_back(col_const_one);
// the third parameter: len
String name = analyzer->getActions(expr.children()[1], actions, false);
argument_names.push_back(name);
return analyzer->applyFunction(func_name, argument_names, actions, getCollatorFromExpr(expr));
}

接下来我们来看一下 本身的接口。

IFunction

class IFunction
{
public:
virtual String getName() const = 0;
virtual DataTypePtr getReturnTypeImpl(const DataTypes & /*arguments*/) const;
virtual void executeImpl(Block & block, const ColumnNumbers & arguments, size_t result) const;
};

有三个主要方法

IFunction

  • : 返回 Function 的 Name,Name 是作为 TiFlash 向量化函数的唯一标识来使用。
  • getName
  • : 负责做向量化函数的类型推导,因为输入参数数据类型的变化可能会导致输出数据类型变化。
  • getReturnTypeImpl
  • : 负责向量化函数的执行逻辑,这也是一个向量化函数的主体部分。一个 TiFlash 向量化函数够不够"向量化",够不够快也就看这里了。
  • executeImpl

接下来以 jsonLength(string) 为例子,讲一下向量化函数的执行

  1. 从 Block 中获取 Json Column
  2. 创建同等大小的 Json Length Column
  3. Foreach Json Column,获取每一个行的 Json
  4. 调用 GetJsonLength(Json)获取 Json Length,将结果插入 Json Length Column 中的对应位置。
  5. 将 Json Length Column 插入到 Block 中,完成单次计算

void executeImpl(Block & block, const ColumnNumbers & arguments, size_t result) const override
{
// 1. 获取 json column,json column 本身是 String 类型,所以 json column 用的是 ColumnString 这个 column 实现
const ColumnPtr column = block.getByPosition(arguments[0]).column;
if (const auto * col = checkAndGetColumn

(column.get()))
{
// 2.创建 json len column, json len 本身是 UInt64 类型,用的是 ColumnUInt64 这个 column 实现
auto col_res = ColumnUInt64::create();
typename ColumnUInt64::Container & vec_col_res = col_res->getData();
{
// 3. 遍历 json column,ColumnString 提供了一些裸操作内部 string 的方法,可以提高效率
const auto & data = col->getChars();
const auto & offsets = col->getOffsets();
const size_t size = offsets.size();
vec_col_res.resize(size);
ColumnString::Offset prev_offset = 0;
for (size_t i = 0; i < size; ++i)
{
// 4. 调用 GetJsonLength,计算出 json 的 length,插入到 json len column 中。
std::string_view sv(reinterpret_cast(&data[prev_offset]), offsets[i] - prev_offset - 1);
vec_col_res[i] = GetJsonLength(sv);
prev_offset = offsets[i];
}
}
// 5. 将 json len column 插入到 Block 中,完成单次计算。
block.getByPosition(result).column = std::move(col_res);
}
else
throw Exception(fmt::format("Illegal column {} of argument of function {}", column->getName(), getName()), ErrorCodes::ILLEGAL_COLUMN);
}

前段时间 TiFlash 有个社区活动,号召大家来参与 TiFlash 函数下推的工作。

以上关于标量函数的内容在社区活动的两篇文章 TiFlash 函数下推必知必会 和 手把手教你实现 TiFlash 向量化函数 都有包含,大家可以通过这两篇文章了解更多关于 TiFlash 标量函数的内容。也欢迎小伙伴们也参与到 TiFlash 函数下推的工作中来。

聚合函数在 TiFlash 中的编译与执行

聚合函数不同于标量函数的点在于输入 m 行数据输出 n 行数据,且有 m >= n。

所以聚合函数不会使用 ExpressionActions 和 IFunction,而是有独立的执行体系。

聚合函数本身由 Aggregate 算子来执行,Aggregate 算子负责管理聚合函数的执行步骤,执行并发度等等。

算子的执行有两部分

Aggregate

  • 前者 ExpressionActions 用于执行标量函数的部分。
  • 后者 Aggregator 用于执行聚合函数的部分。

如下图所示,对于,执行,执行。

select max(a+b)

ExpressionActions

a+b

Aggregator

max

下面详细来说一说 是如何执行聚合函数的。首先会多线程从 Input 读取 Block,调用写入 Thread Local。内部会保存当前线程的部分聚合计算结果。在阶段完成后,会调用将多个合并成一个,输出最终聚合的结果给 Output。

Aggregator

Aggregator

executeOnBlock

AggregatedDataVariants

AggregatedDataVariants

executeOnBlock

Aggregator

mergeAndConvertBlocks

AggregatedDataVariants

会对每一个 Key 都会创建一个 Aggregate Data。

Aggregator::executeOnBlock

输入的 Row 会根据 Key 找到对应的 Aggregate Data, 调用 IAggregateFunction::add,更新 Agg Function 保存在 Aggregate Data 里的聚合结果。

如下图所示,Row1 和 Row4 经由 Aggregate Function 计算后,更新 Key1 保存的 Aggregate Data;Row2 经由 Aggregate Function 计算后,更新 Key2 保存的 Aggregate Data;Row3 和 Row5 经由 Aggregate Function 计算后,更新 Key3 保存的 Aggregate Data。

在 executeOnBlock 完成后,每个线程都会有一个独立的 Aggregate Data。

在 mergeAndConvertBlocks 阶段会把其他 Aggregate Data 都合并到 Aggregate Data0 上面。

用于执行把所有线程计算的部分聚合结果聚合成一个最终聚合结果。

IAggregateFunction::merge

如下图所示,Aggregate Data 在合并的时候,同一个 Key 的数据会合并到一起,与其他 Key 互不干扰。

是聚合函数的实现基类。接下来我们来看一下 本身的接口。

IAggregateFunction

IAggregateFunction

class IAggregateFunction
{
public:
String getName() const = 0;
DataTypePtr getReturnType() const = 0;
void add(AggregateDataPtr __restrict place, const IColumn ** arg_columns, size_t row_num, Arena * arena) const = 0;
void merge(AggregateDataPtr __restrict place, ConstAggregateDataPtr rhs, Arena * arena) const = 0;
void insertResultInto(ConstAggregateDataPtr __restrict place, IColumn & to, Arena * arena) const = 0;
};

  • 用于计算聚合结果并更新到 上
  • add
  • place
  • 用于将 里的聚合结果合并到
  • merge
  • rhs
  • place
  • 用于将 里保存的聚合结果输出成 Block
  • insertResultInto
  • place

接下来我们以 Sum 这个聚合函数为例子来看一下聚合函数的执行过程。

template
using AggregateFunctionSumSimple =
AggregateFunctionSum

typename NearestFieldType

::Type, AggregateFunctionSumData

::Type>>;
template
class AggregateFunctionSum final : public IAggregateFunctionDataHelper

>
{
public:
void add(AggregateDataPtr __restrict place, const IColumn ** columns, size_t row_num, Arena *) const override
{
this->data(place).add(column.getData()[row_num]);
}
void merge(AggregateDataPtr __restrict place, ConstAggregateDataPtr rhs, Arena *) const override
{
this->data(place).merge(this->data(rhs));
}
};
template
struct AggregateFunctionSumData
{
T sum{};
template
void add(U value) { AggregateFunctionSumAddImpl

::add(sum, value); }
void merge(const AggregateFunctionSumData & rhs) { AggregateFunctionSumAddImpl

::add(sum, rhs.sum); }
}
template
struct AggregateFunctionSumAddImpl
{
static void NO_SANITIZE_UNDEFINED ALWAYS_INLINE add(T & lhs, const T & rhs) { lhs += rhs; }
};

  • 实际的 Sum 实现带有很多优化,这里选择最简化的实现
  • 对于 Sum,AggregateData 的实现是 ,内部维护一个保存聚合结果
  • AggregateFunctionSumData
  • T Sum
  • 对于 Sum,和都是执行 += 操作。
  • add
  • merge

下图是对 Sum 执行过程的一个简化描述图。

阶段, 会从 读取数据,执行 ,更新 里的聚合结果。在 完成后,会执行 和 ,将聚合结果合并成一个。最终输出 作为最终结果。

add

Aggregator

Input

sum += input

Aggregate Data

add

sum0 += sum1

sum0 += sum2

sum0

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
37岁李敏镐发福变双下巴,脸部变得圆润,差点没认出来

37岁李敏镐发福变双下巴,脸部变得圆润,差点没认出来

素素娱乐
2024-07-02 09:28:48
坚持三年,每年拍一百部作品的岛国片女星,补贴家用却惨遭家暴

坚持三年,每年拍一百部作品的岛国片女星,补贴家用却惨遭家暴

不二砖家
2024-07-01 13:34:55
太心酸了!就业形势有多严峻?网友:已经颠成我不敢想象的样子了

太心酸了!就业形势有多严峻?网友:已经颠成我不敢想象的样子了

时尚的弄潮
2024-06-28 00:45:46
“爱达·魔都号”运营拖了中国造船后腿

“爱达·魔都号”运营拖了中国造船后腿

闻旅派
2024-07-02 11:40:59
日本仍然大崩盘!人民币终于觉醒,央行出手打爆空头

日本仍然大崩盘!人民币终于觉醒,央行出手打爆空头

农村阿祖
2024-07-02 18:15:13
一天内解决俄乌冲突?俄常驻联合国代表:不可能

一天内解决俄乌冲突?俄常驻联合国代表:不可能

参考消息
2024-07-02 12:58:12
本就是高级货!这位当今足坛身价最贵门将,创造欧洲杯纪录不意外

本就是高级货!这位当今足坛身价最贵门将,创造欧洲杯纪录不意外

体坛老球迷
2024-07-02 09:16:22
南方医科大学不会撤销对俞莉的处罚,原因曝光,网友表示沉默

南方医科大学不会撤销对俞莉的处罚,原因曝光,网友表示沉默

平老师666
2024-07-01 22:05:50
韩国明星张紫妍被迫陪睡上100次,曾惨遭虐待,同时接待4位客人

韩国明星张紫妍被迫陪睡上100次,曾惨遭虐待,同时接待4位客人

V盟文史
2023-07-04 18:02:03
轮到绿军交超级奢侈税了!快船勇士4年交了9.5亿,明年都不用交了

轮到绿军交超级奢侈税了!快船勇士4年交了9.5亿,明年都不用交了

无术不学
2024-07-02 10:01:31
火箭队连签3人,15人大名单出炉!奇兵2年960万回归 休城仍留后手

火箭队连签3人,15人大名单出炉!奇兵2年960万回归 休城仍留后手

锅子篮球
2024-07-02 12:54:32
笑麻了,吴艳妮全妆跑完赛后假睫毛掉了,评论区炸锅

笑麻了,吴艳妮全妆跑完赛后假睫毛掉了,评论区炸锅

皖声微言
2024-07-01 14:32:11
改明抢了,菲海警滞留仙宾礁70天,水泥油料上舰试图建立永久基地

改明抢了,菲海警滞留仙宾礁70天,水泥油料上舰试图建立永久基地

文雅笔墨
2024-07-02 19:04:24
“跌落神坛”的5大保健品,实际根本没有什么用,你吃过哪几种?

“跌落神坛”的5大保健品,实际根本没有什么用,你吃过哪几种?

梦录的西方史话V
2024-06-19 18:05:56
意大利姐俩在家中地下室里发现1.58亿里拉,想兑换成欧元困难重重

意大利姐俩在家中地下室里发现1.58亿里拉,想兑换成欧元困难重重

萍聚德国
2024-07-02 01:29:00
知名五星级酒店暂停营业!曾多次接待美国总统、日本首相等政要,大股东背后是李嘉诚!工作人员:不会倒闭

知名五星级酒店暂停营业!曾多次接待美国总统、日本首相等政要,大股东背后是李嘉诚!工作人员:不会倒闭

每日经济新闻
2024-07-01 20:46:07
NBA的现实——卡鲁索的交易,成德罗赞决定离队的最后一根稻草

NBA的现实——卡鲁索的交易,成德罗赞决定离队的最后一根稻草

篮球扫地僧
2024-07-03 04:59:37
温网首日爆大冷!击败郑钦文的孙璐璐是谁?她是华裔、母亲是华人

温网首日爆大冷!击败郑钦文的孙璐璐是谁?她是华裔、母亲是华人

体育妞世界
2024-07-02 07:12:55
董宇辉用8个字点化东方甄选,山河破碎真不能用来形容贵州山水

董宇辉用8个字点化东方甄选,山河破碎真不能用来形容贵州山水

蜜桔娱乐
2024-07-02 23:30:02
景甜生图真是“肥而不腻”,当镜头拉近放大后,是钢铁直男的菜

景甜生图真是“肥而不腻”,当镜头拉近放大后,是钢铁直男的菜

木子爱娱乐大号
2024-07-02 18:16:45
2024-07-03 07:20:49
PingCAP
PingCAP
分布式数据库TiDB背后团队
484文章数 622关注度
往期回顾 全部

科技要闻

旧车比新车贵,比亚迪断了二手车贩子活路

头条要闻

欧洲杯-中卫双响土耳其2-1奥地利 进8强将战荷兰

头条要闻

欧洲杯-中卫双响土耳其2-1奥地利 进8强将战荷兰

体育要闻

世界第二打第三,成了一场英格兰模仿秀

娱乐要闻

未火先塌?流量的路子不好走啊

财经要闻

张军:房地产是经济收缩的受害者而非原因

汽车要闻

18.96万 奕派eπ007 540纯电四驱Pro上市

态度原创

教育
艺术
本地
公开课
军事航空

教育要闻

【教育聚焦】初中地理试卷出现多个涉华为题目,为什么说接触人性最暗面的职业是老师?

艺术要闻

穿越时空的艺术:《马可·波罗》AI沉浸影片探索人类文明

本地新闻

冷知识:东北雪糕才是最早的网红雪糕

公开课

连中三元是哪三元?

军事要闻

泽连斯基称不排除通过中间方谈判的可能性 克宫表态

无障碍浏览 进入关怀版