Як знайти прогалини в послідовній нумерації в mysql?


119

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

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

повинен повернути 100, але він повертає 87 замість цього. Чи можна виконати запит, який поверне значення пропущених чисел? Наприклад, записи можуть існувати для id 1-70 та 83-100, але немає записів з id 71-82. Я хочу повернути 71, 72, 73 і т.д.

Чи можливо це?


Це може не працювати в MySQL, але в роботі (Oracle) нам знадобилося щось подібне. Ми написали збережену програму, яка взяла число як значення Max. Потім Stored Proc створив темп-таблицю з одним стовпцем. Таблиця містила всі числа від 1 до Макс. Тоді це з'єднання NOT IN між таблицею temp та нашою цікавою таблицею. Якщо ви зателефонували за допомогою параметра Max = Select max (id) з arrc_vouchers, він поверне всі пропущені значення.
saunderl

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 - перший ідентифікатор у поточному проміжку
  • gap_ends_at - останній ідентифікатор у поточному проміжку

6
Я вже навіть не працюю над цією компанією, але це найкраща відповідь, яку я бачив, і її, безумовно, варто пам’ятати для подальшого ознайомлення. Дякую!
Еммі

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
Працював, але потрібно було близько 5 годин, щоб бігати за столом з 700000 записів
Метт

98

Це просто працювало для мене, щоб знайти прогалини в таблиці з більш ніж 80k рядків:

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.


8
приємне рішення, для мене це краще, ніж бажана відповідь - дякую
Wee Zel

6
Це набагато ефективніше, ніж прийнята відповідь.
symcbean

1
набагато швидше, ніж прийнята відповідь. Єдине, що я б додав - це CONVERT( YourCol, UNSIGNED )дасть кращі результати, якщо YourCol ще не є цілим числом.
Barton Chittenden

1
@AlexandreCassagne: Якщо я правильно розумію ваше запитання, я б просто зробив окремий запит, як вбудований, для пошуку хвилини:SELECT MAX(YourCol) FROM YourTable;
ConfexianMJS

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, який існує, і скільки їх не вистачає між ... наприклад

 
ідентифікатор next_id відсутній_поміж
 1 4 2
68 70 1
75 87 11

1
Це спрацювало для мене чудово. Дякую.! Я зміг легко змінити це для своїх цілей.
Рахім Ходжа

Здається, це найкраща відповідь, коли шукати "наступний ідентифікатор" в проміжках. На жаль, це НАДАЧНО повільно для таблиць з 10K рядками. Я чекав більше 10 хвилин на столі ~ 46K, тоді як з @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

Добре, 1 - 100 був просто простим способом навести приклад. У цьому випадку ми дивимося на 20 000 - 85 000. Тож я можу створити темп-таблицю з 65 000 рядків, пронумерованих 20000 - 85000? І як мені це робити? Я використовую phpMyAdmin; якщо я встановити значення стовпця за замовчуванням на 25000 і зробить його автоматичним збільшенням, чи можу я просто вставити 65000 рядків, і він почне автоматичне збільшення з 25000?
EmmyS

У мене була подібна ситуація (у мене 100 предметів у порядку і потрібно знайти 100 відсутніх предметів). Для цього я створив ще одну таблицю 1-100, потім виконаю на ній це твердження і воно прекрасно працює. Це замінює дуже складну функцію для створення тимчасових таблиць. Просто порада для когось із подібних ситуацій: іноді швидше створити таблицю, ніж тимчасові таблиці.
новинні новини

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 не обробляється ефективно.

Це поверне один запис на centralValue (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

Отже, основна ідея полягає в тому, щоб ПРАВО і ВЛЕВО приєднується до тієї ж таблиці, бачачи, чи є у нас суміжні значення на значення (тобто: якщо центральне значення дорівнює "3", ми перевіряємо 3-1 = 2 зліва і 3 + 1 на праворуч), і коли 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' як НЕЗНАЧЕНО, тому воно не дозволить зменшити його під нуль. Це не є строго необхідним, якщо ви зберігаєте c.value> 0, як зазначено в наступній примітці, але я включаю його так само, як doc.
  2. Я фільтрую нульове центральне значення, оскільки нас не цікавить жодне попереднє значення, і ми можемо отримувати значення пошти з наступного рядка.

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);
  • Таблиця_назви - 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


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;

це знаходить першу прогалину, яку не задавали питання.
drewish

0

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

SELECT MIN (l.number_sequence + 1), що є непридатним для пацієнтів, так як LEFT OUTER ПРИЄДНАЙТЕ пацієнтів, як r на l.number_sequence + 1 = r.number_sequence WHERE r.number_sequence NULL. Кілька інших сценаріїв та рішень, обговорених там, з 2005 року!

Як знайти відсутні параметри в послідовності з SQL

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.