Особливості роботи з Microsoft SQL Server в Delphi, Інші СУБД, Бази даних, статті

Огляд сервера


Microsoft SQL Server – прямий нащадок Sybase SQL Server, з яким його досі пов’язує багато спільного, в першу чергу мова програмування Transact-SQL (далі T-SQL). Однак у версії 7.0, як заявляє Microsoft, сервер переписаний повністю і більше не містить коду від Sybase. Головним достоїнством Microsoft SQL Server є його тісна інтеграція з Windows NT і сімейством продуктів Back Office – загальна модель захисту, що базується на захисті Windows NT, єдина консоль адміністрування (Microsoft Management Console), єдиний набір програмних інтерфейсів для доступу до даних (OLE DB). Поточна версія сервера на момент написання цієї книги – Microsoft SQL Server 2000. Сервер випускається в наступних редакціях:



Крім того, існує версія SQL Server для Windows CE, призначена для портативних комп’ютерів. Вона сумісна з рештою версіями сервера за мовою для написання серверного коду і має можливості реплікації даних з ними.


Таким чином, сервер працює на всій лінійці операційних систем Microsoft на процесорах Intel і Alpha.


Не можна не згадати і такий продукт, як Microsoft Data Engine (MSDE), – це версія Microsoft SQL Server без графічних засобів адміністрування і з обмеженнями за розміром бази даних (2 Гбайт) і кількості користувачів (5). MSDE призначений для побудови вбудованих систем, які при необхідності легко можуть бути перенесені на повнофункціональну редакцію сервера, а також, наприклад, використані для створення демонстраційних версій продуктів. Адміністрування MSDE може проводитися за допомогою утиліти osql, клієнтських утиліт від Microsoft SQL Server або з Microsoft Access 2000. MSDE постачається у складі Microsoft Office 2000 Professional і Microsoft Visual Studio.


З додаткових можливостей слід зазначити:


 SELECT ProductName
FROM Products
WHERE CONTAINS(ProductName, “spread NEAR Boysenberry”)


Клієнтська частина Microsoft SQL Server реалізована на платформі Win32. У стандартний комплект поставки входять драйвери, що працюють під управлінням Windows 95 і Windows NT. Таким чином, в якості клієнта Microsoft SQL Server можуть виступати всі платформи, підтримувані Delphi.


Все перераховане робить Microsoft SQL Server привабливим рішенням для реалізації баз даних на платформі Windows NT.








Особливості реалізації клієнтської частини


До версії 6.5 основним інтерфейсом доступу до Microsoft SQL Server з боку клієнта була бібліотека DB-Library. Вона реалізовувала набір низькорівневих інтерфейсів, що дозволяють організувати взаємодію з сервером. Однак у версії 7 був введений новий інтерфейс доступу – OLE DB. У зв’язку з цим розвиток DB-Library було припинено, і тепер бібліотека служить лише для забезпечення зворотної сумісності. Доступ через неї не підтримує нових розширень сервера (Unicode, текстові поля до 8 Кбайт, тип даних GUID). Проте старі додатки, які не використовують цієї функціональності, зберігають працездатність, проте у зв’язку із змінами в 7-й версії може знадобитися деяка їх переробка. Драйвер SQL Links реалізує доступ за допомогою DB-Library, тому скористатися цими розширеннями з його допомогою неможливо. Для забезпечення повноцінного доступу до Microsoft SQL Server 7.0 і вище необхідно використовувати в додатку новий набір компонентів ADOExpress, включений в Delphi 5. Можливо також застосування BDE, але при цьому сервер доступний в обсязі можливостей версії 6.х. Існує також ODBC-драйвер, за допомогою якого можливий повнофункціональний доступ до сервера. При роботі з сервером версії 2000 на застосування BDE накладаються додаткові обмеження, пов’язані з використанням індексів по обчислюваним полям.








Доступ за допомогою ADOExpress


ActiveX Data Objects (ADO) – надбудова над інтерфейсом OLE DB, що дозволяє забезпечити бізнес-додаткам високорівнева доступ до даних. Ця технологія включена в Windows 2000, а для інших версій Windows доступна у вигляді безкоштовного оновлення. ADO автоматично інсталюється на комп’ютер при установці клієнта Microsoft SQL Server.


Починаючи з 5.0-й версії в Delphi (в редакції Enterprise) включений набір компонентів, що дозволяють працювати з ADO. Користувачі Delphi Professional можуть придбати ці компоненти у вигляді окремого продукту.


Як драйверів баз даних ADO використовує так звані OLE DB-провайдери, які представляють собою COM-сервери, що реалізують визначений набір COM-інтерфейсів. Наприклад, для доступу до набору даних служить інтерфейс IRowset, що повертається OLE DB при відкритті цього набору даних. Для того щоб вказати, який провайдер і з якими параметрами повинен використовуватися ADO, передбачена так звана рядок підключення (Connection String), що міститься у властивості ConnectionString компонентів ADOExpress.


Для її побудови Delphi використовує відповідний діалог:


Для підключення до Microsoft SQL Server необхідно вказати тип провайдера “Microsoft OLE DB Provider for SQL Server” і в наступному вікні заповнити інформацію, необхідну для підключення:


Після цього можна використовувати компоненти ADO в якості звичайних спадкоємців класу TDataSet.


Слід зазначити, що технологія ADO в значній мірі оптимізована для використання спільно з Microsoft SQL Server 7.0. Повністю підтримується модель роботи Prepare-Execute, що дозволяє ефективно кешувати плани запитів, серверні курсори (властивості CursorLocation і CursorType), можливість повернення запитом або збереженої процедурою декількох наборів даних, прямий доступ до таблиць на сервері (Без проміжної генерації запиту). Наприклад, задавши в TADOQuery.SQL наступний код:

SELECT * FROM One
SELECT * FROM Two

можна у додатку отримати два набори даних:

ADOQuery1.Open;
ADODataSet1.RecordSet := ADOQuery1.NextRecordSet;

Вирішено проблему, що виникає при спробі виконати запит при не до кінця завантажені даних іншого запиту. В цьому випадку ADOExpress автоматично відкриває новий тимчасовий з’єднання з сервером і виконує запит, використовуючи це з’єднання.


В початковій версії Delphi 5 технологія ADOExpress містить ряд серйозних помилок, які роблять роботу за допомогою вищезгаданих компонентів практично неможливою. Тому настійно рекомендується установка пакетів оновлень Delphi і останнього поновлення ADO (AePatch.exe), доступного з сайту community.borland.com з розділу Code Central.








Доступ за допомогою BDE


Цей спосіб доступу не є рекомендованим, проте для багатьох додатків, написаних ще для версії 6.х, він може знадобитися з міркувань зворотної сумісності.


У складі BDE поставляється драйвер SQL Link для Microsoft SQL Server, що використовує бібліотеку DB-Library, що виключає повноцінний доступ до можливостей сервера. При роботі за допомогою цього драйвера, наприклад, не можна використовувати символьні дані в кодуванні Unicode, а також деякі системні функції. Якщо необхідно отримати доступ до таких даних, в запиті необхідно явно перетворити їх в один з підтримуваних типів даних, наприклад:

SELECT CAST(USER_NAME() AS VARCHAR(255)) 

При роботі з сервером за допомогою BDE і драйвера SQL Link необхідно пам’ятати наступне:


 ANSI_NULLS ON
ANSI_PADDING ON
ANSI_WARNINGS ON
ARITHABORT ON
CONCAT_NULL_YIELDS_NULL ON
QUOTED_IDENTIFIER ON
NUMERIC_ROUNDABORT OFF

В іншому випадку таблиця, для якої визначено індекс по обчислюваному полю, буде доступна тільки для читання. Наведений набір налаштувань відповідає значенням за умовчанням для сесій OLE DB і ODBC. DB-Library ініціалізує їх по-іншому, тому ви повинні явно задати необхідні значення після з’єднання з сервером.








Особливості реалізації серверної частини


Microsoft SQL Server має вбудовану мову програмування Transact SQL, що є процедурним розширенням стандарту ANSI SQL 92 entry level. T-SQL має повний набір засобів для написання збережених процедур і тригерів. Крім того, реалізовані деякі розширення стандартного мови SQL, які необхідно знати розробнику.








SELECT


У ранніх версіях зовнішнє об’єднання таблиць задавалося виразом * = і = * в реченні WHERE. Цей синтаксис підтримується, але не рекомендується і у чергових версіях буде виключений. Починаючи з версії 6.5 сервер підтримує стандартний синтаксис {LEFT / RIGHT / FULL} [OUTER] JOIN.


При виконанні SELECT в таблицю (SELECT INTO) функція IDENTITY (data_type [, seed, increment]) дозволяє створити в цій таблиці автоінкрементне поле IDENTITY і заповнити його. За допомогою цієї функції і тимчасових таблиць можна пронумерувати результати запиту.

SELECT IDENTITY(INTEGER, 1, 1) AS Counter, Name
INTO #Temp
FROM MyTable
ORDER BY Name

SELECT * FROM #Temp


Починаючи з версії 7.0 оператор SELECT має модифікатори TOP n [PERSENT] [WITH TIES], що дозволяють вивести першу n записів або n відсотків записів. Вказавши WITH TIES, можна змусити сервер включити в результат всі записи з таким же значенням сортованого поля, як і в останньої з n записів. Якщо SELECT не має фрази ORDER BY, то набір записів не обов’язково буде однаковою.


В якості однієї з таблиць у запиті можна використовувати вкладений запит:

SELECT A.Name, A.Population, B.AvgPop
FROM City A INNER JOIN
(SELECT Country, AVG(Population) AS AvgPop
FROM City GROUP BY Country ) AS B
ON A.Country = B.Country

Цей запит для кожного міста виведе його назву, кількість жителів, а також середня кількість жителів на місто в тій країні, де він знаходиться.


Функції OPENQUERY і OPENROWSET дозволяють використовувати в якості однієї з таблиць у запиті вибірку з будь-якого OLE DB-сумісного джерела даних.


У Microsoft SQL Server 2000 можна в запиті вказати вираз FOR XML, в результаті чого буде повернута рядок, що містить XML-представлення вибірки. Наприклад, запит:

SELECT O.OrderID, O.CustomerID, O.OrderDate,
O.ShipName, O.ShipAddress, O.ShipCity, O.ShipRegion,
P.ProductName, OD.UnitPrice, OD.Quantity
FROM Orders O
INNER JOIN [Order Details] OD ON O.OrderId = OD.OrderId
INNER JOIN Products P ON OD.ProductId = P.ProductId
WHERE O.OrderId = “10248”
FOR XML AUTO

поверне результат:

<O OrderID=”10248″
CustomerID=”VINET”
OrderDate=”1996-07-04T00:00:00″
ShipName=”Vins et alcools Chevalier”
ShipAddress=”59 rue de l'Abbaye”
ShipCity=”Reims”>
<P ProductName=”Queso Cabrales”>
<OD UnitPrice=”14.0000″ Quantity=”12″/>
</P>
<P ProductName=”Singaporean Hokkien Fried Mee”>
<OD UnitPrice=”9.8000″ Quantity=”10″/>
</P>
<P ProductName=”Mozzarella di Giovanni”>
<OD UnitPrice=”34.8000″ Quantity=”5″/>
</P>
</O>

Передбачено як автоматичне форматування XML-результатів запиту, так і завдання способи форматування програмістом.


Крім того, можливе використання XML-даних як таблиці в запиті. Розглянемо, наприклад, збережену процедуру, видає дані по заздалегідь невідомому кількістю записів. Ідентифікатори записів передаються в неї у вигляді XML-документа:

CREATE PROCEDURE XMLParam
@Ids VARCHAR(8000)
AS
 DECLARE @idoc int
EXEC sp_xml_preparedocument @idoc OUTPUT, @Ids
SELECT O.*
FROM Orders O
INNER JOIN OPENXML (@idoc, “/ROOT/Ids”, 1) WITH (ID INT) AS T ON
.OrderId = T.Id
EXEC sp_xml_removedocument @idoc
GO

Виклик цієї процедури виглядає наступним чином:

DECLARE @S VARCHAR(8000)

SET @S = “<ROOT>
<Ids ID=”10250″/>
<Ids ID=”10257″/>
<Ids ID=”10258″/>
</ROOT>”

EXECUTE XMLParam @S


Очевидно, що відповідний рядок параметрів може бути легко побудована і клієнтським додатком.








INSERT


На додаток до стандартних можливостей Microsoft SQL Server дозволяє вставити в таблицю набір даних, отриманий в результаті виконання процедури, за допомогою синтаксису:

INSERT author_sales EXECUTE get_author_sales 







UPDATE і DELETE


Сервер підтримує розширений синтаксис

UPDATE MyTable 
 SET Name = ‘Іванов’
FROM MyTable T INNER JOIN AnotherTable A ON T.Id = A.MyTableId
AND A.SomeField = 20







CREATE TABLE


У версії 7.0 підтримуються наступні типи даних:
































































































Тип Синонім Примітка
BIT Ціле число, рівне 0 або 1. В Delphi можливе звернення до поля цього типу за допомогою властивості AsBoolean (1 = True, 0 = False)
INT INTEGER 32-бітове ціле число в діапазоні від – 2147483648 до 2147483647
SMALLINT 16-бітове ціле число в діапазоні від 32 768 до 32 767
TINYINT 8-бітове ціле число в діапазоні від 0 до 255
DECIMAL[(P[, S])] NUMERIC, DEC Десяткове число з фіксованою точністю в діапазоні від – 1038 -1 До 1038 – 1. P – максимальна кількість знаків у числі S – кількість знаків після коми
MONEY Грошовий тип даних. Ціле 64-бітове число, молодші 4 розряду якого відведені під дробову частину. Може зберігати числа в діапазоні від -922 337 203 685 477,5808 до 922 337 203 685 477,5807. В Delphi відповідає типу даних Currency
SMALLMONEY Аналогічний Money, але 32-розрядний і обмежений діапазоном від -214 748,3648 до 214 748,3647
FLOAT DOUBLE PRECISION Число з плаваючою крапкою в діапазоні від-1.79E + 308 до 1.79E + 308
REAL Число з плаваючою крапкою в діапазоні від-3.40E + 38 до 3.40E + 38
DATETIME Дата і час в діапазоні від 1 січня 1753 до 31 грудня 9999 р. з точністю 3.33 мс
SMALLDATETIME Дата і час в діапазоні від 1 січня 1900 р. до 6 червня 2079 з точністю до 1 хв
TIMESTAMP Унікальний ідентифікатор в межах бази даних. Цей тип даних НЕ МІСТИТЬ часу і лише гарантує, що поле цього типу унікально в рамках бази даних
UNIQUEIDENTIFIER Глобальний унікальний ідентифікатор. Статистично унікальне 16-бітове значення. Над цим типом даних визначені лише операції =, <>, IS NULL і IS NOT NULL
CHAR[(N)] CHARACTER,
VARYING VARCHAR
Рядок фіксованої довжини. N – довжина рядка. Максимальна довжина – 8 тис. символів
VARCHAR[(N)] CHARACTER VARYING(N) Рядок змінної довжини. N – довжина рядка. Максимальна довжина – 8 тис. символів
TEXT Рядок довільної довжини (до 2147483647 символів)
NCHAR[(N)] NATIONAL CHARACTER,
NATIONAL CHAR
Рядок фіксованої довжини в форматі Unicode. N – довжина рядка. Максимальна довжина – 4 тис. символів
NVARCHAR[(N)] NATIONAL CHARACTER VARYING(N),
NATIONAL CHAR VARYING(N)
Рядок змінної довжини в форматі Unicode N – довжина рядка. Максимальна довжина – 4000 символів
NTEXT NATIONAL TEXT Рядок довільної довжини (до 1073741823 символів)
BINARY[(N)] VARYING VARBINARY Двійкові дані фіксованої довжини (до 8000 байт) N – довжина даних
VARBINARY[(N)] Двійкові дані змінної довжини (до 8000 байт) N – довжина даних
IMAGE Двійкові дані довільної довжини (до 2147483647 байт)

У версії SQL 2000 додатково передбачені наступні типи даних:
















Тип Синонім Примітка
BIGINT 64-бітове ціле число
SQL_VARIANT Може зберігати дані довільного типу

У версії 7.0 підтримується створення обчислюваних полів

CREATE TABLE MyTable (
Direction BIT NOT NULL,
Amount MONEY,
CASE Direction
WHEN 1 THEN Amount
ELSE -Amount
END AS SignedAmount
)

Вираз не повинно містити підзапитів. У версії Microsoft SQL Server 2000 по обчислюваному полю може бути побудований індекс.








Написання тригерів


Тригери в Microsoft SQL Server спрацьовують після оновлення і лише один раз на оператор (а не на кожну оновлену запис). Кількість тригерів на таблицю не обмежена. У тригері доступні оновлена таблиця і дві віртуальні таблиці Inserted і Deleted.


У них знаходяться:




















Inserted Deleted

INSERT

Вставлені записи Немає записів

UPDATE

Нові версії записів Старі версії записів

DELETE

Немає записів Дистанційні записи

Грунтуючись на змісті цих таблиць, тригер може здійснити додаткову модифікацію даних або скасувати транзакцію, що викликала цей оператор. Наприклад:

CREATE TRIGGER T1 ON MyTable FOR INSERT, UPDATE
AS BEGIN – Заносимо в поля: – LastUserName – ім’я користувача, останнім обновив запис – LastDateTime – дату і час останнього оновлення
UPDATE MyTable
SET LastUserName = SUSER_NAME(),
LastDateTime = GETDATE()
FROM Inserted I INNER JOIN MyTable T ON I.Id = T.Id
END
 
CREATE TRIGGER T2 ON MyTable FOR DELETE
AS BEGIN – Цей тригер відкатує і знімає всю транзакцію – Викликала помилку
IF EXISTS (SELECT * FROM Deleted
WHERE Position = ‘Boss’) BEGIN RAISERROR (‘Не можна видаляти начальника’, 16, 1)
ROLLBACK
END
END
 
CREATE TRIGGER T3 ON MyTable FOR DELETE
AS BEGIN – А цей просто не дає видалити запис – Дозволяючи продовжити транзакцію
IF EXISTS (SELECT * FROM Deleted
WHERE Position = ‘Programmer’) BEGIN
INSERT INTO MyTable
SELECT * FROM Deleted
WHERE Position = ‘Programmer’ RAISERROR (‘Програміста видалити теж не вийде’, 16, 1)
END
END

Оскільки тригер спрацьовує після оновлення таблиці, в ньому не можна реалізовувати каскадні оновлення даних при наявності зовнішніх ключів. Наприклад, якщо є дві таблиці:

CREATE TABLE Main (
Id INTEGER PRIMARY KEY
)

CREATE TABLE Child (

 Id INTEGER PRIMARY KEY, 
 MainId INTEGER NOT NULL REFERENCES Main(Id)
)

то при видаленні запису з Main, на яку є посилання в Child, тригер на Main не спрацює. Щоб обійти цю проблему, рекомендується створити збережену процедуру

CREATE PROCEDURE DeleteFromMain
@Id INTEGER
AS BEGIN
DECLARE @Result INTEGER
BEGIN TRANSACTION
SAVE TRANSACTION DeleteFromMain
DELETE Child WHERE MainId = @Id
DELETE Main WHERE Id = @Id
SET @Result = @@ERROR
IF @Result <> 0
ROLLBACK TRANSACTION DeleteFromMain
COMMIT
END

Інший спосіб – реалізація обмежень посилальної цілісності тільки за допомогою тригерів.


Крім того, у версії Microsoft SQL Server 2000 можливе створення тригерів INSTEAD OF, які виконуються замість викликала їх операції. При цьому відповідальність за запис даних у таблиці повністю лежить на програміста. Такі тригери можуть бути створені на уявленнях (VIEW), що дозволяє зробити оновлюваним будь уявлення, незалежно від його складності.








Пакети команд


Оператори можуть бути відправлені на сервер не поодинці, а пакетами. Пакетом (batch) називається група команд, відправлена ​​клієнтським додатком на сервер одночасно. Весь пакет компілюється в єдиний план виконання. Така техніка дозволяє зменшити мережевий трафік і збільшити ефективність програми. Типовий пакет може виглядати наступним чином:

BEGIN TRANSACTION
INSERT One (SomeField) VALUES (:1)
INSERT Two (AnotherField) VALUES (:2)
IF @@ERROR = 0
COMMIT
ELSE
ROLLBACK

Всередині пакету можливо оголошення змінних. Область їх видимості обмежена пакетом, в якому вони оголошені.


Весь пакет не виконується у разі синтаксичної помилки в будь-якому з операторів пакета. Однак у випадку помилки виконання будь-якого оператора інші оператори продовжують виконуватися до закінчення пакета.


Роздільником пакетів команд служить оператор GO.








Обробка помилок


Для того щоб поінформувати клієнтську програму про помилку, Microsoft SQL Server використовує функцію RAISERROR. При цьому необхідно пам’ятати, що:



При виникненні помилки в якому-небудь з операторів усередині пакета виконання пакету триває, а функція @ @ ERROR повертає код помилки, який можна обробити.

INSERT MyTable (Name) VALUES (‘Петров’)
IF @@ERROR != 0 PRINT ‘Помилка вставки’.

Після успішного оператора @ @ ERROR повертає 0, тому якщо значення помилки може знадобитися згодом, то його необхідно зберегти в змінній.

DECLARE @ErrCode INTEGER

SET @ErrCode = 0

BEGIN TRANSACTION INSERT MyTable (Name) VALUES (‘Іванов’)
IF @@ERROR != 0
@ErrCode = @@ERROR
INSERT MyTable (Name) VALUES (‘Петров’)
IF @@ERROR != 0
@ErrCode = @@ERROR

IF @ErrCode = 0
COMMIT
ELSE BEGIN
ROLLBACK RAISERROR (‘Не вдалося оновити дані’, 16, 1)
END


Якщо оператор поновлення даних не знайшов жодного запису, помилки не виникає. Перевірити цю ситуацію можна за допомогою функції @ @ ROWCOUNT, яка повертає кількість записів, оброблених останнім оператором.

UPDATE MyTable SET Name = ‘Сидоров’ WHERE Name = ‘Петров’

IF @@ROWCOUNT = 0 PRINT ‘Петров не знайдено’








Блокування


Microsoft SQL Server підтримує блокування на рівні запису для всіх операцій модифікації даних. Якщо оптимізатор вирішить, що кількість заблокованих записів в таблиці занадто велике, то він може призвести ескалацію блокувань на групу сторінок або на всю таблицю. Це відбувається, наприклад, при одночасному відновленні значної кількості записів. У подібному випадку набагато зручніше заблокувати таблицю (Або групу сторінок в ній), внести зміни, а потім розблокувати її, замість того, щоб накладати блокування на кожну запис. Сервер не надає коштів для управління ескалацією блокувань і здійснює її автоматично.


Іншою важливою проблемою є модель забезпечення рівнів ізоляції транзакцій REPEATABLE READ і SERIAZABLE. При виконанні транзакції з цим рівнем ізоляції сервер блокує діапазони значень полів, за якими здійснюється вибірка даних для запобігання вставки “фантомних” значень. Наприклад, якщо в транзакції з рівнем ізоляції SERIAZABLE буде виконаний запит

SELECT * FROM MyTable WHERE Name BETWEEN ‘A’ AND ‘C’ 

то сервер накладе блокування по запису (Shared Lock) на діапазон значень, що потрапили в результат запиту, запобігаючи тим самим вставку “фантомних” записів іншими транзакціями. Блокування буде утримуватися до кінця транзакції. На змінені транзакцією записи накладається блокування з читання (Exclusive Lock), що запобігає читання їх іншими транзакціями. Тому транзакції з високими рівнями ізоляції необхідно ретельно планувати і робити їх максимально короткими.








Обробка транзакцій


У Microsoft SQL Server підтримуються всі визначені стандартом ANSI SQL 92 рівні ізоляції транзакцій:
















READ UNCOMMITTED Дозволяє транзакції читати непідтверджені дані інших транзакцій
READ COMMITTED Запобігає зчитування транзакцією даних, не підтверджених інший транзакцією
REPEATABLE READ Все блокування утримуються до кінця транзакції, гарантуючи ідентичність повторно лічених даних прочитаним раніше
SERIALIZABLE Гарантує відсутність “фантомів”. Реалізується за рахунок блокування діапазонів записів, всередині яких ці “фантоми” можуть з’явитися

Для установки поточного рівня ізоляції використовується оператор

SET TRANSACTION ISOLATION LEVEL
{
READ COMMITTED
/ READ UNCOMMITTED
/ REPEATABLE READ
/ SERIALIZABLE
}

Момент початку транзакції регулюється установкою

SET IMPLICIT_TRANSACTION ON/OFF 

За замовчуванням вона встановлена ​​в ON, і кожен оператор виконується в окремій транзакції. По його завершенні неявно виконується COMMIT. Якщо необхідно виконати транзакцію, що складається з декількох операторів, її треба явно почати командою BEGIN TRANSACTION. Закінчується транзакція оператором COMMIT або ROLLBACK.


Наприклад:

INSERT MyTable VALUES (1) 
– Виконав всередині окремої транзакції
BEGIN TRANSACTION – Почали явну транзакцію
INSERT MyTable VALUES (2)
INSERT MyTable VALUES (3)
COMMIT – Завершили явну транзакцію

При видачі команди

SET IMPLICIT_TRANSACTION OFF 

сервер починає нову транзакцію, якщо вона ще не розпочато і виконався один з наступних операторів:





















ALTER TABLE


FETCH


REVOKE


CREATE


GRANT


SELECT


DELETE


INSERT


TRUNCATE TABLE


DROP


OPEN


UPDATE


Транзакція триває до тих пір, поки не буде видана команда COMMIT або ROLLBACK.


Можливе створення вкладених транзакцій. При цьому функція @ @ TRANCOUNT показує глибину вкладеності транзакції. Наприклад:

BEGIN TRANSACTION SELECT @ @ TRANCOUNT – Видасть 1
BEGIN TRANSACTION SELECT @ @ TRANCOUNT – Видасть 2
COMMIT SELECT @ @ TRANCOUNT – Видасть 1
COMMIT SELECT @ @ TRANCOUNT – Видасть 0

Вкладений BEGIN TRANSACTION не починає нову транзакцію. Він лише збільшує @ @ TRANCOUNT на одиницю. Аналогічно вкладений оператор COMMIT не завершує транзакцію, а лише зменшує @ @ TRANCOUNT на одиницю. Реальне завершення транзакції відбувається, коли @ @ TRANCOUNT стає рівним нулю. Такий механізм дозволяє писати збережені процедури, що містять транзакцію, наприклад:

 CREATE PROCEDURE Foo
AS BEGIN
BEGIN TRANSACTION
INSERT MyTable VALUES (1)
INSERT MyTable VALUES (1)
COMMIT
END

При запуску поза контекстом транзакції процедура виконає свою транзакцію. Якщо вона запущена в транзакції, внутрішні BEGIN TRANSACTION і COMMIT просто збільшать і зменшать лічильник транзакцій.


Оператор ROLLBACK веде себе по-іншому. Він завжди, незалежно від поточного рівня вкладеності, встановлює значення змінної @ @ TRANCOUNT рівним нулю і скасовує всі зміни, від початку самої зовнішньої транзакції. Якщо в збереженій процедурі можливий відкат її дій виходячи з якогось умови, можна використовувати точки збереження (savepoint)

CREATE PROCEDURE Foo
AS BEGIN
BEGIN TRANSACTION – Цей оператор не може бути скасований поза контекстом – Основний транзакції
INSERT MyTable VALUES (1)
SAVE TRANSACTION InsideFoo – Оператори, починаючи звідси, можуть бути скасовані – Без відкату основний транзакції
INSERT MyTable VALUES (2)
INSERT MyTable VALUES (3)
IF (SELECT COUNT(*) FROM MyTable) > 3
ROLLBACK TRANSACTION InsideFoo – Скасовуємо зміни, внесені після – Останнього savepoint
COMMIT
END

Окремого обговорення заслуговує команда ROLLBACK, викликана в тригері.


В цьому випадку не тільки відкочується транзакція, в рамках якої відбулося спрацьовування тригера, але і припиняється виконання пакету команд, всередині якого це сталося. Всі оператори, такі за оператором, що викликав тригер, не будуть виконані. Розглянемо цю ситуацію на прикладі:

CREATE TABLE MyTable (Id INTEGER)

GO

CREATE TRIGGER MyTrig ON MyTable FOR INSERT
AS BEGIN

 IF (SELECT MAX(Id) FROM Inserted) >= 2 BEGIN
ROLLBACK
RAISERROR(‘Id >= 2’, 17, 1)
END
END

GO

INSERT MyTable VALUES (1) INSERT MyTable VALUES (2) – Викличе ROLLBACK в тригері – Оператори, починаючи звідси, не виконаються
INSERT MyTable VALUES (3)
INSERT MyTable VALUES (4)








Відповідність стандарту ANSI SQL 92


У Microsoft SQL Server є налаштування, що дозволяють змінювати ступінь відповідності сервера стандарту ANSI SQL 92.


SET ANSI_NULLS {ON / OFF} – регулює результат порівняння значень, що містять NULL. Якщо ANSI_NULLS = OFF, то запит

SELECT * FROM MyTable WHERE MyField = NULL 

поверне всі рядки, в яких MyField встановлено в NULL. Якщо ANSI_NULLS = OFF, то відповідно до стандарту ANSI SQL92 порівняння з NULL повертає UNKNOWN. Інші установки, на які слід звернути увагу:






















SET CURSOR_CLOSE_ON_COMMIT Встановлює режим закриття курсорів по завершенні транзакції
SET ANSI_NULL_DFLT_ON і
SET ANSI_NULL_DFLT_OFF
Встановлюють можливість прийняття значення NULL полем за замовчуванням при створенні таблиці
SET IMPLICIT_TRANSACTIONS Встановлює режим Autocommit
SET ANSI_PADDING Встановлює режим “відсікання” кінцевих пробілів для новостворюваних полів
SET QUOTED_IDENTIFIER Дозволяє виділення ідентифікаторів подвійними лапками
SET ANSI_WARNINGS Встановлює реакцію на математичні помилки

Розгляд цих параметрів виходить за рамки даної статті, проте при читанні документації необхідно звернути на них увагу.


Параметр SET ANSI_DEFAULTS встановлює режим максимальної сумісності з ANSI SQL 92. При установці SET ANSI_DEFAULTS ON встановлюються в ON наступні параметри:
















SET ANSI_NULLS SET CURSOR_CLOSE_ON_COMMIT
SET ANSI_NULL_DFLT_ON SET IMPLICIT_TRANSACTIONS
SET ANSI_PADDING SET QUOTED_IDENTIFIER
SET ANSI_WARNINGS

За замовчуванням ANSI_DEFAULTS = ON для клієнтів ODBC і OLE DB (ADO) і OFF для клієнта DB-Library (BDE). Оскільки кращим (і підтримуваним в майбутньому) методом доступу є OLE DB, то при розробці клієнтської частини, що використовує BDE, рекомендується явно встановлювати SET ANSI_DEFAULTS ON. З відмінностями в значенні цього параметра пов’язана і проблема, що виникає при розробці запитів за допомогою Query Analyzer. Якщо в ньому і в клієнтському додатку є різні настройки сумісності з ANSI, одні й ті ж запити можуть видавати різні результати. Тому рекомендується перевіряти налаштування Query Analyzer на предмет їх відповідності тим, які передбачаються в клієнтському додатку.








Модель безпеки


Ролі


Замість груп користувачів, які фігурували в попередніх версіях, версія 7.0 використовує ролі користувачів. Права можуть даватися або відніматися у ролі, і це відбивається на правах усіх асоційованих з неї користувачів. При цьому користувач може бути асоційований одночасно з декількома ролями.


Для полегшення управління сервером і базами даних підтримуються вбудовані серверні ролі і ролі баз даних. Так, наприклад, серверна роль serveradmin дає право на настройку конфігурації і зупинку сервера, а роль бази даних db_securityadmin дозволяє управляти правами доступу користувачів. Набір вбудованих ролей дає можливість не надавати адміністративних привілеїв користувачам, які потребують в будь-яких специфічних права. Так, роль db_securityadmin не надає своїм членам прав на читання або модифікацію користувальницьких таблиць.








Інтегрована безпека


Сильною стороною Microsoft SQL Server є його тісна інтеграція з системою безпеки Windows NT. Права на доступ до сервера і баз даних можна давати користувачам і групам Windows NT. Механізм делегування повноважень дозволяє користувачам, що підключились до одного з серверів, мати доступ до інших серверів в мережі зі своїми правами, що відрізняються від прав сервера, до якого вони підключилися. Також можлива прозора для користувача перевірка його повноважень при доступі до сервера через Microsoft Internet Information Server або Microsoft Transaction Server.



Оптимізатор запитів Microsoft SQL Server


У версії 7.0 істотно перероблений оптимізатор запитів. Сервер може використовувати кілька індексів на кожну таблицю в запиті; один запит може виконуватися паралельно на декількох процесорах. У SQL Server 7.0 реалізовані три методи виконання операції злиття таблиць (JOIN):



  1. LOOP JOIN – для кожного запису в одній з таблиць проводиться цикл по зв’язаних записів другої таблиці. Цей метод найбільш ефективний для малих результуючих наборів даних.
  2. MERGE JOIN – вимагає, щоб обидва набору даних були розсортовані по зливається полю (набору полів). У цьому випадку сервер здійснює злиття за один прохід по кожному з наборів даних. Оскільки вони вже впорядковані, немає необхідності переглядати всі записи, достатньо вибирати їх починаючи з поточної, поки значення поля не зміниться. Це найшвидший метод злиття великих наборів даних.
  3. HASH JOIN використовується, коли неможливо використовувати MERGE JOIN, а набори даних великі. По одному з наборів будується хеш-таблиця, а потім для кожного запису з другого набору обчислюється та ж хеш-функція і виробляється її пошук в таблиці. У разі великих невідсортованих наборів даних цей алгоритм істотно ефективніше, ніж LOOP JOIN.

При фільтрації за індексом сервер не здійснює відразу вибірку даних з таблиці. Замість цього будується набір “закладок” (Bookmark), а потім здійснюється вибірка даних в однієї операції (Bookmark Lookup). Це дозволяє різко знизити кількість звернень до диску.


Нові стратегії оптимізації вимагають обліку при проектуванні бази даних і структури індексів. Наприклад, для наступної структури таблиць:

CREATE TABLE T1 (
Id INTEGER PRIMARY KEY,

)

CREATE TABLE T2 (
Id INTEGER PRIMARY KEY,

)

CREATE TABLE T3 (
Id INTEGER PRIMARY KEY,
T1Id INTEGER REFERENCES T1(Id),
T2Id INTEGER REFERENCES T2(Id),

)

запит 
SELECT *
FROM T1
INNER JOIN T3 ON T1.Id = T3.T1Id
INNER JOIN T2 ON T2.Id = T3.T2Id
WHERE …

може бути істотно прискорений створенням індексів:

CREATE INDEX T3_1 ON T3(T1Id, T2Id)

Після злиття T3 з T1 він дозволяє отримати впорядкований по T2Id набір даних, який може бути злитий з T2 шляхом ефективного алгоритму MERGE JOIN. Втім, кращий ефект, можливо, дасть індекс:

CREATE INDEX T3_2 ON T3(T2Id, T1Id) 

Це залежить від кількості записів в T1, T2 і розподілу їх поєднань в T3. В OLAP-системі (або в слабо завантаженому OLTP-додатку) краще побудувати обидва цих індексу, в той час як при інтенсивному відновленні таблиці T3, можливо, від одного з них доведеться відмовитися. Сервер може сам видати рекомендації з побудови індексів – для цього в нього включений Index Tuning Wizard, доступний через Query Analyzer. Він аналізує запит (або потік команд, зібраний за допомогою SQL Trace) і видає рекомендації по структурі індексів в конкретній базі даних.


В процесі роботи з Microsoft SQL Server в оптимізаторі запитів автором були виявлені два “тонких” місця, які рекомендується враховувати.



  1. Алгоритм вибору способу об’єднання таблиць не завжди видає оптимальний результат. Це звичайно буває пов’язано з неможливістю визначити точну кількість записів, що беруть участь в об’єднанні на момент генерації плану запиту.
    DECLARE @I INTEGER

    SET @I = 10

    SELECT *
    FROM History H
    INNER JOIN Objects O ON O.Id = H.ObjectId
    WHERE H.StatusId = @I


    Сервер згенерував наступний план виконання:


    Увага: в якості параметра виступає змінна, при цьому сервер не може точно оцінити, в який діапазон статистики вона потрапить. В цьому випадку він робить припущення, що кількість записів, отриманих з History, дорівнюватиме середній селективності по використовуваному полю, помноженої на кількість записів у таблиці (в даному випадку – 10 151). Виходячи з цього вибирається алгоритм злиття HASH JOIN, що вимагає значних накладних витрат на побудову хеш-таблиці. У разі якщо реальна кількість записів відчутно менше (реально цей запит вибирає 100-200 записів, що мають відповідний StatusId за останній день), алгоритм LOOP JOIN дає в багато разів більшу продуктивність. Отже, якщо ви точно знаєте, що фільтрація по конкретному полю дасть обмежений набір даних (не більше кількох сотень записів), а сервер про це “не здогадується”, – вкажіть йому алгоритм злиття явно.

    SELECT *
    FROM History H
    INNER LOOP JOIN Objects O ON O.Id = H.ObjectId
    WHERE H.StatusId = @I

    Робити це треба, тільки якщо ви впевнені, що цей запит буде виконуватися зі значеннями параметра, що мають високу селективність. На великих наборах даних виконання LOOP JOIN буде набагато повільніше.


  2. Ціна операції Bookmark Lookup (витяг даних з таблиці по відомим значенням індексу) явно завищена. Тому іноді, навіть при наявності відповідного індексу, замість INDEX SCAN (пошук за індексом) з подальшим Bookmark Lookup (вибірка з таблиці) сервер приймає рішення про повне сканування таблиці (TABLE SCAN або CLUSTERED INDEX SCAN). Приклад такого запиту наведено на малюнку. Зверніть увагу на передбачувану вартість запиту (Estimated subtree cost) для випадку, коли для таблиці явно заданий пошук за індексом: вона надзвичайно завищена. Видно, що 100% розрахункової вартості виконання дає операція Bookmark Lookup. Реально ж цей запит швидше виконується при індексному доступі, ніж при скануванні таблиці. В цьому випадку рекомендується спробувати явно вказати індекс для доступу до таблиці.

Однак необхідно застерегти користувачів від занадто частого використання підказок оптимізатору. Їх можна використовувати, тільки якщо ви впевнені, що цей запит буде виконуватися в конкретних умовах і вам краще, ніж оптимізатору, відомо розподіл даних у таблиці. У більшості випадків оптимізатор запитів сам добре планує його виконання. Кращим способом оптимізації представляється грамотне планування структури індексів.



Інші особливості Microsoft SQL Server


Отримання унікальних ідентифікаторів


Сервер має засоби для автоматичної генерації унікальних ідентифікаторів, які можуть використовуватися в якості первинного ключа. Такими ідентифікаторами можуть служити цілі числа або новий тип даних UNIQUEIDENTIFIER.


Для отримання цілочисельного унікального ідентифікатора запису в Microsoft SQL використовується ключове слово IDENTITY [(seed, increment)].


Тут:


seed – початкове значення


increment – приріст


За замовчуванням seed і increment рівні 1.


Щоб створити в таблиці автоматичний стовбець, необхідно записати:

 CREATE TABLE TableName (
Id INTEGER NOT NULL IDENTITY
)

Мати атрибут IDENTITY в таблиці може тільки одна з колонок: TINYINT, SMALLINT, INT або DECIMAL (p, 0). Після цього при вставці нових записів поле Id буде отримувати нове значення лічильника. Якщо таблиця має поле зі встановленим IDENTITY, то до цього поля можна звернутися за допомогою ключового слова IDENTITYCOL. Наприклад, запит

 SELECT IDENTITYCOL FROM TableName 

еквівалентний

 SELECT Id FROM TableName 

якщо поле Id створено з атрибутом IDENTITY.


Значення останнього поля з IDENTITY, вставленого поточної сесією, можна отримати функцією @ @ IDENTITY. Наприклад, наступний скрипт додає записи в головну і дочірню таблиці:

DECLARE @IdentityValue INTEGER
INSERT MainTable (Name) VALUES (‘Петров’)

SELECT @IdentityValue = @@IDENTITY
INSERT ChildTable (MainId, Data) VALUES (@ IdentityValue, ‘Перша’) INSERT ChildTable (MainId, Data) VALUES (@ IdentityValue, ‘Друга’)


Слід звернути увагу, що якщо значення ключового поля потрібно для декількох вставок, то його необхідно зберегти в змінній, інакше при наявності у таблиці ChildTable свого поля з IDENTITY після першої вставки в неї @ @ IDENTITY поверне вже значення для ChildTable.


Використання вищеописаної техніки вимагає дотримання обережності при написанні тригерів. Якщо тригер на MainTable сам виробляє вставку в якісь таблиці з IDENTITY, то після

INSERT MainTable (Name) VALUES (‘Петров’) 

функція @ @ IDENTITY вже не поверне значення для MainTable.


У версії Microsoft SQL Server 2000 з’явилася функція SCOPE_IDENTITY (), аналогічна @ @ IDENTITY, проте повертає значення, вставлене в поточному контексті (тригері, збереженої процедурою, пакеті команд). Наприклад, у попередньому прикладі SCOPE_IDENTITY () поверне значення, вставлене в MainTable, незалежно від операцій в тригері, оскільки вони виконуються вже не в поточному контексті.


Значення seed і increment можна використовувати, наприклад, для надання діапазонів значень первинного ключа в розподіленій базі даних. Наприклад, один філія може генерувати значення, починаючи з 1, інший – з 1000000 і т.д.


За замовчуванням у поле з IDENTITY не може бути вставлено явне значення. Проте Microsoft SQL Server дозволяє вирішити таку вставку шляхом установки

 SET IDENTITY_INSERT [database.[owner.]]{table} {ON/OFF} 

Вставка може бути дозволена тільки для однієї таблиці в сесії. Якщо в таблицю вставляється число, більше максимального значення, сервер використовує це число як основу для генерації наступних значень IDENTITY.


Іншим способом генерації унікальних ідентифікаторів, що з’явилося в Microsoft SQL 7.0, є тип даних UNIQUEIDENTIFIER. Фізично це 16-байтове число. Цей тип аналогічний GUID (Global Unique Identifier), активно використовується в технології COM. Над цим типом даних визначені тільки операції =, <>, IS NULL і IS NOT NULL. Порівняння>, <і т.п. не допускається. Для генерації значень використовується функція NEWID ()

CREATE TABLE MyUniqueTable (
UniqueColumn UNIQUEIDENTIFIER DEFAULT NEWID(),
Characters VARCHAR(10)
)

GO

INSERT INTO MyUniqueTable(Characters) VALUES (“abc”)
INSERT INTO MyUniqueTable VALUES (NEWID(), “def”)


Наведені оператори вставки еквівалентні, і обидва створюють записи з унікальними значеннями UniqueColumn. Аналогічно, значення може бути надано клієнтським додатком допомогою функції CoCreateGUID за допомогою властивості AsGUID класів TField і TParam, без побоювання, що воно виявиться неунікальним.



Тимчасові таблиці


Для проміжної обробки даних клієнтське додаток може створювати тимчасові таблиці. Тимчасової називається таблиця, ім’я якої починається з # або # #. Таблиця, ім’я якої починається з #, є локальної і видима тільки тієї сесії, в якій вона була створена. Після завершення сеансу тимчасові таблиці, створені ним, автоматично видаляються. Якщо тимчасова таблиця створена всередині збереженої процедури, вона автоматично видаляється після завершення процедури. Якщо ім’я таблиці починається з # #, то вона є глобальною і видима всіма сесіями. Таблиця видаляється автоматично, коли завершується остання з використовували її сесій.


Для прикладу розглянемо збережену процедуру, яка видає значення продажів по місяцях. Якщо в даному місяці продажів не було, виводиться ім’я місяця і NULL.

CREATE PROCEDURE AmountsByMonths
AS BEGIN
DECLARE @I INTEGER
CREATE TABLE #Months (
Id INTEGER,
Name CHAR(20)
)
SET @I = 1
WHILE (@I <= 12) BEGIN
INSERT Months (Id, Name) VALUES
(@I, DATENAME(month, “1998” + REPLACE(STR(@I,2),” “,”0″)+”01”))
SET @I = @I + 1
END
SELECT M.Name, SUM(P.Amount)
FROM #Months M INNER JOIN Payment P
ON M.Id = DATEPART(month, P.Date)
DROP TABLE #Months
END

У версії Microsoft SQL 2000 з’явилася можливість створювати змінні типу table, що представляють собою таблицю. Робота з такою змінної може виглядати наступним чином:

DECLARE @T TABLE (Id INT)

INSERT @T (Id) VALUES (10250)
INSERT @T (Id) VALUES (10257)
INSERT @T (Id) VALUES (10259)

SELECT O.*
FROM Orders O
INNER JOIN @T AS T ON O.OrderId = T.Id


Змінні типу table більш кращі для використання, ніж тимчасові таблиці, оскільки останні призводять до неможливості кешування плану запиту (він генерується при кожному виконанні), а в разі використання змінних цього не відбувається.



Створення збережених процедур і тригерів


Якщо логіка роботи процедури або тригера вимагає установки яких-небудь SET-параметрів в певні значення, процедура або тригер можуть встановити їх усередині свого коду. По завершенні їх виконання будуть відновлені вихідні параметри, які були на сервері до запуску процедури або оператора, який викликав спрацьовування тригера. Винятком є ​​SET QUOTED_IDENTIFIER і SET ANSI_NULLS для збережених процедур. Сервер запам’ятовує їх на момент створення процедури і автоматично відновлює при виконанні.


Microsoft SQL Server використовує відкладене дозвіл імен об’єктів і дозволяє створювати процедури і тригери, що посилаються на об’єкти, що не існують при їх створенні.



Кластерні індекси


Microsoft SQL Server дозволяє мати в таблиці Один кластерний (CLUSTERED) індекс. Дані в таблиці фізично розташовані на нижньому рівні B-дерева цього індексу, тому доступ по ньому є найшвидшим. За замовчуванням такий індекс автоматично створюється по полю, оголошеному первинним ключем. Всі інші індекси в якості посилання на запис зберігають значення кластерного індексу цього запису, тому не рекомендується будувати його по полях великого розміру. Крім того, для оптимізації операції вставки записів рекомендується будувати цей індекс по полю з монотонно зростаючими значеннями. Виходячи з цих рекомендацій кращий кандидат на побудову кластерного індексу – поле INTEGER (мінімальний розмір), IDENTITY (зростання), оголошене як первинний ключ (завідомо унікальне, частий доступ за цим індексом).



Зумовлені користувачем функції


У версії MicrosoftSQL 2000 передбачена можливість створювати функції в базі даних. Функції можуть бути трьох типів:



  1. Скалярні функції – повертають скалярну величину. Вони аналогічні функціям в будь-якій мові програмування
    CREATE FUNCTION FirstWord (@S VARCHAR(255))
    RETURNS VARCHAR(255)
    AS
    BEGIN
    DECLARE @I INT
    SET @I = CHARINDEX(” “, @S)
    RETURN CASE @I WHEN 0 THEN @S
    ELSE LEFT(@S, @I-1)
    END
    END
    GO

    SELECT dbo.FirstWord (“Hello world !”)


  2. Inline-табличні функції – складаються з одного оператора SELECT і повертають його результат у вигляді таблиці
    CREATE FUNCTION OrdersByCustomer (@S VARCHAR(255))
    RETURNS TABLE
    AS
    RETURN SELECT * FROM Orders WHERE CustomerId = @S
    GO

    SELECT *
    FROM OrdersByCustomer(“VINET”) AS T
    INNER JOIN [Order Details] OD ON OD.OrderId = T.OrderId


  3. Многооператорние табличні функції. Ці функції найбільш цікаві, оскільки дозволяють динамічно сформувати таблицю з необхідними даними, яку потім можна використовувати в запиті.

Як приклад розглянемо функцію, що генерує таблицю, яка містить номери та назви місяців року. Параметр дозволяє згенерувати цю таблицю за один квартал.

CREATE FUNCTION Months (@Quoter INT)
RETURNS @table_var TABLE
(Id int,
Name VARCHAR(20))
AS
BEGIN
DECLARE @Start INTEGER, @End INTEGER

SET @Start = CASE
WHEN @Quoter = 2 THEN 4
WHEN @Quoter = 3 THEN 7
WHEN @Quoter = 4 THEN 10
ELSE 1
END

SET @End = CASE
WHEN @Quoter = 1 THEN 3
WHEN @Quoter = 2 THEN 6
WHEN @Quoter = 3 THEN 9
ELSE 12
END

WHILE (@Start <= @End) BEGIN
INSERT @table_var (Id, Name) VALUES
(@Start, DATENAME(month, “1998” + REPLACE(STR(@Start,2),” “,”0″)+”01”))
SET @Start = @Start + 1
END

RETURN
END
GO

 
SELECT T.Name, SUM(O.Freight)
FROM dbo.Months(NULL) AS T
INNER JOIN Orders O ON DATEPART(month, O.OrderDate) = T.Id
GROUP BY T.Name

За допомогою подібних функцій можна також легко розкривати ієрархії та виконувати інші завдання, які в попередніх версіях сервера вимагали тимчасових таблиць.



Поради по роботі з Microsoft SQL Server



КомпьютерПресс 6 “2001


Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*