跳到主要内容

一文弄懂字符编码,面对乱码不再抓瞎

· 阅读需 5 分钟

我们都知道,计算机起源自美国。那时候的电脑普遍支持 8 bits 运算。如果从零开始编号,128 就足以将英文字母和其他字符全部索引进来。因此,7 位二进制数用作表示字符,即 ASCII 码(8 bit 里面多出来的 1 bit 留出来用于校验等目的)。

后来,计算机卖到了世界各国,混沌之初,一切无序。各国按照各自的语言,在 128 位以上的广阔天地里自由飞翔。

这样就导致了一个问题,美国人发来的英文邮件在西班牙的电脑上有乱码,根本看不懂!

光是想想,每个国家都有自己的小册子,上面记录了 128 位以上的字母(字符)表示,这要求每个计算机都要存储这些小册子,太疯狂了!

历史车轮滚滚向前,Unicode 出场了。它带着将世界上每一个字符都赋予编码的使命来了。

Unicode 的思路是:每一个字母都分配一个抽象的编号,比如'Hello' - U+0048 U+0065 U+006C U+006C U+006F.

好的,现在赋予编号完成了,可是计算机是基于二进制呀,Unicode 如何存储在计算机中呢?

先做一个简单的心算。上面 Hello 的编码,去掉 U+: 00 48 00 65 00 6C 00 6C 00 6F. 每个数字的范围 0-F,二进制需要四位,也就是半个 byte,所以 00 是一个 byte,一个字母 H 是两个 byte!

这时候,美国人跳出来表达了不满:我们的 ASCII 码用的好好的,八位就可以表达所有的字母,现在要我们用 Unicode,每个字母都要用 16 位,那文件大小生生比原来多了两倍!

于是,UTF-8 为了解决上述痛点,依旧将 128 以下的编码用于英文字母,且只用一个字节,只有 128 以上的,才用两个字节存储。这样,对于英语用户之间的文件传递,没有任何影响。

注意概念上的分类:Unicode 和 ASCII 都是字符集。而 UTF-8 是编码方式。

特性字符集 (Character Set)编码方式 (Encoding)
核心问题定义字符和代码点的映射定义代码点如何存储为比特序列
抽象层次抽象(逻辑层):字符和数字的关系具体(实现层):数据如何表示
示例Unicode(U+0041 表示字母 A)UTF-8、UTF-16、UTF-32 等
依赖关系编码方式依赖于字符集字符集需要通过编码方式来实现
计算机表现形式提供标准编号(如 U+0041)提供实际存储格式(如 0x41 或 11000010)

Unicode 只是一个抽象的操作,将任何字符转化为一个 U+字符串。任何计算机上实际显示一个字符串需要一个编码方案的支持。可以想象的 hello 字符串的 unicode 为...,然后某个编码方案 iso-xxx 中找不到这些码,于是显示?或者 �。

最后,作者特别指出一句话:

It does not make sense to have a string without knowing what encoding it uses.

参考文献:

The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)

VSCode 使用效率大提升!从常用快捷键谈起

· 阅读需 6 分钟

快捷键

记录一些明显提升工作效率的快捷键,用表格形式,并注明场景。

命令用途场景
ctrl+p快速定位,加冒号后还可以定位行号,加@后定位变量,@后再加冒号还可以按分类查看变量多标签页切换或者大文件中定位行
ctrl+shift+p命令画板记不清快捷键的时候,调出来尝试用自然语言调用
ctrl+tab切换标签页多标签页
alt+click多光标选中批量添加字符
Ctrl+Shift+L选中相同的字符批量选中
CTRL+D选中相同的字符(one by one)批量选中
Shift+Alt+drag按列选中-
alt+鼠标滚轮加速上下滚动大文件查看
shift+alt+up/down向上下复制类似内容批量创建(替代 ctrlcv)
alt+up/down向上下移动代替剪切粘贴
ctrl+shift+[/]折叠、展开代码大文件折叠(根据光标位置,自动选择层级)
ctrl+K+Q快速定位到上次编辑位置大文件编辑查看
ctrl+shift+K删除行当一行较长,避免了拉鼠标和敲退格
ctrl+shift+enter插入行当一行较长,不用移动到行末按回车
home(笔记本 Fn+home)、end定位到行首、行尾很长的一行
ctrl+home/end定位到文件的开头、结尾很长的一个文件
Shift + End选中光标到行尾的文本选中很长的一行

参考资料:

vscode 官方 tips

多项目管理利器:工作区

随着工作接受的项目越来越多,如何更轻松地管理多个项目成为一个问题。目前我有三个项目,每次重启,需要分别启动三个不同的窗口,如果加上最近搞的 side project,可能同时打开 6 个窗口!所以,我打算寻找一个解决方案。

我之前用过一个叫 Project Manager 的插件,但是这次装回来体验了一下依旧乏善可陈。工作区(Workspace)是 vscode 自带的功能,它指的是你在一个窗口中打开的一个或多个文件夹及其相关配置的集合。工作区允许你将相关的项目文件夹、配置和设置组织在一起,方便进行项目开发。

工作区的创建过程

  1. 打开一个空白的新窗口。

  2. 左上角,File -> Add Folder To Workspace, 把项目文件夹添加进来,有几个项目就重复几次这一步操作。

  3. 待项目全部添加完毕后,File -> Save Workspace As,选一个方便的地方存,确认后会生成一个名为xxx.code-workspace的文件,内容示例:

{
"folders": [
{
"path": "frontend"
},
{
"path": "backend"
}
],
"settings": {
"workbench.colorCustomizations": {
"editor.lineHighlightBackground": "#1073cf2d",
"editor.lineHighlightBorder": "#9fced11f",
"activityBar.background": "#080A9B",
"titleBar.activeBackground": "#0B0EDA",
"titleBar.activeForeground": "#FCFCFF"
}
}
}

后续点击这个文件,即可打开工作空间。

工作区的使用

一个工作空间会同时列出多个项目,可以同时管理。我的使用感受是,搜索文件似乎反倒有些不便了,比如搜 index.js,会出现非常多的结果,因为三个文件夹的搜索结果都汇聚起来了。此外,explorer 和 source control 卡,都把所有项目并列显示。

还有一个非常常用的部分,就是 Terminal。不同项目有不同路径的 Terminal,还好 vscode 会帮我们标注出来。不过,更好的做法是,用 split 将不同 terminal 表示为一组,这样既可以并列观察,也可以对 terminal 分组。

remote explorer

相较于 xshell+xftp 来连接远程服务器和文件操作,vscode 的 remote explorer 显然更操作友好。它同样支持密钥和密码登录两种模式,密钥登录虽然稍微麻烦,但是安全性更高,应该首先考虑。增添连接的路径为:SSH 栏的设置图标,打开.ssh/config文件,添加如下内容:

Host <别名>
HostName <服务器地址>
User <用户名>
Port <端口号>
IdentityFile <私钥路径>

如果是密码登录,就不用IdentityFile了。这里假设已经从服务器申请到了私钥。windows 还需要对私钥的权限进行设置。

在 Windows 上,修改文件权限可以通过以下步骤:

  1. 右键点击 jason.pem 文件,选择 "属性"

  2. 切换到 "安全" 选项卡,然后点击 "高级" 按钮。

  3. 在弹出的窗口中,点击 "禁用继承"

  4. 选择 "从此对象中删除所有继承的权限"

  5. 点击 "添加" 按钮,然后点击 "选择主体"

  6. 输入 你的 Windows 用户名 ,点击 确定

  7. 在权限窗口中,勾选 "完全控制" ,点击 确定

  8. 确保只有当前用户拥有权限,点击 应用确定

HTTPS原理详解,从出发点到实现方式

· 阅读需 4 分钟

这一篇文章,主要记录我在学习网络安全相关知识的一些体会。

https

了解 https 之前,首先学习一下哈希算法。弄明白哈希算法的输入输出:输入一串不定长的数据,输出的字符串是定长的(比如 256bit)。

关于 https,首先在脑海中呈现的,应该是这样一个模型:

17109479490611710947948667.png

可以看出,https 就是在 http 的基础上,套了一层 SSL/TLS,而 SSL/TLS 的功能其实本质上是如何协商出安全的对称加密密钥以利用此密钥进行后续通讯的过程

我们来解释一下上面这句话的意思。因为 http 是明文传输,所以应该想办法对传输数据进行加密,使得黑客拿到的数据都是一堆无意义的字符串。加密需要密钥,密钥就像一把箱子的钥匙,数据就被所在箱子里。看起来,明文传输的问题解决了。

然而,密钥也是通过互联网进行传输的,意味着黑客也可以获得密钥,这意味着数据还是裸奔。所以,有必要对密钥也进行加密!听起来是不是在套娃了呢?别急,非对称加密终结了这个死结。所谓的非对称加密,就是让客户端(浏览器)保存公钥(任何人可获得),服务器保存私钥(绝对不能让其他人知道)。客户端带着用公钥加密的对称加密密钥(这里别被绕晕了哈),传给服务器,只有服务器的私钥才能解密,获得对称加密密钥,到这里,我们是不是可以理解出上面加粗句子中的协商的意思呢。

然而,问题到这里还没有结束。客户端是如何获得真实的公钥呢?肯定是不能由服务器传来的啦!这时候就要引入一个可信的第三方(CA)的概念了!服务器会去向这个权威第三方申请一个证书,证书的签发如下图所示。证书内带了站点的公钥CA 的私钥会加密这个证书,然后等到客户端获得证书时,再用CA 的公钥来解密证书,获得站点的公钥。而且,CA 的公钥是操作系统内置的,代表绝对可信。这样就能实现安全性的闭环了。

17110787820041711078781120.png

最后结合一点工作上的实际经验加深体会。常常听到别人说,某个项目网站的 https 证书过期了,要花钱续期(也不便宜了,一年几千)。这个证书就是上面的服务端证书,需要向 CA 申请!所以说,不能随便信任证书。

参考链接:


20 张图让你彻底弄懂 HTTPS 原理.md https://github.com/allentofight/easy-cs/blob/main/%E7%BD%91%E7%BB%9C/20%E5%BC%A0%E5%9B%BE%E8%AE%A9%E4%BD%A0%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82HTTPS%E5%8E%9F%E7%90%86.md


nginx 入门

· 阅读需 4 分钟

出现报错,nginx 的错误日志位置:

tail -f /var/log/nginx/error.log

静态网页

server{

listen 8000;
server_name localhost;

location / {
root /home/AdminLTE-3.2.0;
index index.html index2.html index3.html;
}

}

location 的运行方式有点绕。首先在浏览器输入网址,最重要的三部分:ip(域名)、端口号、路由。ip 的话,显然不需要写在配置里,因为是固定的。上面的 server_name 字段只是作为一个标识符,起区别作用。端口号对应的就是 listen。路由这部分由 location 确定。location 后面跟的就是路由,那么 nginx 服务器把 root 拼接上述的路由,得到的结果作为文件路径去硬盘里寻找文件(如 index.html)。

反向代理

反向代理本身可以视为一种基本能力,是后面的负载均衡、缓存等高级功能的前提。

server {

listen 8001;

server_name ruoyi.localhost;

location / {
proxy_pass http://localhost:8088;
}

}

将 8001 端口收到的请求,转发到 8088 端口。

proxy_pass 配置说明:

location /some/path/ {
proxy_pass http://localhost:8080;
}

如果 proxy-pass 的地址只配置到端口,不包含/或其他路径,那么 location 将被追加到转发地址中。 如上所示,访问 http://localhost/some/path/page.html 将被代理到 http://localhost:8080/some/path/page.html

location /some/path/ {
proxy_pass http://localhost:8080/zh-cn/;
}

如果 proxy-pass 的地址包括/或其他路径,那么/some/path 将会被替换,如上所示,访问 http://localhost/some/path/page.html 将被代理到 http://localhost:8080/zh-cn/page.html。

动静分离

动静分离是指图片、css、js 等文件的请求直接由 nginx 返回响应,而不进行转发,减少宝贵的动态服务器资源占用。

有两种动静分离的方法,第一种是把静态文件手动放到一个目录里,以后的所有静态文件请求都指向这个目录。另一种是比较熟悉的,首次请求走动态服务器,并且把文件存在特定的目录,这样也是被缓存。第二种方式避免了手动和跨服务器操作,故推荐。

proxy_cache_path /var/cache/nginx/data keys_zone=mycache:10m;

server {

listen 8001;
server_name ruoyi.localhost;

location / {
#设置buffer
proxy_buffers 16 4k;
proxy_buffer_size 2k;
proxy_pass http://localhost:8088;

}


location ~ \.(js|css|png|jpg|gif|ico) {
#设置cache
proxy_cache mycache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_valid any 5m;

proxy_pass http://localhost:8088;
}

location = /html/ie.html {

proxy_cache mycache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_valid any 5m;

proxy_pass http://localhost:8088;
}

location ^~ /fonts/ {

proxy_cache mycache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_valid any 5m;

proxy_pass http://localhost:8088;
}

}

proxy_cache_path 规定了缓存文件存放位置。keys_zone 作为标识供 location 引用。10m 表示缓存文件大小不超过 10m。location 中的 proxy_cache_valid 规定了缓存失效时间。

负载均衡

upstream ruoyi-apps {
#不写,采用轮循机制
ip_hash; #可确保来自同一客户端的请求将始终定向到同一服务器
hash $request_uri consistent;
server localhost:8080;
server localhost:8088;

}

server {

listen 8003;
server_name ruoyi.loadbalance;

location / {
proxy_pass http://ruoyi-apps;
}

}

用一个例子来记住 k8s 核心概念

· 阅读需 5 分钟

k8s 一直以概念繁杂著称。在容器编排这个领域,它是统治级的存在,可以说它就代表了这个领域内的几乎所有知识。因为我对这个领域并不熟悉,所以很难记住这些概念。只能一次次地翻看基础教程。

k8s 的背景是容器化部署盛行,加上微服务要求,一个系统的容器数量成百上千,这样,必须有一个专门的编排工具,达到高可用、可伸缩等目的。

linux上kafka安装和入门

· 阅读需 2 分钟

安装

之前在 windows 上安装 kafka 算是吃尽苦头。现在用 linux 机器再试一次。像 kafka 这种依赖 zookeeper 的服务,最简单的方法还得是 docker。具体的教程就参考这篇文字:


使用 Docker 部署 Kafka 单机版 https://ken.io/note/kafka-standalone-docker-install


kafkajs

公司项目的 kafka 是用 kafkajs 这个库来驱动的。但是,文档给出的例子没法直接运行,所以让 chatgpt 给出了一段能用的代码:

const { Kafka } = require("kafkajs");

// 创建 Kafka 客户端实例
const kafka = new Kafka({
clientId: "my-kafka-app",
brokers: ["localhost:9092"], // 你的 Kafka 服务器地址
});

// 创建生产者实例
const producer = kafka.producer();

// 创建消费者实例
const consumer = kafka.consumer({ groupId: "test-group" });

// 发送消息函数
const sendMessage = async () => {
await producer.connect();
await producer.send({
topic: "test-topic",
messages: [{ value: "Hello KafkaJS!" }],
});
await producer.disconnect();
};

// 接收消息函数
const receiveMessage = async () => {
await consumer.connect();
await consumer.subscribe({ topic: "test-topic", fromBeginning: true });

await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
console.log({
topic,
partition,
offset: message.offset,
value: message.value.toString(),
});
},
});
};

// 发送和接收消息
sendMessage()
.then(() => receiveMessage())
.catch(console.error);

17085683262981708568325402.png

在输出消息之前,控制台还打印出了几条日志。目前主要关注最后两条。消费者启动并加入到消费者组。为了确定 kafka 的主题中是否真的有这个消息,运行:

kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test-topic --from-beginning

基于Linux的Redis实战

· 阅读需 6 分钟

安装

**更新系统:**首先,确保您的系统已经更新到最新版本。您可以通过以下命令执行此操作: sudo yum update

**安装 Redis:**使用 yum 包管理器安装 Redis: sudo yum install redis

**启动 Redis 服务:**安装完成后,启动 Redis 服务: sudo systemctl start redis

**设置 Redis 开机自启动:**如果您希望 Redis 在系统启动时自动启动,可以执行以下命令: sudo systemctl enable redis

**验证 Redis 是否正在运行:**您可以使用以下命令检查 Redis 服务的运行状态: sudo systemctl status redis

如果一切正常,您应该会看到 Redis 正在运行的输出。

**测试 Redis:**您可以使用 redis-cli 命令行工具连接到 Redis 服务器并执行一些基本操作,例如设置键值对、获取键值对等: redis-cli

如果输入命令后出现报错:(error) NOAUTH Authentication required., 说明在 Redis 配置文件中启用了身份验证(通常是 redis.conf 文件),则需要输入密码才能登录。使用以下命令输入密码:

AUTH your_password

**关闭 Redis 服务(可选):**如果需要,您可以随时停止 Redis 服务: sudo systemctl stop redis

这样,您就在 CentOS 上成功安装和配置了 Redis。您可以根据需要进一步配置 Redis,例如更改端口、设置密码等。

五大数据类型

因为《redis 实战》这本书中的示例代码是用 python,所以这篇文章也以 python 为媒介来学习 redis。

Python 中有几个常用的 Redis 客户端库,其中最流行的是 redis-py。以下是 redis-py 中一些常用的 API 及其用法:

  • 连接到 Redis 服务器:

import redis

# 创建 Redis 连接
r = redis.Redis(host='localhost', port=6379, db=0)

# 或者使用连接池
pool = redis.ConnectionPool(host='localhost', port=6379, db=0)
r = redis.Redis(connection_pool=pool)
  • 字符串操作:

# 设置键值对
r.set('key', 'value')

# 获取值
value = r.get('key')

# 批量设置键值对
r.mset({'key1': 'value1', 'key2': 'value2'})

# 批量获取值
values = r.mget(['key1', 'key2'])
  • 哈希操作:
# 设置哈希值
r.hset('hash_key', 'field', 'value')

# 获取哈希值
value = r.hget('hash_key', 'field')

# 批量设置哈希值
r.hmset('hash_key', {'field1': 'value1', 'field2': 'value2'})

# 批量获取哈希值
values = r.hmget('hash_key', ['field1', 'field2'])
  • 列表操作:
# 在列表左侧添加元素
r.lpush('list_key', 'value1', 'value2')

# 在列表右侧添加元素
r.rpush('list_key', 'value3', 'value4')

# 获取列表范围内的元素
values = r.lrange('list_key', 0, -1)
  • 集合操作:
# 添加元素到集合
r.sadd('set_key', 'member1', 'member2')

# 获取集合所有成员
members = r.smembers('set_key')

# 从集合中移除元素
r.srem('set_key', 'member1')
  • 有序集合操作:
# 添加元素到有序集合
r.zadd('zset_key', {'member1': 1, 'member2': 2})

# 获取有序集合范围内的元素
members = r.zrange('zset_key', 0, -1, withscores=True)

五种数据类型分别是字符串、列表、哈希、集合、有序集合。都知道 redis 是一种键值对数据库,每种数据类型都体现了键值对。字符串、列表、集合比较简单,一级结构,键名对应的就是存储的数据。哈希和有序集合是二级结构,外层的键名指向数据结构的实例,内层的键名才是实际存值的地方。

有一个问题:如果我知道键名,但不知道数据类型,怎么获得值呢?

具体步骤如下:

  • 使用 TYPE 命令确定键的数据类型。
  • 根据数据类型执行相应的命令来获取值。

例如,假设你要获取名为 mykey 的键的值,但不确定其数据类型,可以按照以下步骤进行:

使用 TYPE 命令确定键的数据类型:TYPE mykey

根据返回的数据类型执行相应的命令来获取值。以下是不同数据类型的获取值的命令示例:

如果 mykey 的数据类型是字符串(string):GET mykey

如果 mykey 的数据类型是列表(list):LRANGE mykey 0 -1

如果 mykey 的数据类型是集合(set):SMEMBERS mykey

如果 mykey 的数据类型是有序集合(sorted set):ZRANGE mykey 0 -1 WITHSCORES

如果 mykey 的数据类型是哈希(hash):HGETALL mykey

根据实际情况选择合适的命令来获取值。

sql查询调优实战过程

· 阅读需 5 分钟

例 1 筛选字段没加索引

在处理一个长达 500 行的大型 sql 查询文件的过程中,我发现了响应非常慢。这段 sql 是为后台报表服务的,仅仅查询一天的数据就要花费接近 20 秒的时间,那么如果是一个月的话肯定超时了。毫无疑问,这个 sql 需要优化。

首先查看执行计划。如果是执行 explain 命令来获得执行计划,得到的 cost 并不能直接看出速度的快慢。因此,需要用explain (analyze)。由于查询是由好几个 CTE 子表组成的,所以执行计划也是分别给出了几个 CTE 子表的花费时间。虽然很长,但是,细心的查看后,果然发现了异常:

17068653903891706865390291.png

可以看到,在查询 money_bag_balance 这张子表时,actutal time(实际执行时间)达到了 2295 毫秒,他就是元凶。接下来看,发现是在 trade_list_internal 这张流水表上进行全表扫描,难怪这么慢!于是,我定位到 money_bag_balance 的 sql 语句,这张表是由 income 和 balance 合并而来,以 income 为例:

money_bag_income as (
select
COALESCE(SUM(CASE WHEN method = 'wx_pay' THEN real_fee ELSE 0 END),0) AS wechat_income,
COALESCE(SUM(CASE WHEN method = 'ali_pay' THEN real_fee ELSE 0 END),0) AS alipay_income,
COALESCE(SUM(CASE WHEN method = 'epay' THEN real_fee ELSE 0 END),0) AS epay_income,
COALESCE(SUM(CASE WHEN method = 'wallet_pay' THEN real_fee ELSE 0 END),0) AS money_bag_income_real,
COALESCE(SUM(CASE WHEN method = 'virtual_pay' THEN real_fee ELSE 0 END),0) AS money_bag_income_virtual,
COALESCE(SUM(CASE WHEN method = 'union_pay' THEN real_fee ELSE 0 END),0) AS union_pay_income,
COALESCE(SUM(CASE WHEN method = 'ccb_pay' THEN real_fee ELSE 0 END),0) AS ccb_pay_income,
COALESCE(SUM(CASE WHEN method = 'ccb_pay_dc' THEN real_fee ELSE 0 END),0) AS ccb_pay_dc_income
from trade_list_internal
where trade_type='charge'
and status='success'
-- and commit_time between '2023-07-17' and '2023-07-18'
and ( (''='2024-02-01' and ''='2024-02-02') or
(''<>'2024-02-01' and ''='2024-02-02' and commit_time>='2024-02-01' ) or
(''='2024-02-01' and ''<>'2024-02-02' and commit_time<'2024-02-02' ) or
(''<>'2024-02-01' and ''<>'2024-02-02' and commit_time >= '2024-02-01' and commit_time<'2024-02-02' ) )
),

commit_time 这个字段没加索引,所以走了全表查询。经过沟通,换了一个正确的且带索引的字段,查询在 0.2s 内完成!一下子提升了四十多倍!

例 2 在筛选字段上进行计算导致索引失效

和例 1 一样,本例也是一个营收分析的脚本。一样从执行计划开始。观察后发现,多次出现了一个过滤条件:

> Parallel Seq Scan on statistics_by_area_block_street a  (cost=0.00..66759.73 rows=911 width=282) (actual time=825.208..825.444 rows=120 loops=5)

Filter: (((data_owner_id)::text = ANY ('{66ebc3d0-5870-11ea-bbb2-d5c9d3c42033,8d51f590-21c7-11ec-8c04-8f0bedcb705d,95885640-cd3e-11ed-856c-93f3797cd92f,a65816d0-1489-11ee-a19d-0765044c45ed}'::text[])) AND (to_char((sta_date)::timestamp with time zone, 'yyyy-MM-dd'::text) >= '2024-02-03'::text) AND (to_char((sta_date)::timestamp with time zone, 'yyyy-MM-dd'::text) <= '2024-02-03'::text))'

Rows Removed by Filter: 172940

对应的 sql 片段:

where (  (''='2024-02-03' and ''='2024-02-03') or
(''<>'2024-02-03' and ''='2024-02-03' and to_char(sta_date,'yyyy-MM-dd')>='2024-02-03' ) or
(''='2024-02-03' and ''<>'2024-02-03' and to_char(sta_date,'yyyy-MM-dd')<='2024-02-03' ) or
(''<>'2024-02-03' and ''<>'2024-02-03' and to_char(sta_date,'yyyy-MM-dd') between '2024-02-03' and '2024-02-03' )
)
and a.data_owner_id in ('66ebc3d0-5870-11ea-bbb2-d5c9d3c42033','8d51f590-21c7-11ec-8c04-8f0bedcb705d','95885640-cd3e-11ed-856c-93f3797cd92f','a65816d0-1489-11ee-a19d-0765044c45ed')

data_owner_id 和 sta_date 两个字段,前者没有索引,后者加了索引。于是我先把 data_owner_id 加了索引,再运行,速度没有提升。看到后面,发现是 to_char 这个函数用在 sta_date 上导致索引失效。修改成 sta_date<'2024-02-03'后,速度从原来的 5.2s 提升到 0.6s。

例 3 count distinct 问题

原 sql 如下:

SELECT
count(CASE WHEN type = 'user' THEN 1 END) "total_count",
count(CASE WHEN "type" = 'user' AND created_at > CURRENT_DATE THEN 1 END) "today_count",
count(CASE WHEN "type" = 'user' AND created_at > CURRENT_DATE - 7 THEN 1 END) "7days_count",
count(CASE WHEN "type" = 'user' AND created_at > CURRENT_DATE - 30 THEN 1 END) "30days_count",
(SELECT count(DISTINCT user_id) FROM bind_plate_no WHERE deleted = FALSE) bind_total_count -- 执行慢的部分
FROM base_user

17082445872911708244586983.png

base_user 表的数据量是百万级。从执行计划看出,耗时部分出现在 bind_plate_no 表的 aggregate 上。对应的 sql 是一个典型的 count distinct 问题。虽然 user_id 加了索引,但是在 count 内部 distinct 需要大量的额外计算,因此很慢。试过去掉 distinct 后,这句 sql 就变得很快了。但是,不能破坏原有业务逻辑啊。解决方案是先去重、再汇总。

select count(*) from (select distinct user_id FROM bind_plate_no WHERE deleted = FALSE) tmp

新的 sql 运行速度从 9 秒,提升到 2.5 秒。

另外,我也试了另一种解决方案,即 count group by,本质也是先去重、再汇总。结果来看,比上述方案慢了 1 秒。

参考资料:


SQL 优化(二) 快速计算 Distinct Count http://www.jasongj.com/2015/03/15/count_distinct/


子查询和表联结的实践整理

· 阅读需 3 分钟

子查询

子查询是指嵌套在其他查询里的查询。主要有两个使用位置,一个是利用子查询进行过滤,另一个是作为计算字段使用子查询。

利用子查询进行过滤

SELECT cust_id 
FROM Orders
WHERE order_num IN (SELECT order_num
FROM OrderItems
WHERE prod_id = 'RGAN01');

顺序是先执行子查询,由内向外。实际上,这种用法的查询效率低,尽量用联结查询代替。

作为计算字段使用子查询

SELECT cust_name, 
cust_state,
(SELECT COUNT(*)
FROM Orders
WHERE Orders.cust_id = Customers.cust_id) AS orders
FROM Customers
ORDER BY cust_name;

-- 用left join优化
select a.cust_name, a.cust_state, b.count
from Customers a left join (select cust_id, count(cust_id) from Orders group by cust_id) b on a.cust_id=b.cust_id
order by cust_name

上述语句的执行顺序是,先在外层找到符合条件的数据,然后子查询对检索出的每条数据执行一次。

最后还有一种在from后面使用的子查询,作为主查询的数据源:

SELECT column1, column2
FROM (SELECT column3 FROM table1 WHERE condition) AS subquery;

这种情况下,用视图来替代应该是更聪明的选择,因为可以降低sql语句的复杂性。

扩展资料:


SQL子查询优化,看这一篇就够了 https://developer.aliyun.com/article/711481



SQL优化:慎用标量子查询,改用left join提升查询效率 https://www.cnblogs.com/goloving/p/15193637.html


表联结

想起来有一次回答full join是什么时,以为就是笛卡尔积的尴尬场景。

首先要了解联结的本质:多张表根据条件列出匹配的行,如果没有条件,就会返回笛卡尔积(也就是返回M*N行)。内联结(inner join)就是只返回表之间符合条件的行。而外连接还包括了没有关联行的行。

基于echarts的hexo博客热力图

· 阅读需 8 分钟

最近觉得博客的 archive 页太空旷了,和首页雷同,且文章多了之后,archieve 页很长。archive 页平时也不做停留,基本没有意义。因此,我决定改造一下 archive,修复并添加一些东西。首先是历史文章要支持展开和收起,这样,archive 页面就不会过长了。然后,仿照 github contribution, 做一个 博客热力图,记录历史文章提交日历图。

展开/收起控件添加

在年份上添加一个 onclick 事件,来控制对应年份文章的 display 属性。难点是原模板中年份 div 和 post-item div 是平级的,即:

<div class="year"></div>
<div class="post-item"></div>
<div class="post-item"></div>
<div class="post-item"></div>
...
<div class="year"></div>
<div class="post-item"></div>
<div class="post-item"></div>
<div class="post-item"></div>
...

这样兄弟选择器是没法只选中对应的年份的所有文章的。解决方法是给文章的 div 添加对应的年份 class,如'y2023'.并且给 onclick 事件传入年份变量。

post contribution

echarts 是一个开源的图表库,支持创建丰富类型的图表,且配置性高。


echarts 使用手册 https://echarts.apache.org/handbook/zh/get-started/


开始并不顺利,安装就卡住了。按照'hexo echarts'去 google,得到的方案是装一个 npm 包,但是实际上无效,图表没有被解析。后来用了质朴的 cdn 导入方式才成功。安装成功后,跑了一个 demo 也成功了,可是实现我的定制化需求时又犯了难。我的需求是,显示过去一年内,每天的文章创建数。echarts 有很多配置项,不看文档是搞不清楚的。然而,配置项文档都是文字说明,没有对应的效果展示。还好,有 chatgpt。描述好需求,gpt 给出了非常接近的答案,再经过几轮问答补充细节后,我想要的效果就达成了。

接着是数据的问题。我一开始的想法是,先在 ejs 中遍历 site.posts,获得一个键为日期,值为当日发布文章数的对象。然后创建一个数据生成函数,遍历近一年的每一天,从之前的对象中取值。但是,当我实现到一半,发现一个问题:ejs 创建的对象,无法被 script 标签读到。又是 gpt 给出了解决方案:将对象绑定在全局 window 上。

完整代码:

  <div id="main" style="width:100%; height:300px; margin: 0 auto"></div>
<%
const postsCountByDate = {};
site.posts.each((item, index) => {
let postDate = new Date(item.date);
// 获取年、月、日
let formattedDate = postDate.getFullYear() + '-' + (postDate.getMonth() + 1) + '-' + postDate.getDate();
postsCountByDate[formattedDate] = (postsCountByDate[formattedDate] || 0) + 1;
});
%>

<script>
// 将 postsCountByDate 绑定到全局对象 window 上
window.postsCountByDate = <%- JSON.stringify(postsCountByDate) %>;
</script>



<script type="text/javascript">


var chartDom = document.getElementById('main');
var myChart = echarts.init(chartDom);
var option;

function generateRandomData(startDate, endDate) {
let currentDate = new Date(startDate);
const endDateObj = new Date(endDate);
const data = [];


while (currentDate <= endDateObj) {
let formattedCur = currentDate.getFullYear() + '-' + (currentDate.getMonth() + 1) + '-' + currentDate.getDate();
const val = postsCountByDate[formattedCur] || 0
// const val = Math.floor(Math.random() * 100); // 生成 0 到 99 之间的随机数
data.push([currentDate.toLocaleDateString(), val]);

currentDate.setDate(currentDate.getDate() + 1);
}

return data;
}

const startDate = new Date(); //
startDate.setFullYear(startDate.getFullYear() - 1); // 一年前的日期 e.g 2023.1.10
const endDate = new Date(); // 当前日期 e.g 2024.1.10

const simulatedData = generateRandomData(startDate, endDate);

option = {
title: {
top: 30,
left: 'center',
text: 'Post Contribution'
},
tooltip: {},
visualMap: {
show: true,
min: 0,
max: 3, // 你的数据中的最大值
calculable: true,
orient: 'horizontal',
left: 'right',
bottom: 10,
inRange: {
color: ['#FFFFFF', '#FF0000'] // 白色到其他颜色的渐变色,可以根据需要调整颜色值
},
pieces: [
{ value: 0, color: '#FFFFFF' }, // 将值为0的数据映射到白色
{ min: 1 } // 其他数据按照设定的颜色渐变
]
},

calendar: {
top: 120,
left: 30,
right: 30,
cellSize: ['auto', 13],
range: [startDate, endDate],
itemStyle: {
borderWidth: 0.5
},
yearLabel: { show: false }
},
series: {
type: 'heatmap',
coordinateSystem: 'calendar',
data: simulatedData
}
};

option && myChart.setOption(option);


</script>

附上改造后的 archive 页:

17051159772241705115976390.png

更新

经过一段时间的使用,发现了以下问题:

  • 右下角的范围筛选控件很丑,但是删不掉;
  • 手机上显示效果很差。

在参考别人博客的类似效果后,我想实现:

  • 鼠标悬停在单元格上显示的是标题;
  • 去掉筛选控件

这一次,又是 chatgpt 的答案就几近完美。看来它对 echarts 非常熟悉。

// 获取博客文章数据
const postsCountByDate = {};
site.posts.each((item, index) => {
let postDate = new Date(item.date);
// 获取年、月、日
let formattedDate =
postDate.getFullYear() +
"-" +
(postDate.getMonth() + 1) +
"-" +
postDate.getDate();
let arrofTitle = postsCountByDate[formattedDate] || [];
arrofTitle.push(item.title);
postsCountByDate[formattedDate] = arrofTitle;
});

// heatmap options数据准备
var chartDom = document.getElementById("heatmap");
var myChart = echarts.init(chartDom);
var option;

var dates = Object.keys(postsCountByDate);
var colors = ["#FFFFFF", "#FFCCCC", "#FF9999", "#FF6666", "#FF3333", "#FF0000"];

// 提取数据中的标题
var titles = [];
Object.keys(postsCountByDate).forEach(function (key) {
titles = titles.concat(postsCountByDate[key]);
});

// 计算最大值
var maxCount = Math.max.apply(
null,
Object.values(postsCountByDate).map((arr) => arr.length)
);

// 计算开头结尾时间,作为calendar的range
const startDate = new Date(); //
startDate.setFullYear(startDate.getFullYear() - 1); // 一年前的日期 e.g 2023.1.10
// const startDate = moment().subtract(6, 'months').toDate(); // 6个月前的日期 e.g 2023.7.10
const endDate = new Date(); // 当前日期 e.g 2024.1.10
let startDate_ = new Date(startDate);
let endDate_ = new Date(endDate);
formatted_startDate =
startDate_.getFullYear() +
"-" +
(startDate_.getMonth() + 1) +
"-" +
startDate_.getDate();
formatted_endDate =
endDate_.getFullYear() +
"-" +
(endDate_.getMonth() + 1) +
"-" +
endDate_.getDate();

option = {
title: {
top: 0,
left: "center",
// text: 'Post Contribution'
},
tooltip: {
position: "top",
formatter: function (params) {
var date = params.data[0];
var titles = postsCountByDate[date] || [];
return titles.join("<br>");
},
},

calendar: {
top: "middle",
left: "center",
cellSize: ["auto", 13],
range: [formatted_startDate, formatted_endDate],
itemStyle: {
borderWidth: 0.5,
},
yearLabel: { show: false },
monthLabel: {
nameMap: [
"Jan",
"",
"Mar",
"",
"May",
"",
"Jul",
"",
"Sep",
"",
"Nov",
"",
],
},
dayLabel: { show: true, firstDay: 1, nameMap: "en" },
},
series: {
type: "heatmap",
coordinateSystem: "calendar",
data: dates.map(function (date) {
return [date, postsCountByDate[date].length];
}),
label: {
show: false,
formatter: function (params) {
return params.value[1];
},
},
itemStyle: {
normal: {
color: function (params) {
var count = params.value[1];
var level = Math.ceil(count / (maxCount / colors.length));
level = Math.min(level, colors.length - 1);
return colors[level];
},
},
},
},
};

option && myChart.setOption(option);

// 响应式图表
window.addEventListener("resize", function () {
myChart.resize();
});