今天,SQL 中将不再有复杂的情况和复杂的算法。 一切都会非常简单,在 Captain Obvious 的水平上 - 让我们开始吧 查看事件注册表 按时间排序。
也就是数据库中有一个标志 events
,她有一个领域 ts
- 正是我们想要有序显示这些记录的时间:
CREATE TABLE events(
id
serial
PRIMARY KEY
, ts
timestamp
, data
json
);
CREATE INDEX ON events(ts DESC);
很明显,我们不会有十几条记录,所以我们需要某种形式的 页面导航.
#0。 “我是我母亲的大屠杀犯”
cur.execute("SELECT * FROM events;")
rows = cur.fetchall();
rows.sort(key=lambda row: row.ts, reverse=True);
limit = 26
print(rows[offset:offset+limit]);
这几乎不是一个笑话——它很罕见,但却是在野外发现的。 有时,在使用 ORM 后,可能很难切换到“直接”使用 SQL。
#1. 抵消
SELECT
...
FROM
events
ORDER BY
ts DESC
LIMIT 26 OFFSET $1; -- 26 - записей на странице, $1 - начало страницы
26这个数字是怎么来的? 这是填满一个屏幕的大致条目数。 更准确地说,显示了 25 条记录,再加上 1 条,表明样本中至少还有其他内容,因此继续前进是有意义的。
当然,这个值不能“缝”到请求体中,而是通过参数传递。 但在这种情况下,PostgreSQL 调度程序将无法依赖记录相对较少的知识 - 并且很容易选择无效的计划。
在应用程序界面中,查看注册表是通过在可视“页面”之间切换来实现的,很长一段时间没有人注意到任何可疑之处。 直到那一刻,为了方便起见,UI/UX 决定将界面重新设计为“无限滚动”——也就是说,所有注册表项都绘制在用户可以上下滚动的单个列表中。
所以,在下一次测试中,你被抓住了 重复记录 在注册表中。 为什么呢,因为表有正常索引 (ts)
,您的查询依赖于哪个?
正是因为你没有考虑到这一点 ts
不是唯一的密钥 在此表中。 事实上,并且 它的值并不唯一,就像现实条件下的任何“时间”一样 - 因此,由于对相同键值进行排序的框架内的最终顺序不同,两个相邻查询中的相同记录很容易从一个页面“跳转”到另一个页面。
事实上,这里还隐藏着第二个问题,这个问题更难被注意到—— 某些条目将不会显示 根本! 毕竟,“重复”记录取代了别人的位置。 详细的解释和精美的图片可以找到
扩大索引
狡猾的开发人员知道索引键需要唯一,最简单的方法是用明显唯一的字段扩展它,PK 非常适合:
CREATE UNIQUE INDEX ON events(ts DESC, id DESC);
并且请求发生了变化:
SELECT
...
ORDER BY
ts DESC, id DESC
LIMIT 26 OFFSET $1;
#2. 切换到“光标”
一段时间后,DBA 来找您,并对您的请求感到“高兴”
SELECT
...
WHERE
(ts, id) < ($1, $2) -- последние полученные на предыдущем шаге значения
ORDER BY
ts DESC, id DESC
LIMIT 26;
直到它到来,你才松了一口气……
#3。 清洁指标
因为有一天你的 DBA 读到 (ts DESC)
.
但是如何解决页面之间“跳跃”记录的最初问题呢?...一切都很简单 - 您需要选择记录数量不固定的块!
一般来说,谁禁止我们读的不是“正好26”,而是“不少于26”? 例如,在下一个块中有 具有明显不同含义的记录 ts
- 那么就不会出现块之间“跳跃”记录的问题了!
以下是实现这一目标的方法:
SELECT
...
WHERE
ts < $1 AND
ts >= coalesce((
SELECT
ts
FROM
events
WHERE
ts < $1
ORDER BY
ts DESC
LIMIT 1 OFFSET 25
), '-infinity')
ORDER BY
ts DESC;
这里发生了什么?
- 我们“向下”步进 25 条记录并得到“边界”值
ts
. - 如果那里已经没有任何内容,则将 NULL 值替换为
-infinity
. - 我们在接收到的值之间减去整个段的值
ts
以及从接口传递的 $1 参数(之前的“最后”渲染值)。 - 如果返回的块的记录少于 26 条,则它是最后一个。
或者同一张图片:
因为现在我们有 样本没有任何特定的“开始”,那么没有什么可以阻止我们向相反的方向“扩展”这个请求,并实现从“参考点”两个方向(向下和向上)动态加载数据块。
备注
- 是的,在这种情况下,我们访问索引两次,但一切都是“纯粹通过索引”。 因此,子查询只会导致 到一项额外的仅索引扫描.
- 很明显,只有当你有价值观时才能使用这种技术
ts
只有偶然才能穿越,并且 其中很少有人。 如果您的典型情况是“00:00:00.000 有一百万条记录”,您不应该这样做。 我的意思是,你不应该允许这样的情况发生。 但如果发生这种情况,请使用带有扩展索引的选项。
来源: habr.com