PostgreSQL技术内幕(八)源码分析 ——投影算子和表达式计算

news2024/11/24 11:49:26

在上期Postgres技术内幕系列直播中,我们为大家介绍了Postgres投影算子和表达式计算实现原理和底层细节。本文根据直播内容整理,作者现任HashData内核研发工程师。

投影 (projection)

关系代数中的一种,  用于从关系R中选出属性包含在A中的列。
在PG中也用于引入额外的列(比如计算表达式)

快问快答
create table t (int a, int b);
以下哪些 sql 会触发投影逻辑?
select * from t;
select a, b from t;
select b, a from t;
select a as b, b as a from t;
select a from t;
select a, b, b from t;
select a + 1, b from t;
select ctid, * from t;

欢迎留言,写下你的答案哦~

PG对投影的实现

整体实现可以分为三个主要部分:

1、判断是否需要投影

当且仅当被扫描的关系的描述符和计划节点的targetlist不匹配的时候,才需要进行投影,tlist_matches_tupdesc 函数用于实现相关的判断逻辑。

static bool
tlist_matches_tupdesc(PlanState *ps, List *tlist, Index varno, TupleDesc tupdesc)
{
 int   numattrs = tupdesc->natts;
 int   attrno;
 ListCell   *tlist_item = list_head(tlist);

 /* Check the tlist attributes */
 for (attrno = 1; attrno <= numattrs; attrno++)
 {
  Form_pg_attribute att_tup = TupleDescAttr(tupdesc, attrno - 1);
  Var     *var;

  if (tlist_item == NULL)
   return false;  /* tlist too short */
  var = (Var *) ((TargetEntry *) lfirst(tlist_item))->expr;
  if (!var || !IsA(var, Var))
   return false;  /* tlist item not a Var */
  /* if these Asserts fail, planner messed up */
  Assert(var->varno == varno);
  Assert(var->varlevelsup == 0);
  if (var->varattno != attrno)
   return false;  /* out of order */
  if (att_tup->attisdropped)
   return false;  /* table contains dropped columns */
  if (att_tup->atthasmissing)
   return false;  /* table contains cols with missing values */

  /*
   * Note: usually the Var's type should match the tupdesc exactly, but
   * in situations involving unions of columns that have different
   * typmods, the Var may have come from above the union and hence have
   * typmod -1.  This is a legitimate situation since the Var still
   * describes the column, just not as exactly as the tupdesc does. We
   * could change the planner to prevent it, but it'd then insert
   * projection steps just to convert from specific typmod to typmod -1,
   * which is pretty silly.
   */
  if (var->vartype != att_tup->atttypid ||
   (var->vartypmod != att_tup->atttypmod &&
    var->vartypmod != -1))
   return false;  /* type mismatch */

  tlist_item = lnext(tlist, tlist_item);
 }

 if (tlist_item)
  return false;   /* tlist too long */

 return true;
}

其核心逻辑就是遍历targetlist并判断与扫描表的描述符是否匹配,比较简单,这里不再展开讨论。

2、构建投影所需要的信息

ProjectionInfo *
ExecBuildProjectionInfo(List *targetList,
      ExprContext *econtext,
      TupleTableSlot *slot,
      PlanState *parent,
      TupleDesc inputDesc)
{
 /* Insert EEOP_*_FETCHSOME steps as needed */
 ExecInitExprSlots(state, (Node *) targetList);
 /* Now compile each tlist column */
 foreach(lc, targetList)
 {
        /* 考虑投影列为var的情况 */
  if (tle->expr != NULL &&
   IsA(tle->expr, Var) &&
   ((Var *) tle->expr)->varattno > 0)
  {
   if (inputDesc == NULL)
    isSafeVar = true; /* can't check, just assume OK */
   else if (attnum <= inputDesc->natts)
   {
    Form_pg_attribute attr = TupleDescAttr(inputDesc, attnum - 1);

    /*
     * If user attribute is dropped or has a type mismatch, don't
     * use ASSIGN_*_VAR.  Instead let the normal expression
     * machinery handle it (which'll possibly error out).
     */
    if (!attr->attisdropped && variable->vartype == attr->atttypid)
    {
     isSafeVar = true;
    }
   }
   
           /* 对于简单的情况只需要 EEOP_ASSIGN_*_VAR 即可 */
            if (isSafeVar)
            {
                /* Fast-path: just generate an EEOP_ASSIGN_*_VAR step */
                switch (variable->varno)
                {
                    case INNER_VAR:
                        /* get the tuple from the inner node */
                        scratch.opcode = EEOP_ASSIGN_INNER_VAR;
                        break;

                    case OUTER_VAR:
                        /* get the tuple from the outer node */
                        scratch.opcode = EEOP_ASSIGN_OUTER_VAR;
                        break;

                        /* INDEX_VAR is handled by default case */

                    default:
                        /* get the tuple from the relation being scanned */
                        scratch.opcode = EEOP_ASSIGN_SCAN_VAR;
                        break;
                }

               /* 
                 * 这里是核心逻辑 构建了投影所需要的执行步骤 在执行过程中按照步骤依次执行即可
                 * 这么做的本质是为了降低函数递归调用的运行成本
                 */
                ExprEvalPushStep(state, &scratch);
            }
           else
            {
               /* 具体来说,包含表达式计算,或者系统变量等情况时,要按照常规方式处理表达式 */
                /*
                 * Otherwise, compile the column expression normally.
                 *
                 * We can't tell the expression to evaluate directly into the
                 * result slot, as the result slot (and the exprstate for that
                 * matter) can change between executions.  We instead evaluate
                 * into the ExprState's resvalue/resnull and then move.
                 */
                ExecInitExprRec(tle->expr, state,
                                &state->resvalue, &state->resnull);
               
               // 投影求值计算的时候会用到 attnum 和 resultnum
                scratch.d.assign_var.attnum = attnum - 1;
                scratch.d.assign_var.resultnum = tle->resno - 1;
                ExprEvalPushStep(state, &scratch); 
            }
        }
       
    }
}

本节我们主要注解了上述代码中 fast-path 相关逻辑,其余逻辑在后续展开讲解。

这段代码的核心逻辑在于通过调用 ExprEvalPushStep 构建了用数组表示的投影的执行过程,并通过opcode标识每个步骤的类型,这样在执行阶段即可根据opcode调用不同的过程,请参考下文的执行过程一起理解。
相比于传统的表达式求值逻辑,这样写的好处在于减少函数递归调用。

3、执行投影算子

执行器投影算子入口函数,可以看到关键函数是ExecEvalExprSwitchContext,在PG中和表达式求值有关的逻辑都通过这个函数实现。

#ifndef FRONTEND
static inline TupleTableSlot *
ExecProject(ProjectionInfo *projInfo)
{
 ExprState  *state = &projInfo->pi_state;
 TupleTableSlot *slot = state->resultslot; // 投影之后的结果;目前还是未计算的状态

   /* Run the expression, discarding scalar result from the last column. */
 (void) ExecEvalExprSwitchContext(state, econtext, &isnull);

 return slot;
}

首先要介绍一下PG表达式求值的框架,投影求值也是利用表达式求值实现的,即调用ExecEvalExprSwitchContext ,其底层调用了 ExecInterpExpr

整个执行过程建立在一套由宏定义实现的分发器机制之上,实现了对之前构建的表达式求值步骤顺次执行的逻辑。在过程执行中,我们会用ExprState来存储中间计算结果和其他执行状态。具体代码如下:

// opcode对应步骤的实现逻辑的标识 用于goto
#define EEO_CASE(name)  CASE_##name:
// 分发至步骤的执行逻辑
#define EEO_DISPATCH()  goto *((void *) op->opcode)
// 
#define EEO_OPCODE(opcode) ((intptr_t) dispatch_table[opcode])
// 当前步骤执行完毕时移动至下一个需要执行的步骤
#define EEO_NEXT() \
 do { \
  op++; \
  EEO_DISPATCH(); \
 } while (0)

ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
{
 op = state->steps; // 存储所有的步骤,我们通过宏不断移动当前执行的步骤
 resultslot = state->resultslot; // 用于存放最后返回的结果值
 innerslot = econtext->ecxt_innertuple;
 outerslot = econtext->ecxt_outertuple;
 scanslot = econtext->ecxt_scantuple;
  
 EEO_DISPATCH();

   EEO_CASE()
  EEO_CASE(EEOP_DONE)
  {
   goto out;
  }
  
  EEO_CASE(EEOP_SCAN_FETCHSOME)
  {
   CheckOpSlotCompatibility(op, scanslot);

   slot_getsomeattrs(scanslot, op->d.fetch.last_var);

   EEO_NEXT();
  }

    EEO_CASE(EEOP_ASSIGN_SCAN_VAR)
  {
   int   resultnum = op->d.assign_var.resultnum;
   int   attnum = op->d.assign_var.attnum;

   /*
    * We do not need CheckVarSlotCompatibility here; that was taken
    * care of at compilation time.  But see EEOP_INNER_VAR comments.
    */
   resultslot->tts_values[resultnum] = scanslot->tts_values[attnum];
   resultslot->tts_isnull[resultnum] = scanslot->tts_isnull[attnum];

   EEO_NEXT();
  }
out:
   *isnull = state->resnull
   return state->resvalue
}

这样,我们就实现了投影列计算的逻辑,最终的tuple存储在 state->resultslot里,供上层算子使用。

表达式计算

下面我们介绍表达式计算的实现。表达式计算的过程和前面投影列求值的过程复用了同样的逻辑,即利用同样的分发机制进行求值。

明显不同的地方包括:

1)计算逻辑通常更为复杂,需要多个步骤完成,本质上是以迭代的方式对表达式树进行求值;

2)表达式可以预计算,其中常量的部分在优化器阶段就可以求值,避免迭代过程中重复求值。

下面我们通过一个例子来研究一下表达式树对应的求值步骤时如何构建的。explain select (SQRT(POWER(i,i))) from generate_series(1,5) i;首先,我们看一下PG在内存中是如何表示表达式树的,以上面的查询为例,下述FuncExpr可以非常清晰的找出到(SQRT(POWER(i,i)))的对应关系。

FuncExpr [funcid=1344 funcresulttype=701 funcretset=false funcvariadic=false funcformat=COERCE_EXPLICIT_CALL is_tablefunc=false]
        FuncExpr [funcid=1368 funcresulttype=701 funcretset=false funcvariadic=false funcformat=COERCE_EXPLICIT_CALL is_tablefunc=false]
                FuncExpr [funcid=316 funcresulttype=701 funcretset=false funcvariadic=false funcformat=COERCE_IMPLICIT_CAST is_tablefunc=false]
                        Var [varno=1 varattno=1 vartype=23 varnosyn=1 varattnosyn=1]
                FuncExpr [funcid=316 funcresulttype=701 funcretset=false funcvariadic=false funcformat=COERCE_IMPLICIT_CAST is_tablefunc=false]
                        Var [varno=1 varattno=1 vartype=23 varnosyn=1 varattnosyn=1]

一般来说,求解这样的表达式树通常采用递归计算的方式,先算底层表达式,再算高层表达式,整体类似于后序遍历树的过程。

PG为了提高执行效率,选择在执行阶段用投影求值的方式迭代执行,因此在执行初始化阶段需要采用类似于后序遍历树的方式,将每个子表达式加入求值步骤数组中。

构建求值步骤的调用栈如下:

-> ExecBuildProjectionInfo
 -> ExecInitExprSlots // EEOP_*_FETCHSOME
     ->ExprEvalPushStep
    for targetList:
      -> ExecInitExprRec(tle->expr, )
          scratch.resvalue = resv // 当前步骤的结果;上层通过指针传入我们应该存放的地址
          case T_FuncExpr:
            -> ExecInitFunc // 当前函数 [funcid=1344]
                fcinfo = scratch->d.func.finfo
                for args: // 这层有一个参数
                    -> ExecInitExprRec(arg, state, &fcinfo->args[argno].value) // resv - where to stor the result of the node into
                        case T_FuncExpr:
                          -> ExecInitFunc // 当前函数 [funcid=1368]
                              for args: // 这层有两个参数
                                  -> ExecInitExprRec()
                                      case T_FuncExpr:
                                          -> ExecInitFunc // 当前函数 [funcid=316]
                                              for args: // 这层有一个参数 Var [varno=1 varattno=1 vartype=23 varnosyn=1 varattnosyn=1]
                                                  -> ExecInitExprRec()
                                                      case T_Var:
                                                          // regular user column
                                                          scratch.d.var.attnum = variable->varattno - 1;
                                                          -> ExprEvalPushStep()
                                      ExprEvalPushStep()
                          ExprEvalPushStep()
            ExprEvalPushStep()   
   ExprEvalPushStep()
 scratch.opcode = EEOP_DONE;
 ExprEvalPushStep()                                              

整个过程的主体是对表达式树的递归遍历。表达式树通常中间包含若干个 T_FuncExpr或其他类型的表达式节点,每个节点有若干个参数,参数可能同样是一个表达式,子节点求值步骤全部生成后,才将当前步骤生成放入数组;叶子节点通常是 T_Var 或者 T_Const,处理方式和投影一致。

本节重点介绍一下对于 T_FuncExpr 类型的步骤构建过程和之前没讲的非 fast_path 的表达式求值逻辑。主要包含两个函数:ExecInitExprRec 和 ExecInitFunc

其中 ExecInitExprRec 是表达式求值的关键函数,也是递归调用发生的地方;代码会根据不同的表达式类型,调用不同的逻辑,每个分支根据具体的情况会递归调用 ExecInitExprRec,随后将当前步骤推入步骤数组 ExprEvalPushStep 。其中,有一个很重要的步骤是 scratch.resvalue = resv,这样当前步骤计算完成的值就可以被上层调用者以传指针的方式传入(相当于上层表达式可以拿到子表达式的求值结果),从而将整个递归的计算过程串联起来。

ExecInitFunc 是对函数表达式类型计算的过程,由于比较复杂,写成了一个独立的函数。其主要逻辑是遍历当前函数的参数,分别通过调用ExecInitExprRec进行求值步骤的初始化;子表达式求值的结果则可以通过 &fcinfo->args[argno].value 获取;完成后,将当前函数的求值步骤推入步骤数组。

上述例子的实际求值过程如下,在前述的分发机制中依次执行以下步骤即可:

    EEO_CASE(EEOP_SCAN_FETCHSOME)

    EEO_CASE(EEOP_SCAN_VAR) // scan i
    EEO_CASE(EEOP_FUNCEXPR_STRICT) // i4tod

    EEO_CASE(EEOP_SCAN_VAR) // scan i
    EEO_CASE(EEOP_FUNCEXPR_STRICT) // i4tod
    
    EEO_CASE(EEOP_FUNCEXPR_STRICT) // dpow
   EEO_CASE(EEOP_FUNCEXPR_STRICT) // dsprt
    
    EEO_CASE(EEOP_ASSIGN_TMP) // 将计算结果赋值到resultslot
    
   EEO_CASE(EEOP_DONE)

常量预计算优化

对于表达式树中的常量部分,我们可以在优化阶段就计算好,避免重复求值。同样利用一个例子来讨论这个问题。在下述例子中,如果POWER(2,3)可以在优化阶段即被替换为8,那么在执行阶段,显然就可以避免重复计算5次POWER(2,3)。

select i+POWER(2,3) from generate_series(1,5) i;

调用栈如下:

-> preprocess_expression
 -> eval_const_expressions
     -> eval_const_expressions_mutator
         -> expression_tree_mutator (case T_List)
             -> eval_const_expressions_mutator
                 -> expression_tree_mutator (case T_TargetEntry)
        -> eval_const_expressions_mutator (case T_OpExpr)
         -> simplify_function
                             // 对表达式列表递归调用 eval_const_expressions_mutator
                             -> args = expand_function_arguments()
          -> args = (List *) expression_tree_mutator(args,eval_const_expressions_mutator)
                             -> evaluate_function // try to pre-evaluate a function call 
           -> evaluate_expr // pre-evaluate a constant expression
            // 初始化表达式节点的执行状态信息
                                        -> ExecInitExpr
                                         -> ExecInitExprSlots() // Insert EEOP_*_FETCHSOME steps as needed
                                            -> ExecInitExprRec() // 将执行步骤填入 ExprState->steps 数组
                                             case T_FuncExpr:
             -> ExecInitFunc // 主要工作是将 argument 求值;并放入 state 的 list 中
                                                     foreach(lc, args)
                                                        if IsA(arg, Const)
                                                             fcinfo->args[argno].value = con->constvalue
                                                            else
                                                             ExecInitExprRec() // 递归对表达式参数求值
                                                    -> ExprEvalPushStep
                                        -> const_val = ExecEvalExprSwitchContext
                                         -> evalfunc()
                                             op = state->steps
                                                resultslot = state->resultslot
                                                outerslot = econtext->ecxt_outertuple
                                                EEO_DISPATCH() // goto op->opcode

代码的核心逻辑在于通过 eval_const_expressions_mutator 和 express_tree_mutator 函数遍历表达式树;如果遇到 op_expr 则调用 simplify_function()->evaluate_function() 简化子表达树中的常量。
evaluate_function 会检查子表达式是否含有非常量;如果全是常量,则可以继续简化。

简化的过程实质就是将执行器阶段的求值过程提前至优化阶段:首先生成节点执行状态信息和求值步骤数组;然后调用 ExecEvalExprSwitchContext 顺次执行;最后通过makeConst生成一个常量节点,替换原来复杂的表达式子节点。

至此,我们就系统地介绍了PG中关于投影和表达式计算的实现逻辑。投影在大部分情况下几乎是一个必须的操作,为数不多的优化手段可能在于将上层算子的投影下推,不再展开讨论。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

简单易懂,一篇文章告诉你什么是云渲染!

随着5G、AI、云计算、云服务等资讯来袭&#xff0c;我们发现云渲染被更多人知晓和关注&#xff0c;通过搜集和调研云渲染业内的相关信息和模式&#xff0c;尽量用简明的方式为大家解答到底 什么是云渲染。 用户将本地任务提交到远程服务器&#xff0c;通过远程的计算机集群资源…

C++ 类和对象(静态的static、友元、内部类、匿名对象、explicit)知识点+完整思维导图+实操图+深入细节通俗易懂建议收藏

绪论 时间的步伐有三种&#xff1a;未来姗姗来迟&#xff0c;现在像箭一样飞逝&#xff0c;过往永远静立不动。本章是类和对象的一个收尾篇&#xff0c;相信通过这三篇类和对象的学习&#xff0c;应该你已经更加清楚了解了C的类和对象。 话不多说安全带系好&#xff0c;发车啦&…

耗时162天,从华为外包5k转岗正式员工15k,经历的心酸只有自己知道····

一提及外包测试&#xff0c;大部分人的第一印象就是&#xff1a;工作强度大&#xff0c;技术含量低&#xff0c;没有归属感&#xff01; 本人毕业于某普通二本院校非计算机专业&#xff0c;跨专业入行测试&#xff0c;至今有近 5年工作经验。 第一份测试工作是在华为做了两年外…

【LCM】LCM通讯入门

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍LCM的通讯。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习知识&#xff0c;共同进步。 &#x1f95e;喜欢的朋友可以关注一下&#xff0c;下次更新…

Linux-初学者系列6_kvm虚拟机

速通配置kvm虚拟机 通过虚拟化技术将一台计算机虚拟为多台逻辑计算机。 在一台计算机上同时运行多个逻辑计算机&#xff0c;每个逻辑计算机可以运行不同的操作系统&#xff0c;并且应用程序都可以在相互独立的空间内运行互不影响&#xff0c;提高计算机的工作效率。 一、 配置…

真题详解(include)-软件设计(八十二)

真题详解&#xff08;快速排序&#xff09;-软件设计&#xff08;八十一)https://blog.csdn.net/ke1ying/article/details/130806047 generalliza&#xff08;泛化&#xff09;&#xff1a;泛化是一种继承关系&#xff0c;子类将继承基类所有行为&#xff0c;关系和通信&#…

章节2:Burp Suite配置启动

章节2&#xff1a;Burp Suite配置启动 01 Burp Suite程序下载 程序下载 Burp Suite jar包JDK&#xff0c;JDK包含JRE&#xff08;Java运行时环境&#xff09;激活jar包汉化jar包 Burp jar包 推荐2021&#xff0c;jar包版本 https://portswigger.net/burp/releases JRE环…

带你手撕链式二叉树—【C语言】

前言&#xff1a; 普通二叉树的增删查改没有意义&#xff1f;那我们为什么要先学习普通二叉树呢&#xff1f; 给出以下两点理由&#xff1a; 1.为后面学习更加复杂的二叉树打基础。&#xff08;搜索二叉树、ALV树、红黑树、B树系列—多叉平衡搜索树&#xff09; 2.有很多二叉树…

【C++进阶之路】模板

前言 假如需要你写一个交换函数&#xff0c;交换两个相同类型的值&#xff0c;这时如果交换的是int 类型的值&#xff0c;你可能会写一个Swap函数&#xff0c;其中参数是两个int类型的&#xff0c;假如再让你写一个double类型的呢&#xff1f;你可能又要写一个Swap的函数重载&…

运营-14.优惠券规则

优惠券使用场景 1. 需要先领取&#xff1b; 2. 在购买商品的时候可以抵用部分费用&#xff1b; 3. 在有效期内可以随时使用&#xff1b; &#xff08;根据场景和类型会有区别&#xff09; 优惠券的优势 1. 可以控制数量&#xff0c;确保活动成本可控&#xff1b; 2. 使用灵活&a…

「超强」ChatGPT撰写的艾思科技软件定制开发行业可行性报告分析

I. 引言 - 报告目的和范围 本报告的目的是分析山东艾思软件科技有限公司在定制软件开发行业的可行性&#xff0c;并提供相关建议和指导。本报告主要分析定制软件开发行业的市场概况、市场规模和增长潜力、市场需求、供应情况以及市场财务可行性。本报告旨在为山东艾思软件科技…

BIM建模|什么样的计算机可以支撑BIM应用?

BIM建模&#xff5c;什么样的计算机可以支撑BIM应用&#xff1f; 随着国内BIM技术的不断发展&#xff0c;越来越多企业和个人开始重视BIM&#xff0c;而作为BIM应用的数据生产载体&#xff0c;计算机硬件实力显得尤为重要。 现阶段BIM软件种类繁多&#xff0c;不同项目、不同…

Mysql安装与卸载(Windows版本)

Mysql的安装 这里使用的Mysql版本是8.0.26 界面操作描述信息 1. 接受条款&#xff0c;下一步&#xff0c;准备开始安装image-202111220927156542. 选择Custom&#xff0c;自定义安装&#xff0c;Nextimage-202111221638172083. 以自己的操作系统为准&#xff0c;不过大多数都…

批发零售商城小程序开发功能优势有哪些?

阿里发展到今天可能是很多人都意想不到的&#xff0c;谁能行到当初马云的一个大胆决定会让其成为批发零售行业的龙头呢。随着互联网技术的深入发展&#xff0c;现在越来越的商家企业也都寻求新的经营发展方式&#xff0c;批发零售商城小程序开发作为一种新型的电商模式&#xf…

达梦数据库安装教程

目录 安装教程 安装前准备 新建dmdba用户 修改文件打开最大数 挂载镜像 新建安装目录 数据库安装 配置环境变量 配置实例 注册服务 启动停止服务 启动 查看端口 停止 数据库目录结构介绍 数据库安装目录 达梦数据库 DM8下载地址产品下载 | 达梦数据库 (dameng.…

【Vue基础】Vue路由,实现页面跳转

一、需求说明 点击不同的模块实现页面跳转&#xff0c;如下点击“员工管理”右侧会显示员工管理页面&#xff0c;如下图1&#xff1b;点击“入住信息”右侧会显示入住信息&#xff0c;如下图二 二、涉及文件 1、 主要上图在这几个文件中修改相关代码 2、知识点整理 1&#x…

25K 入职腾讯的那天,我特么哭了

悲催的经历&#xff1a; 先说一下自己的个人情况&#xff0c;计算机专业&#xff0c;17 年本科毕业&#xff0c;一毕业就进入了“阿里”软件测试 岗(进去才知道是接了个阿里外包项目&#xff0c;可是刚毕业谁知道什么外包不外包的)。 更悲催的是&#xff1a;刚入职因为家里出…

工厂蓝牙定位技术的原理、应用场景、优势及潜在问题

蓝牙定位技术是近年来在工业领域中得到广泛应用的一项技术。随着工业自动化的快速发展和物联网技术的普及&#xff0c;工厂蓝牙定位成为了提高生产效率、优化生产流程和管理的重要工具。本文将详细介绍工厂蓝牙定位技术的原理、应用场景以及其在工业生产中的优势。 首先&#x…

redis集群读写,容错切换,从属调整,扩容,缩容

rediscluster 读写一定要注意redis实例的区间实例范围。需要路由到位。 比如 hashsolthash(k1) mod 1638412706,而12706槽位不在6391上&#xff0c;在6393上。 如何让rediscluster 路由到槽呢&#xff1f; redis-cli命令尾部加上 -c即可。防止路由失效。如果k1不在6391上&am…

企业如何将采购成本最小化?

从人员成本到运输和手续费&#xff0c;采购成本涵盖了广泛的费用&#xff0c;这些费用可能会迅速增加。这就是为什么要有一个明确的采购流程&#xff0c;鼓励竞争性招标&#xff0c;并使供应商轻松与你合作。但是&#xff0c;降低采购成本的最有效方法也许是通过实施一个采购软…