Як я можу перетворити перші 100 мільйонів додатних цілих чисел у рядки?


13

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

Спробую почати з дещо формального визначення. Рядок входить у ряд, якщо він складається лише з великих літер від A - Z. Перший член серії - "A". Серія складається з усіх дійсних рядків, відсортованих за довжиною перший та типовим алфавітним порядком другим. Якщо рядки були в таблиці в стовпчику, що називається STRING_COL, порядок можна було б визначити в T-SQL як ORDER BY LEN(STRING_COL) ASC, STRING_COL ASC.

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

1 -> A, 2 -> B, 3 -> C, ..., 25 -> Y, 26 -> Z, 27 -> AA, 28 -> AB, ...

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

╔════════════╦════════╗
 ROW_NUMBER  STRING 
╠════════════╬════════╣
          1  A      
          2  B      
         25  Y      
         26  Z      
         27  AA     
         28  AB     
         51  AY     
         52  AZ     
         53  BA     
         54  BB     
      18278  ZZZ    
      18279  AAAA   
     475253  ZZZY   
     475254  ZZZZ   
     475255  AAAAA  
  100000000  HJUNYV 
╚════════════╩════════╝

Мета - написати SELECTзапит, який повертає перші 100000000 рядків у визначеному вище порядку. Я провів тестування, запустивши запити в SSMS з відхиленим набором результатів, а не зберегти його в таблиці:

відкинути набір результатів

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

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

Відповіді:


7

Ваше рішення працює на моєму ноутбуці протягом 35 секунд . Наступний код займає 26 секунд (включаючи створення та заповнення тимчасових таблиць):

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

DROP TABLE IF EXISTS #T1, #T2, #T3, #T4;

CREATE TABLE #T1 (string varchar(6) NOT NULL PRIMARY KEY);
CREATE TABLE #T2 (string varchar(6) NOT NULL PRIMARY KEY);
CREATE TABLE #T3 (string varchar(6) NOT NULL PRIMARY KEY);
CREATE TABLE #T4 (string varchar(6) NOT NULL PRIMARY KEY);

INSERT #T1 (string)
VALUES
    ('A'), ('B'), ('C'), ('D'), ('E'), ('F'), ('G'),
    ('H'), ('I'), ('J'), ('K'), ('L'), ('M'), ('N'),
    ('O'), ('P'), ('Q'), ('R'), ('S'), ('T'), ('U'),
    ('V'), ('W'), ('X'), ('Y'), ('Z');

INSERT #T2 (string)
SELECT T1a.string + T1b.string
FROM #T1 AS T1a, #T1 AS T1b;

INSERT #T3 (string)
SELECT #T2.string + #T1.string
FROM #T2, #T1;

INSERT #T4 (string)
SELECT #T3.string + #T1.string
FROM #T3, #T1;

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

Основний код

SELECT TOP (100000000)
    UA.string + UA.string2
FROM
(
    SELECT U.Size, U.string, string2 = '' FROM 
    (
        SELECT Size = 1, string FROM #T1
        UNION ALL
        SELECT Size = 2, string FROM #T2
        UNION ALL
        SELECT Size = 3, string FROM #T3
        UNION ALL
        SELECT Size = 4, string FROM #T4
    ) AS U
    UNION ALL
    SELECT Size = 5, #T1.string, string2 = #T4.string
    FROM #T1, #T4
    UNION ALL
    SELECT Size = 6, #T2.string, #T4.string
    FROM #T2, #T4
) AS UA
ORDER BY 
    UA.Size, 
    UA.string, 
    UA.string2
OPTION (NO_PERFORMANCE_SPOOL, MAXDOP 1);

Це просте об'єднання, що зберігає порядок * чотирьох попередньо обчислених таблиць, з 5-символьними та 6-символьними рядками, отриманими у міру необхідності. Відокремлення префікса від суфікса дозволяє уникнути сортування.

План виконання

100 мільйонів рядів


* Ніщо в SQL вище, що безпосередньо визначає об'єднання, що зберігає замовлення . Оптимізатор вибирає фізичні оператори з властивостями, які відповідають специфікації запитів SQL, включаючи порядок вищого рівня за. Тут він вибирає конкатенацію, реалізовану фізичним оператором злиття, щоб уникнути сортування.

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


6

Я опублікую відповідь, щоб почати. Моя перша думка полягала в тому, що слід використовувати можливість збереження порядку вкладеного вкладеного циклу разом з кількома допоміжними таблицями, які мають по одному рядку для кожної літери. Трюкова частина мала бути циклічною таким чином, щоб результати були впорядковані по довжині, а також уникнення дублікатів. Наприклад, при перехресному приєднанні до CTE, що включає всі 26 великих літер разом із символом '', ви можете закінчити генерування, 'A' + '' + 'A'і '' + 'A' + 'A'це, звичайно, однаковий рядок.

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

SELECT 'A'
UNION ALL SELECT 'B'
...
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'

Порівняно з використанням CTE, запит зайняв 3X більше часу з кластеризованою таблицею і 4X довше купою. Я не вірю, що проблема полягає в тому, що дані є на диску. Його слід читати в пам'ять як одну сторінку і обробляти в пам'яті за весь план. Можливо, SQL Server може працювати з даними оператора постійного сканування ефективніше, ніж це може бути з даними, що зберігаються на типових сторінках зберігання рядків.

Цікаво, що SQL Server вирішує помістити впорядковані результати з однієї сторінки tempdb таблиці з упорядкованими даними в котушку таблиці:

поганий пул

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

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

постійний порядок сканування

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

(SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR)

Це доповнення до запиту повинно гарантувати повернення результатів у правильному порядку. Я очікував, що всі види матимуть великий негативний вплив на продуктивність. Оптимізатор запитів очікував цього також на основі розрахункових витрат:

дорогі сорти

Дуже дивно, що я не міг помітити жодної статистично значущої різниці у часі чи час виконання процесора з явним замовленням або без нього. Якщо що-небудь, запит, здавалося, працює швидше з ORDER BY! У мене немає пояснення такої поведінки.

Хитра частина проблеми полягала в тому, щоб з'ясувати, як вставити порожні символи в потрібні місця. Як було сказано раніше, простий CROSS JOINпризведе до повторення даних. Ми знаємо, що 100000000-й рядок матиме довжину шість символів, оскільки:

26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 = 914654 <100000000

але

26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 + 26 ^ 6 = 321272406> 100000000

Тому нам потрібно лише шість разів приєднатися до листа CTE. Припустимо, що ми приєднуємось до CTE шість разів, захопимо по одній букві від кожного CTE та з'єднаємо їх усі разом. Припустимо, лівий лівий лист не порожній. Якщо будь-який з наступних літер порожній, це означає, що рядок довжиною менше шести символів, тому він є дублікатом. Тому ми можемо запобігти дублікатам, знайшовши перший не порожній символ і вимагаючи, щоб усі символи після нього також не були порожніми. Я вирішив відстежити це, призначивши FLAGстовпець одному з CTE і додавши чек до WHEREпункту. Це повинно бути зрозумілішим після перегляду запиту. Остаточний запит такий:

WITH FIRST_CHAR (CHR) AS
(
    SELECT 'A'
    UNION ALL SELECT 'B'
    UNION ALL SELECT 'C'
    UNION ALL SELECT 'D'
    UNION ALL SELECT 'E'
    UNION ALL SELECT 'F'
    UNION ALL SELECT 'G'
    UNION ALL SELECT 'H'
    UNION ALL SELECT 'I'
    UNION ALL SELECT 'J'
    UNION ALL SELECT 'K'
    UNION ALL SELECT 'L'
    UNION ALL SELECT 'M'
    UNION ALL SELECT 'N'
    UNION ALL SELECT 'O'
    UNION ALL SELECT 'P'
    UNION ALL SELECT 'Q'
    UNION ALL SELECT 'R'
    UNION ALL SELECT 'S'
    UNION ALL SELECT 'T'
    UNION ALL SELECT 'U'
    UNION ALL SELECT 'V'
    UNION ALL SELECT 'W'
    UNION ALL SELECT 'X'
    UNION ALL SELECT 'Y'
    UNION ALL SELECT 'Z'
)
, ALL_CHAR (CHR, FLAG) AS
(
    SELECT '', 0 CHR
    UNION ALL SELECT 'A', 1
    UNION ALL SELECT 'B', 1
    UNION ALL SELECT 'C', 1
    UNION ALL SELECT 'D', 1
    UNION ALL SELECT 'E', 1
    UNION ALL SELECT 'F', 1
    UNION ALL SELECT 'G', 1
    UNION ALL SELECT 'H', 1
    UNION ALL SELECT 'I', 1
    UNION ALL SELECT 'J', 1
    UNION ALL SELECT 'K', 1
    UNION ALL SELECT 'L', 1
    UNION ALL SELECT 'M', 1
    UNION ALL SELECT 'N', 1
    UNION ALL SELECT 'O', 1
    UNION ALL SELECT 'P', 1
    UNION ALL SELECT 'Q', 1
    UNION ALL SELECT 'R', 1
    UNION ALL SELECT 'S', 1
    UNION ALL SELECT 'T', 1
    UNION ALL SELECT 'U', 1
    UNION ALL SELECT 'V', 1
    UNION ALL SELECT 'W', 1
    UNION ALL SELECT 'X', 1
    UNION ALL SELECT 'Y', 1
    UNION ALL SELECT 'Z', 1
)
SELECT TOP (100000000)
d6.CHR + d5.CHR + d4.CHR + d3.CHR + d2.CHR + d1.CHR
FROM (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d6
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d5
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d4
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d3
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d2
CROSS JOIN (SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR) d1
WHERE (d2.FLAG + d3.FLAG + d4.FLAG + d5.FLAG + d6.FLAG) =
    CASE 
    WHEN d6.FLAG = 1 THEN 5
    WHEN d5.FLAG = 1 THEN 4
    WHEN d4.FLAG = 1 THEN 3
    WHEN d3.FLAG = 1 THEN 2
    WHEN d2.FLAG = 1 THEN 1
    ELSE 0 END
OPTION (MAXDOP 1, FORCE ORDER, LOOP JOIN, NO_PERFORMANCE_SPOOL);

КТЕ такі, як описано вище. ALL_CHARприєднується до п’яти разів, тому що містить рядок для порожнього символу. Останній символ в рядку не повинно бути порожнім тому окреме КТР визначається для нього FIRST_CHAR. Додатковий стовпчик прапорця в ALL_CHARвикористовується для запобігання дублікатів, як описано вище. Можливо, є більш ефективний спосіб зробити цю перевірку, але, безумовно, є більш неефективні способи зробити це. Одна з моїх спроб LEN()і POWER()зробила запит виконуватись у шість разів повільніше, ніж поточна версія.

Підказки MAXDOP 1та FORCE ORDERпідказки мають важливе значення для того, щоб зберегти порядок у запиті. Анотаційний кошторисний план може бути корисним, щоб зрозуміти, чому з'єднання відбуваються в їх поточному порядку:

анотована оцінка

Плани запитів часто читаються справа наліво, але запити рядків трапляються зліва направо. В ідеалі, SQL Server вимагає рівно 100 мільйонів рядків від d1оператора постійного сканування. Коли ви рухаєтеся зліва направо, я очікую, що від кожного оператора потрібно буде запитувати менше рядків. Ми можемо бачити це в реальному плані виконання . Крім того, нижче показаний знімок екрана програми SQL Sentry Plan Explorer:

дослідник

Ми отримали рівно 100 мільйонів рядків з d1, що добре. Зауважте, що співвідношення рядків між d2 та d3 майже рівно 27: 1 (165336 * 27 = 4464072), що має сенс, якщо ви думаєте про те, як буде працювати перехресне з'єднання. Співвідношення рядків між d1 і d2 становить 22,4, що являє собою деяку витрачену роботу. Я вважаю, що додаткові рядки є з дублікатів (через порожні символи в середині рядків), які не проходять мимо вкладеного оператора приєднання до циклу, який здійснює фільтрацію.

LOOP JOINНатяк технічно непотрібний , бо CROSS JOINможе бути реалізована тільки в вигляді петлі приєднатися до SQL Server. Це NO_PERFORMANCE_SPOOLполягає у запобіганні непотрібного спушування столу. Якщо натякнути натяк на котушку, на моїй машині запит тривав 3 рази більше.

Час останнього запиту становить близько 17 секунд та загальний час, що минув 18 секунд. Це було під час запуску запиту через SSMS та відкидання набору результатів. Мені дуже цікаво бачити інші методи генерування даних.


2

У мене є рішення, оптимізоване для отримання рядкового коду для будь-якого конкретного числа до 217,180,147,158 (8 символів). Але я не можу перемогти твій час:

На моїй машині, за допомогою SQL Server 2014, ваш запит займає 18 секунд, а мій займає 3 м 46. В обох запитах використовується недокументований прапор трас 8690, оскільки 2014 не підтримує NO_PERFORMANCE_SPOOLпідказку.

Ось код:

/* precompute offsets and powers to simplify final query */
CREATE TABLE #ExponentsLookup (
    offset          BIGINT NOT NULL,
    offset_end      BIGINT NOT NULL,
    position        INTEGER NOT NULL,
    divisor         BIGINT NOT NULL,
    shifts          BIGINT NOT NULL,
    chars           INTEGER NOT NULL,
    PRIMARY KEY(offset, offset_end, position)
);

WITH base_26_multiples AS ( 
    SELECT  number  AS exponent,
            CAST(POWER(26.0, number) AS BIGINT) AS multiple
    FROM    master.dbo.spt_values
    WHERE   [type] = 'P'
            AND number < 8
),
num_offsets AS (
    SELECT  *,
            -- The maximum posible value is 217180147159 - 1
            LEAD(offset, 1, 217180147159) OVER(
                ORDER BY exponent
            ) AS offset_end
    FROM    (
                SELECT  exponent,
                        SUM(multiple) OVER(
                            ORDER BY exponent
                        ) AS offset
                FROM    base_26_multiples
            ) x
)
INSERT INTO #ExponentsLookup(offset, offset_end, position, divisor, shifts, chars)
SELECT  ofst.offset, ofst.offset_end,
        dgt.number AS position,
        CAST(POWER(26.0, dgt.number) AS BIGINT)     AS divisor,
        CAST(POWER(256.0, dgt.number) AS BIGINT)    AS shifts,
        ofst.exponent + 1                           AS chars
FROM    num_offsets ofst
        LEFT JOIN master.dbo.spt_values dgt --> as many rows as resulting chars in string
            ON [type] = 'P'
            AND dgt.number <= ofst.exponent;

/*  Test the cases in table example */
SELECT  /*  1.- Get the base 26 digit and then shift it to align it to 8 bit boundaries
            2.- Sum the resulting values
            3.- Bias the value with a reference that represent the string 'AAAAAAAA'
            4.- Take the required chars */
        ref.[row_number],
        REVERSE(SUBSTRING(REVERSE(CAST(SUM((((ref.[row_number] - ofst.offset) / ofst.divisor) % 26) * ofst.shifts) +
            CAST(CAST('AAAAAAAA' AS BINARY(8)) AS BIGINT) AS BINARY(8))),
            1, MAX(ofst.chars))) AS string
FROM    (
            VALUES(1),(2),(25),(26),(27),(28),(51),(52),(53),(54),
            (18278),(18279),(475253),(475254),(475255),
            (100000000), (CAST(217180147158 AS BIGINT))
        ) ref([row_number])
        LEFT JOIN #ExponentsLookup ofst
            ON ofst.offset <= ref.[row_number]
            AND ofst.offset_end > ref.[row_number]
GROUP BY
        ref.[row_number]
ORDER BY
        ref.[row_number];

/*  Test with huge set  */
WITH numbers AS (
    SELECT  TOP(100000000)
            ROW_NUMBER() OVER(
                ORDER BY x1.number
            ) AS [row_number]
    FROM    master.dbo.spt_values x1
            CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 676) x2
            CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 676) x3
    WHERE   x1.number < 219
)
SELECT  /*  1.- Get the base 26 digit and then shift it to align it to 8 bit boundaries
            2.- Sum the resulting values
            3.- Bias the value with a reference that represent the string 'AAAAAAAA'
            4.- Take the required chars */
        ref.[row_number],
        REVERSE(SUBSTRING(REVERSE(CAST(SUM((((ref.[row_number] - ofst.offset) / ofst.divisor) % 26) * ofst.shifts) +
            CAST(CAST('AAAAAAAA' AS BINARY(8)) AS BIGINT) AS BINARY(8))),
            1, MAX(ofst.chars))) AS string
FROM    numbers ref
        LEFT JOIN #ExponentsLookup ofst
            ON ofst.offset <= ref.[row_number]
            AND ofst.offset_end > ref.[row_number]
GROUP BY
        ref.[row_number]
ORDER BY
        ref.[row_number]
OPTION (QUERYTRACEON 8690);

Хитрість тут полягає в тому, щоб визначити, з чого починаються різні перестановки:

  1. Коли вам потрібно вивести одну таблицю, у вас є 26 ^ 1 перестановки, які починаються з 26 ^ 0.
  2. Коли вам потрібно вивести 2 символи, у вас є 26 ^ 2 перестановки, які починаються з 26 ^ 0 + 26 ^ 1
  3. Коли потрібно вивести 3 символи, у вас є 26 ^ 3 перестановки, які починаються з 26 ^ 0 + 26 ^ 1 + 26 ^ 2
  4. повторити для n символів

Інший трюк, який використовується, - це просто використовувати суму, щоб дістати потрібне значення, а не намагатися конкретувати. Щоб досягти цього, я просто зміщую цифри від бази 26 до бази 256 і додаю значення ascii 'A' для кожної цифри. Таким чином ми отримуємо двійкове представлення рядка, який ми шукаємо. Після цього деякі строкові маніпуляції завершують процес.


-1

Ок, ось мій останній сценарій.

Без циклу, без рекурсивності.

Це працює лише за 6 чар

Найбільший недолік - це потрібно близько 22 хв за 1,00,00 000

Цього разу мій сценарій дуже короткий.

SET NoCount on

declare @z int=26
declare @start int=@z+1 
declare @MaxLimit int=10000000

SELECT TOP (@MaxLimit) IDENTITY(int,1,1) AS N
    INTO NumbersTest1
    FROM     master.dbo.spt_values x1   
   CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 500) x2
            CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 500) x3
    WHERE   x1.number < 219
ALTER TABLE NumbersTest1 ADD CONSTRAINT PK_NumbersTest1 PRIMARY KEY CLUSTERED (N)


select N, strCol from NumbersTest1
cross apply
(
select 
case when IntCol6>0 then  char((IntCol6%@z)+64) else '' end 
+case when IntCol5=0 then 'Z' else isnull(char(IntCol5+64),'') end 
+case when IntCol4=0 then 'Z' else isnull(char(IntCol4+64),'') end 
+case when IntCol3=0 then 'Z' else isnull(char(IntCol3+64),'') end 
+case when IntCol2=0 then 'Z' else isnull(char(IntCol2+64),'') end 
+case when IntCol1=0 then 'Z' else isnull(char(IntCol1+64),'') end strCol
from
(
select  IntCol1,IntCol2,IntCol3,IntCol4
,case when IntCol5>0 then  IntCol5%@z else null end IntCol5

,case when IntCol5/@z>0 and  IntCol5%@z=0 then  IntCol5/@z-1 
when IntCol5/@z>0 then IntCol5/@z
else null end IntCol6
from
(
select IntCol1,IntCol2,IntCol3
,case when IntCol4>0 then  IntCol4%@z else null end IntCol4

,case when IntCol4/@z>0 and  IntCol4%@z=0 then  IntCol4/@z-1 
when IntCol4/@z>0 then IntCol4/@z
else null end IntCol5
from
(
select IntCol1,IntCol2
,case when IntCol3>0 then  IntCol3%@z else null end IntCol3
,case when IntCol3/@z>0 and  IntCol3%@z=0 then  IntCol3/@z-1 
when IntCol3/@z>0 then IntCol3/@z
else null end IntCol4

from
(
select IntCol1
,case when IntCol2>0 then  IntCol2%@z else null end IntCol2
,case when IntCol2/@z>0 and  IntCol2%@z=0 then  IntCol2/@z-1 
when IntCol2/@z>0 then IntCol2/@z
else null end IntCol3

from
(
select case when N>0 then N%@z else null end IntCol1
,case when N%@z=0 and  (N/@z)>1 then (N/@z)-1 else  (N/@z) end IntCol2 

)Lv2
)Lv3
)Lv4
)Lv5
)LV6

)ca

DROP TABLE NumbersTest1

Схоже, що похідна таблиця перетворюється в єдиний обчислювальний скаляр, який становить понад 400000 символів коду. Я підозрюю, що в цьому підрахунку є багато накладних витрат. Ви можете спробувати щось подібне до цього: dbfiddle.uk/… Не соромтеся інтегрувати компоненти цього у свою відповідь.
Джо Оббіш
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.