V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  wxf666  ›  全部回复第 4 页 / 共 34 页
回复总数  665
1  2  3  4  5  6  7  8  9  10 ... 34  
@wuhao1 相比磁盘存储,为嘛不选 SQLite 数据库呢?感觉优点还行呀:


1. 单表 1.3 亿 100 GB 大小时,六七年前轻薄本上,仍能上万随机读/写事务并发。且只占用 16 MB 内存。[源码在隔壁帖]( /t/1075881#reply65 )


2. 官方宣称,相比文件系统,10 KB 文件存数据库里,能快 35%,节省 20% 空间。[文章]( https://sqlite.org/fasterthanfs.html )

你按章节来存的话,假设每章 5000 汉字,UTF-16 存,恰好 10 KB 。


3. 支持全文索引,甚至拼音/首字母/多音字。可无限搜索事务同时进行。

微信宣称,手机端百万百字聊天记录,搜三字词,只需 0.0029 秒。10 秒全文索引完毕。[文章]( https://mp.weixin.qq.com/s/Ph0jykLr5CMF-xFgoJw5UQ )


4. 备份迁移时,没有天量小文件烦恼。

两周后,SQLite 会发布一个,[远程增量同步工具]( https://sqlite.org/draft/rsync.html )。
@815979670 #17 全是一个 SQLite 文件,应该也没啥不妥吧。。

我在隔壁帖,测试了单表 1.3 亿 100 GB 数据,六七年前轻薄本上,还能上万随机读/写事务并发。。


压缩存储,感觉用 zstd 较好?压缩率接近 lzma ,但解压速度快 7 倍。。

可以测试一下,每 N 个章节一起压缩后总大小 S0 ,与整本小说压缩后大小 S1 ,的关系。

选 N 尽量小(只读取一章时,不用浪费太多力气解压更多章节),S0 又尽可能逼近 S1 的( N 太小,会浪费很多空间,反复存储词典啥的?导致 S0 远大于 S1 。。)


或者试试,行级别的 zstd 压缩插件( https://github.com/phiresky/sqlite-zstd ),或者页级别的( https://github.com/mlin/sqlite_zstd_vfs ),或者官方 $4000 的( https://sqlite.org/purchase/zipvfs


对了,中文内容的话,换成 UTF-16 存储,能直接省 1/3 空间。。
87 天前
回复了 iamtuzi3333 创建的主题 数据库 大佬们,想请教一下数据库设计
用 SQLite 试了一下,亿级数据,上万并发,好像没啥问题?



- 单表数据:1.3 亿,100 GB

- 事务每秒:4.6 W 随机读,1 W 随机写

- 内存占用:16 MB ( Python 脚本,包括 SQLite 内存缓存)

- 测试硬件:六七年前轻薄本,SATA 低端固态

- 测试内容:模拟 500 设备,每秒各保存 200 浮点数据,连续三天



## 脚本使用方法

- 随机写入测试

```shell
# 从上次保存时间戳开始(不存在则为年初),每递增一秒,就写入 500 设备,各 200 浮点数据。直至写入 1W 记录为止
$ python3 test.py -w -d 设备数(默认 500 ) -n 写入行数(默认 1W )
```

- 随机读取测试

```shell
# 从 500 设备中,随机选一台,再随机选某个时间,取数据。直至读取 1W 记录为止
$ python3 test.py -r -d 设备数(默认 500 ) -n 读取行数(默认 1W )

# 最多运行 10 秒
$ timeout -s INT 10 python3 test.py -r

# 八进程同时测试
$ seq 8 | xargs -P 8 -I{} python3 test.py -r
```



## 测试脚本代码

```python
# V 站吞空格,缩进改为全角空格了

import time
import apsw
import random
import struct
import argparse
import itertools
from datetime import datetime, timezone

DEFAULT_DEVICES = 500
DEFAULT_RECORDS = 10000
SQLITE_MAX_WAL_PAGES = 10000
DB_PATH = '/数据库路径/文件名.db'
DEFAULT_START_TIME = int(datetime.strptime('2024-01-01 00:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc).timestamp())

count = 0
db: apsw.Connection
devices = DEFAULT_DEVICES
records = DEFAULT_RECORDS
dev_time_range: list[range] = []


def parse_args():
   parser = argparse.ArgumentParser(description="SQLite 测试读写多传感器数据")
   group = parser.add_mutually_exclusive_group(required=True)
   group.add_argument('-r', action='store_true', help="随机读取")
   group.add_argument('-w', action='store_true', help="随机写入")
   parser.add_argument('-d', type=int, default=DEFAULT_DEVICES, help=f"设备数(默认 {DEFAULT_DEVICES})")
   parser.add_argument('-n', type=int, default=DEFAULT_RECORDS, help=f"要测试的记录数(默认 {DEFAULT_RECORDS})")

   global devices, records
   args = parser.parse_args()
   devices = args.d
   records = args.n
   return args


# 随机写的页面足够多时,确保落盘并重置 WAL 文件
def sqlite3_wal_hook(db: apsw.Connection, name: str, pages: int):
   if pages > SQLITE_MAX_WAL_PAGES:
     db.wal_checkpoint(mode=apsw.SQLITE_CHECKPOINT_RESTART)
   return apsw.SQLITE_OK


def init_db():
   global db
   db = apsw.Connection(DB_PATH)
   db.execute('PRAGMA journal_mode = WAL')
   db.execute('PRAGMA busy_timeout = 5000')
   db.execute('PRAGMA synchronous = NORMAL')
   db.setwalhook(sqlite3_wal_hook)

   db.execute('''
     CREATE TABLE IF NOT EXISTS device_data (
       id     INTEGER PRIMARY KEY,
       dev_id   AS (id >> 32),
       created AS (id & 0xFFFFFFFF),
       data    BLOB
    )
  ''')


# 获取每个设备,已保存数据的时间范围
def get_dev_time_range():
   rows = db.execute('''
     SELECT dev_min.created, dev_max.created + 1
     FROM (SELECT (max(id) >> 32) + 1 dev_count FROM device_data)
     JOIN json_each(REPLACE(FORMAT('[%*.*s]', dev_count, dev_count, '0'), ' ', '0,')) dev
     JOIN device_data dev_min ON dev_min.id = (SELECT min(id) FROM device_data WHERE id >= dev.key << 32)
     JOIN device_data dev_max ON dev_max.id = (SELECT max(id) FROM device_data WHERE id <= dev.key << 32 | 0xFFFFFFFF)
  ''').fetchall()

   dev_time_range.extend(list(itertools.starmap(range, rows))[:devices])
   dev_time_range.extend([range(DEFAULT_START_TIME, DEFAULT_START_TIME)] * max(devices - len(rows), 0))


def test_read():
   global count
   items = list(enumerate(dev_time_range))
   weights = list(itertools.accumulate(map(lambda i: i.stop - i.start, dev_time_range)))

   while count < records:
    # 以每设备时长为权重,随机抽取一个设备,再从其时间范围随机抽取时间点
     dev, time_range = random.choices(items, cum_weights=weights)[0]
     db.execute('''
       SELECT data
       FROM device_data
       WHERE id = ? << 32 | ?
    ''', (dev, random.choice(time_range))).fetchone()
     count += 1


def test_write():
   global count
   start_time = min(dev_time_range, key=lambda i: i.stop).stop

   for ts in itertools.count(start_time):
     for dev in range(devices):
       if count >= records:
         return
       elif ts in dev_time_range[dev]:
         continue

       floats = [random.random() for i in range(200)]
       data = struct.pack('200f', *floats)

       db.execute('BEGIN IMMEDIATE')
       db.execute('''
         INSERT INTO device_data (id, data)
         VALUES (? << 32 | ?, ?)
      ''', (dev, ts, data))
       db.execute('COMMIT')
       count += 1


def test(is_read: bool):
   init_db()
   get_dev_time_range()
   start_time = time.time()
   try:
     test_read() if is_read else test_write()
   except KeyboardInterrupt:
     pass
   finally:
     duration = time.time() - start_time
     print(f'在 {duration:6.2f} 秒内,随机{"写读"[is_read]} {count:6d} 行,平均 {count / duration:8.2f} 行/秒')


if __name__ == '__main__':
   args = parse_args()
   test(args.r)
```



## 1.3 亿 100 GB 数据库,文件结构信息分析

https://i.imgur.com/yL0rJ7E.png
91 天前
回复了 iamtuzi3333 创建的主题 数据库 大佬们,想请教一下数据库设计
@Mithril #36 这种普通关系型数据库,应该也没问题吧?

表结构:( 主键:《设备 ID ,毫秒时间戳》,200 个浮点数组)
91 天前
回复了 iamtuzi3333 创建的主题 数据库 大佬们,想请教一下数据库设计
@iamtuzi3333 #31

你们一天,就要存几千万行,共计 几十 GB 数据吗?


如,假设 300 设备,每设备每秒 200 单精度浮点,

算下来,一天就有 300 x 86400 = 2600W 行数据?

光是存浮点数据,共计就要 2600W x 200 x 4 / 2 ^ 30 = 19.3 GB ?
91 天前
回复了 iamtuzi3333 创建的主题 数据库 大佬们,想请教一下数据库设计
@iamtuzi3333 #24

换成一般关系数据库,你的表结构是这样吗:

( ID ,设备 ID ,毫秒时间戳,浮点数据),其中加了(设备 ID ,毫秒时间戳)索引,

你查询需求是:某设备,在某时间段内的,所有浮点数据?
94 天前
回复了 wuhao1 创建的主题 MySQL 老生常谈 关于 子查询的应用
@ldx78203199 #2

所以数据库的推荐用法,只能是当成 KV 数据库,得到数据后,再手动组装?



@sagaxu #4

不缓存应该也没事吧?

现在千元消费级固态,都能做到 100W 随机读写 / 秒( 4200 x 1024 / 4 = 107.5W ),

换句话说,数据库就算不用内存缓存,读写数据全走固态 IO ,应该也能做到几十万并发?

是这样算吗?

https://i.imgur.com/F29pmQ6.png https://i.imgur.com/F29pmQ6.png

https://i.imgur.com/uB205yf.jpeg
@lonelyparasol #23 三年前,SQLite 支持字段严格检查类型了: https://sqlite.org/stricttables.html
你自己测试好极限速度,给甲方速度保证,可以吗?


比如,几天前用 V 友的场景,测试了下速度:


Python 都能每秒 1W 写入事务,每事务记录 12 字段(几百字节) + 64 字符 唯一索引验证(这个也算读取了吧?)。

1000W 记录时,仍能 7000 TPS (六七年前的低压轻薄本上测试,[帖子及源码]( /t/1070957#reply13 ))
@JensenQian #2 升级较大的,是核显部分?两倍性能?

另外,这网站是不是屏蔽国内了。。

我记得前一段时间,可以直接访问,还有中文选项的呀。。
104 天前
回复了 tdb11039gg 创建的主题 数据库 有没有推荐用的轻量本地数据库
@tdb11039gg #22

前两天,我还见人吐槽,Python 的 *args, **kwargs (相当于支持接收一个 array 和 object )

原因是动态字段太灵活,太不严谨,文档代码对不上,后期维护太困难等问题。


我很疑惑,用 MongoDB 的,是怎么解决这些问题的呢。。

加上 json 还能字段想变就变,会不会前后期数据,字段都不统一呢。。


帖子:/t/1071840#reply91
104 天前
回复了 tdb11039gg 创建的主题 数据库 有没有推荐用的轻量本地数据库
@tdb11039gg #17


1. 请教下,存 json 的优势是什么呢?

- 增删查改方便?(如果不写 SQL ,而是用 ORM ,应该无所谓吧?)
- 物理上,这些数据聚在一块儿,查找起来速度更快?
- ……?



2. 逻辑上说,非关系能干的,关系应该都能干吧?

就是可能慢些,或其他缺陷?



3. SQLite 支持 json 增删查改聚合处理( json_* 或 jsonb_* 系列函数,或者 -> 及 ->> 操作符)

但没有 jsonPath 那样简洁的方式。。(做个插件,或者宿主语言里实现个函数给 SQLite 用也行)

比如查某帖里,某人近一年来的回复:

```sql
SELECT 回复 ->> '内容'
FROM 帖子表
JOIN json_each(回复数组) 回复
WHERE 帖子 ID = 1072404
  AND 回复 ->> '作者' == '某人'
  AND 回复 ->> '时间' >= '2023-09-13'
```

也支持对 json 字段做索引( Indexes On Expressions ),但目前不支持多值索引(如索引一个数组)。

真要实现,可以做个触发器,将数组里的差异项,更新到另一个索引里(如《帖子 ID ,回复人 ID ,楼层 or 楼层数组》索引)。



但我感觉,这些实现成一般关系表,就足够了。。
104 天前
回复了 tdb11039gg 创建的主题 数据库 有没有推荐用的轻量本地数据库
@dreamk #10

1. 有提供 Cli 版本,以及其他语言使用的库吗?

2. 支持并行写吗?





@tdb11039gg #12

SQLite 还不能满足你吗?


1. Python 都能每秒 1W 写入事务,每事务几百字节 + 64 字符 唯一索引验证。

1000W 记录时,仍能 7000 TPS 。(六七年前的低压轻薄本上测试,[源码](/t/1070957#reply13))



2. 几年前,微信说,手机上百万百字聊天记录,

搜索三个词,只需 0.0029 秒。10 秒全文索引完毕。

可无限读事务同时查询,也不影响写事务并行。

支持:拼音及首字母(自动解决多音字)和汉字任意混合、模糊(通过 NEAR )、同义词(错别字是这个原理吗?)等

([文章链接]( https://mp.weixin.qq.com/s/Ph0jykLr5CMF-xFgoJw5UQ))



3. 我测试过,在电视盒子上(单核 Nginx 默认页压测 1W QPS ,性能不及 6 年前骁龙 636 千元机一半),

Python 的 FastAPI + SQLite + 去年本站被爬的千万数据(此时 5GB 数据库):

- 200 模拟发帖回帖 + 全文索引 / 秒
- 1100 获取整帖(包括回帖者信息) / 秒
@Chinsung #6

1. 会不会多个请求,同时发现 redis 里没有,又都从 DB 读,导致多次消耗,却只有一次记录呢?

2. 感觉楼主不是关注一致性问题,而是 redis 持久性没得到保证问题。。
106 天前
回复了 saveai 创建的主题 程序员 请问如何实现大文件夹即时浏览器下载
@okakuyang #4 能不能遍历下文件,提前算好总大小,以《打包存储》形式压缩,传输过程中由 gzip 压缩呢?
106 天前
回复了 weiqk 创建的主题 Python 这几天被 Python 搞得快崩溃了
@yb2313 #6
@miaotaizi #13

动态字段,很烦人吗?

那用 MongoDB 的,是咋忍下去的。。
108 天前
回复了 shrugginG 创建的主题 MySQL mysql 小白请教大佬一个问题
感觉你的场景,用 SQLite 也挺好呀。。


## 测试结果

*(同样表结构,每次开事务插入一行再提交)*

- 100W 行,104 秒,平均约 10000 TPS ,共 360MB
- 1000W 行,1450 秒,平均约 7000 TPS ,共 3.6GB


## 环境

- CPU:i5-8250U (六七年前的低压轻薄本)
- 内存:测试时占用 14 MB
- 系统:Deepin V20 (基于 Debian 10 )
- 软件:Python 3.9
- 固态:顺序:500 MB/s 读,300 MB/s 写;随机:20 MB/s 读,64 MB/s 写


## 代码

```python
# V 站吞空格,缩进改为全角空格了

import time
import random
import hashlib
import sqlite3

ROWS_PER_INSERT = 1
ROWS_TOTAL = 1000_0000
DB_PATH = '/数据库存放路径/名称.db'

db = sqlite3.connect(DB_PATH)
db.execute('PRAGMA journal_mode = WAL')
db.execute('PRAGMA synchronous = NORMAL')
db.execute('PRAGMA wal_autocheckpoint = 10000')

db.execute('''
   CREATE TABLE IF NOT EXISTS phishtank_database (
     id          INTEGER PRIMARY KEY,
     phish_id       INT,
     url         TEXT,
     url_sha256      TEXT UNIQUE,
     phish_detail_url   TEXT,
     submission_time   DATETIME,
     verified       TEXT,
     verification_time DATETIME,
     online        TEXT,
     target        TEXT,
     created_at      DATETIME DEFAULT (datetime('now', 'localtime')),
     etag         TEXT
  )
''')

insert_sql = '''
   INSERT OR IGNORE INTO phishtank_database
  (phish_id, url, url_sha256, phish_detail_url, submission_time, verified, verification_time, online, target, etag)
   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''

first_id, = db.execute('SELECT IFNULL(MAX(id), 0) + 1 FROM phishtank_database').fetchone()
for next_id in range(first_id, first_id + ROWS_TOTAL, ROWS_PER_INSERT):

   rows = []
   now = time.strftime('%Y-%m-%d %H:%M:%S')
   for phish_id in range(next_id, min(next_id + ROWS_PER_INSERT, first_id + ROWS_TOTAL)):
     url = f'https://free-{phish_id}.webadorsite.com/'
     detail_url = f'http://www.phishtank.com/phish_detail.php?phish_id={phish_id}'
     rows.append((
       phish_id, url, hashlib.sha256(url.encode('utf-8')).hexdigest(),
       detail_url, now, 'yes', now, 'yes', 'Other', random.randbytes(16).hex(),
    ))

   with db:
     db.executemany(insert_sql, rows)
```
@PopRain #10 有没有 greaterEqualThan() 啥的。。

还是叫 greaterThanOrEqualTo()。。

https://i.imgur.com/krir4IG.png https://i.imgur.com/krir4IG.png
@ty29022 #11 为嘛 / 只能是除法的意思,而不能是分隔符呢?(比如斜杠青年)

那同理,字符串拼接,也不该用 +,而是像拼接矩阵那样,[str1 str2 str3] ?

https://i.imgur.com/F29pmQ6.png https://i.imgur.com/F29pmQ6.png
111 天前
回复了 archxm 创建的主题 Android 安卓手机的性能够不够外接显示器?
如果真能替代了,厂商的低端本可咋卖呀。。
1  2  3  4  5  6  7  8  9  10 ... 34  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5425 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 29ms · UTC 01:28 · PVG 09:28 · LAX 17:28 · JFK 20:28
Developed with CodeLauncher
♥ Do have faith in what you're doing.