Как найти пробелы в последовательной нумерации в mysql?

120

У нас есть база данных с таблицей, значения которой были импортированы из другой системы. Есть столбец с автоинкрементом, нет повторяющихся значений, но есть пропущенные значения. Например, при выполнении этого запроса:

select count(id) from arrc_vouchers where id between 1 and 100

должен вернуть 100, но вместо этого возвращает 87. Могу ли я выполнить какой-либо запрос, который вернет значения отсутствующих чисел? Например, записи могут существовать для идентификаторов 1-70 и 83-100, но отсутствуют записи с идентификаторами 71-82. Хочу вернуть 71, 72, 73 и т. Д.

Это возможно?

Emmys
источник
Это может не работать в MySQL, но на работе (Oracle) нам нужно было нечто подобное. Мы написали хранимую процедуру, которая принимает число в качестве максимального значения. Затем Stored Proc создал временную таблицу с одним столбцом. В таблице были все числа от 1 до Max. Затем было выполнено соединение NOT IN между временной таблицей и интересующей нас таблицей. Если вы вызвали его с Max = Select max (id) из arrc_vouchers, тогда он вернет все недостающие значения.
saunderl 02
2
Что плохого в пробелах в нумерации? Значение суррогатного ключа обычно не имеет смысла; важно только то, что он уникален. Если ваше приложение не может обрабатывать несмежные идентификаторы, вероятно, это ошибка в приложении, а не в данных.
Wyzard
4
В данном случае это проблема, потому что данные, которые мы унаследовали от старой системы, использовали номер автоинкремента, связанный с записью, в качестве ключа для печати на физической карте, которая раздается людям. Это НЕ была наша идея. Чтобы узнать, какие карточки отсутствуют, нам необходимо знать, где в последовательной нумерации есть пробелы.
EmmyS
xaprb.com/blog/2005/12/06/… select l.id + 1 as start from sequence as l left outer join sequence as r on l.id + 1 = r.id where r.id is null;
Вы можете использовать генерацию серий для генерации чисел от 1 до самого высокого идентификатора вашей таблицы. Затем запустите запрос, где id не входит в эту серию.
Цветелин Салуцкий

Ответы:

170

Обновить

ConfexianMJS дал гораздо лучший ответ с точки зрения производительности.

Ответ (не самый быстрый)

Вот версия, которая работает с таблицей любого размера (а не только со 100 строками):

SELECT (t1.id + 1) as gap_starts_at, 
       (SELECT MIN(t3.id) -1 FROM arrc_vouchers t3 WHERE t3.id > t1.id) as gap_ends_at
FROM arrc_vouchers t1
WHERE NOT EXISTS (SELECT t2.id FROM arrc_vouchers t2 WHERE t2.id = t1.id + 1)
HAVING gap_ends_at IS NOT NULL
  • gap_starts_at - первый id в текущем промежутке
  • gap_ends_at - последний id в текущем промежутке
матовый
источник
6
Я даже больше не работаю в этой компании, но это лучший ответ, который я видел, и его определенно стоит запомнить для использования в будущем. Спасибо!
EmmyS
4
единственная проблема в том, что он не «сообщает» о возможном начальном пробеле. например, если первые 5 идентификаторов отсутствуют (с 1 по 5), это не показывает этого ... Как мы можем отображать ссадные пробелы в самом начале?
DiegoDD
Примечание. Этот запрос не работает с временными таблицами. Моя проблема заключалась в том, что order numberя искал пробелы в неразличимых (таблица хранит строки заказов, поэтому номер заказа, которому они принадлежат, повторяется для каждой строки). 1-й запрос: 2812 строк в наборе (1 мин 31,09 сек) . Сделал еще одну таблицу, выбрав отдельные номера заказов. Ваш запрос без моих повторов: 1009 строк в наборе (18,04 сек)
Chris K
1
@DiegoDD Что не так SELECT MIN(id) FROM table?
Воздух
8
Работает, но для запуска на таблице с 700000 записями потребовалось около 5 часов
Мэтт
98

Это помогло мне найти пробелы в таблице с более чем 80 тыс. Строк:

SELECT
 CONCAT(z.expected, IF(z.got-1>z.expected, CONCAT(' thru ',z.got-1), '')) AS missing
FROM (
 SELECT
  @rownum:=@rownum+1 AS expected,
  IF(@rownum=YourCol, 0, @rownum:=YourCol) AS got
 FROM
  (SELECT @rownum:=0) AS a
  JOIN YourTable
  ORDER BY YourCol
 ) AS z
WHERE z.got!=0;

Результат:

+------------------+
| missing          |
+------------------+
| 1 thru 99        |
| 666 thru 667     |
| 50000            |
| 66419 thru 66456 |
+------------------+
4 rows in set (0.06 sec)

Обратите внимание, что порядок столбцов expectedи gotимеет решающее значение.

Если вы знаете, что YourColон не начинается с 1 и это не имеет значения, вы можете заменить

(SELECT @rownum:=0) AS a

с участием

(SELECT @rownum:=(SELECT MIN(YourCol)-1 FROM YourTable)) AS a

Новый результат:

+------------------+
| missing          |
+------------------+
| 666 thru 667     |
| 50000            |
| 66419 thru 66456 |
+------------------+
3 rows in set (0.06 sec)

Если вам нужно выполнить какую-то задачу сценария оболочки с отсутствующими идентификаторами, вы также можете использовать этот вариант, чтобы напрямую создать выражение, которое вы можете перебирать в bash.

SELECT GROUP_CONCAT(IF(z.got-1>z.expected, CONCAT('$(',z.expected,' ',z.got-1,')'), z.expected) SEPARATOR " ") AS missing
FROM (  SELECT   @rownum:=@rownum+1 AS expected,   IF(@rownum=height, 0, @rownum:=height) AS got  FROM   (SELECT @rownum:=0) AS a   JOIN block   ORDER BY height  ) AS z WHERE z.got!=0;

Это дает такой результат

$(seq 1 99) $(seq 666 667) 50000 $(seq 66419 66456)

Затем вы можете скопировать и вставить его в цикл for в терминале bash, чтобы выполнить команду для каждого идентификатора.

for ID in $(seq 1 99) $(seq 666 667) 50000 $(seq 66419 66456); do
  echo $ID
  # fill the gaps
done

Это то же самое, что и выше, только читаемое и исполняемое. Изменив команду «CONCAT» выше, можно сгенерировать синтаксис для других языков программирования. Или даже SQL.

ConfexianMJS
источник
8
хорошее решение, для меня это лучше, чем предпочтительный ответ - спасибо
Wee Zel
6
Это намного эффективнее, чем принятый ответ.
symcbean
1
намного быстрее, чем принятый ответ. Единственное, что я бы добавил, это то, CONVERT( YourCol, UNSIGNED )что даст лучшие результаты, если YourCol еще не является целым числом.
Бартон Читтенден
1
@AlexandreCassagne: Если я правильно понимаю ваш вопрос, я бы просто сделал отдельный запрос, подобный встроенному, для поиска SELECT MAX(YourCol) FROM YourTable;
минимума
1
@temuri При необходимости переключитесь на вариант GROUP_CONCAT:SELECT IF((z.got-IF(z.over>0, z.over, 0)-1)>z.expected, CONCAT(z.expected,' thru ',(z.got-IF(z.over>0, z.over, 0)-1)), z.expected) AS missing FROM ( SELECT @rownum:=@rownum+1 AS expected, @target-@missing AS under, (@missing:=@missing+IF(@rownum=YourCol, 0, YourCol-@rownum))-@target AS over, IF(@rownum=YourCol, 0, @rownum:=YourCol) AS got FROM (SELECT @rownum:=0, @missing:=0, @target:=10) AS a JOIN YourTable ORDER BY YourCol ) AS z WHERE z.got!=0 AND z.under>0;
ConfexianMJS
11

Быстрый и грязный запрос, который должен помочь:

SELECT a AS id, b AS next_id, (b - a) -1 AS missing_inbetween
FROM 
 (
SELECT a1.id AS a , MIN(a2.id) AS b 
FROM arrc_vouchers  AS a1
LEFT JOIN arrc_vouchers AS a2 ON a2.id > a1.id
WHERE a1.id <= 100
GROUP BY a1.id
) AS tab

WHERE 
b > a + 1

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

 
id next_id missing_inbetween
 1 4 2
68 70 1
75 87 11
Бен
источник
1
Это отлично сработало для меня. Спасибо.! Я смог легко изменить это для своих целей.
Rahim Khoja
Кажется, это лучший ответ при поиске «следующего идентификатора» в пробелах. К сожалению, это ОЧЕНЬ медленно для таблиц с 10 КБ строк. Я ждал более 10 минут на таблице размером ~ 46 КБ, тогда как с @ConfexianMJS я получил результаты менее чем за секунду!
BringBackCommodore64
5

Если вы используете, у MariaDBвас есть более быстрый (800%) вариант с использованием механизма хранения последовательностей :

SELECT * FROM seq_1_to_50000 WHERE SEQ NOT IN (SELECT COL FROM TABLE);
Моше Л
источник
2
Чтобы расширить эту идею, можно установить максимум последовательности, используя "SELECT MAX(column) FROM table"и установив переменную из результата, скажем, $ MAX ... затем можно написать оператор sql, "SELECT * FROM seq_1_to_". $MAX ." WHERE seq not in (SELECT column FROM table)" мой синтаксис основан на php
me_
или вы можете использовать SELECT @var:= max FROM ....; select * from .. WHERE seq < @max;с переменными MySQL.
Моше Л.
2

Создайте временную таблицу со 100 строками и одним столбцом, содержащим значения 1–100.

Внешний Присоедините эту таблицу к вашей таблице arrc_vouchers и выберите значения одного столбца, в которых идентификатор arrc_vouchers равен нулю.

Кодирование это вслепую, но должно работать.

select tempid from temptable 
left join arrc_vouchers on temptable.tempid = arrc_vouchers.id 
where arrc_vouchers.id is null
amelvin
источник
Хорошо, 1–100 - это простой способ привести пример. В данном случае мы смотрим на 20 000–85 000. Итак, мне создать временную таблицу с 65 000 строками с номерами от 20000 до 85000? И как мне это сделать? Я использую phpMyAdmin; если я установлю значение столбца по умолчанию на 25000 и сделаю его автоматическим приращением, могу ли я просто вставить 65000 строк, и он начнет автоматическое приращение с 25000?
EmmyS 02
У меня была похожая ситуация (у меня 100 элементов в порядке, а в 100 нужно найти недостающие). Для этого я создал еще одну таблицу 1-100, затем выполнил на ней этот оператор, и он отлично работает. Это заменяет очень сложную функцию создания временных таблиц. Просто совет для кого-то в подобной ситуации, иногда таблицу создать быстрее, чем временные таблицы.
newshorts
2

Альтернативным решением, требующим запроса + некоторого кода, выполняющего некоторую обработку, было бы:

select l.id lValue, c.id cValue, r.id rValue 
  from 
  arrc_vouchers l 
  right join arrc_vouchers c on l.id=IF(c.id > 0, c.id-1, null)
  left  join arrc_vouchers r on r.id=c.id+1
where 1=1
  and c.id > 0 
  and (l.id is null or r.id is null)
order by c.id asc;

Обратите внимание, что запрос не содержит подзапроса, который, как мы знаем, не обрабатывается планировщиком MySQL эффективно.

Это вернет одну запись на центральное значение (cValue), которое не имеет меньшего значения (lValue) или большего значения (rValue), то есть:

lValue |cValue|rValue
-------+------+-------
{null} | 2    | 3      
8      | 9    | {null} 
{null} | 22   | 23     
23     | 24   | {null} 
{null} | 29   | {null} 
{null} | 33   | {null} 


Не вдаваясь в подробности (мы увидим их в следующих параграфах), этот вывод означает, что:

  • Нет значений от 0 до 2
  • Нет значений от 9 до 22
  • Нет значений от 24 до 29
  • Нет значений от 29 до 33
  • Нет значений от 33 до MAX VALUE

Таким образом, основная идея состоит в том, чтобы выполнить соединения RIGHT и LEFT с одной и той же таблицей, чтобы увидеть, есть ли у нас значения смежности для каждого значения (например, если центральное значение равно '3', тогда мы проверяем 3-1 = 2 слева и 3 + 1 в right), а когда ROW имеет значение NULL в RIGHT или LEFT, мы знаем, что смежного значения нет.

Полный необработанный вывод моей таблицы:

select * from arrc_vouchers order by id asc;

0  
2  
3  
4  
5  
6  
7  
8  
9  
22 
23 
24 
29 
33 

Некоторые примечания:

  1. Оператор SQL IF в условии соединения необходим, если вы определяете поле id как UNSIGNED, поэтому он не позволит вам уменьшить его до нуля. В этом нет строгой необходимости, если вы сохраняете c.value> 0, как указано в следующем примечании, но я включаю его просто как документ.
  2. Я фильтрую нулевое центральное значение, так как нас не интересует какое-либо предыдущее значение, и мы можем получить значение сообщения из следующей строки.
mgo1977
источник
2

Если существует последовательность, в которой между двумя числами пробел не превышает единицы (например, 1,3,5,6), то можно использовать следующий запрос:

select s.id+1 from source1 s where s.id+1 not in(select id from source1) and s.id+1<(select max(id) from source1);
  • table_name - source1
  • имя_столбца - id
ПРАХАР ГУПТА
источник
1

на основе ответа, приведенного выше Лучеком, эта хранимая процедура позволяет вам указать имена таблиц и столбцов, которые вы хотите протестировать, чтобы найти несмежные записи - таким образом, отвечая на исходный вопрос, а также демонстрируя, как можно использовать @var для представления таблиц и / или столбцы в хранимой процедуре.

create definer=`root`@`localhost` procedure `spfindnoncontiguous`(in `param_tbl` varchar(64), in `param_col` varchar(64))
language sql
not deterministic
contains sql
sql security definer
comment ''
begin
declare strsql varchar(1000);
declare tbl varchar(64);
declare col varchar(64);

set @tbl=cast(param_tbl as char character set utf8);
set @col=cast(param_col as char character set utf8);

set @strsql=concat("select 
    ( t1.",@col," + 1 ) as starts_at, 
  ( select min(t3.",@col,") -1 from ",@tbl," t3 where t3.",@col," > t1.",@col," ) as ends_at
    from ",@tbl," t1
        where not exists ( select t2.",@col," from ",@tbl," t2 where t2.",@col," = t1.",@col," + 1 )
        having ends_at is not null");

prepare stmt from @strsql;
execute stmt;
deallocate prepare stmt;
end
Профессор Абронсиус
источник
1

Я пробовал это разными способами, и лучшая производительность, которую я обнаружил, была это простой запрос:

select a.id+1 gapIni
    ,(select x.id-1 from arrc_vouchers x where x.id>a.id+1 limit 1) gapEnd
    from arrc_vouchers a
    left join arrc_vouchers b on b.id=a.id+1
    where b.id is null
    order by 1
;

... одно левое соединение, чтобы проверить, существует ли следующий идентификатор , только если следующий, если не найден, тогда подзапрос находит следующий идентификатор, который существует, чтобы найти конец пробела. Я сделал это, потому что запрос с равным (=) имеет лучшую производительность, чем оператор больше (>).

Используя sqlfiddle, он не показывает такую ​​разную производительность по сравнению с другими запросами, но в реальной базе данных этот запрос выше приводит в 3 раза быстрее, чем другие.

Схема:

CREATE TABLE arrc_vouchers (id int primary key)
;
INSERT INTO `arrc_vouchers` (`id`) VALUES (1),(4),(5),(7),(8),(9),(10),(11),(15),(16),(17),(18),(19),(20),(21),(22),(23),(24),(25),(26),(27),(28),(29)
;

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

select a.id+1 gapIni
    ,(select x.id-1 from arrc_vouchers x where x.id>a.id+1 limit 1) gapEnd
    from arrc_vouchers a
    left join arrc_vouchers b on b.id=a.id+1
    where b.id is null
    order by 1
;
select *, (gapEnd-gapIni) qt
    from (
        select id+1 gapIni
        ,(select x.id from arrc_vouchers x where x.id>a.id limit 1) gapEnd
        from arrc_vouchers a
        order by id
    ) a where gapEnd <> gapIni
;
select id+1 gapIni
    ,(select x.id from arrc_vouchers x where x.id>a.id limit 1) gapEnd
    #,coalesce((select id from arrc_vouchers x where x.id=a.id+1),(select x.id from arrc_vouchers x where x.id>a.id limit 1)) gapEnd
    from arrc_vouchers a
    where id+1 <> (select x.id from arrc_vouchers x where x.id>a.id limit 1)
    order by id
;
select id+1 gapIni
    ,coalesce((select id from arrc_vouchers x where x.id=a.id+1),(select x.id from arrc_vouchers x where x.id>a.id limit 1)) gapEnd
    from arrc_vouchers a
    order by id
;
select id+1 gapIni
    ,coalesce((select id from arrc_vouchers x where x.id=a.id+1),concat('*** GAT *** ',(select x.id from arrc_vouchers x where x.id>a.id limit 1))) gapEnd
    from arrc_vouchers a
    order by id
;

Может кому поможет и пригодится.

Вы можете увидеть и протестировать мой запрос, используя этот sqlfiddle :

http://sqlfiddle.com/#!9/6bdca7/1

lynx_74
источник
0

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

Я использовал это, и он находит пробел или следующий доступный (последний использованный + 1) с гораздо более быстрым возвратом из запроса.

SELECT a.id as beforegap, a.id+1 as avail
FROM table_name a
where (select b.id from table_name b where b.id=a.id+1) is null
limit 1;
обкрадывать
источник
это обнаруживает первый пробел, о котором не спрашивается.
нарисовал
0

Возможно, это не актуально, но я искал что-то вроде этого, чтобы перечислить пробелы в последовательности чисел, и нашел этот пост, в котором есть несколько разных решений в зависимости от того, что именно вы ищете. Я искал первый доступный пробел в последовательности (то есть следующий доступный номер), и, похоже, это работает нормально.

ВЫБЕРИТЕ MIN (l.number_sequence + 1) как nextavabile из пациентов как l LEFT OUTER JOIN пациентов как r на l.number_sequence + 1 = r.number_sequence ГДЕ r.number_sequence имеет значение NULL. Там обсуждались еще несколько сценариев и решений, начиная с 2005 года!

Как найти пропущенные значения в последовательности с помощью SQL

SScotti
источник