Հազարավոր մենեջերներ վաճառքի գրասենյակներից ամբողջ երկրում գրանցում են
Հետևաբար, զարմանալի չէ, որ ևս մեկ անգամ վերլուծելով «ծանր» հարցումները ամենաբեռնված տվյալների բազաներից մեկում՝ մեր սեփական
Ավելին, հետագա հետաքննությունը բացահայտեց մի հետաքրքիր օրինակ սկզբում օպտիմիզացում, իսկ հետո կատարողականի դեգրադացիա հարցումն իր հաջորդական ճշգրտմամբ մի քանի թիմերի կողմից, որոնցից յուրաքանչյուրը գործել է բացառապես լավագույն մտադրություններով:
0: ինչ էր ուզում օգտվողը:
[KDPV
Ի՞նչ է սովորաբար նկատի ունենում օգտատերը, երբ խոսում է անվանական «արագ» որոնման մասին: Գրեթե երբեք չի ստացվում, որ նման ենթալարի «ազնիվ» որոնում է ... LIKE '%роза%'
- որովհետև արդյունքը ներառում է ոչ միայն 'Розалия'
и 'Магазин Роза'
Սակայն 'Гроза'
եւ նույնիսկ 'Дом Деда Мороза'
.
Օգտագործողը ենթադրում է ամենօրյա մակարդակով, որը դուք նրան կտրամադրեք որոնել ըստ բառի սկզբի վերնագրում և ավելի համապատասխան դարձնել այն սկսվում է մտել է. Եվ դուք դա կանեք գրեթե ակնթարթորեն - միջգծային մուտքագրման համար:
1: Սահմանափակել առաջադրանքը
Եվ դեռ ավելին, մարդը կոնկրետ չի մտնի 'роз магаз'
, այնպես որ յուրաքանչյուր բառը պետք է որոնել ըստ նախածանցի։ Ոչ, օգտվողի համար շատ ավելի հեշտ է պատասխանել վերջին բառի արագ ակնարկին, քան կանխամտածված «թերճշգրտել» նախորդները. տեսեք, թե ինչպես է ցանկացած որոնողական համակարգ դա անում:
Ընդհանրապես, ճիշտ Խնդրի պահանջների ձևակերպումը լուծման կեսից ավելին է: Երբեմն զգույշ օգտագործման դեպքերի վերլուծություն
Ի՞նչ է անում վերացական մշակողը:
1.0. արտաքին որոնման համակարգ
Օ,, որոնումը դժվար է, ես ընդհանրապես ոչինչ չեմ ուզում անել. եկեք դա տանք devops-ին: Թող որոնողական համակարգ տեղադրեն տվյալների բազայից դուրս՝ Sphinx, ElasticSearch,...
Աշխատանքային տարբերակ, թեև աշխատատար՝ համաժամացման և փոփոխությունների արագության առումով: Բայց ոչ մեր դեպքում, քանի որ որոնումն իրականացվում է յուրաքանչյուր հաճախորդի համար միայն իր հաշվի տվյալների շրջանակներում։ Եվ տվյալները բավականին բարձր փոփոխականություն ունեն, և եթե կառավարիչը այժմ մտել է քարտը 'Магазин Роза'
, ապա 5-10 վայրկյան հետո նա կարող է արդեն հիշել, որ մոռացել է այնտեղ նշել իր էլ.փոստը և ցանկանում է գտնել այն և ուղղել այն։
Հետեւաբար - եկեք որոնել «ուղղակիորեն տվյալների բազայում». Բարեբախտաբար, PostgreSQL-ը մեզ թույլ է տալիս դա անել, և ոչ միայն մեկ տարբերակ՝ մենք կդիտարկենք դրանք:
1.1. «ազնիվ» ենթատող
Մենք կառչում ենք «ենթալարի» բառից: Բայց ենթալարի միջոցով ինդեքսների որոնման համար (և նույնիսկ սովորական արտահայտություններով!) կա գերազանց
Մոդելը պարզեցնելու համար փորձենք վերցնել հետևյալ ափսեը.
CREATE TABLE firms(
id
serial
PRIMARY KEY
, name
text
);
Մենք այնտեղ վերբեռնում ենք իրական կազմակերպությունների 7.8 միլիոն գրառում և ինդեքսավորում դրանք.
CREATE EXTENSION pg_trgm;
CREATE INDEX ON firms USING gin(lower(name) gin_trgm_ops);
Եկեք փնտրենք միջգծային որոնման առաջին 10 գրառումները.
SELECT
*
FROM
firms
WHERE
lower(name) ~ ('(^|s)' || 'роза')
ORDER BY
lower(name) ~ ('^' || 'роза') DESC -- сначала "начинающиеся на"
, lower(name) -- остальное по алфавиту
LIMIT 10;
Դե, դա ... 26 ms, 31 ՄԲ կարդալ տվյալներ և ավելի քան 1.7K զտված գրառումներ՝ 10 որոնվածների համար: Վերադիր ծախսերը շատ բարձր են, ավելի արդյունավետ բան չկա՞։
1.2. որոնել ըստ տեքստի: Դա FTS է:
Իրոք, PostgreSQL-ն ապահովում է շատ հզոր
CREATE INDEX ON firms USING gin(to_tsvector('simple'::regconfig, lower(name)));
SELECT
*
FROM
firms
WHERE
to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'роза:*')
ORDER BY
lower(name) ~ ('^' || 'роза') DESC
, lower(name)
LIMIT 10;
Այստեղ հարցումների կատարման զուգահեռացումը մեզ մի փոքր օգնեց՝ ժամանակը կիսով չափ կրճատելով 11 ms. Իսկ մենք պետք է կարդայինք 1.5 անգամ ավելի քիչ՝ ընդհանուր 20MB. Բայց այստեղ, որքան քիչ, այնքան լավ, քանի որ որքան մեծ է մեր կարդացած ծավալը, այնքան մեծ է քեշը բաց թողնելու հավանականությունը, և սկավառակից կարդացվող տվյալների յուրաքանչյուր լրացուցիչ էջ պոտենցիալ «արգելակ» է հարցման համար:
1.3: Դեռ Հավանում եմ:
Նախորդ խնդրանքը բոլորի համար լավ է, բայց միայն թե օրը հարյուր հազար անգամ քաշես, կգա 2TB կարդալ տվյալները. Լավագույն դեպքում՝ հիշողությունից, բայց եթե անհաջողակ ես, ապա սկավառակից։ Այսպիսով, եկեք փորձենք այն փոքրացնել:
Եկեք հիշենք, թե ինչ է ուզում տեսնել օգտվողը առաջինը «որոնք սկսվում են ...». Այսպիսով, սա իր մաքուր ձևով է text_pattern_ops
! Եվ միայն այն դեպքում, եթե մենք «չունենք բավարար» մինչև 10 ձայնագրություններ, որոնք փնտրում ենք, ապա մենք պետք է ավարտենք դրանք կարդալը FTS որոնման միջոցով.
CREATE INDEX ON firms(lower(name) text_pattern_ops);
SELECT
*
FROM
firms
WHERE
lower(name) LIKE ('роза' || '%')
LIMIT 10;
Գերազանց կատարում - ընդհանուր 0.05 մս և 100 ԿԲ-ից մի փոքր ավելի կարդալ! Միայն մենք մոռացանք տեսակավորել ըստ անվանմանորպեսզի օգտվողը չկորչի արդյունքների մեջ.
SELECT
*
FROM
firms
WHERE
lower(name) LIKE ('роза' || '%')
ORDER BY
lower(name)
LIMIT 10;
Օ՜, ինչ-որ բան այլևս այնքան էլ գեղեցիկ չէ. թվում է, թե ինչ-որ ցուցանիշ կա, բայց տեսակավորումն անցնում է դրա կողքով... Դա, իհարկե, արդեն բազմապատիկ ավելի արդյունավետ է, քան նախորդ տարբերակը, բայց...
1.4. «ավարտել ֆայլով»
Բայց կա մի ինդեքս, որը թույլ է տալիս որոնել ըստ տիրույթի և դեռ նորմալ օգտագործել տեսակավորումը. կանոնավոր բծ!
CREATE INDEX ON firms(lower(name));
Միայն դրա խնդրանքը պետք է «ձեռքով հավաքվի».
SELECT
*
FROM
firms
WHERE
lower(name) >= 'роза' AND
lower(name) <= ('роза' || chr(65535)) -- для UTF8, для однобайтовых - chr(255)
ORDER BY
lower(name)
LIMIT 10;
Գերազանց - տեսակավորումն աշխատում է, և ռեսուրսների սպառումը մնում է «մանրադիտակային», հազարավոր անգամ ավելի արդյունավետ, քան «մաքուր» FTS-ը! Մնում է միայն այն միավորել մեկ խնդրանքի մեջ.
(
SELECT
*
FROM
firms
WHERE
lower(name) >= 'роза' AND
lower(name) <= ('роза' || chr(65535)) -- для UTF8, для однобайтовых кодировок - chr(255)
ORDER BY
lower(name)
LIMIT 10
)
UNION ALL
(
SELECT
*
FROM
firms
WHERE
to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'роза:*') AND
lower(name) NOT LIKE ('роза' || '%') -- "начинающиеся на" мы уже нашли выше
ORDER BY
lower(name) ~ ('^' || 'роза') DESC -- используем ту же сортировку, чтобы НЕ пойти по btree-индексу
, lower(name)
LIMIT 10
)
LIMIT 10;
Նկատի ունեցեք, որ երկրորդ ենթահարկը կատարվում է միայն այն դեպքում, եթե առաջինը վերադարձավ սպասվածից քիչ վերջին LIMIT
տողերի քանակը. Ես խոսում եմ հարցումների օպտիմալացման այս մեթոդի մասին
Այսպիսով, այո, մենք այժմ սեղանին ունենք և՛ btree, և՛ gin, բայց վիճակագրորեն պարզվում է, որ Հարցումների 10%-ից պակասը հասնում է երկրորդ բլոկի կատարմանը. Այսինքն, առաջադրանքի համար նախապես հայտնի նման բնորոշ սահմանափակումներով մենք կարողացանք կրճատել սերվերի ռեսուրսների ընդհանուր սպառումը գրեթե հազար անգամ:
1.5*. մենք կարող ենք անել առանց ֆայլի
Վերեւում LIKE
Մեզ թույլ չտվեցին օգտագործել սխալ տեսակավորում: Բայց այն կարող է «դրվել ճիշտ ուղու վրա»՝ նշելով USING օպերատորը.
Լռելյայն ենթադրվում է
ASC
. Բացի այդ, դուք կարող եք նշել կոնկրետ տեսակավորման օպերատորի անունը դրույթումUSING
. Տեսակավորման օպերատորը պետք է լինի B-ծառի օպերատորների որոշ ընտանիքի անդամներից փոքր կամ ավելի մեծ:ASC
սովորաբար համարժեքUSING <
иDESC
սովորաբար համարժեքUSING >
.
Մեր դեպքում «պակաս» է ~<~
:
SELECT
*
FROM
firms
WHERE
lower(name) LIKE ('роза' || '%')
ORDER BY
lower(name) USING ~<~
LIMIT 10;
2. ինչպես են խնդրանքները դառնում թթու
Այժմ մենք թողնում ենք մեր խնդրանքը, որպեսզի «եփվի» վեց ամիս կամ մեկ տարի, և մենք զարմացած ենք, որ այն կրկին գտնում ենք «վերևում»՝ հիշողության ընդհանուր օրական «մղման» ցուցանիշներով (բուֆերների ընդհանուր հիթ) մեջ 5.5TB - այսինքն նույնիսկ ավելին, քան ի սկզբանե էր։
Ո՛չ, իհարկե, մեր բիզնեսն աճել է, և մեր ծանրաբեռնվածությունն ավելացել է, բայց ոչ նույնքանով։ Սա նշանակում է, որ այստեղ ինչ-որ բան ձուկ է, եկեք պարզենք:
2.1. Էջավորման ծնունդ
Ինչ-որ պահի, մեկ այլ մշակման թիմ ցանկացավ հնարավորություն տալ «ցատկել» արագ բաժանորդագրության որոնումից դեպի ռեեստր նույն, բայց ընդլայնված արդյունքներով: Ի՞նչ է ռեեստրն առանց էջի նավիգացիայի: Եկեք խեղաթյուրենք այն:
( ... LIMIT <N> + 10)
UNION ALL
( ... LIMIT <N> + 10)
LIMIT 10 OFFSET <N>;
Այժմ հնարավոր եղավ ցուցադրել որոնման արդյունքների ռեեստրը «էջ առ էջ» բեռնումով, առանց մշակողի համար որևէ սթրեսի:
Իհարկե, փաստորեն, տվյալների յուրաքանչյուր հաջորդ էջի համար ավելի ու ավելի շատ է ընթերցվում (բոլորը նախորդ անգամից, որը մենք կթողնենք, գումարած անհրաժեշտ «պոչը») - այսինքն, սա հստակ հակաօրինաչափություն է: Բայց ավելի ճիշտ կլինի որոնումը սկսել հաջորդ կրկնությունից՝ ինտերֆեյսում պահվող բանալիից, բայց դրա մասին մեկ այլ անգամ։
2.2. Ես ուզում եմ ինչ-որ էկզոտիկ բան
Ինչ-որ պահի ծրագրավորողը ցանկացավ դիվերսիֆիկացնել ստացված նմուշը տվյալների հետ մեկ այլ աղյուսակից, որի համար ամբողջ նախորդ հարցումն ուղարկվել է CTE.
WITH q AS (
...
LIMIT <N> + 10
)
SELECT
*
, (SELECT ...) sub_query -- какой-то запрос к связанной таблице
FROM
q
LIMIT 10 OFFSET <N>;
Եվ նույնիսկ այդպես, դա վատ չէ, քանի որ ենթահանձնումը գնահատվում է միայն 10 վերադարձված գրառումների համար, եթե ոչ ...
2.3. ԴԻՍՏԻՆԿՏ-ը անիմաստ է և անողոք
Ինչ-որ տեղ նման էվոլյուցիայի գործընթացում 2-րդ ենթահաշիվից կորել է NOT LIKE
պայման. Հասկանալի է, որ սրանից հետո UNION ALL
սկսեց վերադառնալ որոշ մուտքեր երկու անգամ - սկզբում հայտնաբերվել է տողի սկզբում, իսկ հետո նորից՝ այս տողի առաջին բառի սկզբում: Սահմանում, 2-րդ ենթահաշիվների բոլոր գրառումները կարող են համապատասխանել առաջինի գրառումներին:
Ի՞նչ է անում ծրագրավորողը պատճառը փնտրելու փոխարեն: Հարց չկա:
- կրկնապատկել չափը բնօրինակ նմուշներ
- կիրառել DISTINCTյուրաքանչյուր տողից միայն առանձին օրինակներ ստանալու համար
WITH q AS (
( ... LIMIT <2 * N> + 10)
UNION ALL
( ... LIMIT <2 * N> + 10)
LIMIT <2 * N> + 10
)
SELECT DISTINCT
*
, (SELECT ...) sub_query
FROM
q
LIMIT 10 OFFSET <N>;
Այսինքն, պարզ է, որ արդյունքը, ի վերջո, նույնն է, բայց 2-րդ CTE ենթակետ «թռչելու» հնարավորությունը շատ ավելի մեծ է դարձել, և նույնիսկ առանց դրա, ակնհայտորեն ավելի ընթեռնելի.
Բայց սա ամենատխուրը չէ։ Քանի որ մշակողը խնդրել է ընտրել DISTINCT
ոչ թե կոնկրետների, այլ միանգամից բոլոր ոլորտների համար գրառումները, այնուհետև այնտեղ ավտոմատ կերպով ներառվել է sub_query դաշտը՝ ենթհարցման արդյունքը: Հիմա՝ իրականացնելու համար DISTINCT
, տվյալների բազան արդեն պետք է գործարկվեր ոչ թե 10 ենթհարցումներ, այլ բոլոր <2 * N> + 10!
2.4. ամենից առաջ համագործակցություն:
Այսպիսով, մշակողները շարունակեցին ապրել, նրանք չէին անհանգստանում, քանի որ օգտվողը ակնհայտորեն բավարար համբերություն չուներ ռեեստրը «հարմարեցնելու» նշանակալի N արժեքներին՝ յուրաքանչյուր հաջորդ «էջ» ստանալու քրոնիկական դանդաղումով:
Մինչև նրանց մոտ եկան մեկ այլ բաժնի մշակողները և ցանկացան օգտագործել նման հարմար մեթոդ կրկնվող որոնման համար -այսինքն ինչ-որ նմուշից վերցնում ենք մի կտոր, զտում այն լրացուցիչ պայմաններով, նկարում արդյունքը, հետո հաջորդ կտորը (որը մեր դեպքում ստացվում է N-ի մեծացմամբ) և այդպես շարունակ, մինչև լցնենք էկրանը։
Ընդհանուր առմամբ, բռնված նմուշում N-ը հասել է գրեթե 17K արժեքների, և ընդամենը մեկ օրվա ընթացքում առնվազն 4K նման հարցումներ են կատարվել «շղթայի երկայնքով»։ Դրանցից վերջինները համարձակորեն սկանավորվեցին 1 ԳԲ հիշողություն մեկ կրկնության համար...
Ընդհանուր
Source: www.habr.com