Главная страница
Навигация по странице:

  • Oracle и PostgreSQL

  • лекция. Учебник по sql если вы хотите узнать, что такое sql этот сайт для вас


    Скачать 7.88 Mb.
    НазваниеУчебник по sql если вы хотите узнать, что такое sql этот сайт для вас
    Анкорлекция
    Дата21.12.2022
    Размер7.88 Mb.
    Формат файлаpdf
    Имя файлаMoiseenko_SQL.pdf
    ТипУчебник
    #857633
    страница36 из 47
    1   ...   32   33   34   35   36   37   38   39   ...   47
    name
    John
    Smith
    Tom
    Рекурсивные СТЕ
    У
    CTE
    есть еще одно важное назначение. С его помощью можно написать рекурсивный запрос, т.е. запрос, который, написанный один раз, будет повторяться многократно пока истинно некоторое условие.
    Рекурсивный CTE имеет следующий вид:
    1.
    WITH
    <имя>
    [(
    <список столбцов>
    )]
    2.
    AS
    (
    3. <
    SELECT
    ... >
    -- анкорная часть
    4.
    UNION
    ALL
    -- рекурсивная часть
    5. <
    SELECT
    FROM
    <имя>… >
    6.
    WHERE
    <условие продолжения итераций>
    7.
    )
    От обычного CTE-запроса рекурсивный отличается только рекурсивной частью, которая вводится предложением UNION ALL. Обратите внимание, что в рекурсивной части присутствует ссылка на имя CTE, т.е. внутри CTE ссылается само на себя. Это, собственно, и есть рекурсия. Естественно, анкорная и рекурсивная части должны иметь одинаковый набор столбцов.
    В MySQL рекурсивные CTE (как и обычные) появились в версии 8. Их синтаксис фактически отличается от синтаксиса SQL Server только одним словом RECURSIVE:

    1.
    WITH
    RECURSIVE <имя>
    [(
    <список столбцов>
    )]
    2.
    AS
    (
    ... и так далее
    Перейдем к примеру. Рассмотрим задачу получения алфавита, т.е. таблицы алфавитных символов - прописных латинских букв. Чтобы было с чем сравнивать, решим сначала эту задачу с помощью генерации числовой последовательности, которая рассматривалась в параграфе 8.1 1.
    SELECT
    CHAR
    (
    ASCII
    (
    'A'
    )
    +5
    *
    (
    a
    -1
    )
    + b
    -1
    )
    AS
    num
    2.
    FROM
    (
    SELECT
    1
    a
    UNION
    ALL
    SELECT
    2
    UNION
    ALL
    SELECT
    3 3.
    UNION
    ALL
    SELECT
    4
    UNION
    ALL
    SELECT
    5
    UNION
    ALL
    SELECT
    6 4.
    )
    x
    CROSS
    JOIN
    5.
    (
    SELECT
    1
    b
    UNION
    ALL
    SELECT
    2
    UNION
    ALL
    SELECT
    3 6.
    UNION
    ALL
    SELECT
    4
    UNION
    ALL
    SELECT
    5 7.
    )
    y
    8.
    WHERE
    5
    *
    (
    a
    -1
    )
    + b <=
    26 9.
    ORDER
    BY
    1
    ;
    А вот решение с помощью рекурсивного CTE
    1.
    WITH
    Letters
    AS
    (
    2.
    SELECT
    ASCII
    (
    'A'
    )
    code, CHAR
    (
    ASCII
    (
    'A'
    ))
    letter
    3.
    UNION
    ALL
    4.
    SELECT
    code
    +1
    , CHAR
    (
    code
    +1
    )
    FROM
    Letters
    5.
    WHERE
    code
    +1
    <= ASCII
    (
    'Z'
    )
    6.
    )
    7.
    SELECT
    letter
    FROM
    Letters;
    В запросе анкорной части определяем ASCII-код первой буквы алфавита и соответствующий ему символ. В запросе рекурсивной части мы просто увеличиваем ASCII-код на единицу, обращаясь к CTE в предложении FROM.
    В результате к строке с первым символом будут последовательно добавляться
    (UNION ALL) строки со следующими буквами в порядке их ASCII-кодов.

    Итерации будут продолжаться до тех пор, пока условие code +1 <=
    ascii('Z') будет истинным, т.е. пока не будет добавлена буква "Z".
    Оператор
    1.
    SELECT
    letter
    FROM
    Letters собственно и служит для обращения к CTE, запуска рекурсии и вывода результата. Все остальное можно считать определением.
    Следует заметить, что по умолчанию допускается 100 итераций. Это значит, что если условие прекращения итераций не выполнено ранее, то рекурсия будет остановлена после выполнения 100 итераций. Максимальное число итераций можно изменить с помощью «хинта»
    1.
    OPTION
    (
    MAXRECURSION N
    )
    где N – максимальное число итераций. Значение 0 не ограничивает число итераций. Нужно с осторожностью использовать это значение, т.к. оно чревато зацикливанием.
    Если запрос не был завершен в пределах указанного числа итераций, возникает ошибка (полученные к этому моменту строки возвращаются):
    The statement terminated. The maximum recursion N has been exhausted before
    statement completion. (Выполнение оператора прервано. Достигнут предел
    максимального числа итераций N до завершения выполнения оператора).
    В MySQL наша задача будет иметь аналогичное решение:
    1.
    WITH
    RECURSIVE Letters
    AS
    (
    2.
    SELECT
    ASCII
    (
    'A'
    )
    code, CHAR
    (
    ASCII
    (
    'A'
    ))
    letter
    3.
    UNION
    ALL
    4.
    SELECT
    code
    +1
    , CHAR
    (
    code
    +1
    )
    FROM
    Letters
    5.
    WHERE
    code
    +1
    <= ASCII
    (
    'Z'
    )
    6.
    )
    7.
    SELECT
    letter
    FROM
    Letters;

    Давайте решим еще одну задачу в качестве ответа на вопрос, который мне неоднократно встречался на просторах Интернет.
    Преобразовать текст в столбце таблицы таким образом, чтобы
    каждое слово начиналось с заглавной буквы.
    Вот пример данных и требуемый результат:
    name
    rep
    delta
    Delta
    KSI PSI
    Ksi Psi
    alfa beta gamma zeta Alfa Beta Gamma Zeta
    ALfa and omegA
    Alfa And Omega
    За небольшим числом исключений (среди которых можно упомянуть аббревиатуры и инициалы) можно считать, что слову внутри текста предшествует пробел. Это можно использовать в качестве критерия поиска нужных нам элементов текста. Предлагаю реализовать такой достаточно примитивный алгоритм:
    1. Первую букву текста делаем прописной, а остальные - строчными.
    2. Затем каждую конструкцию "пробел+буква" переводим в верхний регистр.
    С первым пунктом алгоритма все просто:
    1.
    SELECT
    name, UPPER
    (
    LEFT
    (
    name,
    1
    ))
    + LOWER
    (
    SUBSTRING
    (
    name,
    2
    , LEN
    (
    name
    )
    -
    1
    ))
    rep
    2.
    FROM
    3.
    (
    SELECT
    'ALfa and omegA'
    AS
    name
    4.
    UNION
    ALL
    SELECT
    'alfa beta gamma zeta'
    5.
    UNION
    ALL
    SELECT
    'KSI PSI'

    6.
    UNION
    ALL
    SELECT
    'delta'
    7.
    )
    X;
    name
    rep
    ALfa and omegA
    Alfa and omega
    alfa beta gamma zeta Alfa beta gamma zeta
    KSI PSI
    Ksi psi
    delta
    Delta
    Для реализации второго пункта есть варианты. Поскольку букв латинского алфавита не так много (26), можно просто сделать 26 замен. Я не поленюсь и приведу полный вариант, чтобы вы могли поэкспериментировать с запросом.
    Итак,
    1.
    SELECT
    name,
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    2.
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    3.
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    4.
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    REPLACE
    (
    5.
    REPLACE
    (
    REPLACE
    (
    rep,
    ' a'
    ,
    ' A'
    )
    ,
    ' b'
    ,
    ' B'
    )
    ,
    ' c'
    ,
    '
    C'
    )
    ,
    ' d'
    ,
    ' D'
    )
    ,
    6.
    ' e'
    ,
    ' E'
    )
    ,
    ' f'
    ,
    ' F'
    )
    ,
    ' g'
    ,
    ' G'
    )
    ,
    ' h'
    ,
    ' H'
    )
    ,
    ' i'
    ,
    ' I'
    )
    ,
    ' j'
    ,
    ' J'
    )
    ,
    ' k'
    ,
    ' K'
    )
    ,
    7.
    ' l'
    ,
    ' L'
    )
    ,
    ' m'
    ,
    ' M'
    )
    ,
    ' n'
    ,
    ' N'
    )
    ,
    ' o'
    ,
    ' O'
    )
    ,
    ' p'
    ,
    ' P'
    )
    ,
    ' q'
    ,
    ' Q'
    )
    ,
    ' r'
    ,
    ' R'
    )
    ,
    8.
    ' s'
    ,
    ' S'
    )
    ,
    ' t'
    ,
    ' T'
    )
    ,
    ' u'
    ,
    ' U'
    )
    ,
    ' v'
    ,
    ' V'
    )
    ,
    9.
    ' w'
    ,
    ' W'
    )
    ,
    ' x'
    ,
    ' X'
    )
    ,
    ' y'
    ,
    ' Y'
    )
    ,
    ' z'
    ,
    ' Z'
    )
    10.
    FROM
    (
    11.
    SELECT
    name,
    UPPER
    (
    LEFT
    (
    name,
    1
    ))
    +
    LOWER
    (
    SUBSTRING
    (
    name,
    2
    , LEN
    (
    name
    )
    -1
    ))
    rep
    12.
    FROM
    13.
    (
    SELECT
    'ALfa and omegA'
    AS
    name
    14.
    UNION
    ALL
    SELECT
    'alfa beta gamma zeta'
    15.
    UNION
    ALL
    SELECT
    'KSI PSI'

    16.
    UNION
    ALL
    SELECT
    'delta'
    17.
    )
    X
    18.
    )
    Y;
    Нетрудно догадаться, что следующий вариант будет использовать рекурсивный CTE.
    Сначала напишем два простых
    CTE
    , которые формируют наш тестовый пример и определяют ASCII-код первой буквы алфавита (A) - не писать же константу. :-) Далее последует анкорная часть, которая выполняет ранее описанную операцию приведения всего текста к нижнему регистру с заглавной первой буквой. Здесь же выполним замену символа с кодом code и предшествующим ему пробелом на... него же. Пусть вас не смущает такая, казалось бы, бесполезная замена. Дело в том, что для регистронезависимых баз данных символы 'a' и 'A' не различаются. Давайте пока на этом остановимся и посмотрим результат.
    1.
    WITH
    NM
    (
    name
    )
    AS
    2.
    (
    SELECT
    'ALfa and omegA'
    AS
    name
    3.
    UNION
    ALL
    SELECT
    'alfa beta gamma zeta'
    4.
    UNION
    ALL
    SELECT
    'KSI PSI'
    5.
    UNION
    ALL
    SELECT
    'delta'
    6.
    )
    ,
    7. Ascii_code
    AS
    (
    8.
    SELECT
    ASCII
    (
    'A'
    )
    AS
    code
    9.
    )
    ,
    10.
    Repl
    (
    name, code, rep
    )
    AS
    11.
    (
    SELECT
    name, code,
    REPLACE
    (
    UPPER
    (
    LEFT
    (
    name,
    1
    ))
    +
    12.
    LOWER
    (
    SUBSTRING
    (
    name,
    2
    ,
    LEN
    (
    name
    )
    -
    1
    ))
    ,
    '
    '
    +CHAR
    (
    code
    )
    ,
    ' '
    +CHAR
    (
    code
    ))
    rep
    13.
    FROM
    Ascii_code, NM
    14.
    )
    15.
    SELECT
    name, rep
    FROM
    Repl;

    name
    rep
    ALfa and omegA
    Alfa And omega
    alfa beta gamma zeta Alfa beta gamma zeta
    KSI PSI
    Ksi psi
    delta
    Delta
    Добавим, наконец, рекурсивную часть, в которой мы выполним замену буквы с кодом code+1. Рекурсия будет продолжаться до тех пор, пока не будет нарушено условие code < ASCII('Z'), т.е. пока мы не переберем все буквы.
    Что же мы получим на выходе? К строкам, которые были получены в результате выполнения анкорной части, на каждой итерации будут добавлены
    (UNION ALL) те же строки с заменой очередной буквы. Отметим большой объем результата при использовании данного метода; в нашем случае это 4х26
    = 104 строки. Из этого множества строк нас интересуют только те, которые получены в результате последней итерации, т.е. когда были выполнены все замены. Этой последней итерации соответствует условие code = ASCII('Z'), которое и используется в финальном запросе:
    1.
    WITH
    NM
    (
    name
    )
    AS
    2.
    (
    SELECT
    'ALfa and omegA'
    AS
    name
    3.
    UNION
    ALL
    SELECT
    'alfa beta gamma zeta'
    4.
    UNION
    ALL
    SELECT
    'KSI PSI'
    5.
    UNION
    ALL
    SELECT
    'delta'
    6.
    )
    ,
    7. Ascii_code
    AS
    (
    8.
    SELECT
    ASCII
    (
    'A'
    )
    AS
    code
    9.
    )
    ,
    10.
    Repl
    (
    name, code, rep
    )
    AS
    11.
    (
    SELECT
    name, code,
    REPLACE
    (
    UPPER
    (
    LEFT
    (
    name,
    1
    ))
    +
    12.
    LOWER
    (
    SUBSTRING
    (
    name,
    2
    ,
    LEN
    (
    name
    )
    -
    1
    ))
    ,
    '
    '
    +CHAR
    (
    code
    )
    ,
    ' '
    +CHAR
    (
    code
    ))
    rep
    13.
    FROM
    Ascii_code, NM
    14.
    UNION
    ALL

    15.
    SELECT
    name, code
    +1
    code,
    REPLACE
    (
    rep,
    ' '
    +
    CHAR
    (
    code
    +1
    )
    ,
    ' '
    + char
    (
    code +
    1
    ))
    rep
    16.
    FROM
    Repl
    17.
    WHERE
    code < ASCII
    (
    'Z'
    )
    18.
    )
    19.
    SELECT
    name, rep
    FROM
    Repl
    WHERE
    code=ASCII
    (
    'Z'
    )
    ;
    Я хотел бы предостеречь вас от чрезмерного увлечения рекурсивными CTE, поскольку они зачастую проигрывают в производительности "традиционным" методам. Я не буду далеко ходить за примерами и сравню два представленных здесь метода. Увеличив количество обрабатываемых строк до 10000, я получил такое время использования CPU: метод на основе REPLACE: 842 ms рекурсивный метод: 6615 ms
    Безусловно, есть задачи, которые нельзя решить непроцедурно в рамках стандарта SQL-92. В этих случаях использование рекурсивных CTE вполне обоснованно. В остальных случаях я бы рекомендовал выполнять тесты производительности для альтернативных решений.
    Кстати, в Oracle и PostgreSQL есть встроенная функция INITCAP, которая решает данную задачу:
    1.
    SELECT
    INITCAP
    (
    name
    )
    2.
    FROM
    3.
    (
    SELECT
    'ALfa and omegA'
    AS
    name
    4.
    UNION
    ALL
    SELECT
    'alfa beta gamma zeta'
    5.
    UNION
    ALL
    SELECT
    'KSI PSI'
    6.
    UNION
    ALL
    SELECT
    'delta'
    7.
    )
    X;
    Вы можете использовать
    консоль
    , чтобы убедиться в этом.

    О генерации
    числовых
    последовательностей
    в SQL Server
    В очередной реализации MS
    SQL
    Server
    2005 появилась возможность использования рекурсивных
    CTE конструкций, позволяющих реализовывать циклы для генерации числовых последовательностей и итерационных вычислений.
    Введение в рекурсии MS SQL Server можно найти в соответствующих руководствах
    Microsoft, учебнике и
    Интернете. В данном параграфе мы рассмотрим только новые примеры, помогающие в ее практическом освоении. Самым простым примером использования рекурсивного
    CTE
    является генерация ограниченной числовой последовательности натурального ряда:
    1,2,3, ... N.
    1.
    WITH
    Series
    (
    a
    )
    AS
    2.
    (
    3.
    SELECT
    1 4.
    UNION
    ALL
    5.
    SELECT
    a
    +1
    FROM
    Series
    WHERE
    a <
    100 6.
    )
    7.
    SELECT
    *
    FROM
    Series;
    Эта конструкция предназначена для генерации последовательности целых чисел (в виде одноколоночной таблицы) от 1 до 100.
    А. Итерационные вычисления
    В элементарной и высшей математике есть много интересных последовательностей, обладающих замечательными свойствами. Некоторые
    последовательности сходятся, и их можно использовать для реализации вычислительных алгоритмов или для вычисления алгебраических и трансцендентных чисел, значений тригонометрических функций, для нахождения корней уравнений, решения систем линейных алгебраических уравнений и пр. Другие последовательности, такие как факториал n!, коэффициенты бинома Ньютона, числа Фибоначчи представляют собой расходящиеся последовательности, которые имеют широкое использование в теории вероятности и математической статистике.
    Эти последовательности получаются с помощью итераций (рекурсий SQL
    Server), например:
    A1, A2, A3, A4 = fun(A1, A2, A3).
    Здесь A1, A2, A3 - начальные значения для итерационного процесса, fun - функция для вычисления 4-го, пятого и т.д. чисел, которая всегда задействует предыдущие 3 числа. Предположим, что процесс стартует с трех одинаковых чисел A1 = A2 = A3 = 1. Тогда схема реализации с использованием рекурсивных CTE будет иметь следующий вид:
    1.
    WITH
    TABLE
    (
    iter, A1, A2, A3, A4
    )
    AS
    2.
    (
    3.
    SELECT
    iter =
    1
    , A1 =
    1
    , A2 =
    1
    , A3 =
    1
    , A4 = fun
    (
    1
    ,
    1
    ,
    1
    )
    4.
    UNION
    ALL
    5.
    SELECT
    iter +
    1
    , A1 = A2, A2 = A3, A3 = A4, A4=fun
    (
    A2,
    A3, A4
    )
    6.
    FROM
    TABLE
    WHERE
    iter <
    50 7.
    )
    8.
    SELECT
    *
    FROM
    TABLE
    ;
    Здесь колонка iter введена для вывода номера итерации, колонка [A1] содержит пятьдесят первых членов последовательности, [A2], [A3] и [A4] – вспомогательные колонки для хранения необходимых промежуточных результатов.
    Обобщением такого подхода является следующий пример. Пусть n >= 1, m
    >= 1. Тогда последовательные вычисления
    A[n+1] = fun(A[1], A[2], …, A[n])
    A[n+2] = fun(A[2], A[3], …, A[n+1])

    A[n+m] = fun(A[m], A[m+1], …, A[m+n-1])
    приводят к генерации m (m >= 1) новых членов последовательности
    A[n+1],…, A[n+m].
    Мы не будем приводить примера использования рекурсии в общем случае, так как это возможно разве что в виде некоторого псевдокода. Читатели могут попытаться построить конкретные примеры самостоятельно. Отметим, что результирующая таблица выглядит следующим образом:
    A[1]
    A[2]

    A[n]
    A[n+1]
    A[2]
    A[3]

    A[n+1]
    A[n+2]

    A[m+1] A[m+2] … A[m+n-1] A[m+n]
    B. О последовательности чисел Фибоначчи
    Последовательность Фибоначчи может быть вычислена с помощью следующей итерационной формулы:
    A[i+2] = A[i] + A[i+1]; i = 1,…, n; A[1] = A[2] = 1.
    Здесь очень легко увидеть аналогию с общим случаем предыдущего пункта, если ввести функцию f(x,y)=x+y.
    Для хранения номера итерации i можно использовать колонку [iter], для A[i] используется колонка [a], для A[i+1] и A[i+2] - колонки [b] и [c]. Колонка [d] не потребуется. Используя общий подход, расчет чисел Фибоначчи можно выразить следующим кодом:
    1.
    WITH
    Fibonacci
    (
    iter,a,b,c
    )
    AS
    2.
    (
    3.
    SELECT
    iter=
    1
    , a=
    1
    , b=
    1
    , c=
    1+1 4.
    UNION
    ALL
    5.
    SELECT
    iter
    +1
    , a=b, b=c, c=b+c
    6.
    FROM
    Fibonacci
    WHERE
    b <
    1000 7.
    )
    8.
    SELECT
    *
    FROM
    Fibonacci;
    Приведем результат запроса для вычисления чисел Фибоначчи, меньших
    1000
    (они находятся во втором столбце таблицы).

    iter
    a
    b
    c
    1
    1 1
    2
    2
    1 2
    3
    3
    2 3
    5
    4
    3 5
    8
    5
    5 8
    13
    6
    8 13 21
    7
    13 21 34
    8
    21 34 55
    9
    34 55 89
    10 55 89 144
    11 89 144 233
    12 144 233 377
    13 233 377 610
    14 377 610 987
    15 610 987 1597
    16 987 1597 2584
    С. Нахождение корней уравнений
    Большой раздел математического и функционального анализа посвящен нахождению корней уравнений функции одного и многих переменных.
    Корнем уравнения g(x) = 0 называется число r (или вектор r),
    удовлетворяющее условию: g(r) = 0. Общим методом решения таких уравнений является сведение задачи к задаче о неподвижной точке: x=f(x).
    Смысл такого сведения состоит в нахождении такой функции f, при которой уравнения g (x) = 0 и x = f(x) являются эквивалентными. Кроме того, оператор f должен быть сжимающим. То есть, если значение r1 находится рядом с решением r, то значение r2=f(r1) должно быть еще ближе к решению: abs(r-r2) < A * abs(r-r1), где положительная константа A меньше единицы
    (A<1) и не зависит от выбора значения r1.
    Сжимающих отображений может быть много. Предпочтительными являются те из них, для которых константа A принимает меньшее значение. Чем меньше константа, тем быстрее сходится процесс нахождения корня уравнения g(x): r2 = f(r1), r3 = f(r2), r4 = f(r3) …
    Приведем иллюстрирующий пример на SQL.
    D. Итерационное вычисление квадратного корня
    Квадратный корень из числа a – это решение уравнения x*x = a. В терминах предыдущего пункта это корень уравнения g(x) = 0, где функция g(x) = x*x - a.
    Вычисление этих чисел SQL-запросом труда не представляет - есть встроенная функция sqrt(a) или power(a,0.5). Тем не менее, проиллюстрируем на примере нахождения квадратного корня подход, который может использоваться в случаях, когда нет соответствующей встроенной функции, а сжимающее отображение известно.
    Итерационный аналитический алгоритм для вычисления квадратного корня известен из курса школьной математики, и изложен, например, здесь
    Его можно записать в виде сжимающего отображения x = f(x), где f(x)=1/2*(x+a/x).
    Легко убедиться в том, что уравнение x = 1/2*(x + a/x) эквивалентно уравнению x*x = a для x <> 0. Читатель с математическим образованием может попытаться доказать, что это преобразование действительно сжимающее, и, следовательно, может быть использовано для итерационного процесса нахождения корня уравнения.
    Для иллюстрации алгоритма приведем пример sql-кода для вычисления квадратного корня из числа a = 3:

    1.
    WITH
    Square3
    (
    a,b,c
    )
    AS
    2.
    (
    3.
    SELECT
    1
    ,
    CAST
    (
    3
    AS
    float
    (
    53
    ))
    ,
    CAST
    (
    3
    AS
    float
    (
    53
    ))
    4.
    UNION
    ALL
    5.
    SELECT
    a
    +1
    ,
    1
    ./
    2
    .*
    (
    b
    +3
    /b
    )
    ,
    1
    ./
    6
    .*
    3
    *
    (
    c
    +3
    ./c
    )
    FROM
    Square3
    WHERE
    a <
    7 6.
    )
    7.
    SELECT
    iteration=a, Exact=sqrt
    (
    CAST
    (
    3
    AS
    float
    (
    53
    )))
    ,
    Res1=b, Res2=c
    8.
    FROM
    Square3;
    Здесь столбец [a] введен для вывода номера итерации, столбцы [b] и [c] вычисляют квадратный корень двумя арифметически эквивалентными способами. Итерации не используют встроенные операции sqrt или power, но для контроля мы вывели в колонке exact значение квадратного корня, вычисленное с помощью встроенной функции.
    1   ...   32   33   34   35   36   37   38   39   ...   47


    написать администратору сайта