跳到主要内容

基于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();
});

sql优化工具——学会看执行计划

· 阅读需 6 分钟

预备知识

查询 sql 语句执行顺序

(8) SELECT (9)DISTINCT<Select_list>
(1) FROM <left_table> (3) <join_type>JOIN<right_table>
(2) ON<join_condition>
(4) WHERE<where_condition>
(5) GROUP BY<group_by_list>
(6) WITH {CUBE|ROLLUP}
(7) HAVING<having_condtion>
(10) ORDER BY<order_by_list>
(11) LIMIT<limit_number>

具体说明详见:


SQL 的书写顺序和执行顺序 https://zhuanlan.zhihu.com/p/77847158


优化目标

在前一段时间写了不下百段的逻辑库后,sql 优化将是我下一阶段的学习方向,它和业务表现息息相关。奇怪的是,它并没有在任何一个路线图上出现,尽管它其实非常重要。

首先明确什么是 sql 优化:

SQL 优化,就是指将一条 SQL 写的更加简洁,让 SQL 的执行速度更快,易读性与维护性更好。

其中,快自然是首要的优化目标,至于易读性嘛,只能说兼顾,毕竟大家都是在屎山上拉屎的那个。那么如何定义一段好的 sql 语句呢,标准如下:

减小查询的数据量、提升 SQL 的索引命中率

另外,从测试指标上,最基本的有执行时间,这在 navicat 上都有显示。遇到一条执行慢的 sql,应该先用 explain 命令得到查询计划,里面有很多有用的信息。

看懂 pgsql 上的执行计划

首先,最重要的,分清一次查询有没有走了索引: seq scan 是全表扫描,index scan / Bitmap Index Scan / Index Only Scan 是走了索引。

rows 是优化器预估的返回的行数,不是扫描的行数。rows 主要是为优化器选择合适的执行计划做参考的。

条件过滤:出现 Filter。

嵌套循环连接,一般来说,这个是导致 sql 变慢的重要原因之一。在联结(join)操作时就会出现。优化的基本原则是小表驱动大表。下面是一个包含 Nest Loop Join 的简单执行计划的示例:

EXPLAIN SELECT *
FROM table1
JOIN table2 ON table1.column_id = table2.column_id;

这里是一个示例执行计划的输出:

Nested Loop Join
(cost=1000.00..2500.00 rows=100 width=32)
-> Seq Scan on table1
(cost=0.00..500.00 rows=50 width=16)
-> Index Scan using index_column_id on table2
(cost=500.00..1000.00 rows=50 width=16)
Index Cond: (table1.column_id = table2.column_id)

在这个执行计划中,可以看到:

  • Nested Loop Join 表示使用了 Nest Loop Join。
  • Seq Scan on table1 表示对 table1 进行了顺序扫描,即全表扫描。
  • Index Scan using index_column_id on table2 表示对 table2 使用了索引扫描。
  • Index Cond: (table1.column_id = table2.column_id) 表示连接条件是基于列 column_id 的相等条件。

散列连接(Hash Join)是数据库查询中一种常见的连接算法,用于将两个表的数据连接起来。与 Nest Loop Join 不同,Hash Join 的连接过程不是基于嵌套循环,而是通过散列算法将连接条件的列的值映射到一个散列表中,然后在散列表中查找匹配的行。

以下是 Hash Join 的基本步骤:

  • 构建散列表:将连接条件的列的值通过散列算法映射到散列表中。

  • 将第一个表的每一行添加到散列表中。

  • 遍历第二个表的每一行,通过散列算法找到散列表中匹配的行。 相比于 Nest Loop Join,Hash Join 的优势主要体现在以下几个方面:

  • 性能: 在某些情况下,Hash Join 的性能可能比 Nest Loop Join 更好。特别是在连接大型表时,Hash Join 的性能通常更高效,因为它可以利用散列表的快速查找特性。

  • 适用于等值连接: Hash Join 通常用于等值连接(即连接条件是相等关系),而 Nest Loop Join 更适合处理其他类型的连接条件。如果连接条件是等值关系,Hash Join 可能会更为高效。

  • 适用于大型表: 当连接的表很大时,Hash Join 可以更好地利用内存,因为它在内存中构建散列表。这有助于减少 I/O 操作,提高查询性能。

然而,Hash Join 也有一些限制,例如对内存的需求较高,如果内存不足可能导致性能下降。因此,在选择连接算法时,需要根据具体的查询和表结构来进行优化。数据库优化器通常会根据统计信息和查询条件选择合适的连接策略。

面向对象编程及其在 javascript 中的实现

· 阅读需 5 分钟

历史

谈到 OOP(面向对象编程),我的印象总是停留在我刚学一些静态语言时(如 java),相对于 python、JavaScript,OOP 的概念总是被很早的引入。视频里的讲师总是举一些诸如 Animal 类和 Cat\Dog 对象的例子。Ok 能听懂,so what the fuck is next? 这东西到底有啥用?实际编写网站项目时,也用不着什么 Animal 类啊。

在一番探索后,我目前的想法是:OOP 本身是为了降低代码的耦合度而提出的,那么一切代码耦合度较高的地方都可以尝试用 OOP。另外,现代的框架很多时候是这样工作的,框架规范了写法是在类内书写属性和方法,然后创建对象的工作交给了框架在后台进行,故看不到。这样的框架在前后端都有,比如 react、eggjs。也就是说,实际上 OOP 无处不在,学习 OOP 有助于理解框架并更好的使用。

说到 Javascript 的面向对象,ES6 之前是借助一套复杂难懂的原型系统实现,ES6 之后向主流的面向对象语言写法靠近,采用了 class 关键字来创建类。以前的那套只做了解,重点掌握的应该是现代的写法。

规范

OOP 作为一种编程范式,有它自己的一套规定和要素。

属性和方法

class Animal {
constructor(type = "cat", legs = 4) {
this.type = type;
this.legs = legs;
}
}

let dog = new Animal((type = "dog"), (legs = 4));
dog.legs = 2; // 对象属性可以修改

constructor 构造函数执行类的初始化操作,如果不需要初始化,可以不定义构造函数,这种情况相当于构造函数内部为空。

静态方法

无需实例化对象就可以调用类中的方法。

class Animal {
constructor(type = "cat", legs = 4) {
this.type = type;
this.legs = legs;
}

static return10() {
return "10";
}
}

Animal.return10();

getter/setter

明明是方法,但是调用时形式像是属性。

class Animal {
constructor(type = "cat", legs = 4) {
this.type = type;
this.legs = legs;
}

get metaData() {
return `${this.type}, ${this.legs}`;
}

set setType(value) {
this.type = value;
}
}

let dog = new Animal((type = "dog"), (legs = "2"));

console.log(dog.metaData); // getter调用
dog.setType = "alaska dog"; // setter调用

继承

新类拓展父类,而不需要将父类复制一遍,这就是继承。问题是,如何继承?如何拓展?借助 super 关键字。

class Animal {
constructor(type = "cat", legs = 4) {
this.type = type;
this.legs = legs;
}

makeNoise(noise = "loud noise") {
console.log(noise);
}
}

class Cat extends Animal {
constructor(type, legs, tail) {
super(type, legs); // 继承
this.tail = tail;
}

makeNoise(noise, db) {
// 重写
super.makeNoise(noise); // 继承
console.log(`DB is ${db}`); // 拓展的新功能
}
}

let cat = new Cat("cat", 4, true);

cat.makeNoise("meow", 40);

这里的 super 就起到继承的作用,构造函数的继承就是 super(),而方法的继承是 super.methodName()。super 只能在类的构造函数和方法中使用。必须在构造函数中首先调用 super:在子类的构造函数中,必须在使用 this 之前先调用 super。否则会抛出错误。

以下内容只存在于 typescript 中。

权限控制符:public、private、protect

对一个变量、方法的使用范围做限制的修饰符。public 可以在类和对象使用;private 只能在类内使用;private 可以在类及其子类中使用。

interface 接口

接口的含义是,使用这个接口(关键字是 implements)的类,必须将接口中的属性和方法全部实现一遍。

abstract class 抽象类

抽象类是类的模板,可以被类继承,但是不能直接实例化出对象。

拧巴的地方

JavaScript 中并没有类这个数据类型,类的实质是函数:

class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function

ES6 中的 class 很大程度上是基于既有原型机制的语法糖。

es6重要特性

· 阅读需 3 分钟

前言

语言学习看文档是非常无聊的,所以找来视频来学,并做了笔记。

模板语法

当返回一串比较复杂的字符串时,优先使用模板语法:

`${var1}, ${var2}`;

优点:模板所见即所得,即不需要敲换行、空格等占位符。

解构和扩展运算符

一切可迭代的数据类型(数组、对象等)都可以解构。解构出的是子元素。扩展运算府(三个点)也是将子元素拿出来放到一个新数据里。他们两个容易弄混:

解构:
let income = [10, 20, 30];
let [a,b,c]=income //a=10,b=20,c=30

扩展运算符:
let x=[...income] //x=[10,20,30]

看出差别了吧,解构是为了把数据取出来赋值给分散的新变量,而扩展运算符是把数据取出来包裹起来作为整体赋给一个变量。

对象字面量

如果对象的属性是从之前声明的变量获得的,那么在创建变量时,可以直接用变量来进行声明,而不需要写完整的键值对:

let name, age;
(name = "jason"), (age = 18);
let user = {
name,
age,
};

剩余操作符

"Rest operator" 可以翻译为 "剩余操作符" 或者 "剩余参数"。在 JavaScript 中,这个概念指的是使用三个点符号(...)来收集函数中的剩余参数,将它们组合成一个数组。

举个例子,假设有一个函数用来计算总和:

function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3, 4)); // 输出 10

let const

let/const 一直是 ES6 经久不衰的一个重要知识点。首先必须要知道,他俩为什么要发明出来取代 var。因为 var 有一个让人非常不爽的特点:变量提升。这是一个十分臭名昭著的特性,给 JavaScript 代码带来了很多 bug。所以 let、const 被提出,他俩就非常老实,只在自己的块级作用域内生效。也许 javascript 一开始就本该如此,谁知道创始人发了什么疯呢。

export import

见之前博文:javascript 模块系统

Class

见之前博文:面向对象编程及其在 javascript 中的实现

async/await promise

见之前博文:同步异步任务在 javascript 中的实现

进程、线程与nodejs

· 阅读需 9 分钟

这篇文章是补齐我一些关于进程和线程的基础知识,以及他们和 nodejs 的关系。形式为 chatgpt 问答。

Q: 什么是计算机的进程和线程

在计算机科学中,进程(Process)和线程(Thread)是操作系统中用于执行程序的两个重要概念。

进程是指在计算机中正在运行的程序的实例。每个进程都有自己独立的内存空间、程序代码和数据。进程之间是相互独立的,它们不能直接访问其他进程的内存空间。进程有自己的程序计数器(Program Counter)、寄存器集合、打开的文件和其他系统资源等。操作系统通过调度算法来分配处理器时间给不同的进程,使它们能够并发执行。

线程是进程内的执行单元。一个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源。线程之间可以更方便地进行通信和共享数据,因为它们可以直接读取和修改进程的内存。线程拥有自己的程序计数器、栈、寄存器集合,但它们共享进程的其他资源。由于线程共享了大部分资源,所以线程间的切换开销较小,可以实现更高效的并发执行。

与进程相比,线程具有以下优点:

  1. 线程的创建和销毁开销较小,速度更快。
  2. 线程之间的切换开销较小,响应时间更短。
  3. 线程共享进程的内存空间,可以更方便地进行数据共享和通信。

然而,线程也存在一些问题:

  1. 线程之间共享内存,需要进行同步和互斥操作,以避免数据竞争和不一致性。
  2. 一个线程的错误可能会影响到整个进程的稳定性。
  3. 多线程编程更加复杂,需要考虑线程同步、死锁等并发编程问题。

总结起来,进程和线程是操作系统中用于执行程序的两个基本概念。进程是程序的实例,具有独立的内存空间和系统资源;线程是进程内的执行单元,共享进程的内存空间。线程可以实现更高效的并发执行,但也需要注意并发编程带来的问题。

Q: 以 nodejs 处理网络请求为例,说明进程和线程的关系

在 Node.js 中,网络请求是通过事件驱动的非阻塞 I/O(Input/Output)模型来处理的。这个模型基于单线程的事件循环机制。

在 Node.js 中,一个进程通常指的是一个 Node.js 实例,它运行在操作系统的一个进程中。这个进程是由 Node.js 程序启动的,并拥有自己的内存空间、程序代码和系统资源。

在这个进程内部,Node.js 使用了单线程的事件循环机制来处理网络请求。事件循环是一个无限循环,不断地监听事件队列中的事件,并执行相应的回调函数。当有网络请求到达时,Node.js 将其包装成一个事件,并将其放入事件队列中,然后继续执行下一个事件。这样,Node.js 可以在处理一个请求的同时监听其他请求。

这里的关键点是,尽管 Node.js 使用单线程来处理事件循环,但它通过非阻塞 I/O 操作的方式,实现了对并发请求的高效处理。当一个请求需要进行网络 I/O 操作(如读取文件、发送 HTTP 请求等)时,Node.js 会将这个操作交给底层操作系统,并立即返回控制权。在等待 I/O 操作完成的期间,Node.js 可以继续处理其他请求,而不会被阻塞。

底层操作系统通常会使用多线程来处理具体的 I/O 操作,例如在读取文件时,可能会有一个线程负责从磁盘中读取数据,并将数据返回给 Node.js。这个过程对于 Node.js 来说是透明的,它只需要等待 I/O 操作完成,并通过事件机制得到通知。

因此,在 Node.js 中,进程代表了一个 Node.js 实例,而线程的管理是由底层操作系统负责的。Node.js 利用单线程的事件循环机制和底层操作系统的多线程 I/O 处理,实现了高效的网络请求处理。

Q: nodejs 进程只能利用到一个核心吗

在默认情况下,Node.js 是单线程的,它使用单个线程来执行 JavaScript 代码。这意味着一个 Node.js 进程只能利用到一个 CPU 核心。

然而,Node.js 通过使用事件驱动、非阻塞 I/O 和异步编程模型来实现高效的并发处理。它使用事件循环机制来处理并发请求,而不是为每个请求创建一个新的线程。这种设计使得单个 Node.js 进程可以处理大量的并发请求,而无需为每个请求分配一个单独的线程。

尽管 Node.js 的 JavaScript 代码执行是单线程的,但它可以利用多核处理器的优势。通过使用集群模块或者进程管理工具,可以创建多个 Node.js 进程并将请求分发给它们,从而实现并行处理。每个 Node.js 进程都可以运行在不同的 CPU 核心上,以充分利用多核处理器的计算能力。

例如,可以使用 Node.js 的 cluster 模块来创建一个主进程和多个工作进程,每个工作进程都可以运行在不同的 CPU 核心上,以并行处理请求。主进程负责接收和分发请求,而工作进程负责实际的请求处理。

另外,还可以使用一些第三方模块和工具,如 PM2、Nginx 等,来管理和扩展 Node.js 进程,以实现负载均衡和高可用性。

需要注意的是,多进程的部署需要考虑进程间的通信、数据同步和共享资源等问题。此外,由于每个进程都会占用一定的系统资源,过多的进程数量可能会导致系统负载增加。因此,在实际部署时,需要根据具体的场景和硬件资源配置来合理设置进程数量。

pgsql常用且容易忘记的一些用法

· 阅读需 3 分钟
Jason Lee

总结一些工作中非常有用的 pgsql 用法。

日期类

加入 time 列是 timestamp 类型的,那么

需要获得昨日的数据:select ... where time between current_date-1 and current_date;

过去 24 小时:select ... where time > current_timestamp - interval '1 day';

今日的数据:select ... where time > current_date

过去七天的数据并按天列出:select ... where time > current_date-7 group by extract(day from parking_time)

需要注意的是,在 where 子句中慎用强转符号::,因为这会让该列索引失效,从而搜索速度变得很慢。应该变通成上面这些形式,即 sql 会在 timestamp 类型和 date 类型比较时自动将 date 类型转为 timestamp 类型。

在 PostgreSQL 中,可以使用不同的函数和模式来将 timestamp 数据类型转换为特定的时间格式。以下是几种常见的方法:

  1. 使用 TO_CHAR 函数:TO_CHAR(timestamp, 'format') 函数将 timestamp 转换为指定的时间格式。例如,要将 timestamp 转换为年-月-日 小时:分钟:秒 的格式,可以使用以下代码:

    SELECT TO_CHAR(timestamp_column, 'YYYY-MM-DD HH24:MI:SS') FROM your_table;
    ```

  2. 使用 EXTRACT 函数:EXTRACT(field FROM timestamp) 函数允许提取 timestamp 中的特定时间部分(如年、月、日、小时等)。然后,可以将提取的时间部分按照需要的格式进行拼接。例如,以下代码将 timestamp 转换为年-月-日 格式:

    SELECT EXTRACT(YEAR FROM timestamp_column) || '-' || EXTRACT(MONTH FROM timestamp_column) || '-' || EXTRACT(DAY FROM timestamp_column) FROM your_table;
    ```

  3. 使用 TO_TIMESTAMP 函数和 TO_CHAR 函数的组合:如果要在转换过程中进行一些计算或调整,可以使用 TO_TIMESTAMP 函数将 timestamp 转换为特定格式的时间戳,然后再使用 TO_CHAR 函数将其格式化为字符串。例如,以下代码将 timestamp 转换为带有 AM/PM 标记的小时:分钟:秒 格式:

    SELECT TO_CHAR(TO_TIMESTAMP(timestamp_column), 'HH12:MI:SS AM') FROM your_table;
    ```

这些方法只是其中的几种,具体的选择取决于所需的时间格式和转换的要求。可以根据具体情况选择合适的方法。

having 和 where 的区别

这里有另一种理解方法,WHERE 在数据分组前进行过滤,HAVING 在数据分组后进行过滤。这是一个重要的区别,WHERE 排除的行不包括在分组中。这可能会改变计算值,从而影响 HAVING 子句中基于这些值过滤掉的分组。

python机器学习-模型评估与参数调优

· 阅读需 3 分钟
Jason Lee

性能指标(二分类问题)

混淆矩阵

首先,为什么在有准确率(accuracy)的情况下,还要引入别的指标呢?因为受困于数据收集的客观限制,容易出现不平衡问题,比如正类的数量远远大于负类。这样,即使模型将所有样本预测为正类,损失函数也很低,达到了欺骗的效果。因此,我们需要将正类和负类分别的预测结果列出来,如混淆矩阵。

16981566899581698156689045.png

准确率和召回率

基于混淆矩阵,得到了准确率(precision)和召回率(recall):

precision = TP/(TP+FP)
recall = TP/(TP+FN)

ROC 曲线

我们通过真正率(TPR)和假真率(FPR)来衡量分类器的性能。

通过 ROC 空间,我们明白了一条 ROC 曲线其实代表了无数个分类器。那么我们为什么常常用一条 ROC 曲线来描述一个分类器呢?仔细观察 ROC 曲线,发现其都是上升的曲线(斜率大于 0),且都通过点(0,0)和点(1,1)。其实,这些点代表着一个分类器在不同阈值下的分类效果,具体的,曲线从左往右可以认为是阈值从 0 到 1 的变化过程。当分类器阈值为 0,代表不加以识别全部判断为 0,此时 TP=FP=0,TPR=TP/P=0,FPR=FP/N=0;当分类器阈值为 1,代表不加以识别全部判断为 1,此时 FN=TN=0,P=TP+FN=TP, TPR=TP/P=1,N=FP+TN=FP, FPR=FP/N=1。所以,ROC 曲线描述的其实是分类器性能随着分类器阈值的变化而变化的过程。对于 ROC 曲线,一个重要的特征是它的面积,面积为 0.5 为随机分类,识别能力为 0,面积越接近于 1 识别能力越强,面积等于 1 为完全识别。