PostgreSQL: как выяснить пропущенные числа в столбце с помощью generate_series()?
SELECT commandid
FROM results
WHERE NOT EXISTS (
SELECT *
FROM generate_series(0,119999)
WHERE generate_series = results.commandid
);
У меня есть колонка в results
типа int
но различные тесты не прошли и не были добавлены в таблицу. Я хотел бы создать запрос, который возвращает список commandid
которые не найдены в results
, Я думал, что вышеупомянутый запрос будет делать то, что я хотел. Тем не менее, это даже не работает, если я использую диапазон, который находится за пределами ожидаемого возможного диапазона commandid
(как отрицательные числа).
4 ответа
Приведенные примеры данных:
create table results ( commandid integer primary key);
insert into results (commandid) select * from generate_series(1,1000);
delete from results where random() < 0.20;
Это работает:
SELECT s.i AS missing_cmd
FROM generate_series(0,1000) s(i)
WHERE NOT EXISTS (SELECT 1 FROM results WHERE commandid = s.i);
как и эта альтернативная формулировка:
SELECT s.i AS missing_cmd
FROM generate_series(0,1000) s(i)
LEFT OUTER JOIN results ON (results.commandid = s.i)
WHERE results.commandid IS NULL;
Оба приведенных выше результата, по-видимому, приводят к идентичным планам запросов в моих тестах, но вы должны сравнить их со своими данными в базе данных, EXPLAIN ANALYZE
чтобы увидеть, что лучше.
объяснение
Обратите внимание, что вместо NOT IN
Я использовал NOT EXISTS
с подзапросом в одной формулировке и обычным OUTER JOIN
в другом. Серверу БД гораздо проще их оптимизировать, и это позволяет избежать запутанных проблем, которые могут возникнуть при NULL
в NOT IN
,
Я изначально отдавал предпочтение OUTER JOIN
формулировка, но по крайней мере в 9.1 с моими данными испытаний NOT EXISTS
Форма оптимизируется по тому же плану.
Оба будут работать лучше, чем NOT IN
Формулировка ниже, когда серия большая, как в вашем случае. NOT IN
Раньше Pg требовал линейного поиска IN
список для каждого тестируемого кортежа, но изучение плана запроса показывает, что Pg может быть достаточно умен, чтобы хэшировать его сейчас. NOT EXISTS
(превращается в JOIN
планировщиком запросов) и JOIN
работать лучше
NOT IN
Формулировка запутанная в присутствии NULL commandid
и может быть неэффективным:
SELECT s.i AS missing_cmd
FROM generate_series(0,1000) s(i)
WHERE s.i NOT IN (SELECT commandid FROM results);
так что я бы избежал этого. С 1 000 000 строк два других закончились за 1,2 секунды и NOT IN
Формулировка работала с привязкой к процессору, пока мне не стало скучно и я ее отменил.
Как я уже упоминал в комментарии, вы должны сделать обратный запрос выше.
SELECT
generate_series
FROM
generate_series(0, 119999)
WHERE
NOT generate_series IN (SELECT commandid FROM results);
На этом этапе вы должны найти значения, которые не существуют в пределах commandid
столбец в выбранном диапазоне.
Я не очень опытный гуру SQL, но мне нравятся другие способы решения проблемы. Просто сегодня у меня была похожая проблема - найти неиспользуемые числа в одном символьном столбце. Я решил свою проблему с помощью pl/pgsql и был очень заинтересован в том, какова будет скорость моей процедуры. Я использовал метод @Craig Ringer, чтобы сгенерировать таблицу с последовательным столбцом, добавить миллион записей, а затем удалить каждую 99-ю запись. Эта процедура работает около 3 секунд при поиске пропущенных номеров:
-- creating table
create table results (commandid character(7) primary key);
-- populating table with serial numbers formatted as characters
insert into results (commandid) select cast(num_id as character(7)) from generate_series(1,1000000) as num_id;
-- delete some records
delete from results where cast(commandid as integer) % 99 = 0;
create or replace function unused_numbers()
returns setof integer as
$body$
declare
i integer;
r record;
begin
-- looping trough table with sychronized counter:
i := 1;
for r in
(select distinct cast(commandid as integer) as num_value
from results
order by num_value asc)
loop
if not (i = r.num_value) then
while true loop
return next i;
i = i + 1;
if (i = r.num_value) then
i = i + 1;
exit;
else
continue;
end if;
end loop;
else
i := i + 1;
end if;
end loop;
return;
end;
$body$
language plpgsql volatile
cost 100
rows 1000;
select * from unused_numbers();
Может быть, кому-то это пригодится.
Если вы используете красное смещение AWS, вам, возможно, придется игнорировать вопрос, поскольку он не поддерживает generate_series
, Вы получите что-то вроде этого:
select
startpoints.id gapstart,
min(endpoints.id) resume
from (
select id+1 id
from yourtable outer_series
where not exists
(select null
from yourtable inner_series
where inner_series.id = outer_series.id + 1
)
order by id
) startpoints,
yourtable endpoints
where
endpoints.id > startpoints.id
group by
startpoints.id;