Перейти к основному содержимому

Уникальные значения

В реальных данных есть два разных правила «без повторов», и TDC даёт под каждое отдельный инструмент:

МеханизмОбластьСмысл
<distinct>одна строкаполя не совпадают между собой в строке
uniqвсе строкикомбинация полей уникальна по всему датасету

Считайте их близнецами на двух осях. <distinct> работает по горизонтали — внутри одной строки, поэтому вы никогда не получите Иван Иван или «родился в Москве, живёт в Москве». uniq работает по вертикали — вдоль всего датасета, поэтому одна и та же пара (имя, фамилия) не встретится дважды. Они полностью независимы: используйте любой из них или оба сразу.

Примеры вывода — иллюстративные

Значения ниже — это то, что выдаёт типичный запуск. Конкретные вытяжки могут отличаться в зависимости от версии ядра и seed; неизменной остаётся структура, которую гарантирует каждый инструмент (никаких совпадений внутри строки для <distinct>, никаких повторяющихся комбинаций для uniq).

Сколько раз выпало каждое сочетание двух полей. По горизонтали первое поле, по вертикали второе.
  • Aс distinct на 60 строках: диагональ пуста, потому что внутри строки поля не могут совпасть
  • Bс uniq на 6 строках: ни в одной клетке не больше единицы, потому что сочетание не может повториться между строками — а пустые клетки это сочетания, до которых прогон просто не дошёл

<distinct> — разные внутри строки

Два поля <gen>, которые тянут из одного и того же списка, работают независимо, поэтому рано или поздно на какой-то строке выпадает одно и то же значение дважды. Здесь два симптома одного пациента берутся из короткого списка из четырёх, поэтому совпадения видны сразу:

<sequence name="Case">
<gen name="S1" type="text" value="Лихорадка,Кашель,Головная боль,Тошнота"/>
<gen name="S2" type="text" value="Лихорадка,Кашель,Головная боль,Тошнота"/>
</sequence>
...
<data>${{Case.S1}}, ${{Case.S2}}</data>
./run case.tdc (8 строк)
Тошнота, Головная боль
Головная боль, Головная боль
Лихорадка, Кашель
Тошнота, Тошнота
Головная боль, Лихорадка
Кашель, Тошнота
Кашель, Лихорадка
Лихорадка, Кашель

Строки 2 и 4 — Головная боль, Головная боль и Тошнота, Тошнота: это не пациент с двумя жалобами, а пациент с одной жалобой, записанной дважды.

Инструмент. Оборачиваем оба поля в <distinct> — всё остальное как было, даже seed тот же:

<sequence name="Case">
<distinct>
<gen name="S1" type="text" value="Лихорадка,Кашель,Головная боль,Тошнота"/>
<gen name="S2" type="text" value="Лихорадка,Кашель,Головная боль,Тошнота"/>
</distinct>
</sequence>
./run case.tdc (8 строк)
Тошнота, Головная боль
Головная боль, Тошнота
Лихорадка, Кашель
Тошнота, Кашель
Головная боль, Лихорадка
Кашель, Тошнота
Кашель, Лихорадка
Лихорадка, Кашель

Строки, где совпадения не было, остались байт-в-байт теми же — движок их не трогал. А два столкновения он починил: Головная боль, Головная боль стало Головная боль, Тошнота, а Тошнота, ТошнотаТошнота, Кашель. Перетянуто только второе поле и только там, где нужно; порядок и seed не пострадали.

Два уровня

<distinct> работает в двух местах, и правило на обоих одно: прямые дети <distinct> дают в каждой строке разные значения.

1. Внутри <sequence> — оборачивает поля <gen name="…">. Читается как «A ≠ B», здесь по настоящему списку симптомов из пакета:

<sequence name="Case">
<distinct>
<gen name="A" type="template" value="medical.symptom"/>
<gen name="B" type="template" value="medical.symptom"/>
</distinct>
</sequence>
./run case.tdc (6 строк)
Рвота + Головокружение
Потеря аппетита + Рвота
Затуманенное зрение + Насморк
Отёк + Лихорадка
Учащённое сердцебиение + Онемение
Затуманенное зрение + Одышка

Оба поля тянут из одного списка имён, но два значения в каждой строке всегда различаются.

2. Внутри <env> — оборачивает целые блоки <sequence>. Классический случай — «страна рождения» против «страны проживания»: две независимые вытяжки из одного списка стран иногда попадают на одну и ту же страну в одной строке.

<env count="100" seed="s">
<distinct>
<sequence name="Birth"><gen type="template" value="location.country"/></sequence>
<sequence name="Live"><gen type="template" value="location.country"/></sequence>
</distinct>
</env>
./run migration.tdc (6 строк) — рождение -> проживание
Франция    -> Бутан
Панама     -> Чили
Черногория -> Япония
Камерун    -> Кения
Перу       -> Намибия
Гватемала  -> Гренада

Теперь страна рождения и страна проживания в одной строке никогда не совпадут.

Как это работает, и детали

Движок генерирует поля как обычно; если два значения внутри группы столкнулись в строке, он перетягивает одно из них следующим значением генератора, пока они не станут разными. Детерминизм сохраняется — перетяжки идут в фиксированном порядке, поэтому вывод для заданного seed не меняется. Работает и в движке памяти, и в потоковом движке.

Детали, которые стоит знать:

  • Сравниваются значения, а не источники. Если два поля читают из разных файлов, но выпало одно и то же слово, <distinct> всё равно перетянет.
  • Группы независимы. <distinct> для имени/второго имени и отдельный <distinct> для чего-то ещё не мешают друг другу; их может быть несколько.
  • Поля вне <distinct> остаются без каких-либо ограничений.
  • Слишком мало значений — понятная ошибка. Если в списке разных значений меньше, чем полей, которые должны различаться (скажем, одно слово на два поля), TDC выдаст ясную ошибку, а не зациклится навсегда.
  • На уровне <env> в группу идут только последовательности с одним значением — простой <gen>, <mix> или <switch>. Составная (многополевая) последовательность там отвергается с ошибкой TDC129.

uniq — комбинация никогда не повторяется

uniq="true" на составной <sequence> означает, что комбинация всех её полей нигде в датасете не повторяется. (Иван, Смирнов) и (Иван, Кузнецов) — можно; две (Иван, Смирнов) — нельзя.

На простой секвенции — с одним безымянным <gen>uniq="true" означает, что не повторяется само значение: розыгрыш идёт без возвращения. Взвешенный пак сохраняет смысл (частые имена вероятнее попадут в выборку), но ничто не появляется дважды. Если различных значений в источнике меньше, чем записей, прогон честно отказывает заранее и называет оба числа — никаких тихих повторов. Поддерживаются: списки text, паки template, колонки file и простые целые диапазоны (value="1..100000"); increment/decrement уникальны по построению. Генератор, чьи значения нельзя перечислить (regex, date, …), получает отказ, который так и говорит.

<sequence name="Person" uniq="true">
<gen name="first" type="template" value="person.male.firstName"/>
<gen name="last" type="template" value="person.lastName"/>
</sequence>

<block>
<line><data>${{Person.first}} ${{Person.last}}</data></line>
</block>

Ни одна пара (имя, фамилия) не повторится. Если имён 200 и фамилий 500 — уникальных пар до 100 000; попросите больше — получите честную ошибку заранее (см. ниже).

До и после — на крохотном наборе

Два поля с крохотными наборами — first ∈ {Аня, Боря} и last ∈ {Ким, Ли} — дают всего 4 возможные пары. Просим 4 строки.

Без uniq (каждое поле случайно, само по себе):

<sequence name="P">
<gen name="first" type="text" value="Аня,Боря"/>
<gen name="last" type="text" value="Ким,Ли"/>
</sequence>
<block><line><data>${{P.first}} ${{P.last}}</data></line></block>
./run p.tdc (4 строки, с подсчётом)
Аня Ким   2
Боря Ли   2

Комбинации повторяются: Аня Ким и Боря Ли выпали по два раза, а Аня Ли и Боря Ким не выпали вовсе. Случайность про уникальность ничего не знает.

С uniq="true" (та же конфигурация, добавлен один атрибут):

<sequence name="P" uniq="true">
<gen name="first" type="text" value="Аня,Боря"/>
<gen name="last" type="text" value="Ким,Ли"/>
</sequence>
./run p.tdc (4 строки, с подсчётом)
Аня Ким   1
Аня Ли    1
Боря Ким  1
Боря Ли   1

Все 4 пары — по одному разу, ни одного повтора.

Пропорции сохраняются

Движок только переставляет значения полей между строками; он никогда не меняет их количество. Поэтому маска percent остаётся точной — уникальность и точное распределение уживаются вместе. percent="70,30" по-прежнему делит 70/30, даже пока каждая комбинация остаётся уникальной.

Проверка «хватит ли данных» — до генерации

Перед выводом TDC считает, возможно ли вообще собрать count уникальных комбинаций из ваших данных. Если нет — вы получите ошибку сразу, а не через часы генерации:

./run big.tdc
uniq: sequence "Person" requested 10000 unique combinations, but its
data supports at most 5000. Add more values to a field, or lower
the count.

Крохотный набор показывает то же самое. Пар всего 4; просим count="5" — и TDC не мучается впустую, а сразу говорит правду:

./run p5.tdc
tdc: uniq "P" is infeasible — only 4 distinct combinations exist,
but 5 unique rows were requested.
Держите комфортный запас

Максимум уникальных комбинаций ограничен произведением числа разных значений в каждом поле. Когда поле берёт значения случайно (text без percent), перекошенная выборка может уменьшить доступный запас. Для uniq держите комфортный запас (возможных комбинаций заметно больше, чем count) либо задавайте percent для ровного распределения.

<uniq> — по отдельным последовательностям

Когда поля живут в разных последовательностях, оберните их в <uniq>…</uniq> — уникальной по всем строкам станет комбинация значений этих последовательностей:

<uniq>
<sequence name="First"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="Last"><gen type="template" value="person.lastName"/></sequence>
</uniq>
<block><line><data>${{First}} ${{Last}}</data></line></block>

В группу могут идти только последовательности с одним значением (простой <gen>, <mix> или <switch>); составную последовательность туда положить нельзя.

Это не «уникальный id»

Речь про уникальность комбинации полей, а не про счётчик. Для сквозного номера используйте increment.

Как сделать уникальной склеенную строку

uniq — свойство розыгрыша. Последовательность, значение которой вычисляется или выбирается по if= для каждой строки, ни из какого пула не тянет — брать без возврата попросту нечего, — поэтому uniq= на ней отклоняется с TDC218, а не игнорируется молча.

Ставьте uniq на части, а склеивайте их уже в выводе:

<uniq>
<sequence name="Area"><gen type="number" value="900..999"/></sequence>
<sequence name="Group"><gen type="number" value="1..99" length="2" first_zero="true"/></sequence>
<sequence name="Serial"><gen type="number" value="1..9999" length="4" first_zero="true"/></sequence>
</uniq>
<block><line><data>${{Area}}${{Group}}${{Serial}}</data></line></block>

Тройка (Area, Group, Serial) уникальна в каждой строке, а так как ширина каждой части фиксирована — 3, 2 и 4 цифры, — девятизначную строку можно разобрать обратно в тройку ровно одним способом. Значит, уникальная тройка даёт уникальную строку.

Последняя фраза — весь фокус, и она же его граница. Если бы ширина части плавала, две разные тройки склеились бы в одну строку: 9|15… и 91|5… читаются одинаково, когда границы больше нет.

Большие объёмы

uniq работает на диске по умолчанию, без флагов — но ни одна его форма не идёт на быстром потоковом движке, а какой из двух оставшихся его возьмёт, зависит от того, как вы его написали:

  • uniq="true" на одном разыгрываемом столбце — самый частый случай — тянет без возврата, а для этого нужны и пул, и уже взятые значения. Это in-memory движок: память растёт вместе с count, и прогон ограничен объёмом ОЗУ.
  • Составной uniq, uniq на счётчике или группа <uniq> на уровне env уходят на точный движок на диске: он раскладывает каждый столбец, а затем проверяет кортежи и чинит коллизии. Память остаётся ограниченной, время — нет.

Какой движок запустит ваш конфиг описывает всю маршрутизацию, включая четыре не-uniq формы, которые тоже попадают в память.

Точный uniq на огромном выводе — ЭТО МЕДЛЕННО, а uniq + percent — сильнее всего

Проверка «отсортировать и починить» тщательная, и её стоимость растёт быстрее, чем линейно, с числом строк. Память остаётся ограниченной, а время — нет: уже сотни тысяч уникальных строк считаются минутами, а миллионы могут идти часами и дольше. Это честная цена гарантии ни одного повтора на огромном файле.

uniq вместе с percent на одних столбцах — худший случай, какой есть: точные доли и отсутствие повторов одновременно — это раскладка с ограничениями поверх сортировки, ещё медленнее с большим отрывом. Если прогон считается бесконечно, обычно помогает убрать либо percent, либо uniq.

Для уникальности на большом масштабе берите дешёвые по построению виды — счётчик или диапазон number, достаточно широкий, чтобы коллизия была исчезающе маловероятна, — а uniq="true" по числовым/percent-колонкам оставьте для тех объёмов, где исчерпывающая проверка стоит ожидания.

Запасной выход mode="memory" (маленький движок в ОЗУ) поддерживает и все формы uniq — точно, но в пределах ОЗУ. См. Большие объёмы.

См. также