Сёння не будзе ніякіх складаных кейсаў і складаных алгарытмаў на SQL. Усё будзе вельмі проста, на ўзроўні Капітана Відавочна - робім прагляд рэестра падзей з сартаваннем па часе.
Гэта значыць, вось ляжыць у базе таблічка 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. OFFSET
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 запісамі - ён апошні.
Або тое ж самае карцінкай:
Паколькі зараз у нас выбарка не мае нейкага вызначанага «пачатку», то нам нішто не мяшае «разгарнуць» гэты запыт у зваротны бок і рэалізаваць дынамічную падгрузку блокаў дадзеных ад «апорнай кропкі» у абодва бакі – як уніз, так і ўверх.
заўвагу
- Так, у такім выпадку мы звяртаемся да індэкса двойчы, але ўсё "чыста па індэксе". Таму ўкладзены запыт прывядзе ўсяго толькі да аднаго дадатковага Index Only Scan.
- Досыць відавочна, што гэтай методыкай можна карыстацца, толькі калі ў вас значэння
ts
могуць перасекчыся толькі выпадкова, і іх няшмат. Калі ж ваш тыповы кейс - "мільён запісаў у 00:00:00.000", так рабіць не варта. У сэнсе не варта дапускаць такога кейса. Але калі ўжо так атрымалася, выкарыстоўвайце варыянт з пашыраным азначнікам.
Крыніца: habr.com