Pergunta

Minha experiência como DBA não vai muito além do simples armazenamento + recuperação de dados no estilo CMS - então essa pode ser uma pergunta boba, não sei!

Tenho um problema em que preciso pesquisar ou calcular preços de férias para um determinado tamanho de grupo e um determinado número de dias dentro de um determinado período de tempo.Por exemplo.:

Quanto custa um quarto de hotel para 2 pessoas por 4 noites em qualquer momento de janeiro?

Tenho dados de preços e disponibilidade de, digamos, 5.000 hotéis armazenados da seguinte forma:

Hotel ID | Date | Spaces | Price PP
-----------------------------------
     123 | Jan1 | 5      | 100
     123 | Jan2 | 7      | 100
     123 | Jan3 | 5      | 100
     123 | Jan4 | 3      | 100
     123 | Jan5 | 5      | 100
     123 | Jan6 | 7      | 110
     456 | Jan1 | 5      | 120
     456 | Jan2 | 1      | 120
     456 | Jan3 | 4      | 130
     456 | Jan4 | 3      | 110
     456 | Jan5 | 5      | 100
     456 | Jan6 | 7      |  90

Com esta tabela, posso fazer uma consulta assim:

SELECT hotel_id, sum(price_pp)
FROM hotel_data
WHERE
    date >= Jan1 and date <= Jan4
    and spaces >= 2
GROUP BY hotel_id
HAVING count(*) = 4;

resultados

hotel_id | sum
----------------
     123 | 400

O HAVING A cláusula aqui garante que haja uma entrada para cada dia entre as datas desejadas que tenha vagas disponíveis.ou seja.O hotel 456 tinha 1 vaga disponível em 2 de janeiro, a cláusula HAVING retornaria 3, portanto não obtemos resultado para o hotel 456.

Até agora tudo bem.

Porém, há como saber todos os períodos de 4 noites de janeiro em que há vagas disponíveis?Poderíamos repetir a consulta 27 vezes - incrementando as datas a cada vez, o que parece um pouco estranho.Ou outra maneira poderia ser armazenar todas as combinações possíveis em uma tabela de pesquisa como esta:

Hotel ID | total price pp | num_people | num_nights | start_date
----------------------------------------------------------------
     123 |            400 | 2          | 4          | Jan1
     123 |            400 | 2          | 4          | Jan2
     123 |            400 | 2          | 4          | Jan3
     123 |            400 | 3          | 4          | Jan1
     123 |            400 | 3          | 4          | Jan2
     123 |            400 | 3          | 4          | Jan3

E assim por diante.Teríamos que limitar o número máximo de noites e o número máximo de pessoas que procuraríamos - por ex.máximo de noites = 28, máximo de pessoas = 10 (limitado ao número de vagas disponíveis para o período definido a partir dessa data).

Para um hotel, isso poderia nos dar 28*10*365=102.000 resultados por ano.5.000 hotéis = 500 milhões de resultados!

Mas teríamos uma consulta muito simples para encontrar a estadia de 4 noites mais barata em janeiro para 2 pessoas:

SELECT
hotel_id, start_date, price
from hotel_lookup
where num_people=2
and num_nights=4
and start_date >= Jan1
and start_date <= Jan27
order by price
limit 1;

Existe uma maneira de realizar esta consulta na tabela inicial sem precisar gerar a tabela de pesquisa de 500m de linhas!?por exemplo.gerar os 27 resultados possíveis em uma tabela temporária ou alguma outra mágica de consulta interna?

No momento, todos os dados são mantidos em um banco de dados Postgres - se for necessário para esse fim, podemos mover os dados para outro lugar mais adequado?Não tenho certeza se esse tipo de consulta se ajusta aos padrões de mapeamento/redução para bancos de dados de estilo NoSQL...

Foi útil?

Solução

Você pode fazer muito com funções de janela.Apresentando duas soluções:um com e outro sem visão materializada.

Caso de teste

Com base nesta tabela:

CREATE TABLE hotel_data (
   hotel_id int
 , day      date  -- using "day", not "date"
 , spaces   int
 , price    int
 , PRIMARY KEY (hotel_id, day)  -- provides essential index automatically
);

Dias por hotel_id devemos ser exclusivo (aplicado por PK aqui), ou o resto é inválido.

Índice de múltiplas colunas para tabela base:

CREATE INDEX mv_hotel_mult_idx ON mv_hotel (day, hotel_id);

Observe a ordem invertida em comparação com o PK.Você provavelmente precisará de ambos os índices; para a consulta a seguir, o segundo índice é essencial.Explicação detalhada:

Consulta direta sem MATERIALIZED VIEW

SELECT hotel_id, day, sum_price
FROM  (
   SELECT hotel_id, day, price, spaces
        , sum(price)      OVER w * 2   AS sum_price
        , min(spaces)     OVER w       AS min_spaces
        , last_value(day) OVER w - day AS day_diff
        , count(*)        OVER w       AS day_ct
   FROM   hotel_data
   WHERE  day BETWEEN '2014-01-01'::date AND '2014-01-31'::date
   AND    spaces >= 2
   WINDOW w AS (PARTITION BY hotel_id ORDER BY day
                ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING) -- adapt to nights - 1
   ) sub
WHERE  day_ct = 4
AND    day_diff = 3  -- make sure there is not gap
AND    min_spaces >= 2
ORDER  BY sum_price, hotel_id, day;
-- LIMIT 1 to get only 1 winner;

Veja também Variante de @ypercube com lag(), que pode substituir day_ct e day_diff com um único cheque.

Explicar

  • Na subconsulta, considere apenas os dias dentro do seu período ("em janeiro" significa que o último dia está incluído no período).

  • O quadro para as funções da janela abrange a linha atual mais a próxima num_nights - 1 (4 - 1 = 3) linhas (dias).Calcule o diferença em dias , a contagem de linhas e o mínimo de espaços para garantir que o intervalo seja longo O suficiente, sem intervalos e sempre tem espaços suficientes.

    • Infelizmente, a cláusula frame das funções de janela não aceita valores dinâmicos, então ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING` não pode ser parametrizado para uma instrução preparada.
  • Elaborei cuidadosamente todas as funções de janela na subconsulta para reutilizar a mesma janela, usando um solteiro etapa de classificação.

  • O preço resultante sum_price já está multiplicado pelo número de espaços solicitados.

Com MATERIALIZED VIEW

Para evitar a inspeção de muitas linhas sem chance de sucesso, salve apenas as colunas necessárias mais três valores calculados redundantes da tabela base.Certifique-se de que o MV esteja atualizado.Se você não está familiarizado com o conceito, leia o manual primeiro.

CREATE MATERIALIZED VIEW mv_hotel AS
SELECT hotel_id, day
     , first_value(day) OVER (w ORDER BY day) AS range_start
     , price, spaces
     ,(count(*)    OVER w)::int2 AS range_len
     ,(max(spaces) OVER w)::int2 AS max_spaces

FROM  (
   SELECT *
        , day - row_number() OVER (PARTITION BY hotel_id ORDER BY day)::int AS grp
   FROM   hotel_data
   ) sub1
WINDOW w AS (PARTITION BY hotel_id, grp);
  • range_start armazena o primeiro dia de cada intervalo contínuo para duas finalidades:

    • para marcar um conjunto de linhas como membros de um intervalo comum
    • para mostrar o início do intervalo para possíveis outros fins.
  • range_len é o número de dias no intervalo sem intervalo.
    max_spaces é o máximo de espaços abertos no intervalo.

    • Ambas as colunas são usadas para excluir imediatamente linhas impossíveis da consulta.
  • Eu lancei ambos para smallint (máx.32768 deve ser suficiente para ambos) para otimizar o armazenamento:apenas 52 bytes por linha (incl.cabeçalho de tupla de heap e ponteiro de item).Detalhes:

Índice multicoluna para MV:

CREATE INDEX mv_hotel_mult_idx ON mv_hotel (range_len, max_spaces, day);

Consulta baseada em MV

SELECT hotel_id, day, sum_price
FROM  (
   SELECT hotel_id, day, price, spaces
        , sum(price)      OVER w * 2   AS sum_price
        , min(spaces)     OVER w       AS min_spaces
        , count(*)        OVER w       AS day_ct
   FROM   mv_hotel
   WHERE  day BETWEEN '2014-01-01'::date AND '2014-01-31'::date
   AND    range_len >= 4   -- exclude impossible rows
   AND    max_spaces >= 2  -- exclude impossible rows
   WINDOW w AS (PARTITION BY hotel_id, range_start ORDER BY day
                ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING) -- adapt to $nights - 1
   ) sub
WHERE  day_ct = 4
AND    min_spaces >= 2
ORDER  BY sum_price, hotel_id, day;
-- LIMIT 1 to get only 1 winner;

Isso é mais rápido que a consulta na tabela porque mais linhas podem ser eliminadas imediatamente.Novamente, o índice é essencial.Como as partições são sem intervalos aqui, verificando day_ct basta.

SQL violino demonstrando ambos.

Uso repetido

Se você usar muito, eu criaria uma função SQL e passaria apenas parâmetros.Ou um PL/pgSQL funcionar com SQL dinâmico e EXECUTE para permitir a adaptação da cláusula frame.

Alternativa

Tipos de intervalo com date_range armazenar intervalos contínuos em uma única linha pode ser uma alternativa - complicada no seu caso, com possíveis variações de preços ou espaços por dia.

Respostas relacionadas

Outras dicas

Outra forma, usando o LAG() função:

WITH x AS
  ( SELECT hotel_id, day, 
           LAG(day, 3) OVER (PARTITION BY hotel_id 
                             ORDER BY day)
              AS day_start,
           2 * SUM(price) OVER (PARTITION BY hotel_id 
                                ORDER BY day
                                ROWS BETWEEN 3 PRECEDING 
                                         AND CURRENT ROW)
              AS sum_price
    FROM hotel_data
    WHERE spaces >= 2
   -- AND day >= '2014-01-01'::date      -- date restrictions 
   -- AND day <  '2014-02-01'::date      -- can be added here
  )
SELECT hotel_id, day_start, sum_price
FROM x
WHERE day_start = day - 3 ;

Teste em: SQL-Fiddle

SELECT hotel, totprice
FROM   (
       SELECT r.hotel, SUM(r.pricepp)*@spacesd_needed AS totprice
       FROM   availability AS a
       JOIN   availability AS r 
              ON r.date BETWEEN a.date AND a.date + (@days_needed-1) 
              AND a.hotel = r.hotel
              AND r.spaces >= @spaces_needed
       WHERE  a.date BETWEEN '2014-01-01' AND '2014-01-31'
       GROUP BY a.date, a.hotel
       HAVING COUNT(*) >= @days_needed
       ) AS matches
ORDER BY totprice ASC
LIMIT 1;

deve fornecer o resultado que você está procurando sem a necessidade de estruturas extras, embora dependendo do tamanho dos dados de entrada, da estrutura do seu índice e do brilho do planejador de consultas, a consulta interna pode resultar em um spool no disco.Você pode achar que é suficientemente eficiente.Embargo:minha experiência é com o MS SQL Server e os recursos de seu planejador de consultas, então a sintaxe acima pode precisar de ajustes apenas nos nomes das funções (ypercube ajustou a sintaxe para que seja presumivelmente compatível com postgres agora, consulte o histórico de respostas para a variante TSQL).

O acima encontrará estadias que começam em janeiro, mas continuam até fevereiro.Adicionar uma cláusula extra ao teste de data (ou ajustar o valor da data final) resolverá facilmente isso, se não for desejável.

Independentemente do HotelID, você poderia usar uma tabela somatória, com uma coluna calculada, assim:

SummingTable Rev3

Não há chaves primárias ou estrangeiras nesta tabela, pois ela é usada apenas para calcular rapidamente múltiplas combinações de valores.Se você precisar ou quiser mais de um valor calculado, crie uma nova visualização com um novo nome de visualização para cada um dos valores de mês em combinação com cada um dos valores PP de Pessoas e Preço:

EXEMPLO DE PSEUDO CÓDIGO

CREATE VIEW NightPeriods2People3DaysPricePP400 AS (
SELECT (DaysInverse - DaysOfMonth) AS NumOfDays, (NumberOfPeople * PricePP * NumOfDays) AS SummedColumn 
FROM SummingTable
WHERE NumberOfPeople = 2) AND (DaysInverse = 4) AND (DaysOfMonth = 1) AND (PricePP = 400)
)

Coluna Soma = 2400

Por último, junte a visualização ao HotelID.Para fazer isso você precisará armazenar uma lista de todos os HotelIDs em SummingTable (eu fiz na tabela acima), mesmo que HotelID não seja usado para calcular na visualização.Igual a:

MAIS PSEUDO CÓDIGO

SELECT HotelID, NumOfDays, SummedColumn AS Total
FROM NightPeriods2People3DaysPricePP400
INNER JOIN Hotels
ON SummingTable.HotelID = Hotels.HotelID
Licenciado em: CC-BY-SA com atribuição
Não afiliado a dba.stackexchange
scroll top