AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / dba / Perguntas / 100965
Accepted
Villiers Strauss
Villiers Strauss
Asked: 2015-05-09 05:59:08 +0800 CST2015-05-09 05:59:08 +0800 CST 2015-05-09 05:59:08 +0800 CST

Combinando intervalos separados nos maiores intervalos contíguos possíveis

  • 772

Estou tentando combinar vários intervalos de datas (minha carga é de cerca de 500 no máximo, na maioria dos casos 10) que podem ou não se sobrepor nos maiores intervalos de datas contíguos possíveis. Por exemplo:

Dados:

CREATE TABLE test (
  id SERIAL PRIMARY KEY NOT NULL,
  range DATERANGE
);

INSERT INTO test (range) VALUES 
  (DATERANGE('2015-01-01', '2015-01-05')),
  (DATERANGE('2015-01-01', '2015-01-03')),
  (DATERANGE('2015-01-03', '2015-01-06')),
  (DATERANGE('2015-01-07', '2015-01-09')),
  (DATERANGE('2015-01-08', '2015-01-09')),
  (DATERANGE('2015-01-12', NULL)),
  (DATERANGE('2015-01-10', '2015-01-12')),
  (DATERANGE('2015-01-10', '2015-01-12'));

A tabela se parece com:

 id |          range
----+-------------------------
  1 | [2015-01-01,2015-01-05)
  2 | [2015-01-01,2015-01-03)
  3 | [2015-01-03,2015-01-06)
  4 | [2015-01-07,2015-01-09)
  5 | [2015-01-08,2015-01-09)
  6 | [2015-01-12,)
  7 | [2015-01-10,2015-01-12)
  8 | [2015-01-10,2015-01-12)
(8 rows)

Resultados desejados:

         combined
--------------------------
 [2015-01-01, 2015-01-06)
 [2015-01-07, 2015-01-09)
 [2015-01-10, )

Representação visual:

1 | =====
2 | ===
3 |    ===
4 |        ==
5 |         =
6 |             =============>
7 |           ==
8 |           ==
--+---------------------------
  | ====== == ===============>
postgresql aggregate
  • 4 4 respostas
  • 13494 Views

4 respostas

  • Voted
  1. Best Answer
    Erwin Brandstetter
    2015-05-09T11:51:33+08:002015-05-09T11:51:33+08:00

    Suposições / Esclarecimentos

    1. Não há necessidade de diferenciar infinitye abrir o limite superior ( upper(range) IS NULL). (Você pode ter de qualquer maneira, mas é mais simples assim.)
    • NULL vs. infinitynos tipos de intervalo do PostgreSQL
    1. Como dateé um tipo discreto, todos os intervalos têm [)limites padrão. O manual:

    Os tipos de intervalo internos int4range, int8rangee daterangetodos usam uma forma canônica que inclui o limite inferior e exclui o limite superior; isto é, [).

    Para outros tipos (como tsrange!) Eu aplicaria o mesmo, se possível:

    • Evitando entradas adjacentes/sobrepostas com EXCLUDE no PostgreSQL

    Solução com SQL puro

    Com CTEs para maior clareza:

    WITH a AS (
       SELECT range
            , COALESCE(lower(range),'-infinity') AS startdate
            , max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
       FROM   test
       )
    , b AS (
       SELECT *, lag(enddate) OVER (ORDER BY range) < startdate OR NULL AS step
       FROM   a
       )
    , c AS (
       SELECT *, count(step) OVER (ORDER BY range) AS grp
       FROM   b
       )
    SELECT daterange(min(startdate), max(enddate)) AS range
    FROM   c
    GROUP  BY grp
    ORDER  BY 1;
    

    Ou , o mesmo com subconsultas, mais rápido, mas menos fácil de ler:

    SELECT daterange(min(startdate), max(enddate)) AS range
    FROM  (
       SELECT *, count(step) OVER (ORDER BY range) AS grp
       FROM  (
          SELECT *, lag(enddate) OVER (ORDER BY range) < startdate OR NULL AS step
          FROM  (
             SELECT range
                  , COALESCE(lower(range),'-infinity') AS startdate
                  , max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
             FROM   test
             ) a
          ) b
       ) c
    GROUP  BY grp
    ORDER  BY 1;
    

    Como?

    a: Ao ordenar por range, calcule o máximo em execução do limite superior ( enddate) com uma função de janela.
    Substitua os limites NULL (ilimitados) por +/- infinityapenas para simplificar (sem casos NULL especiais).

    b: Na mesma ordem de classificação, se o anterior enddatefor anterior startdate, temos uma lacuna e iniciamos um novo intervalo ( step).
    Lembre-se, o limite superior é sempre excluído.

    c: Forme grupos ( grp) contando passos com outra função de janela.

    Na construção externa SELECTvaria do limite inferior ao superior em cada grupo. Voilá.

    Ou com um nível de subconsulta a menos, mas invertendo a ordem de classificação:

    SELECT daterange(min(COALESCE(lower(range), '-infinity')), max(enddate)) AS range
    FROM  (
       SELECT *, count(nextstart > enddate OR NULL) OVER (ORDER BY range DESC NULLS LAST) AS grp
       FROM  (
          SELECT range
               , max(COALESCE(upper(range), 'infinity')) OVER (ORDER BY range) AS enddate
               , lead(lower(range)) OVER (ORDER BY range) As nextstart
          FROM   test
          ) a
       ) b
    GROUP  BY grp
    ORDER  BY 1;
    

    Classifique a janela na segunda etapa com ORDER BY range DESC NULLS LAST(with NULLS LAST) para obter uma ordem de classificação perfeitamente invertida. Isso deve ser mais barato (mais fácil de produzir, corresponde perfeitamente à ordem de classificação do índice sugerido) e preciso para casos de canto com rank IS NULL. Ver:

    • O PostgreSQL classifica por data e hora asc, nulo primeiro?

    Resposta relacionada com mais explicações:

    • Comparar vários intervalos de datas

    Solução processual com plpgsql

    Funciona para qualquer nome de tabela/coluna, mas apenas para o tipo daterange.
    Soluções procedurais com loops são normalmente mais lentas, mas neste caso especial, espero que a função seja substancialmente mais rápida , pois precisa apenas de uma única varredura sequencial :

    CREATE OR REPLACE FUNCTION f_range_agg(_tbl text, _col text)
      RETURNS SETOF daterange AS
    $func$
    DECLARE
       _lower     date;
       _upper     date;
       _enddate   date;
       _startdate date;
    BEGIN
       FOR _lower, _upper IN EXECUTE
          format(
             $sql$
             SELECT COALESCE(lower(t.%2$I),'-infinity')  -- replace NULL with ...
                  , COALESCE(upper(t.%2$I), 'infinity')  -- ... +/- infinity
             FROM   %1$I t
             ORDER  BY t.%2$I
             $sql$, _tbl, _col)
       LOOP
          IF _lower > _enddate THEN     -- return previous range
             RETURN NEXT daterange(_startdate, _enddate);
             SELECT _lower, _upper  INTO _startdate, _enddate;
       
          ELSIF _upper > _enddate THEN  -- expand range
             _enddate := _upper;
       
          -- do nothing if _upper <= _enddate (range already included) ...
       
          ELSIF _enddate IS NULL THEN   -- init 1st round
             SELECT _lower, _upper  INTO _startdate, _enddate;
          END IF;
       END LOOP;
       
       IF FOUND THEN                    -- return last row
          RETURN NEXT daterange(_startdate, _enddate);
       END IF;
    END
    $func$  LANGUAGE plpgsql;
    

    Ligar:

    SELECT * FROM f_range_agg('test', 'range');  -- table and column name
    

    A lógica é semelhante às soluções SQL, mas podemos nos contentar com uma única passagem.

    SQL Fiddle.

    Relacionado:

    • GROUP BY e valores numéricos sequenciais agregados

    O exercício usual para lidar com a entrada do usuário em SQL dinâmico:

    • Injeção de SQL em funções Postgres vs consultas preparadas

    Índice

    Para cada uma dessas soluções, um índice btree simples (padrão) rangeseria fundamental para o desempenho em tabelas grandes:

    CREATE INDEX foo on test (range);
    

    Um índice btree é de uso limitado para tipos de intervalo , mas podemos obter dados pré-classificados e talvez até mesmo uma varredura somente de índice.

    • 32
  2. dezso
    2015-05-09T06:57:41+08:002015-05-09T06:57:41+08:00

    Eu vim com isso:

    DO $$                                                                             
    DECLARE 
        i date;
        a daterange := 'empty';
        day_as_range daterange;
        extreme_value date := '2100-12-31';
    BEGIN
        FOR i IN 
            SELECT DISTINCT 
                 generate_series(
                     lower(range), 
                     COALESCE(upper(range) - interval '1 day', extreme_value), 
                     interval '1 day'
                 )::date
            FROM rangetest 
            ORDER BY 1
        LOOP
            day_as_range := daterange(i, i, '[]');
            BEGIN
                IF isempty(a)
                THEN a := day_as_range;
                ELSE a = a + day_as_range;
                END IF;
            EXCEPTION WHEN data_exception THEN
                RAISE INFO '%', a;
                a = day_as_range;
            END;
        END LOOP;
    
        IF upper(a) = extreme_value + interval '1 day'
        THEN a := daterange(lower(a), NULL);
        END IF;
    
        RAISE INFO '%', a;
    END;
    $$;
    

    Ainda falta um pouco de afiação, mas a ideia é a seguinte:

    1. explodir os intervalos para datas individuais
    2. fazendo isso, substitua o limite superior infinito por algum valor extremo
    3. com base na ordem de (1), comece a construir os intervalos
    4. quando a união ( +) falhar, retorne o intervalo já construído e reinicie
    5. finalmente, retorne o restante - se o valor extremo predefinido for atingido, substitua-o por NULL para obter um limite superior infinito
    • 7
  3. dnoeth
    2015-05-22T23:59:05+08:002015-05-22T23:59:05+08:00

    Alguns anos atrás, testei diferentes soluções (entre outras, algumas semelhantes às de @ErwinBrandstetter) para mesclar períodos sobrepostos em um sistema Teradata e achei a seguinte a mais eficiente (usando funções analíticas, a versão mais recente do Teradata possui funções integradas para essa tarefa).

    1. classificar as linhas por data de início
    2. encontre a data final máxima de todas as linhas anteriores:maxEnddate
    3. se esta data for menor que a data de início atual, você encontrou uma lacuna. Mantenha apenas essas linhas mais a primeira linha dentro da PARTITION (que é indicada por um NULL) e filtre todas as outras linhas. Agora você obtém a data de início de cada intervalo e a data final do intervalo anterior.
    4. Então você simplesmente maxEnddateusa a próxima linha LEADe está quase pronto. Somente para a última linha LEADretorna um NULL, para resolver isso, calcule a data final máxima de todas as linhas de uma partição na etapa 2 e COALESCEela.

    Por que foi mais rápido? Dependendo dos dados reais, a etapa nº 2 pode reduzir bastante o número de linhas, portanto, a próxima etapa precisa operar apenas em um pequeno subconjunto; além disso, remove a agregação.

    violino

    SELECT
       daterange(startdate
                ,COALESCE(LEAD(maxPrevEnddate) -- next row's end date
                          OVER (ORDER BY startdate) 
                         ,maxEnddate)          -- or maximum end date
                ) AS range
    
    FROM
     (
       SELECT
          range
         ,COALESCE(LOWER(range),'-infinity') AS startdate
    
       -- find the maximum end date of all previous rows
       -- i.e. the END of the previous range
         ,MAX(COALESCE(UPPER(range), 'infinity'))
          OVER (ORDER BY range
                ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS maxPrevEnddate
    
       -- maximum end date of this partition
       -- only needed for the last range
         ,MAX(COALESCE(UPPER(range), 'infinity'))
          OVER () AS maxEnddate
       FROM test
     ) AS dt
    WHERE maxPrevEnddate < startdate -- keep the rows where a range start
       OR maxPrevEnddate IS NULL     -- and keep the first row
    ORDER BY 1;  
    

    Como isso foi mais rápido no Teradata, não sei se é o mesmo para o PostgreSQL, seria bom obter alguns números reais de desempenho.

    • 5
  4. Evan Carroll
    2017-12-28T15:16:30+08:002017-12-28T15:16:30+08:00

    Para me divertir, dei uma chance. Achei esse o método mais rápido e limpo para fazer isso. Primeiro definimos uma função que mescla se houver uma sobreposição ou se as duas entradas forem adjacentes, se não houver sobreposição ou adjacência, simplesmente retornamos o primeiro intervalo de datas. Hint +é uma união de intervalo no contexto de intervalos.

    CREATE FUNCTION merge_if_adjacent_or_overlaps (d1 daterange, d2 daterange)
    RETURNS daterange AS $$
      SELECT
        CASE WHEN d1 && d2 OR d1 -|- d2
        THEN d1 + d2
        ELSE d1
        END;
    $$ LANGUAGE sql
    IMMUTABLE;
    

    Então usamos assim,

    SELECT DISTINCT ON (lower(cumrange)) cumrange
    FROM (
      SELECT merge_if_adjacent_or_overlaps(
        t1.range,
        lag(t1.range) OVER (ORDER BY t1.range)
      ) AS cumrange
      FROM test AS t1
    ) AS t
    ORDER BY lower(cumrange)::date, upper(cumrange)::date DESC NULLS first;
    
    • -1

relate perguntas

  • Posso ativar o PITR depois que o banco de dados foi usado

  • Práticas recomendadas para executar a replicação atrasada do deslocamento de tempo

  • Os procedimentos armazenados impedem a injeção de SQL?

  • Sequências Biológicas do UniProt no PostgreSQL

  • Qual é a diferença entre a replicação do PostgreSQL 9.0 e o Slony-I?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host

    • 12 respostas
  • Marko Smith

    Como fazer a saída do sqlplus aparecer em uma linha?

    • 3 respostas
  • Marko Smith

    Selecione qual tem data máxima ou data mais recente

    • 3 respostas
  • Marko Smith

    Como faço para listar todos os esquemas no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    Como usar o sqlplus para se conectar a um banco de dados Oracle localizado em outro host sem modificar meu próprio tnsnames.ora

    • 4 respostas
  • Marko Smith

    Como você mysqldump tabela (s) específica (s)?

    • 4 respostas
  • Marko Smith

    Listar os privilégios do banco de dados usando o psql

    • 10 respostas
  • Marko Smith

    Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Como faço para listar todos os bancos de dados e tabelas usando o psql?

    • 7 respostas
  • Martin Hope
    Jin conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host 2014-12-02 02:54:58 +0800 CST
  • Martin Hope
    Stéphane Como faço para listar todos os esquemas no PostgreSQL? 2013-04-16 11:19:16 +0800 CST
  • Martin Hope
    Mike Walsh Por que o log de transações continua crescendo ou fica sem espaço? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland Listar todas as colunas de uma tabela especificada 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney O MySQL pode realizar consultas razoavelmente em bilhões de linhas? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx Como posso monitorar o andamento de uma importação de um arquivo .sql grande? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison Como você mysqldump tabela (s) específica (s)? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    Jonas Como posso cronometrar consultas SQL usando psql? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas Como faço para listar todos os bancos de dados e tabelas usando o psql? 2011-02-18 00:45:49 +0800 CST

Hot tag

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve