Твърде често виждаме лошо написани сложни SQL заявки, работещи срещу таблиците на базата данни. Изпълнението на такива заявки може да отнеме много кратко или много дълго време, но те консумират огромно количество процесор и други ресурси. Въпреки това, в много случаи сложните заявки предоставят ценна информация на приложението/лицето. Следователно той носи полезни активи във всички видове приложения.
Сложност на заявките
Нека разгледаме по-отблизо проблемните заявки. Много от тях са сложни. Това може да се дължи на няколко причини:
- Типът данни, избран за данните;
- Организацията и съхранението на данните в базата данни;
- Трансформация и обединяване на данните в заявка за извличане на желания набор от резултати.
Трябва да помислите правилно за тези три ключови фактора и да ги приложите правилно, за да направите заявките да работят оптимално.
Въпреки това, това може да се превърне в почти невъзможна задача както за разработчиците на бази данни, така и за администраторите на данни. Например, може да бъде изключително трудно добавянето на нова функционалност към съществуващи наследени системи. Особено сложен случай е, когато трябва да извлечете и трансформирате данните от наследена система, така че да можете да ги сравните с данните, произведени от новата система или функционалност. Трябва да го постигнете, без да засягате функционалността на наследеното приложение.
Такива заявки могат да включват сложни съединения, като следното:
- Комбинация от подниз и/или конкатенация на няколко колони с данни;
- Вградени скаларни функции;
- Персонализирани UDFs;
- Всяка комбинация от сравнения на клауза WHERE и условия за търсене.
Заявките, както беше описано по-рано, обикновено имат сложни пътища за достъп. По-лошото е, че може да имат много сканирания на таблици и/или пълни сканирания индекс с подобни комбинации от JOIN или търсения.
Преобразуване на данни и манипулации в заявки
Трябва да отбележим, че всички данни, съхранявани постоянно в таблица на база данни, се нуждаят от трансформация и/или манипулация в даден момент, когато запитваме тези данни от таблицата. Трансформацията може да варира от проста трансформация до много сложна. В зависимост от това колко сложна може да е трансформацията може да изразходва много процесор и ресурси.
В повечето случаи трансформациите, извършени в JOIN, се случват след като данните бъдат прочетени и разтоварени в tempdb база данни (SQL Server) или работен файл база данни / временни пространства за таблици както в други системи за бази данни.
Тъй като данните в работния файлне могат да се индексират , времето необходимо за изпълнение на комбинирани трансформации и JOIN нараства експоненциално. Извлечените данни стават по-големи. По този начин получените заявки се превръщат в затруднено място в производителността чрез допълнително нарастване на данните.
И така, как разработчикът на база данни или администраторът на база данни може бързо да разрешат тези затруднения в производителността и също така да си осигурят повече време за повторно проектиране и пренаписване на заявките за оптимална производителност?
Има два начина за ефективно разрешаване на такива постоянни проблеми. Един от тях е чрез използване на виртуални колони и/или функционални индекси.
Функционални индекси и заявки
Обикновено създавате индекси върху колони, които или показват уникален набор от колони/стойности в ред (уникални индекси или първични ключове), или представляват набор от колони/стойности, които са или могат да бъдат използвани в условията за търсене на клауза WHERE на заявка.
Ако нямате такива индекси и сте разработили сложни заявки, както е описано по-рано, ще забележите следното:
- Намаляване на нивата на производителност при използване на обяснението запитване и преглед на сканирания на таблици или сканиране на пълен индекс
- Много високо използване на процесора и ресурси, причинено от заявките;
- Дълги времена за изпълнение.
Съвременните бази данни обикновено решават тези проблеми, като ви позволяват да създадете функционален илибазирани на функции индекс, както е наименован в SQLServer, Oracle и MySQL (v 8.x). Или може да е Индекс на израз/базиран на израз индекси, както в други бази данни (PostgreSQL и Db2).
Да предположим, че имате графа Дата на покупка от типа данни TIMESTAMP или DATETIME във вашата Поръчка таблица и тази колона е индексирана. Започваме да правим заявки за Поръчката таблица с клауза WHERE:
SELECT ...
FROM Order
WHERE DATE(Purchase_Date) = '03.12.2020'
Тази транзакция ще предизвика сканиране на целия индекс. Ако обаче колоната не е индексирана, получавате сканиране на таблицата.
След сканиране на целия индекс, този индекс се премества в tempdb / работен файл (цяла маса ако получите сканиране на таблица ) преди съвпадение на стойността 03.12.2020 .
Тъй като голяма таблица за поръчки използва много процесор и ресурси, трябва да създадете функционален индекс с израза DATE (Purchase_Date ) като една от индексните колони и показани по-долу:
CREATE ix_DatePurchased on sales.Order(Date(Purchase_Date) desc, ... )
По този начин правите съответстващия предикат ДАТА (Дата_на_покупка) =‘03.12.2020’ индексируеми. По този начин, вместо да преместваме индекса или таблицата в tempdb / работния файл преди съвпадението на стойността, ние правим индекса само частично достъпен и/или сканиран. Това води до по-ниско използване на процесора и ресурси.
Вижте друг пример. Има Клиент таблица с колоните first_name, last_name . Тези колони се индексират като такива:
CREATE INDEX ix_custname on Customer(first_name asc, last_name asc),
Освен това имате изглед, който обединява тези колони в customer_name колона:
CREATE view v_CustomerInfo( customer_name, .... ) as
select first_name ||' '|| last_name as customer_name,.....
from Customer
where ...
Имате заявка от система за електронна търговия, която търси пълното име на клиента:
select c.*
from v_CustomerInfo c
where c.customer_name = 'John Smith'
....
Отново, тази заявка ще произведе пълно сканиране на индекс. В най-лошия случай това ще бъде пълно сканиране на таблицата, премествайки всички данни от индекса или таблицата към работния файл преди конкатенацията на first_name и фамилно_име колони и съответстващи на стойността на „Джон Смит“.
Друг случай е създаването на функционален индекс, както е показано по-долу:
CREATE ix_fullcustname on sales.Customer( first_name ||' '|| last_name desc, ... )
По този начин можете да направите конкатенацията в заявката за изглед в индексируем предикат. Вместо пълно сканиране на индекс или сканиране на таблица, имате частично сканиране на индекса. Такова изпълнение на заявка води до по-ниско използване на процесора и ресурси, изключвайки работата в работния файл и по този начин гарантира по-бързо време за изпълнение.
Виртуални (генерирани) колони и заявки
Генерираните колони (виртуални колони или изчислени колони) са колони, които съдържат данните, генерирани в движение. Данните не могат да бъдат изрично зададени на конкретна стойност. Отнася се до данните в други колони, запитани, вмъкнати или актуализирани в DML заявка.
Генерирането на стойности на такива колони е автоматизирано въз основа на израз. Тези изрази могат да генерират:
- Поредица от цели числа;
- Стойността въз основа на стойностите на други колони в таблицата;
- Може да генерира стойности чрез извикване на вградени функции или дефинирани от потребителя функции (UDF).
Също толкова важно е да се отбележи, че в някои бази данни (SQLServer, Oracle, PostgreSQL, MySQL и MariaDB) тези колони могат да бъдат конфигурирани или да съхраняват постоянно данните с изпълнението на операторите INSERT и UPDATE, или да изпълняват основния израз на колона в движение ако направим заявка към таблицата и колоната, спестявайки място за съхранение.
Въпреки това, когато изразът е сложен, както при сложната логика във функцията UDF, спестяването на време за изпълнение, ресурси и разходи за заявка на процесора може да не е толкова голямо, колкото се очаква.
По този начин можем да конфигурираме колоната, така че тя постоянно да съхранява резултата от израза в оператор INSERT или UPDATE. След това създаваме обикновен индекс за тази колона. По този начин ще спестим процесора, използването на ресурси и времето за изпълнение на заявката. Отново може да е известно леко увеличение на производителността INSERT и UPDATE, в зависимост от сложността на израза.
Нека разгледаме един пример. Ние декларираме таблицата и създаваме индекс, както следва:
CREATE TABLE Customer as (
customerID Int GENERATED ALWAYS AS IDENTITY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
customer_name as (first_name ||' '|| last_name) PERSISTED
...
);
CREATE ix_fullcustname on sales.Customer( customer_name desc, ... )
По този начин преместваме логиката на конкатенация от изгледа в предишния пример надолу в таблицата и съхраняваме данните постоянно. Извличаме данните, използвайки съответстващо сканиране на обикновен индекс. Това е най-добрият възможен резултат тук.
Чрез добавяне на генерирана колона към таблица и създаване на редовен индекс върху тази колона, можем да преместим логиката на трансформацията надолу до нивото на таблицата. Тук ние постоянно съхраняваме трансформираните данни в изрази за вмъкване или актуализиране, които иначе биха били трансформирани в заявки. Сканирането JOIN и INDEX ще бъде много по-просто и по-бързо.
Функционални индекси, генерирани колони и JSON
Глобалните уеб и мобилните приложения използват леки структури от данни като JSON за преместване на данните от уеб/мобилното устройство към базата данни и обратно. Малкият отпечатък на JSON структурите от данни прави прехвърлянето на данни през мрежата бързо и лесно. Лесно е да компресирате JSON до много малък размер в сравнение с други структури, т.е. XML. Може да превъзхожда структурите при анализа по време на изпълнение.
Поради увеличеното използване на JSON структури от данни, релационните бази данни имат формат за съхранение JSON като тип данни BLOB или тип данни CLOB. И двата типа правят данните в такива колони неиндексирани, каквито са.
Поради тази причина доставчиците на бази данни въведоха JSON функции за запитване и модифициране на JSON обекти, тъй като можете лесно да интегрирате тези функции в SQL заявката или други DML команди. Тези заявки обаче зависят от сложността на JSON обектите. Те са много процесорни и отнемащи ресурси, тъй като BLOB и CLOB обекти трябва да бъдат разтоварени в паметта или, по-лошо, в работния файл преди запитване и/или манипулиране.
Да приемем, че имаме Клиент таблица с Подробности за клиента данни, съхранявани като JSON обект в колона, наречена CustomerDetail . Настроихме заявка към таблицата, както следва:
SELECT CustomerID,
JSON_VALUE(CustomerDetail, '$.customer.Name') AS Name,
JSON_VALUE(CustomerDetail, '$.customer.Surname') AS Surname,
JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') AS PostCode,
JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 1"') + ' '
+ JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 2"') AS Address,
JSON_QUERY(CustomerDetail, '$.customer.address.Country') AS Country
FROM Customer
WHERE ISJSON(CustomerDetail) > 0
AND JSON_VALUE(CustomerDetail, '$.customer.address.Country') = 'Iceland'
AND JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') IN (101,102,110,210,220)
AND Status = 'Active'
ORDER BY JSON_VALUE(CustomerDetail, '$.customer.address.PostCode')
В този пример правим запитване за данните за клиенти, живеещи в някои части на столичния регион в Исландия. Всички Активни данните трябва да бъдат извлечени в работния файл преди да приложи предиката за търсене. Все пак извличането ще доведе до твърде голямо използване на процесора и ресурси.
Съответно има ефективна процедура за по-бързо изпълнение на JSON заявките. Това включва използване на функционалността чрез генерирани колони, както е описано по-горе.
Постигаме повишаване на производителността чрез добавяне на генерирани колони. Генерирана колона ще търси в документа JSON конкретни данни, представени в колоната, като използва функциите JSON и ще съхранява стойността в колоната.
Можем да индексираме и да правим заявки за тези генерирани колони, използвайки обикновени условия за търсене на SQL where. Следователно търсенето на конкретни данни в JSON обекти става много бързо.
Добавяме две генерирани колони – Държава и Пощенски код :
ALTER TABLE Customer
ADD Country as JSON_VALUE(CustomerDetail,'$.customer.address.Country');
ALTER TABLE Customer
ADD PostCode as JSON_VALUE(CustomerDetail,'$.customer.address.PostCode');
CREATE INDEX ix_CountryPostCode on Country(Country asc,PostCode asc);
Също така създаваме съставен индекс за конкретните колони. Сега можем да променим заявката към примера, показан по-долу:
SELECT CustomerID,
JSON_VALUE(CustomerDetail, '$.customer.customer.Name') AS Name,
JSON_VALUE(CustomerDetail, '$.customer.customer.Surname') AS Surname,
JSON_VALUE(CustomerDetail, '$.customer.address.PostCode') AS PostCode,
JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 1"') + ' '
+ JSON_VALUE(CustomerDetail, '$.customer.address."Address Line 2"') AS Address,
JSON_QUERY(CustomerDetail, '$.customer.address.Country') AS Country
FROM Customer
WHERE ISJSON(CustomerDetail) > 0
AND Country = 'Iceland'
AND PostCode IN (101,102,110,210,220)
AND Status = 'Active'
ORDER BY JSON_VALUE(CustomerDetail, '$.customer.address.PostCode')
Това ограничава извличането на данни до активни клиенти само в част от столичния регион на Исландия. Този начин е по-бърз и по-ефективен от предишната заявка.
Заключение
Като цяло, чрез прилагане на виртуални колони или функционални индекси към таблици, които причиняват затруднения (CPU и заявки, натоварени с ресурси), можем да премахнем проблемите сравнително бързо.
Виртуалните колони и функционалните индекси могат да помогнат при заявки за сложни JSON обекти, съхранявани в обикновени релационни таблици. Трябва обаче да преценим внимателно проблемите предварително и да направим съответните промени.
В някои случаи, ако структурите от данни на заявката и/или JSON са много сложни, част от използването на процесора и ресурсите може да се измести от заявките към процесите INSERT / UPDATE. Това ни дава по-малко общи спестявания на процесор и ресурси от очакваното. Ако се сблъскате с подобни проблеми, по-задълбочен дизайн на таблици и заявки може да е неизбежен.