记一次处理Postgres json字段的有趣历程

任务

1
2
3
前端根据IDs向数据库查询数据。
数据结构复杂,有null/array/object,不排除有其他形式的legacy数据。
array/object字段不一,path不一,可能存在某个key的value属于IDs。

任务被分割成前后端2部分,因为难点在数据库JSON的处理上。任务没有deadline的压力,这是一次学习和实践数据库的机会,选择前后端一起做。

最初的想法,利用Postgresjson处理函数将字段Id提取出来和目标IDs匹配。现实中,字段的形式和数据格式都过于复杂,难以(优雅地)组装在where语句。一筹莫展之际,发现一个捷径:

1
将json字段处理成text, 用text匹配IDs,绕过处理json.

类似于%like%in (1,2,3), Postgres支持SIMILAR TO正则匹配。问题简化成: 使用正则表达式匹配文本字符串。

1
WHERE data::text SIMILAR TO %(id-1|id-2|id-3)%

扩展

虽然SIMILAR TO是现实问题的最优解,却始终不是正解。 同时留下疑问,json to text对查询性能有多少影响? 原生jsonb方法效率怎么样?

这里实现一个包含100w条记录的table, 分别对比char column字段查询 && json 函数查询 && json to string 查询三种查询方式的效率。
官方文档提供了SQL/JSON Path Language, 更加高效的处理jsonb数据,但是额外地,需要将字段GIN indexes, 本次测试没有使用SQL/JSON Path Language, 选择更加通用的一般操作符.

git repo: https://github.com/pingfengafei/pstgres-json-efficiency
测试结果:https://github.com/pingfengafei/pstgres-json-efficiency/blob/master/log

测试结论:

1
2
3
1. 原生字段查询最快,大约是json查询的5倍效率,约是json to string的10倍。
2. json to string查询100w条记录,大约耗时2秒。
3. 尽量避免使用json/jsonb存储数据,必须时用jsonb + GIN index。

knapsack

做完爬楼梯问题后,我们继续挑战01背包问题,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const cap = 100;
const count = 10;
const weights = [0, 20, 5, 30, 6, 15, 13, 10, 27, 17, 13];
const values = [0, 20, 7, 25, 15, 30, 1, 7, 50, 40, 10];

let maps = [[0,0], [0,0]];

const getKnapsackValue = (cap, count, weights, values) => {
for(let i = 1; i <= count; i++){
for (let j = 1; j <= cap; j++) {
maps[i] = maps[i] || [0];
if(j < weights[i]){
maps[i][j] = maps[i-1][j] || 0;
}else{
maps[i][j] = Math.max( maps[i-1][j] || 0, ( maps[i-1][j-weights[i]] || 0 ) + values[i]);
}
}
}
}

getKnapsackValue(cap, count, weights, values)

console.log(maps[10][100])

代码能得出正确的答案,状态转移公式就不上了,网上全是。一些小心得:

1
2
1. weights values 和index遍历0位先填充0,方便遍历
2. maps[i][j]初始化用了一点不优雅的技巧。

这是最原始的版本,背包九讲里可知,O(n^2)时间复杂度是不能优化了,空间可以被优化,上面是O(n^2)

从爬楼梯问题入门动态规划

Description:
n个台阶的楼梯,一次只能爬1个台阶或者2个台阶,爬到n个台阶一共有多少种可能性?

我对这道题印象极其深刻,最早知道它是高中数学课上大象老师提出它,解题思路是:

  1. 记n个台阶的可能性是s(n),因为一次只能爬1或者2个台阶,因此上一步,只能是从第n-1爬一步,或者从第n-2爬2步
  2. 第n-1记s(n-1),第n-2记s(n-2)
  3. 于是可得出递归函数: s(n) = s(n-1) + s(n-2)

LeetCode 70: https://leetcode.com/problems/climbing-stairs/submissions/

我们以迅雷不及掩耳之响铃铛芝士提交代码:

1
2
3
4
5
6
7
8
9
10
11
var climbStairs = function(n) {
if(n === 1) {
return 1;
}

if(n === 2) {
return 2;
}

return climbStairs(n - 1) + climbStairs( n - 2)
};

系统反手一个超时!
问题出现在递归函数,展开递归函数,是一个近乎完全二叉树,容易得出结论:

1
2
3
4
`s(n-2)`计算2
`s(n-3)`计算3
。。。
`s(1)`计算n-1

计算量如此,映射到实际的运行时间上:t(n) = t(n-1) + t(n-2), 实际打表显示的耗时符合这个公式。
触目惊心,额外计算了这么多次。

我们要记录这些中间计算,这样耗时就是线性的n了。
我突然明白了动态第二个核心: 缓存中间计算,大约也叫剪纸吧,因为缓存子问题结果后,就不用再重复计算子问题了。在知乎上看到一个例子:

如何像小学生解释动态规划:

  1. 在纸上写1+1+1+1+1+1+1+1.
  2. 求出答案是8
  3. 在左侧写个1+,问答案是多少?
  4. 瞬间计算出答案9,原因是记录了中间值8.

好,我们用记录中间值得思路去解决这个问题:

1
2
3
4
5
6
7
8
9
10
const stepMap = {};
var climbStairs = function(n) {
if(n === 1) return 1;
if(n === 2) return 2;

let steps = (stepMap[n - 1] || climbStairs(n - 1)) + (stepMap[ n - 2] || climbStairs( n - 2));
stepMap[n] = steps;

return steps;
};

至此再也不用担心超时了。我们继续优化代码到js的最优雅解法, it’s beautiful.

1
2
3
4
var climbStairs = function(n, a=1, b=1) {
if(n == 1) return b;
return climbStairs(n-1,b,a+b);
};

以上可以得出DP的解题思路:

  1. 找出状态转移公式(用递归切割子问题)
  2. 缓存中间状态值。

当缺点变成桎梏.md

自己从小有注意力不集中的毛病,成年后,上课总是分神,起初以为是小问题,一直到工作以后才发现,渐渐地,小毛病变成了阻碍前进
的桎梏。做事情精力不集中又瞻前顾后。到了而立之年发现自己无一能持之以恒的爱好,做事情一叶蔽目缺乏大局观。包括写文章也是,
纵使心中万千思绪,写出来的文字缺杂乱无章缺乏中心。

30岁之后,精力和激情都和20岁小伙子不同,褪去了很多。需要懂得运营和偶尔孤注一掷的魄力。前者因为个人的能力开始走下坡路,就像打dota,纵然
豪情万丈,勤学苦练,30岁也是早已过了巅峰状态,反应慢了,操作更不上了,但是经验也多了。不能再当20岁的愣头青,要考积累的经验
慢慢运营,这是守势。饮食男女中说人生没有100%准备好的事情,遇事总得上,机会抓住站稳也就上去了。

刚才老婆说美股大跌,岔开去看美股了,公司的股价从210USD一路跌回了我上次卖出ESPP的价格。前些日子老婆告诫不要沾沾自喜,股票市场
风云万变,没变现都是浮云。被她一语成谶,犹记得沾沾自喜的嘴脸,现在笑不出来了吧。

距离写上面的内容已经过去快3个礼拜。美股风云变幻,谁主沉浮?好消息是公司股价涨到了160+,祝愿未来几天美股多涨点,容我喝一口汤。
这期间发生了一件让我和老婆都很伤感的悲剧。她的一位怀孕8个月的前同事突然脑溢血离开了,小孩活了下来。她和我同年,独生女,是我和老婆来到
上海后现实生活中活的最精致的一位女士。她是我的生活中第一个离开的同龄人,为此我伤心了几天。时间真是良药,前几天我都处在失落的状态,是万万不能
把事情写在文章中的,现在虽然回想起事情有些伤感,也没有什么大的悲怆了。

understand compose and pipe in JS

redux源代码中看到compose辅助函数

1
2
3
4
5
6
7
8
9
10
11
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

我对这里的compose函数感兴趣, compose的定义是,给定一个内部元素为函数(除了最后一个函数外,其它函数都只接受一个参数)的数组,从右往左依次执行,最终返回结果。和pipe函数相反。很长一段时间我不能理解compse函数,what a pity~

1
2
3
4
5
const add10 = x => x + 10
const minus5 = x => x - 5
const multi3 = x => x * 3
const div2 = x => x / 2
const num = 10

期望compose(add10, minus5, multi3, div2)(num)等价于:add10(minus5(multi3(div2(num))))

用数学归纳法推算规则

1
2
3
compose1 = add10 = x => x + 10
compose2 = y => compose1(minus5(y)) = y => ( x => x + 10 )(minus5(y)) = y => add10(minus5(y))
compose3 = z => compose2(multi3(z)) = z => (y => ( x => x + 10 )(minus5(y)))(multi3(z)) = z => add10(minus5(multi3(z)))

综上,

1
composeN = (...args) => compsoeN-1(fun(...args))

容易用reduce得出最优雅的js compse写法:

1
[add10, minus5, multi3, div2].reduce((a,b)=> (...args) => a(b(...args)))

也就是redux compose的写法. 由于目的是从右往左执行函数,中间步骤一定会被记录,有可能会导致栈溢出。

1
const compose =  Array.from({length: maxLenght }).map( x => x => x + 1).reduce((a,b)=> (...args)=> a(b(...args)))

测试到6000的时候(非精确值)报VM271:1 Uncaught RangeError: Maximum call stack size exceeded

理解了compose再回头看看pipe:

1
2
3
4
5
6
7
8
9
10
const add10 = x => x + 10
const minus5 = x => x - 5
const multi3 = x => x * 3
const div2 = x => x / 2
const num = 10

const chains = [add10,minus5,multi3,div2]

const compose = chains.reduce((a,b) => (...args)=> a(b(...args)))
const pipe = chains.reduce((a,b) => (...args) => b(a(...args)))

ctrip总结

前不久我找到了新东家,于是提出了离职。不知不觉在携程工作了一年9个月。由于一些原因,本应该还在输出bug和代码的我突然空闲了下来。我打算写一写总结,学习一下基础。

为什么要离职

如果说运气是实力的一部分,那么我实力可能比较强。

偶然的机会得知au招聘前端工程师。和岗位匹配,想了3分钟后投了简历。一来是因为维护的项目生命周期已经到了尽头。同时下半年经济环境很差,互联网企业的股票跌的很厉害,jd从5块钱跌到了2块钱,
我厂从5块钱跌成了2块半,其他互联网企业也不遑多让。直觉告诉我接下来会发生什么,于是迅雷不及掩耳之响铃铛之势换了工作,接下来听说到的互联网企业的行动验证了我的直觉。

最后在这个点选择au是因为梦想。在学生时代我对2家公司有特殊的情感,第一家是完美世界,第二家是au。如今,终于有机会补全梦想,即使需要放弃年终奖,即使几乎是平薪跳槽,又何妨?

在ctrip我学到了什么

我见识了大公司前端岗位的工作流程,极大的提高了沟通能力,这对我未来的工作很有庇佑。同时冗余的流程也让我感到了boring,大概现在互联网公司的氛围都差不多,因此我对BAT失去了兴趣。

万分的庆幸我的领导们为人很nice,在当下的氛围里简直是清流,没有官僚主义和形式主义,给三位领导点赞~
关于技术,我从映杰身上看到了学习如何学习的能力,包括不限于:

1
2
3
1.初窥函数式精髓,并且应用在代码中,最近几个高内聚的组件写起来得心应手,通过函数式组合,原来复杂的逻辑只需要一些组合就能实现。
2. 学习的能力,以前我写代码囫囵吞枣,追求大而且全,在携程里,我不断的反思这种风格的弊端,从作者身上学习新方法。
3. 感受作者的人格魅力,高山仰止,景行行止,虽不能至,心向往之。

剩下的几天怎么过?

除了正常的额工作外,查缺补漏。

一月记

忙碌的一月终于要结束了。按照时间顺序理一理。

4号陪老婆去医院建大卡,一顿操作后宝宝终于可以在国妇婴出生了。最近宝宝变得调皮了些许,时不时的踢他妈一脚,每当如此,他妈都开心的像个宝宝~

5号去医院做了个非麻胃镜。为什么选择非麻呢,麻醉胃镜六院排队排了很长,而我本人又不想麻醉,总感觉会伤身体,20来岁的年纪,这点么难受心想着,总应该不算什么。事与愿违,做的时候才明白什么叫做”不可抗力”。管道插进喉咙和胃部时,及时极力的控制自己,仍然忍不住作呕,流泪。不是哭泣的眼泪,只是泪水忍不住留下来。所幸检查结果还不错。

随后母亲要做二次手术了。请了四天假去西安陪母上。看到母上身上插满了仪器和管线,内心五味杂陈。相比这种罪比小小的胃镜要难受千百倍吧。那么母上第一次手术呢?术后去ICU看母亲。母亲说她肩膀疼,让我给她柔柔,我一边揉,一遍流泪。我的妻子呢?她在做手术的时候,我竟然丢下她去上班。留下她和母亲,故作坚强的打车回家。时间回到去年四月,我去ICU看奶奶,跪在地上流泪,一个礼拜后奶奶就走了。

十月回家,傍晚对母亲说去看一眼奶奶。母亲急切跑过来拉着我的手说,我儿,奶奶坟头草已经一人高了,晚上了,不要去,不要去。。。

过去的一年,家里遭受了太多太多苦难。这些苦难全压在父亲一个人身上,诸多辛苦不可言状。到如今,我似乎已经没有退路。家庭的单子,我抗了。

这个月,同时做了3个项目。到今天到了收尾期,写下这篇文章,回忆奶奶,祝贺母上康复,勉励自己。

license

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Copyright (c) <year> <copyright holders>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

纸上得来终觉浅,绝知此事要躬行。决定翻译一波MIT许可。

1
2
3
4
5
6
7
版权 (c) <年> <版权拥有者>
特此授予许可:对任何获得该软件副本以及(该软件)相关文档文件的任何人免费。处理该软件没有任何限制,包括使用,复制,修改,合并,出版,分发,以及出售软件的副本。并且允许个人遵照下列条款修改软件:

上述版权申明和这份许可申明应该被包含在所有软件副本或包含软件主体的软件中。

该软件按照原样提供,没有任何形式的警告,明示,或默许,包括但不限于适销性的保证,
适用于特定用途和非侵权。在任何情况下,作者或版权拥有者对于任何索赔、损毁或者其他责任免责。无论合同行为如何,由使用本软件或与本软件相关软件产生的其他交易的侵权或其他。

总结:

1
2
3
1. 任何责任与我无关
2. 随便用,随便改
3. 使用时必须加上我的lincence

类数组是什么?

js中偶尔会听到一个奇怪的名词,类数组(array-like)。又是一个奇怪的翻译,类,容易让人联想起class,英文名就不会有歧义。在前端,常见的有2种类数组,一种是函数的参数arguments,一种是获取的dom节点,document.getElementsByTagName以及jquery,zepto封装的方法。不禁要问什么是类数组,类数组和真正的数组有什么区别?为什么不能直接用Array的方法,用Array.prototype.method.call(array-like)却可以?

回到一切问题的起源,什么是类数组?
js有array和object,如果array-like不是array,那么必然是object。
举个列子:

1
2
3
4
(function(){
//object!!! 注意,箭头函数没有arguments
console.log(typeof arguments)
})()

很容易测试arguments是object,不是数组。

如何构造一个类数组?
更深刻的问,如何构造一个类似数组的对象,可以通过下标0123访问,有length长度?答案呼之欲出。

1
2
3
4
5
6
var obj = {
0: 'hello world',
1: 'hello react',
2: 'hello vue',
legth : 3
}

为什么可以用Array.prototype.method.call?
初步的理解是Array的内置方法实现了对类数组的遍历,待详查。