阶段2(上):后端专题 Go/Gin/PostgreSQL/MySQL/Redis/MQ/gRPC 的课程、企业场景、面试题和可运行示例
This commit is contained in:
1 parent
0bae9916a9
commit
5eb67c5d08
65 files changed
+6457
-77
No files matched your search
@@ -0,0 +1,114 @@
|
||||
# requires: pymysql
|
||||
"""对比 LIMIT offset 深分页和游标(keyset)分页。
|
||||
|
||||
运行:MYSQL_URL=mysql://user:pass@127.0.0.1:3306/db python paginate.py
|
||||
没有设置 MYSQL_URL 时只打印提示并正常退出。会创建并删除临时表 demo_page_orders。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pymysql
|
||||
|
||||
TABLE = "demo_page_orders"
|
||||
ROWS = 100_000
|
||||
PAGE = 20
|
||||
|
||||
|
||||
def connect(url: str) -> pymysql.connections.Connection:
|
||||
u = urlparse(url)
|
||||
return pymysql.connect(host=u.hostname, port=u.port or 3306, user=u.username, password=u.password or "",
|
||||
database=u.path.lstrip("/"), autocommit=True)
|
||||
|
||||
|
||||
def seed(cur) -> None:
|
||||
cur.execute(f"DROP TABLE IF EXISTS {TABLE}")
|
||||
cur.execute(f"""CREATE TABLE {TABLE} (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
note VARCHAR(100) NOT NULL,
|
||||
KEY idx_user_time_id (user_id, created_at, id)
|
||||
) ENGINE = InnoDB""")
|
||||
batch = []
|
||||
for i in range(ROWS):
|
||||
batch.append((1, f"note-{i}")) # 全部属于 user 1,便于构造深分页
|
||||
if len(batch) == 5000:
|
||||
cur.executemany(f"INSERT INTO {TABLE} (user_id, created_at, note) VALUES (%s, '2026-01-01', %s)", batch)
|
||||
batch.clear()
|
||||
# 时间按 id 递增;让每 3 行共用一个时间,演示“时间相同按 id 排序”
|
||||
cur.execute(f"UPDATE {TABLE} SET created_at = '2026-01-01' + INTERVAL (id DIV 3) SECOND")
|
||||
cur.execute(f"ANALYZE TABLE {TABLE}")
|
||||
cur.fetchall()
|
||||
|
||||
|
||||
def offset_page(cur, offset: int) -> list[tuple]:
|
||||
cur.execute(f"SELECT id, created_at, note FROM {TABLE} WHERE user_id = 1 "
|
||||
f"ORDER BY created_at DESC, id DESC LIMIT %s OFFSET %s", (PAGE, offset))
|
||||
return list(cur.fetchall())
|
||||
|
||||
|
||||
def deferred_join_page(cur, offset: int) -> list[tuple]:
|
||||
# 延迟关联:先在覆盖索引上找出 id,再回表取这一页的整行
|
||||
cur.execute(f"""SELECT o.id, o.created_at, o.note FROM {TABLE} o
|
||||
JOIN (SELECT id FROM {TABLE} WHERE user_id = 1 ORDER BY created_at DESC, id DESC LIMIT %s OFFSET %s) t
|
||||
ON o.id = t.id ORDER BY o.created_at DESC, o.id DESC""", (PAGE, offset))
|
||||
return list(cur.fetchall())
|
||||
|
||||
|
||||
def keyset_page(cur, after: tuple | None) -> list[tuple]:
|
||||
if after is None:
|
||||
cur.execute(f"SELECT id, created_at, note FROM {TABLE} WHERE user_id = 1 "
|
||||
f"ORDER BY created_at DESC, id DESC LIMIT %s", (PAGE,))
|
||||
else:
|
||||
created_at, last_id = after
|
||||
cur.execute(f"SELECT id, created_at, note FROM {TABLE} WHERE user_id = 1 "
|
||||
f"AND (created_at < %s OR (created_at = %s AND id < %s)) "
|
||||
f"ORDER BY created_at DESC, id DESC LIMIT %s", (created_at, created_at, last_id, PAGE))
|
||||
return list(cur.fetchall())
|
||||
|
||||
|
||||
def timed(fn, *args) -> tuple[float, list[tuple]]:
|
||||
t = time.perf_counter()
|
||||
rows = fn(*args)
|
||||
return (time.perf_counter() - t) * 1000, rows
|
||||
|
||||
|
||||
def main() -> int:
|
||||
url = os.environ.get("MYSQL_URL")
|
||||
if not url:
|
||||
print("未设置 MYSQL_URL,跳过(示例:mysql://app:app@127.0.0.1:3306/learn)")
|
||||
return 0
|
||||
conn = connect(url)
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
seed(cur)
|
||||
offset = ROWS - 1000 # 很深的一页
|
||||
ms1, a = timed(offset_page, cur, offset)
|
||||
ms2, b = timed(deferred_join_page, cur, offset)
|
||||
# 游标分页:拿到上一页最后一行 (created_at, id),直接从那里继续
|
||||
prev = offset_page(cur, offset - PAGE)[-1]
|
||||
ms3, c = timed(keyset_page, cur, (prev[1], prev[0]))
|
||||
assert a == b == c, "三种写法结果应该一致"
|
||||
print(f"OFFSET {offset}: {ms1:.1f} ms;延迟关联: {ms2:.1f} ms;游标分页: {ms3:.1f} ms(耗时与机器有关,看相对差异)")
|
||||
# 游标分页逐页翻完不重不漏
|
||||
seen, after = 0, None
|
||||
while True:
|
||||
rows = keyset_page(cur, after)
|
||||
if not rows:
|
||||
break
|
||||
seen += len(rows)
|
||||
after = (rows[-1][1], rows[-1][0])
|
||||
if seen >= 2000:
|
||||
break
|
||||
assert seen == 2000
|
||||
print("游标分页连续翻页正确")
|
||||
return 0
|
||||
finally:
|
||||
cur.execute(f"DROP TABLE IF EXISTS {TABLE}")
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user