2024年6月

1 案例说明

  1. 设置网关采集EthernetIP IO设备数据
  2. 把采集的数据转成opc ua协议转发给其他系统。

2 VFBOX网关工作原理
VFBOX网关是协议转换网关,是把一种协议转换成另外一种协议。网关可以采集西门子,欧姆龙,三菱,AB PLC,DLT645,DLT698电表,modbus rtu tcp,环保的HJ212协议,opc ua和opc da,电力的IEC103 IEC104, IEC61850等,以及EthernetIP,Profinet IO,CCLink,EtherCAT现场总线协议,bacnet,MQTT,mysql,sqlserver数据库。不同型号的网关支持不同的协议,具体支持的协议参考网关侧面标签上的型号。依托500多种型号的网关产品,可以实现多种不同协议之间的互联互通。
网关连接在不同协议的设备之间,就像一个“翻译”,把一种设备支持的协议,转换成另一种设备支持的协议,从而实现两个不同协议的设备之间的数据交换。工作架构如下:

3 准备工作

  1. 仰科网关。支持采集EthernetIP IO数据,opc ua协议转发。
  2. 电脑。IP设置成192.168.1.198,和网关在同一个网段。
  3. 网线、12V电源。

4 网关采集EthernetIP IO数据

  1. EthernetIP IO协议一般用于采集机器人,控制器等设备的数据。

  2. 安装VFBOX Studio软件。打开软件后,点击新建工程,设备类型选择实际网关的型号。可选的型号有:VB301-1100,VB301-1200,VB301-1400,VB302-2400,VB303-2400.
    image

  3. 点击“新建设备”,驱动类型选择如下:
    image

  4. 输入被采集设备的IP地址和端口号,端口号一般默认是44818
    image

  5. 按照被采集数据的设备配置网关的输入块、输出块的参数。这些参数和被采集设备保持一致。这些参数在被采集设备上查看。
    image

  6. 添加要采集的数据。需要知道这个点位的地址和数据类型,填写到如下的选项中。
    image

  7. 写数据到EIP设备的配置如下,读写类型设置成“写”
    image

  8. 配置完成后点击菜单“下载”,把工程下载到网关里。

  9. 点击菜单“查看数据”,查看网关采集到的数据。对比一下当前值,“状态”显示Good,更新时间为当前时间就代表采集到了数据。
    image

5 启动opc ua协议转发采集的数据

  1. 在左侧服务中选择“OPC UA”,Enable设置成Yes。
    image

  2. 添加转发的采集标签,并把工程下载工程到网关。
    image

  3. 在电脑上就可以用OPC UA client软件从网关中读取数据了。

6 从设备的的EDS文件获取参数信息

  1. 有些EIP的从站设备提供了EDS文件,可以从EDS文件中获取点位信息。这些信息是需要填写到网关中。

  2. 一般EIP设备上也有对应的显示和配置。请参考设备的使用说明书,先把EIP设备设置成EIP的从站,并且启动连接通道。
    image

  3. 打开设备的eds文件,找到如下格式的内容。定位到 [Connection Manager],找到Connection1的Path,类似 "20 04 24 64 2C 96 2C 65"; $ Path。定位24 2C 2C, 24后面的0x64是Configruation(100), 2C 后面的0x96是Output(150),第2个2C后面的0x65是 Input(101)。Instance的值是1。填写如下:
    image

  4. 如果 Path类似"20 04 25 00 64 00 2D 00 C6 00 2D 00 C7 00"; 则定位 25 00, 2D 00 2D 00, 后面的2byte是Instance,这种情况vs中Instance Number Size需配置成2。另外,常用的Input/Ouput/Configuration Instance 范围0-255,1个字节,但有些从站Instance范围用0-65535,需要2个字节。

  5. 一些EIP设备是用下面的类似的格式定义了Instance Number Size的值。下图中的Param6定义了输入举例(Input Instance)的值3。
    image

  6. Configruation Size 一般填写0

7 案例总结
使用协议转换网关可以很方便的实现不同协议的设备之间的数据转换。大量节省了项目实施过程的时间成本,人力成本。VFBOX网关产品都是工业级品质,符合工业应用的场景。只需要简单的参数配置,可以很快完成设备和系统之间的连接。方案优点:

  1. 不需要修改设备里的程序。
  2. 不影响设备原有的工作方式和功能。
  3. 只需要简单的配置就可以实现功能需求。
  4. 设备长期稳定工作。
  5. 网关运行架构如下

image


title: Nuxt3 的生命周期和钩子函数(一)
date: 2024/6/25
updated: 2024/6/25
author:
cmdragon

excerpt:
摘要:本文是关于Nuxt3的系列文章之一,主要探讨Nuxt3的生命周期和钩子函数,引导读者深入了解其在前端开发中的应用。文章提供了往期相关文章链接,涉及Nuxt中间件、Composables、状态管理、路由系统、组件开发等多个方面,帮助读者全面掌握Nuxt3框架的特性和实践技巧。

categories:

  • 前端开发

tags:

  • Nuxt3
  • 生命周期
  • 钩子函数
  • 前端开发
  • Web框架
  • Vue.js
  • 应用教程


image

image

扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长

app:created

  • 描述
    :在初始 Vue 应用实例创建时调用。这个钩子是在 Nuxt 应用启动时,Vue 应用实例被创建之后立即触发的。
  • 服务器端
    :✅
  • 客户端
    :✅

用法

app:created
钩子可以在
nuxt.config.ts
文件中的
plugins
或者在页面组件中使用。

示例代码


nuxt.config.ts
中的插件中使用:

// nuxt.config.ts
export default defineNuxtConfig({
  plugins: [
    {
      src: '~/plugins/my-plugin',
      mode: 'client', // 或者 'server' 或 'all'
    },
  ],
})

// plugins/my-plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:created', () => {
    console.log('Vue 应用实例已创建');
    // 这里可以执行一些初始化逻辑
  });
});

在页面组件中使用:

<template>
  <div>页面内容</div>
</template>

<script setup>
import { onBeforeMount } from 'vue'

onBeforeMount(() => {
  // 这个钩子会在 'app:created' 之后调用
  console.log('页面组件即将挂载');
})

const nuxtApp = useNuxtApp()

nuxtApp.hook('app:created', () => {
  console.log('Vue 应用实例已创建 - 页面级别');
});
</script>

在这个示例中,
app:created
钩子被用于在 Vue 应用实例创建时输出一条消息。这可以用于执行一些全局的初始化任务,比如设置全局变量或状态,注册全局组件等。需要注意的是,在服务器端渲染 (SSR) 的上下文中,这个钩子会在服务器上为每个请求调用一次。在客户端,这个钩子只在应用初始化时调用一次。

app:error err 服务器端和客户端 在发生致命错误时调用。

详细解释

app:error
是 Nuxt 3 的一个全局生命周期钩子,它在服务器端和客户端发生未捕获的致命错误时被调用。这个钩子允许你定义一个自定义的错误处理函数,以便在发生错误时执行特定的逻辑,比如记录错误信息、发送错误报告、显示错误消息或者重定向到错误页面。

用法

在 Nuxt 3 应用中,你可以通过在
defineNuxtPlugin
函数中注册
app:error
钩子来使用它。

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:error', (err, ctx) => {
    // 自定义错误处理逻辑
  });
});

案例Demo

// plugins/error-handler.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:error', (err, ctx) => {
    console.error('An error occurred:', err.message);
    
    // 如果是客户端错误,可以显示一个错误消息给用户
    if (process.client) {
      alert('An error occurred: ' + err.message);
    }
    
    // 如果是服务器端错误,可以记录到日志文件或发送到错误追踪服务
    if (process.server) {
      // 例如,使用 winston 或其他日志库记录错误
      // logger.error('Server error:', err);
      
      // 或者发送错误到错误追踪服务,如 Sentry
      // Sentry.captureException(err);
    }
    
    // 可以根据错误类型决定是否重定向
    if (err.statusCode === 404) {
      // 重定向到404页面
      ctx.redirect('/404');
    }
  });
});

在这个案例中,我们定义了一个
app:error
钩子,当发生错误时,它会打印错误信息到控制台。如果是客户端错误,它会弹出一个包含错误信息的警告框。如果是服务器端错误,它可以记录错误日志或发送错误到错误追踪服务。此外,如果错误是一个特定的状态码(例如404),我们可以重定向用户到一个特定的页面。

app:error:cleared { redirect? } 服务器端和客户端 在致命错误被清除后调用。

详细解释

app:error:cleared
是 Nuxt 3 的一个全局生命周期钩子,它在服务器端和客户端的致命错误被清除后调用。这个钩子允许你在错误被处理后执行后续操作,比如重定向用户到另一个页面。这个钩子接收一个可选的
redirect
参数,它允许你指定一个重定向的路径。

用法

在 Nuxt 3 应用中,你可以通过在
defineNuxtPlugin
函数中注册
app:error:cleared
钩子来使用它。

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:error:cleared', (redirect, ctx) => {
    // 自定义错误清除后的逻辑
  });
});

案例Demo

// plugins/error-clear-handler.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:error:cleared', (redirect, ctx) => {
    // 如果提供了重定向路径,则进行重定向
    if (redirect) {
      ctx.redirect(redirect);
    }
    
    // 可以在这里执行其他清理工作,例如清除错误状态
    // 或者通知用户错误已经被处理
    if (process.client) {
      // 例如,更新UI来反映错误已经被处理
      // updateErrorState(false);
    }
    
    // 如果是服务器端,可能需要清除服务器状态或发送通知
    if (process.server) {
      // 清理服务器状态或发送通知
      // serverCleanup();
    }
  });
});

在这个案例中,我们定义了一个
app:error:cleared
钩子,当致命错误被清除后,它会检查是否提供了重定向路径,如果提供了,则执行重定向。此外,它还可以执行其他清理工作,例如在客户端更新UI状态以反映错误已经被处理,或者在服务器端清理服务器状态或发送通知。这个钩子是处理错误后恢复应用状态的有用工具。

app:data:refresh keys? 服务器端和客户端 (内部)

详细解释

app:data:refresh
是 Nuxt 3 中的一个内部生命周期钩子,主要用于数据刷新。它在需要重新获取数据时被触发,通常发生在用户刷新页面或者在某些情况下数据更新后。
keys
参数是可选的,它是一个数组,包含需要刷新的数据的键名,如果不提供,会刷新所有数据。

用法

在 Nuxt 3 中,你通常不会直接在
defineNuxtPlugin
中使用这个钩子,因为它是内部调用的。然而,如果你需要在插件中影响数据刷新行为,可以通过监听相关事件来间接使用。

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.on('dataRefresh', (keys) => {
    // 在这里处理数据刷新操作,例如更新缓存、请求新数据等
    if (keys) {
      // 如果提供了keys,只刷新指定的数据
      keys.forEach((key) => {
        nuxtApp.$store.dispatch('refreshData', key);
      });
    } else {
      // 否则刷新所有数据
      nuxtApp.$store.dispatch('refreshAllData');
    }
  });
});

案例Demo

// plugins/data-refresh.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.on('dataRefresh', async (keys) => {
    if (keys) {
      const promises = keys.map(async (key) => {
        const freshData = await fetchNewData(key);
        nuxtApp.$store.commit('updateData', { key, data: freshData });
      });
      await Promise.all(promises);
    } else {
      const allData = await fetchAllData();
      nuxtApp.$store.commit('updateAllData', { data: allData });
    }
  });
});

// 在 store 中定义 mutation
export const mutations = {
  updateData(state, { key, data }) {
    state[key] = data;
  },
  updateAllData(state, { data }) {
    state = data;
  },
};

在这个案例中,我们创建了一个插件,监听
dataRefresh
事件。当事件触发时,我们根据提供的
keys
刷新指定的数据,或者刷新所有数据。然后,我们更新 Vuex 存储中的数据。注意,实际的
fetchNewData

fetchAllData
需要你根据你的应用逻辑来实现。

vue:setup - 服务器端和客户端 (内部)

详细解释

vue:setup
是 Nuxt 3 中的一个生命周期钩子,它在每个 Vue 组件的 setup 阶段被调用。这个钩子主要用于在组件的初始化阶段进行数据处理、依赖注入、API 设置等操作。由于它是基于 Vue 的,所以它在客户端和服务器端都会执行,但服务器端渲染时,它主要作用于预渲染阶段。

用法

在 Nuxt 3 中,
vue:setup
通常用于创建自定义组件,它会接收一个
app
对象,你可以通过这个对象访问 Nuxt 应用实例。例如,你可以注入
$store

$router
,并定义组件的局部状态和方法。

export default defineComponent({
  setup() {
    const store = inject('store');
    const router = inject('router');

    // 在这里定义组件的局部状态和方法
    const count = ref(0);
    const increment = () => {
      count.value++;
    };

    // 使用store和router
    watch(() => router.currentRoute.value.name, () => {
      // 当路由改变时执行某些操作
    });

    return {
      count,
      increment,
    };
  },
});

案例Demo

// components/MyComponent.vue
export default defineComponent({
  setup() {
    const { $store } = inject();
    const myData = ref($store.state.myData);

    // 在setup中获取并使用store中的数据
    useEffect(() => {
      async function fetchData() {
        const newData = await fetchMyData();
        myData.value = newData;
      }
      fetchData();
    }, []);

    // 示例方法,使用store数据
    const updateData = (newValue) => {
      myData.value = newValue;
      $store.commit('updateMyData', newValue);
    };

    return {
      myData,
      updateData,
    };
  },
});

// 在store/index.js中定义mutation
export const mutations = {
  updateMyData(state, newData) {
    state.myData = newData;
  },
};

在这个案例中,我们在
vue:setup
中注入
$store
,获取并使用存储中的数据。当组件挂载时,我们会从服务器获取数据并更新组件状态。同时,我们定义了一个
updateData
方法,用于更新 store 中的数据。在客户端和服务器端,这个组件都会执行这些操作。

vue:error - err, target, info - 服务器端和客户端 - 当 Vue 错误传播到根组件时调用。了解更多。

详细解释

vue:error
是 Nuxt 3 中的一个生命周期钩子,它在 Vue 应用程序的错误处理过程中起作用。当 Vue 错误传播到根组件时,这个钩子会被调用。这个钩子可以用于在服务器端和客户端记录和处理错误。

app:rendered - renderContext - 服务器端 - 在 SSR 渲染完成时调用

app:rendered

钩子:app:redirected

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长
,阅读完整的文章:
Nuxt3 的生命周期和钩子函数(一) | cmdragon's Blog

往期文章推荐:

往期文章归档:

问题

有很多应用程序在验证JSON数据的时候用到了JSON Schema。
在微服务架构下,有时候各个微服务由于各种历史原因,它们所生成的数据对JSON Object属性名的大小写规则可能并不统一,它们需要消费的JSON数据的属性名可能需要大小写无关。
遗憾的是,目前的JSON Schema没有这方面的标准,标准中都是大小写敏感的。在类似上述情况下,这给使用JSON Schema进行数据验证造成了困难。

解决方案

一种
临时解决方案
是利用JSON Schema中的
patternProperties
关键字,写正则表达式来表示当前属性名是大小写无关的。
比如你的数据是这样的:

[
  { "Count": 1 },
  { "count": 3 }
]

那么你可以这样写JSON Schema:

{
  "type": "array",
  "items": {
    "patternProperties": {
      "^[Cc]ount$": { "minimum": 1 }
    }
  }
}

显然这样的JSON Schema会比原来的更复杂。

更优雅的解决方案

想象一下.NET下的JSON library
System.Text.Json
,它的反序列化器支持 属性名大小写无关的选项
PropertyNameCaseInsensitive
,这个是用来反序列化的。

那么,有没有JSON Schema实现库支持大小写无关的扩展选项呢?在.NET下,目前 实现库
Lateapexearlyspeed.Json.Schema
支持 属性名大小写无关的 JSON Schema级验证。由于该实现库遵循标准JSON Schema(规定属性名只能大小写敏感),所以这个扩展功能需要显式配置:

/// <summary>
/// Gets or sets a value that determines whether a property's name uses a case-insensitive comparison during validation. The default value is false.
/// </summary>
/// <returns>
/// true to compare property names using case-insensitive comparison; otherwise, false.
/// </returns>
public bool PropertyNameCaseInsensitive { get; set; }

例子:

string jsonSchema = """
    {
      "properties": {
        "A": {
                "properties": { 
                  "B": {"type": "string"} 
                }
        }
      }
    }
    """;

string jsonInstance = """
    {
      "a": {
        "b": 123
      }
    }
    """;

// By default, JSON Schema validation is property names case sensitive, so instance data's property names are not matched:
ValidationResult validationResult = new JsonValidator(jsonSchema).Validate(jsonInstance);
Assert.True(validationResult.IsValid);

// Opt in to feature of property names case Insensitive:
validationResult = new JsonValidator(jsonSchema, new JsonValidatorOptions { PropertyNameCaseInsensitive = true }).Validate(jsonInstance);

Assert.False(validationResult.IsValid);

Assert.Equal("Expect type(s): 'String' but actual is 'Number'", validationResult.ErrorMessage);
Assert.Equal("/a/b", validationResult.InstanceLocation!.ToString());
Assert.Equal("/properties/A/properties/B/type", validationResult.RelativeKeywordLocation!.ToString());

总结

本文介绍了.NET下 实现属性名大小写无关的JSON Schema验证方法,其中最优雅的方式应该是用 .NET实现库
Lateapexearlyspeed.Json.Schema
中的扩展选项
PropertyNameCaseInsensitive

欢迎大家将使用过程中发现的问题报到repo issue,希望.NET实现库
Lateapexearlyspeed.Json.Schema
能帮到大家。

Github repo:
https://github.com/lateapexearlyspeed/Lateapexearlyspeed.JsonSchema

1、概述

通常我们要对数据库进行优化,主要可以通过以下五种方法。

  1. 计算机硬件调优
  2. 应用程序调优
  3. 数据库索引优化
  4. SQL语句优化
  5. 事务处理调优

数据库优化

本篇文章将向大家介绍数据库中索引类型和使用场合,本文以SQL Server为例,对于其他技术平台的朋友也是有参考价值的,原理差不多。

查询数据时索引使数据库引擎执行速度更快,有针对性的数据检索,而不是简单地整表扫描(Full table scan)。

为了有效的使用索引,我们必须对索引的构成有所了解,而且我们知道在数据表中添加索引必然需要创建和维护索引表,所以我们要全局地衡量添加索引是否能提高数据库系统的查询性能。

2、数据库中的文件和文件组

在物理层面上,数据库由数据文件组成,而这些数据文件组成文件组,然后存储在磁盘上。每个文件包含许多区,每个区的大小为64K由八个物理上连续的页组成(一个页8K),我们知道
页是SQL Server数据库中的数据存储的基本单位
。为数据库中的数据文件(.mdf 或 .ndf)分配的磁盘空间可以从逻辑上划分成页(从0到n连续编号)。

页中存储的类型有:
数据

索引

溢出

在SQL Server中,通过文件组这个逻辑对象对存放数据的文件进行管理。

文件和文件组

在顶层是我们的数据库,由于数据库是由一个或多个文件组组成,而文件组是由一个或多个文件组成的
逻辑组
,所以我们可以把文件组分散到不同的磁盘中,使用户数据尽可能跨越多个设备,多个I/O 运转,避免 I/O 竞争,从而均衡I/O负载,克服访问瓶颈。

3、区和页

如下图所示,文件是由区组成的,而区由八个物理上连续的页组成,由于区的大小为64K,所以每当增加一个区文件就增加64K。

区和页

页中保存的数据类型有:表数据、索引数据、溢出数据、分配映射、页空闲空间、索引分配等。

页类型 内容
Data 当 text in row 设置为 ON 时,包含除 text、 ntext、image、nvarchar(max)、varchar(max)、varbinary(max) 和 xml 数据之外的所有数据的数据行。
Index 索引条目。
Text/Image 大型对象数据类型:text 、 ntext、image、nvarchar(max)、varchar(max)、varbinary(max) 和 xml 数据。数据行超过 8 KB 时为可变长度数据类型列:varchar 、nvarchar、varbinary 和 sql_variant
Global Allocation Map、Shared Global Allocation Map 有关区是否分配的信息。
Page Free Space 有关页分配和页的可用空间的信息。
Index Allocation Map 有关每个分配单元中表或索引所使用的区的信息。
Bulk Changed Map 有关每个分配单元中自最后一条 BACKUP LOG 语句之后的大容量操作所修改的区的信息。
Differential Changed Map 有关每个分配单元中自最后一条 BACKUP DATABASE 语句之后更改的区的信息。

在数据页上,数据行紧接着页头(标头)按顺序放置;页头包含标识值,如页码或对象数据的对象ID;数据行持有实际的数据;最后,页的末尾是行偏移表,对于页中的每一行,每个行偏移表都包含一个条目,每个条目记录对应行的第一个字节与
页头
的距离,行偏移表中的条目的顺序与页中行的顺序相反。

数据页

4、索引的基本结构

“索引(Index)提供查询的速度”这是对索引的最基本的解释,接下来我们将通过介绍索引的组成,让大家对索引有更深入的理解。

索引是数据库中的一个独特的结构,由于它保存数据库信息,那么我们就需要给它分配磁盘空间和维护索引表。创建索引并不会改变表中的数据,它只是创建了一个新的数据结构指向数据表;打个比方,平时我们使用字典查字时,首先我们要知道查询单词起始字母,然后翻到目录页,接着查找单词具体在哪一页,这时我们目录就是索引表,而目录项就是索引了。

当然,索引比字典目录更为复杂,因为数据库必须处理插入,删除和更新等操作,这些操作将导致索引发生变化。

叶节点

假设我们磁盘上的数据是物理有序的,那么数据库在进行插入,删除和更新操作时,必然会导致数据发生变化,如果我们要保存数据的连续和有序,那么我们就需要移动数据的物理位置,这将增大磁盘的I/O,使得整个数据库运行非常缓慢;使用索引的主要目的是使数据逻辑有序,使数据独立于物理有序存储。

为了实现数据逻辑有序,索引使用双向链表的数据结构来保持数据逻辑顺序,如果要在两个节点中插入一个新的节点只需修改节点的前驱和后继,而且无需修改新节点的物理位置。

双向链表
(Doubly linked list)也叫双链表,是
链表
的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

理论上说,从双向链表中删除一个元素操作的时间复杂度是O(1),如果希望删除一个具体有给定关键字的元素,那么最坏的情况下的时间复杂度为O(n)。

在删除的过程中,我们只需要将要删除的节点的前节点和后节点相连,然后将要删除的节点的前节点和后节点置为null即可。

//伪代码
node.prev.next=node.next; 
node.next.prev=node.prev; 
node.prev=node.next=null;

索引的叶节点和相应的表数据

如上图,索引叶节点包含索引值和相应的RID(ROWID),而且叶节点通过双向链表有序地连接起来;同时我们主要到数据表不同于索引叶节点,表中的数据无序存储,它们不全是存储在同一表块中,而且块之间不存在连接。

总的来说,索引保存着具体数据的物理地址值。

5、索引的类型

索引的类型主要有两种:
聚集索引

非聚集索引

聚集索引
:物理存储按照索引排序。

指数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。一个表只能有一个聚集索引,因为一个表的物理顺序只有一种情况,所以,对应的聚集索引只能有一个。如果某索引不是聚集索引,则表中的行物理顺序与索引顺序不匹配,与非聚集索引相比,聚集索引有着更快的检索速度。

非聚集索引
:物理存储不按照索引排序。

该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。除了聚集索引以外的索引都是非聚集索引,只是人们想细分一下非聚集索引,分成普通索引,唯一索引,全文索引。如果非要把非聚集索引类比成现实生活中的东西,那么非聚集索引就像新华字典的偏旁字典,他结构顺序与实际存放顺序不一定一致。

5.1、聚集索引

聚集索引
的数据页是物理有序地存储,数据页是聚集索引的叶节点,数据页之间通过双向链表的形式连接起来,而且实际的数据都存储在数据页中。当我们给表添加索引后,表中的数据将根据索引进行排序。

假设我们有一个表T_Pet,它包含四个字段分别是:animal,name,sex和age,而且使用animal作为索引列,具体SQL代码如下:

-----------------------------------------------------------
---- Create T_Pet table in tempdb. 
-----------------------------------------------------------
USE tempdb

CREATE TABLE T_Pet
(
    animal    VARCHAR(20),
    [name]    VARCHAR(20),
    sex        CHAR(1),
    age        INT
)

CREATE UNIQUE  CLUSTERED INDEX T_PetonAnimal1_ClterIdx ON T_Pet (animal)
-----------------------------------------------------------
---- Insert data into data table.
-----------------------------------------------------------

DECLARE @i int

SET @i=0
WHILE(@i<1000000)
BEGIN

    INSERT INTO T_Pet (
        animal,
        [name],
        sex,
        age
    )
    SELECT  [dbo].random_string(11) animal,
            [dbo].random_string(11) [name],
            'F'                        sex,
            cast(floor(rand()*5) as int) age    

    SET @i=@i+1

END

INSERT INTO T_Pet VALUES('Aardark', 'Hello', 'F', 1)
INSERT INTO T_Pet VALUES('Cat', 'Kitty', 'F', 2)
INSERT INTO T_Pet VALUES('Horse', 'Ma', 'F', 1)
INSERT INTO T_Pet VALUES('Turtles', 'SiSi', 'F', 4)
INSERT INTO T_Pet VALUES('Dog', 'Tomma', 'F', 2)
INSERT INTO T_Pet VALUES('Donkey', 'YoYo', 'F', 3)

聚集索引

如上图所示,从左往右的第一和第二层是索引页,第三层是数据页(叶节点),数据页之间通过双向链表连接起来,而且数据页中的数据根据索引排序;假设,我们要查找名字(name)为Xnnbqba的动物Ifcey,这里我们以animal作为表的索引,所以数据库首先根据索引查找,当找到索引值animal = ‘Ifcey时,接着查找该索引的数据页(叶节点)获取具体数据。具体的查询语句如下:

SET STATISTICS PROFILE ON
SET STATISTICS TIME ON

SELECT animal, [name], sex, age
FROM T_Pet
WHERE animal = 'Ifcey'

SET STATISTICS PROFILE OFF
SET STATISTICS TIME OFF

当我们执行完SQL查询计划时,把鼠标指针放到“聚集索引查找”上,这时会出现如下图信息,我们可以查看到一个重要的信息Logical Operation——Clustered Index Seek,SQL查询是直接根据聚集索引获取记录,查询速度最快。

查询计划

从下图查询结果,我们发现查询步骤只有2步,首先通过Clustered Index Seek快速地找到索引Ifcey,接着查询索引的叶节点(数据页)获取数据。

查询执行时间:CPU 时间= 0 毫秒,占用时间= 1 毫秒。

查询结果

现在我们把表中的索引删除,重新执行查询计划,这时我们可以发现Logical Operation已经变为Table Scan,由于表中有100万行数据,这时查询速度就相当缓慢。

查询计划

从下图查询结果,我们发现查询步骤变成3步了,首先通过Table Scan查找animal = ‘Ifcey’,在执行查询的时候,SQL Server会自动分析SQL语句,而且它估计我们这次查询比较耗时,所以数据库进行并发操作加快查询的速度。

查询执行时间:CPU 时间= 329 毫秒,占用时间= 182 毫秒。

查询结果

通过上面的有聚集索引和没有的对比,我们发现了查询性能的差异,如果使用索引数据库首先查找索引,而不是漫无目的的全表遍历。

5.2、非聚集索引

在没有聚集索引的情况下,表中的数据页是通过堆(Heap)形式进行存储,堆是不含聚集索引的表;SQL Server中的堆存储是把新的数据行存储到最后一个页中。

非聚集索引
是物理存储不按照索引排序,非聚集索引的叶节点(Index leaf pages)包含着指向具体数据行的
指针

聚集索引
,数据页之间没有连接是相对独立的页。

假设我们有一个表T_Pet,它包含四个字段分别是:animal,name,sex和age,而且使用animal作为非索引列,具体SQL代码如下:

-----------------------------------------------------------
---- Create T_Pet table in tempdb with NONCLUSTERED INDEX. 
-----------------------------------------------------------
USE tempdb

CREATE TABLE T_Pet
(
    animal    VARCHAR(20),
    [name]    VARCHAR(20),
    sex        CHAR(1),
    age        INT
)

CREATE UNIQUE  NONCLUSTERED INDEX T_PetonAnimal1_NonClterIdx ON T_Pet (animal)

非聚集索引

接着我们要查询表中animal = ‘Cat’的宠物信息,具体的SQL代码如下:

SET STATISTICS PROFILE ON
SET STATISTICS TIME ON

SELECT animal, [name], sex, age
FROM T_Pet
WHERE animal = 'Cat'

SET STATISTICS PROFILE OFF
SET STATISTICS TIME OFF

如下图所示,我们发现查询计划的最右边有两个步骤:RID和索引查找。由于这两种查找方式相对于聚集索引查找要慢(Clustered Index Seek)。

查询计划

查询计划

首先SQL Server查找索引值,然后根据RID查找数据行,直到找到符合查询条件的结果。

查询执行时间:CPU 时间= 0 毫秒,占用时间= 1 毫秒

查询结果

5.3、堆表非聚集索引

由于堆是不含聚集索引的表,所以非聚集索引的叶节点将包含指向具体数据行的指针。

以前面的T_Pet表为例,假设T_Pet使用animal列作为非聚集索引,那么它的堆表非聚集索引结构如下图所示:

堆表非聚集索引

通过上图,我们发现非聚集索引通过双向链表连接,而且叶节点包含指向具体数据行的指针。

如果我们要查找animal = ‘Dog’的信息,首先我们遍历第一层索引,然后数据库判断Dog属于Cat范围的索引,接着遍历第二层索引,然后找到Dog索引获取其中的保存的指针信息,根据指针信息获取相应数据页中的数据,接下来我们将通过具体的例子说明。

现在我们创建表employees,然后给该表添加
堆表非聚集索引
,具体SQL代码如下:

USE tempdb

---- Creates a sample table.
CREATE TABLE employees (
    employee_id   NUMERIC       NOT NULL,
    first_name    VARCHAR(1000) NOT NULL,
    last_name     VARCHAR(900)  NOT NULL,
    date_of_birth DATETIME                   ,
    phone_number  VARCHAR(1000) NOT NULL,
    junk          CHAR(1000)             ,
    CONSTRAINT employees_pk PRIMARY KEY NONCLUSTERED (employee_id)
);
GO

现在我们查找employee_id = 29976的员工信息。

SELECT * 
FROM employees
WHERE employee_id = 29976

查询计划如下图所示:

查询计划

首先,查找索引值employee_id = ‘29976’的索引,然后根据RID查找符合条件的数据行;所以说,堆表索引的查询效率不如聚集表,接下来我们将介绍聚集表的非聚集索引。

5.4、聚集表非聚集索引

当表上存在聚集索引时,任何非聚集索引的叶节点不再是包含指针值,而是包含聚集索引的索引值。

以前面的T_Pet表为例,假设T_Pet使用animal列作为非聚集索引,那么它的索引表非聚集索引结构如下图所示:

索引表非聚集索引

通过上图,我们发现非聚集索引通过双向链表连接,而且叶节点包含索引表的索引值。

如果我们要查找animal = ‘Dog’的信息,首先我们遍历第一层索引,然后数据库判断Dog属于Cat范围的索引,接着遍历第二层索引,然后找到Dog索引获取其中的保存的索引值,然后根据索引值获取相应数据页中的数据。

接下来我们修改之前的employees表,首先我们删除之前的堆表非聚集索引,然后增加索引表的非聚集索引,具体SQL代码如下:

ALTER TABLE employees
    DROP CONSTRAINT employees_pk

ALTER TABLE employees 
    ADD CONSTRAINT employees_pk PRIMARY KEY CLUSTERED (employee_id)
GO 

SELECT * FROM employees
WHERE employee_id=29976

查询计划

6、索引的有效性

SQL Server每执行一个查询,首先要检查该查询是否存在执行计划,如果没有,则要生成一个执行计划,那么什么是执行计划呢?简单来说,它能帮助SQL Server制定一个最优的查询计划。

下面我们将通过具体的例子说明SQL Server中索引的使用,首先我们定义一个表testIndex,它包含三个字段testIndex,bitValue和filler,具体的SQL代码如下:

-----------------------------------------------------------
---- Index Usefulness sample
-----------------------------------------------------------

CREATE TABLE testIndex
(
    testIndex int identity(1,1) constraint PKtestIndex primary key,
    bitValue bit,
    filler char(2000) not null default (replicate('A',2000))
)

CREATE INDEX XtestIndex_bitValue on testIndex(bitValue)
GO

INSERT INTO testIndex(bitValue)
    VALUES (0)
GO 20000 --runs current batch 20000 times.

INSERT INTO testIndex(bitValue)
    VALUES (1)
GO 10 --puts 10 rows into table with value 1

接着我们查询表中bitValue = 0的数据行,而且表中bitValue = 0的数据有2000行。

SELECT *
FROM   testIndex
WHERE  bitValue = 0

查询计划

现在我们查询bitValue = 1的数据行。

SELECT *
FROM   testIndex
WHERE  bitValue = 1

查询计划

现在我们注意到对同一个表不同数据查询,居然执行截然不同的查询计划,这究竟是什么原因导致的呢?

我们可以通过使用DBCC SHOW_STATISTICS查看到表中索引的详细使用情况,具体SQL代码如下:

UPDATE STATISTICS dbo.testIndex
DBCC SHOW_STATISTICS('dbo.testIndex', 'XtestIndex_bitValue')
WITH HISTOGRAM

直方图

通过上面的直方图,我们知道SQL Server估计bitValue = 0数据行行有约19989行,而bitValue = 1估计约21;SQL Server优化器根据数据量估算值,采取不同的执行计划,从而到达最优的查询性能,由于bitValue = 0数据量大,SQL Server只能提供扫描聚集索引获取相应数据行,而bitValue = 1实际数据行只有10行,SQL Server首先通过键查找bitValue = 1的数据行,然后嵌套循环联接到聚集索引获得余下数据行。

7、索引的优缺点

优点
第一,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
第二,可以大大加快 数据的检索速度,这也是创建索引的最主要的原因。
第三,可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
第四,在使用分组和排序 子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
第五,通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

缺点
第一,创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
第二,索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
第三,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。

8、索引创建原则

1.定义主键的数据列一定要建立索引。

2.定义有外键的数据列一定要建立索引。

3.对于经常查询的数据列最好建立索引。

4.对于需要在指定范围内的快速或频繁查询的数据列;

5.经常用在WHERE子句中的数据列。

6.经常出现在关键字order by、group by、distinct后面的字段。如果建立的是复合索引,索引的字段顺序要和这些关键字后面的字段顺序一致,否则索引不会被使用。

7.对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。

8.对于定义为text、image和bit的数据类型的列不要建立索引。

9.对于经常存取的列不要建立索引

10.限制表上的索引数目。对一个存在大量更新操作的表,所建索引的数目一般不要超过3个,最多不要超过5个。索引虽说提高了访问速度,但太多索引会影响数据的更新操作。

11.对复合索引,按照字段在查询条件中出现的频度建立索引。在复合索引中,记录首先按照第一个字段排序。对于在第一个字段上取值相同的记录,系统再按照第二个字段的取值排序,以此类推。因此只有复合索引的第一个字段出现在查询条件中,该索引才可能被使用,因此将应用频度高的字段,放置在复合索引的前面,会使系统最大可能地使用此索引,发挥索引的作用。

9、参考文章

索引 - SQL Server | Microsoft Learn

聚集与非聚集索引 - SQL Server | Microsoft Learn

《ORACLE PL/SQL编程详解》全原创(共八篇)--系列文章导航

8 种主流数据迁移工具技术选型

SQLServer中的CTE(Common Table Expression)通用表表达式使用详解

[推荐推荐]ORACLE SQL:经典查询练手系列文章收尾(目录篇)

国思RDIF低代码快速开发平台(支持vue2、vue3)

在 Apache APISIX 中,通过
proxy-rewrite
插件来修改上游配置时,需要确保插件的执行顺序和上下文环境正确。你提到在自己的插件中调用
proxy_rewrite.rewrite({host="new_upstream"}, ctx)
,但新上游没有生效,这可能是由于以下几个原因:

  1. 插件执行顺序
    :确保你的自定义插件在
    proxy-rewrite
    插件之后执行,proxy-rewrite.priority是1008。
  2. 上下文环境
    :确保在正确的阶段(如
    rewrite
    阶段)进行上游修改。

下面是一个示例,展示如何在自定义插件中调用
proxy-rewrite
插件并修改上游配置。

自定义插件示例

假设你的插件名为
my-plugin
,我们需要在
rewrite
阶段调用
proxy-rewrite
插件来修改上游。

local core = require("apisix.core")
local proxy_rewrite = require("apisix.plugins.proxy-rewrite")

local plugin_name = "my-plugin"

local _M = {
    version = 0.1,
    priority = 1000, -- 设置插件的优先级,值超大,优先级越高,越先执行
    name = plugin_name,
}

-- 定义插件的 schema
_M.schema = {
    type = "object",
    properties = {
        new_host = {type = "string"}
    },
    required = {"new_host"}
}

function _M.check_schema(conf)
    return core.schema.check(_M.schema, conf)
end

function _M.rewrite(conf, ctx)
    local rewrite_conf = {
        host = conf.new_host
    }

    -- 调用 proxy-rewrite 插件的 rewrite 方法
    proxy_rewrite.rewrite(rewrite_conf, ctx)

    core.log.info("Upstream host rewritten to: ", conf.new_host)
end

return _M

使用示例

在配置文件中启用并配置该插件:

{
    "plugins": {
        "my-plugin": {
            "new_host": "new_upstream"
        }
    },
    "upstream": {
        "nodes": {
            "127.0.0.1:1980": 1
        },
        "type": "roundrobin"
    }
}

注意事项

  1. 插件优先级
    :确保你的插件优先级低于proxy_rewrite,你插件的priority要小于1008
  2. 插件依赖
    :确保
    proxy_rewrite
    插件已加载,并且可以被调用。
  3. 日志检查
    :通过 APISIX 日志检查插件是否正确执行,并输出相关调试信息。

通过以上方法,你应该能够在自定义插件中调用
proxy-rewrite
插件,并成功修改上游配置。如果问题仍然存在,请检查 APISIX 的错误日志以获取更多信息。