sqlpostgresqlstringsrepeat

REPEAT в SQL: как повторять строку для разделителей, плейсхолдеров и текстовых диаграмм

REPEAT(str, n) повторяет строку n раз и собирает разделители отчетов, плейсхолдеры для IN-списков и текстовые бар-чарты прямо в выдаче SQL.

11 мин чтенияСправочникsql · postgresql · strings · repeat · mysql · formatting

Иногда в SQL нужно не искать данные и не считать сумму, а просто аккуратно собрать строку. Например, нарисовать разделитель из дефисов, добавить отступ, сделать маленькую текстовую диаграмму или собрать список плейсхолдеров для динамического запроса.

Для таких задач есть функция REPEAT.

Она делает ровно то, что написано в названии: повторяет строку нужное количество раз.

SELECT REPEAT('-', 10) AS line;

Результат:

----------

Мы попросили SQL повторить символ - десять раз. Получилась одна строка из десяти дефисов.

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

Что делает REPEAT

REPEAT(str, n) повторяет строку str ровно n раз подряд и возвращает результат как одну строку.

SELECT REPEAT('ab', 3) AS result;

Результат:

ababab

Важно: результат — это одна строка, а не несколько строк таблицы.

То есть REPEAT('ab', 3) возвращает:

ababab

А не:

ab
ab
ab

Если вам нужно создать несколько строк в результате запроса, для этого используют другие инструменты, например generate_series в PostgreSQL. REPEAT нужен именно для повторения текста внутри одного значения.

Базовый синтаксис

У функции два аргумента:

REPEAT(string, count)

Первый аргумент — что повторяем.

Второй аргумент — сколько раз повторяем.

Примеры:

SELECT REPEAT('x', 5) AS result;

Результат:

xxxxx
SELECT REPEAT('SQL ', 3) AS result;

Результат:

SQL SQL SQL
SELECT REPEAT('=-', 4) AS result;

Результат:

=-=-=-=-

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

REPEAT возвращает строку

Даже если вы повторяете цифру, результат всё равно будет строкой, а не числом.

SELECT REPEAT('0', 5) AS result;

Результат:

00000

Это не число 0. Это текстовая строка из пяти нулей.

Эта разница важна. Например, строку 00000 можно склеивать с другими строками:

SELECT 'USR-' || REPEAT('0', 3) || '42' AS user_code;

Результат:

USR-00042

Но для настоящего форматирования кодов чаще используют LPAD, потому что он не просто повторяет символ, а дополняет конкретное значение до нужной длины.

REPEAT и обычная склейка строк

REPEAT часто используют вместе с оператором ||, который склеивает строки в PostgreSQL.

Например, сделаем отступ перед текстом:

SELECT REPEAT(' ', 4) || 'Hello' AS result;

Результат:

    Hello

Здесь REPEAT(' ', 4) создаёт четыре пробела, а потом мы приклеиваем к ним слово Hello.

Такой приём можно использовать для простого форматирования текстовых отчётов.

Разделители для отчётов

Самый понятный сценарий для REPEAT — сделать ровную линию-разделитель.

Например:

SELECT REPEAT('-', 30) AS line;

Результат:

------------------------------

Такой разделитель можно использовать в текстовом отчёте:

SELECT REPEAT('=', 40) AS report_line
UNION ALL
SELECT '        MONTHLY REVENUE REPORT'
UNION ALL
SELECT REPEAT('=', 40);

Результат:

========================================
        MONTHLY REVENUE REPORT
========================================

Конечно, современные отчёты чаще строятся в BI-системах, Excel, HTML или интерфейсе приложения. Но для консольных отчётов, технических выгрузок, логов и отладочных запросов такие приёмы всё ещё бывают полезны.

Главное преимущество здесь в том, что вы не пишете руками строку из сорока символов. Вы явно говорите REPEAT('=', 40). Это читается проще и выглядит аккуратнее.

Разделитель по длине значения

Иногда хочется, чтобы длина линии зависела от данных.

Например, нужно вывести имя и под ним линию такой же длины:

SELECT
    name,
    REPEAT('-', LENGTH(name)) AS underline
FROM users
ORDER BY name;

Если в таблице есть такие имена:

name
---------
Alex
Maria
Konstantin

результат будет таким:

name       | underline
-----------+-----------
Alex       | ----
Maria      | -----
Konstantin | ----------

Здесь LENGTH(name) считает длину имени, а REPEAT('-', LENGTH(name)) создаёт линию такой же длины.

Это хороший пример того, зачем вообще нужен REPEAT: количество повторов можно не хардкодить, а вычислять прямо из данных.

Отступы и простое форматирование

REPEAT удобно использовать для отступов.

Например, представим таблицу категорий, где есть уровень вложенности:

name          | level
--------------+------
Products      | 0
Electronics   | 1
Phones        | 2
Laptops       | 2
Clothes       | 1

Можно вывести категории с отступами:

SELECT
    REPEAT('  ', level) || name AS category_tree
FROM categories
ORDER BY id;

Результат:

Products
  Electronics
    Phones
    Laptops
  Clothes

Здесь мы повторяем два пробела столько раз, сколько указано в колонке level.

Если level = 0, отступа нет.

Если level = 1, будет два пробела.

Если level = 2, будет четыре пробела.

Получается простой текстовый вид дерева прямо в SQL.

Плейсхолдеры для динамических запросов

Ещё один практический сценарий — собрать строку с плейсхолдерами.

Например, приложению нужно подготовить SQL-запрос с условием IN, где количество значений заранее неизвестно.

Для трёх значений нужен такой фрагмент:

?,?,?

Его можно собрать через REPEAT:

SELECT RTRIM(REPEAT('?,', 3), ',') AS placeholders;

Результат:

?,?,?

Как это работает: REPEAT('?,', 3) сначала создаёт строку:

?,?,?,

А потом RTRIM(..., ',') убирает лишнюю запятую справа.

В итоге остаётся:

?,?,?

Такой приём удобен, когда повторяется один и тот же элемент с разделителем: «плейсхолдер плюс запятая».

Почему мы сначала добавляем лишнюю запятую

На первый взгляд странно: зачем сначала делать лишнюю запятую, а потом её удалять?

Потому что так проще.

Если собирать строку вручную, постоянно появляется проблема последнего элемента:

?, ?, ?

После первых двух плейсхолдеров запятая нужна, а после последнего — нет.

В программировании это частая задача. Один из простых способов её решить: повторять фрагмент вместе с разделителем, а потом убрать последний разделитель.

SELECT RTRIM(REPEAT('?, ', 4), ', ') AS placeholders;

Результат:

?, ?, ?, ?

Здесь строка сначала получится такой:

?, ?, ?, ?,

А потом хвостовой разделитель будет убран.

Важное предупреждение про динамический SQL

REPEAT может помочь собрать форму строки, например ?,?,?.

Но он не должен использоваться для склейки пользовательских значений прямо в SQL.

Плохая идея:

-- never concatenate raw user input into SQL
SELECT 'SELECT * FROM users WHERE id IN (' || user_input || ')';

Если user_input пришёл от пользователя, в нём может оказаться что угодно. Это риск SQL-инъекции.

Правильный подход: значения должны передаваться как параметры, а плейсхолдеры должны только обозначать места для этих параметров.

То есть REPEAT может помочь собрать каркас:

?,?,?

Но сами значения должны безопасно подставляться через механизм параметров в приложении или драйвере базы данных.

Нумерованные плейсхолдеры в PostgreSQL

В PostgreSQL часто используются нумерованные плейсхолдеры:

$1, $2, $3

Для них REPEAT уже не так удобен, потому что каждый элемент отличается номером.

Если нужно собрать именно такую строку внутри PostgreSQL, лучше использовать generate_series и string_agg:

SELECT string_agg('$' || n, ', ' ORDER BY n) AS placeholders
FROM generate_series(1, 3) AS g(n);

Результат:

$1, $2, $3

Это важное отличие.

REPEAT хорош, когда повторяется одинаковый фрагмент:

?, ?, ?

А если каждый фрагмент отличается:

$1, $2, $3

лучше использовать генерацию строк и агрегацию.

Текстовые бар-чарты

Один из самых приятных примеров REPEAT — маленькие текстовые диаграммы прямо в результате SQL-запроса.

Допустим, у нас есть пользователи по странам. Мы хотим не только вывести количество, но и показать простой визуальный столбик.

SELECT
    country,
    COUNT(*) AS users_count,
    REPEAT('#', (COUNT(*) / 10)::int) AS bar
FROM users
GROUP BY country
ORDER BY users_count DESC;

Пример результата:

country    | users_count | bar
-----------+-------------+----------
Georgia    | 52          | #####
Kazakhstan | 31          | ###
Armenia    | 18          | #

Здесь один символ # означает примерно 10 пользователей.

Если пользователей 52, получится 5 символов.

Если 31 — 3 символа.

Если 18 — 1 символ.

Это не замена настоящей визуализации, но для быстрого анализа в консоли очень удобно.

Как выбрать масштаб для бара

В текстовой диаграмме важно правильно выбрать масштаб.

Если написать так:

SELECT REPEAT('#', COUNT(*)::int) FROM users GROUP BY country;

и в стране 5000 пользователей, SQL нарисует строку из 5000 символов. Это будет неудобно и некрасиво.

Поэтому метрику обычно делят:

SELECT REPEAT('#', (COUNT(*) / 10)::int) FROM users GROUP BY country;

или:

SELECT REPEAT('#', (COUNT(*) / 100)::int) FROM users GROUP BY country;

Масштаб зависит от данных.

Например:

  • если значения маленькие, можно делать 1 символ = 1 единица;
  • если значения десятки, можно делать 1 символ = 10 единиц;
  • если значения тысячи, можно делать 1 символ = 100 или 1000 единиц.

Хорошая диаграмма должна помещаться на экране и помогать сравнивать значения, а не превращаться в бесконечную строку.

Ограничение максимальной длины бара

Чтобы случайно не получить слишком длинную строку, можно ограничить количество символов сверху через LEAST.

Например:

SELECT
    country,
    COUNT(*) AS users_count,
    REPEAT('#', LEAST((COUNT(*) / 10)::int, 30)) AS bar
FROM users
GROUP BY country
ORDER BY users_count DESC;

Здесь бар не будет длиннее 30 символов.

Даже если пользователей очень много, строка останется аккуратной.

Запись LEAST((COUNT(*) / 10)::int, 30) означает: взять меньшее из двух значений — рассчитанную длину бара или 30.

Это полезная защита для отчётов, особенно если данные могут резко вырасти.

Минимальный видимый бар

Есть и обратная проблема.

Если значение маленькое, после деления может получиться 0.

Например, 18 / 100 при целочисленном делении даёт 0.

Тогда REPEAT('#', 0) вернёт пустую строку, и визуально покажется, будто значения вообще нет.

Если вам нужно показывать хотя бы один символ для ненулевых значений, можно использовать GREATEST.

Например:

SELECT
    country,
    COUNT(*) AS users_count,
    REPEAT('#', GREATEST((COUNT(*) / 10)::int, 1)) AS bar
FROM users
GROUP BY country
ORDER BY users_count DESC;

Но тут есть нюанс: если значение действительно равно нулю, такой запрос всё равно нарисует один символ. Поэтому для точной логики лучше использовать CASE.

SELECT
    country,
    COUNT(*) AS users_count,
    CASE
        WHEN COUNT(*) = 0 THEN ''
        ELSE REPEAT('#', GREATEST((COUNT(*) / 10)::int, 1))
    END AS bar
FROM users
GROUP BY country
ORDER BY users_count DESC;

Так мы говорим:

  • если значение нулевое — бара нет;
  • если значение больше нуля — покажи хотя бы один символ.

REPEAT вместе с LPAD и RPAD

REPEAT часто используют вместе с LPAD и RPAD.

REPEAT рисует сам бар:

SELECT REPEAT('#', 5);

Результат:

#####

А LPAD или RPAD помогают выровнять его до нужной ширины.

Например, выведем среднюю зарплату по отделам:

SELECT
    dept,
    ROUND(AVG(salary)) AS avg_salary,
    RPAD(REPEAT('#', (AVG(salary) / 1000)::int), 30, ' ') AS bar
FROM employees
GROUP BY dept
ORDER BY avg_salary DESC;

Результат может выглядеть так:

dept        | avg_salary | bar
------------+------------+------------------------------
Engineering | 5200       | #####
Marketing   | 3100       | ###
Support     | 2400       | ##

Здесь REPEAT('#', (AVG(salary) / 1000)::int) создаёт бар, а RPAD(..., 30, ' ') дополняет его пробелами справа до 30 символов, чтобы все строки занимали одинаковую ширину.

Если хочется выровнять бар вправо, можно использовать LPAD:

SELECT
    dept,
    ROUND(AVG(salary)) AS avg_salary,
    LPAD(REPEAT('#', (AVG(salary) / 1000)::int), 30, ' ') AS bar
FROM employees
GROUP BY dept
ORDER BY avg_salary DESC;

Тогда символы будут прижаты к правому краю.

Пример: рейтинг товаров в виде звёздочек

REPEAT можно использовать для простых рейтингов.

Допустим, в таблице товаров есть рейтинг от 1 до 5:

product_name | rating
-------------+-------
Keyboard     | 4
Mouse        | 5
Monitor      | 3

Можно вывести рейтинг звёздочками:

SELECT
    product_name,
    rating,
    REPEAT('★', rating) AS stars
FROM products
ORDER BY rating DESC;

Результат:

product_name | rating | stars
-------------+--------+-------
Mouse        | 5      | ★★★★★
Keyboard     | 4      | ★★★★
Monitor      | 3      | ★★★

Такой формат удобен для демонстрационных отчётов и учебных задач.

Если нужно всегда показывать шкалу из пяти символов, можно добавить пустые звёзды:

SELECT
    product_name,
    rating,
    REPEAT('★', rating) || REPEAT('☆', 5 - rating) AS stars
FROM products
ORDER BY rating DESC;

Результат:

product_name | rating | stars
-------------+--------+-------
Mouse        | 5      | ★★★★★
Keyboard     | 4      | ★★★★☆
Monitor      | 3      | ★★★☆☆

Вот здесь REPEAT уже помогает собрать не просто повтор, а маленький понятный визуальный элемент.

Что будет, если количество повторов равно нулю

Если количество повторов равно 0, функция вернёт пустую строку.

SELECT REPEAT('x', 0) AS result;

Результат:

Там не будет ни одного символа.

Это не NULL и не ошибка. Это просто пустая строка.

В текстовых диаграммах это важно: если значение после расчёта превратилось в 0, столбик исчезнет.

Например:

SELECT REPEAT('#', (5 / 10)::int) AS bar;

Результат будет пустым, потому что 5 / 10 при целочисленном расчёте может дать 0.

Поэтому для баров часто используют GREATEST, если нужно показать хотя бы один символ.

Что будет, если количество повторов отрицательное

В PostgreSQL и MySQL отрицательное количество повторов обычно даёт пустую строку.

SELECT REPEAT('x', -3) AS result;

Результат:

Ошибки нет, но и символов тоже нет.

Это может быть неожиданно.

Например, если вы считаете длину бара по формуле value - 10 и value оказался меньше 10, количество повторов станет отрицательным. В результате бар просто исчезнет.

Чтобы защититься, можно использовать GREATEST:

SELECT REPEAT('#', GREATEST(value - 10, 0)) AS bar
FROM metrics;

Так мы говорим: если расчёт дал отрицательное число, возьми 0.

Что будет с NULL

Если строка или количество повторов равны NULL, результат тоже будет NULL.

SELECT REPEAT(NULL, 3) AS result;

Результат:

NULL
SELECT REPEAT('x', NULL) AS result;

Результат:

NULL

Это логично: если SQL не знает, что повторять или сколько раз повторять, он не может собрать строку.

Если в данных может быть NULL, используйте COALESCE.

Например:

SELECT REPEAT('#', COALESCE(score, 0)) AS bar
FROM metrics;

Здесь COALESCE(score, 0) означает: если score равен NULL, возьми 0.

Тогда вместо NULL получится пустая строка.

Если нужно заменить NULL в самой строке:

SELECT REPEAT(COALESCE(label, ''), 3) AS repeated_label
FROM labels;

Дробное количество повторов

Количество повторов должно быть целым числом.

Но в реальных запросах вы часто получаете дробное значение после деления или среднего значения.

Например, выражение AVG(salary) / 1000 может быть дробным.

Чтобы передать это значение в REPEAT, лучше явно привести его к целому типу.

В PostgreSQL:

SELECT REPEAT('#', (AVG(salary) / 1000)::int) AS bar
FROM employees;

Так запрос читается понятнее: мы явно показываем, что количество символов должно быть целым.

Но помните: приведение к int может округлять значение. Поэтому для отчётов лучше заранее решить, какая логика вам нужна:

  • округлять;
  • отбрасывать дробную часть;
  • брать минимум;
  • брать максимум;
  • ограничивать сверху.

Например:

SELECT REPEAT('#', LEAST(ROUND(AVG(salary) / 1000)::int, 30)) AS bar
FROM employees;

Здесь мы округляем значение, приводим его к int и ограничиваем бар 30 символами.

Не давайте REPEAT бесконтрольно раздувать строку

REPEAT может создать очень большую строку.

Например:

SELECT REPEAT('ab', 1000000);

Это строка на два миллиона символов.

В учебном примере это просто странно. А в реальном запросе такое может нагрузить память, сеть и клиент, который получает результат.

Особенно осторожно нужно быть, если количество повторов приходит из данных или пользовательского ввода.

Плохая идея:

SELECT REPEAT('#', user_provided_number) AS bar
FROM metrics;

Если user_provided_number = 10000000, результат будет огромным.

Лучше ограничивать длину:

SELECT REPEAT('#', LEAST(user_provided_number, 100)) AS bar
FROM metrics;

Для текстовых диаграмм обычно достаточно 30, 50 или 100 символов. Всё, что больше, уже плохо читается.

REPEAT в PostgreSQL

В PostgreSQL функция называется repeat.

Обычно её пишут в верхнем регистре в статьях и примерах:

SELECT REPEAT('ha', 3) AS result;

Результат:

hahaha

Но в PostgreSQL имена функций не чувствительны к регистру, поэтому эти варианты эквивалентны:

SELECT repeat('ha', 3);
SELECT REPEAT('ha', 3);

Для строковой склейки в PostgreSQL часто используют оператор ||:

SELECT REPEAT('-', 10) || ' report ' || REPEAT('-', 10) AS title;

Результат:

---------- report ----------

REPEAT в MySQL

В MySQL функция тоже называется REPEAT.

Синтаксис такой же:

SELECT REPEAT('ab', 3) AS result;

Результат:

ababab

Для склейки строк в MySQL обычно используют CONCAT, а не ||:

SELECT CONCAT(REPEAT('-', 10), ' report ', REPEAT('-', 10)) AS title;

Результат:

---------- report ----------

Это важное отличие для новичков: сама функция REPEAT похожа, но способы склеивания строк в разных СУБД могут отличаться.

REPEAT в ClickHouse

В ClickHouse тоже есть функция repeat.

Пример:

SELECT repeat('ab', 3) AS result;

Результат:

ababab

Для текстовых отчётов и простых баров идея та же:

SELECT repeat('#', 5) AS bar;

Результат:

#####

При переносе запросов между PostgreSQL, MySQL и ClickHouse стоит отдельно проверить граничные случаи: 0, отрицательные значения, NULL и слишком большие числа. На обычных примерах функция выглядит одинаково, а различия чаще появляются именно на краях.

REPEAT не заменяет LPAD и RPAD

Иногда новички пытаются использовать REPEAT там, где лучше подходит LPAD или RPAD.

Например, нужно превратить 42 в 000042.

Через REPEAT можно попытаться сделать так:

SELECT REPEAT('0', 4) || '42' AS code;

Результат:

000042

Но это работает только потому, что мы заранее знаем: нужно ровно четыре нуля.

Если значение будет другим, например 7 или 12345, количество нулей уже нужно считать отдельно.

Для таких задач лучше использовать LPAD:

SELECT LPAD('42', 6, '0') AS code;

Результат:

000042

LPAD сам понимает, сколько символов нужно добавить слева до нужной длины.

Запомните разницу:

  • REPEAT просто повторяет строку заданное количество раз;
  • LPAD дополняет значение слева до нужной длины;
  • RPAD дополняет значение справа до нужной длины.

REPEAT не создаёт строки таблицы

Ещё одна частая путаница: REPEAT повторяет текст внутри одной строки, но не создаёт несколько строк результата.

SELECT REPEAT('row', 3);

Результат:

rowrowrow

Если вам нужны три отдельные строки, в PostgreSQL используйте generate_series:

SELECT 'row' AS value
FROM generate_series(1, 3);

Результат:

value
-----
row
row
row

Это разные задачи.

REPEAT — для строкового форматирования.

generate_series — для генерации строк в результате запроса.

Практический пример: отчёт по отделам

Допустим, есть таблица сотрудников:

dept        | salary
------------+-------
Engineering | 5200
Engineering | 4800
Marketing   | 3100
Marketing   | 2900
Support     | 2200

Хотим вывести среднюю зарплату и маленькую диаграмму:

SELECT
    dept,
    ROUND(AVG(salary)) AS avg_salary,
    REPEAT('#', ROUND(AVG(salary) / 1000)::int) AS salary_bar
FROM employees
GROUP BY dept
ORDER BY avg_salary DESC;

Результат:

dept        | avg_salary | salary_bar
------------+------------+-----------
Engineering | 5000       | #####
Marketing   | 3000       | ###
Support     | 2200       | ##

Такой запрос сразу показывает не только числа, но и визуальное сравнение.

Для серьёзной аналитики лучше использовать нормальные графики. Но для быстрой проверки данных в SQL-консоли это очень удобно.

Практический пример: линия под заголовком

Допустим, нужно вывести название курса и линию под ним:

SELECT
    title,
    REPEAT('-', LENGTH(title)) AS underline
FROM courses;

Если данные такие:

title
----------------
SQL Basics
Window Functions

результат будет таким:

title            | underline
-----------------+-----------------
SQL Basics       | ----------
Window Functions | ----------------

Линия автоматически подстраивается под длину заголовка. Если название изменится, длина подчёркивания тоже изменится.

Практический пример: шкала прогресса

Допустим, у пользователя есть прогресс по курсу от 0 до 100.

user_name | progress
----------+---------
Alex      | 80
Maria     | 45
Ivan      | 10

Можно вывести простую шкалу из 10 делений:

SELECT
    user_name,
    progress,
    '[' ||
    RPAD(REPEAT('#', (progress / 10)::int), 10, '.') ||
    ']' AS progress_bar
FROM users_progress
ORDER BY progress DESC;

Результат:

user_name | progress | progress_bar
----------+----------+--------------
Alex      | 80       | [########..]
Maria     | 45       | [####......]
Ivan      | 10       | [#.........]

Здесь REPEAT('#', (progress / 10)::int) рисует заполненную часть шкалы, а RPAD(..., 10, '.') добивает её точками до 10 символов.

Получается компактный текстовый прогресс-бар.

Коротко

REPEAT — это функция, которая повторяет строку указанное количество раз.

SELECT REPEAT('ab', 3);

Результат:

ababab

Она полезна, когда нужно:

  • сделать разделитель для отчёта;
  • добавить отступ;
  • собрать повторяющийся текстовый фрагмент;
  • подготовить плейсхолдеры вида ?,?,?;
  • нарисовать простую текстовую диаграмму;
  • сделать рейтинг звёздочками;
  • собрать прогресс-бар прямо в SQL.

Главные правила:

  • REPEAT возвращает одну строку, а не несколько строк таблицы;
  • повторять можно не только символ, но и целую строку;
  • если количество повторов равно 0, получится пустая строка;
  • если количество повторов отрицательное, в PostgreSQL и MySQL обычно тоже получится пустая строка;
  • если строка или количество повторов равны NULL, результат будет NULL;
  • слишком большое количество повторов может создать огромную строку;
  • для дополнения значения до нужной длины чаще подходят LPAD и RPAD;
  • для генерации нескольких строк нужен не REPEAT, а другие инструменты, например generate_series.

Самое простое правило: используйте REPEAT, когда нужно не изменить данные, а красиво собрать текстовое представление прямо в запросе.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр