Estou trabalhando em um sistema de lista de desejos, no qual os usuários podem adicionar itens às suas várias listas de desejos e pretendo permitir que os usuários reordenem os itens posteriormente. Não tenho muita certeza sobre a melhor maneira de armazenar isso em um banco de dados, mantendo-se rápido e sem virar uma bagunça (esse aplicativo será usado por uma base de usuários bastante grande, por isso não quero que ele desça para limpar as coisas).
Inicialmente, tentei uma position
coluna, mas parece que seria bastante ineficiente ter que alterar o valor da posição de todos os outros itens quando você os move.
Vi pessoas usando uma auto-referência para se referir ao valor anterior (ou próximo), mas, novamente, parece que você precisaria atualizar muitos outros itens da lista.
Outra solução que eu vi é usar números decimais e apenas colocar itens nas lacunas entre eles, o que parece ser a melhor solução até agora, mas tenho certeza de que deve haver uma maneira melhor.
Eu diria que uma lista típica conteria cerca de 20 itens, e provavelmente a limitará a 50. A reordenação usaria o recurso de arrastar e soltar e provavelmente será feita em lotes para evitar condições de corrida e outras coisas. solicitações ajax. Estou usando o postgres (no heroku), se isso importa.
Alguém tem alguma idéia?
Felicidades por qualquer ajuda!
fonte
Respostas:
Primeiro, não tente fazer nada inteligente com números decimais, porque eles vão te incomodar.
REAL
eDOUBLE PRECISION
são inexatos e podem não representar adequadamente o que você coloca neles.NUMERIC
é exato, mas a sequência correta de movimentos o esgotará com precisão e sua implementação será interrompida.Limitar movimentos a altos e baixos únicos torna toda a operação muito fácil. Para uma lista de itens numerados seqüencialmente, você pode mover um item para cima, decrementando sua posição e aumentando o número da posição de qualquer que seja o decremento anterior. (Em outras palavras, o item
5
se tornaria4
e o que era item4
se tornaria5
, efetivamente uma troca como os idiotas descreveram em sua resposta.) Movê-lo para baixo seria o oposto. Indexe sua tabela de acordo com o que identifica exclusivamente uma lista e posição e você pode fazê-lo com doisUPDATE
segundos em uma transação que será executada muito rapidamente. A menos que seus usuários reorganizem suas listas em velocidades sobre-humanas, isso não causará muita carga.Movimentos de arrastar e soltar (por exemplo, mover item
6
para ficar entre itens9
e10
) são um pouco mais complicados e precisam ser feitos de maneira diferente, dependendo se a nova posição está acima ou abaixo da antiga. No exemplo acima, é necessário abrir um furo incrementando todas as posições maiores que9
, atualizando6
a posição do item para ser o novo10
e diminuindo a posição de tudo maior do6
que preencher o local vazio. Com a mesma indexação descrita anteriormente, isso será rápido. Você pode realmente fazer isso ir um pouco mais rápido do que eu descrevi, minimizando o número de linhas que a transação toca, mas essa é uma micro otimização que você não precisa até que possa provar que há um gargalo.De qualquer forma, tentar superar o banco de dados com uma solução caseira e inteligente demais pela metade geralmente não leva ao sucesso. Bancos de dados que valem a pena foram cuidadosamente escritos para executar essas operações muito, muito rapidamente por pessoas que são muito, muito boas nisso.
fonte
Mesma resposta a partir daqui https://stackoverflow.com/a/49956113/10608
Solução: faça
index
uma string (porque as strings, em essência, têm infinita "precisão arbitrária"). Ou, se você usar um int, aumenteindex
em 100 em vez de 1.O problema de desempenho é este: não há valores "entre" entre dois itens classificados.
Em vez disso, faça o seguinte (melhor solução abaixo):
Melhor ainda: eis como Jira resolve esse problema. Sua "classificação" (o que você chama de índice) é um valor de string que permite uma tonelada de espaço para respirar entre os itens classificados.
Aqui está um exemplo real de um banco de dados jira com o qual trabalho
Observe este exemplo
hzztzz:i
. A vantagem de um ranking de cordas é que você fica sem espaço entre dois itens e ainda não precisa classificar mais nada. Você apenas começa a acrescentar mais caracteres à string para restringir o foco.fonte
Por quê? Digamos que você adote uma abordagem de tabela de lista vinculada com colunas (listID, itemID, nextItemID).
Inserir um novo item em uma lista custa uma inserção e uma linha modificada.
Reposicionar um item custa três modificações de linha (o item que está sendo movido, o item antes dele e o item antes de seu novo local).
A remoção de um item custa uma exclusão e uma linha modificada.
Esses custos permanecem os mesmos, independentemente de a lista ter 10 itens ou 10.000 itens. Nos três casos, há uma modificação a menos se a linha de destino for o primeiro item da lista. Se você estiver operando com mais frequência no último item da lista, pode ser benéfico armazenar o prevItemID em vez do próximo.
fonte
Você mediu isso? Ou isso é apenas um palpite? Não faça tais suposições sem nenhuma prova.
Honestamente, isso não é "um monte de itens", para mim isso soa muito pouco.
Sugiro que você se atenha à abordagem "coluna da posição" (se essa for a implementação mais simples para você). Para esses tamanhos de lista pequenos, não inicie a otimização desnecessária antes de ter problemas reais de desempenho
fonte
Esta é realmente uma questão de escala e caso de uso.
Quantos itens você espera em uma lista? Se milhões, acho que a rota decimal é a mais óbvia.
Se 6, a renumeração de números inteiros é a escolha óbvia. s Além disso, a questão é como as listas são reorganizadas. Se você estiver usando as setas para cima e para baixo (movendo para cima ou para baixo um slot de cada vez), o i usaria números inteiros e depois trocaria com o anterior (ou o próximo) em movimento.
Também com que frequência você confirma, se o usuário pode fazer 250 alterações e confirmar de uma só vez, do que eu digo números inteiros com renumeração novamente ...
tl; dr: Precisa de mais informações.
Edit: "Wish lists" parece um monte de listas pequenas (suposição, isso pode ser falso) .. Então eu digo Inteiro com renumeração. (Cada lista contém sua própria posição)
fonte
Se o objetivo é minimizar o número de operações do banco de dados por operação de reordenação:
Assumindo que
Armazene a lista de desejos classificada do usuário como uma sequência compactada de números inteiros (matrizes inteiras) em uma coluna. Sempre que a lista de desejos é reordenada, a matriz inteira (linha única; coluna única) é atualizada - o que deve ser executado com uma única atualização SQL.
https://www.postgresql.org/docs/current/static/arrays.html
Se o objetivo for diferente, continue com a abordagem "coluna da posição".
Em relação à "velocidade", certifique-se de comparar a abordagem do procedimento armazenado. Embora a emissão de mais de 20 atualizações separadas para um shuffle de uma lista de desejos possa ser lenta, pode haver uma maneira rápida de usar o procedimento armazenado.
fonte
OK Enfrento esse problema complicado recentemente, e todas as respostas neste post de perguntas e respostas deram muitas inspirações. Do meu ponto de vista, cada solução tem seus prós e contras.
Se o
position
campo tiver que ser seqüencial sem intervalos, será necessário reordenar a lista inteira. Esta é uma operação O (N). A vantagem é que o lado do cliente não precisaria de nenhuma lógica especial para obter o pedido.Se queremos evitar a operação O (N), MAS AINDA manter uma sequência precisa, uma das abordagens é usar "auto-referência para se referir ao valor anterior (ou próximo)". Este é um cenário de lista vinculada a livros didáticos. Por design, NÃO incorrerá em "muitos outros itens da lista". No entanto, isso requer que o lado do cliente (um serviço da Web ou talvez um aplicativo móvel) implemente a lógica travesal da lista vinculada para derivar o pedido.
Algumas variações não usam referência, ou seja, lista vinculada. Eles escolhem representar o pedido inteiro como um blob independente, como um JSON-array-in-a-string
[5,2,1,3,...]
; essa ordem será armazenada em um local separado. Essa abordagem também tem o efeito colateral de exigir que o código do lado do cliente mantenha esse blob de pedido separado.Em muitos casos, não precisamos realmente armazenar a ordem exata, apenas precisamos manter uma classificação relativa entre cada registro. Portanto, podemos permitir lacunas entre registros seqüenciais. As variações incluem: (1) usando números inteiros com intervalos como 100, 200, 300 ... mas você ficará rapidamente sem intervalos e precisará do processo de recuperação; (2) usando decimal que vem com lacunas naturais, mas você precisará decidir se pode viver com a eventual limitação de precisão; (3) usando classificação baseada em string, conforme descrito nesta resposta, mas tenha cuidado com as armadilhas de implementação complicadas .
A resposta real pode ser "depende". Revise seus requisitos de negócios. Por exemplo, se for um sistema de lista de desejos, pessoalmente eu usaria felizmente um sistema organizado por apenas algumas fileiras como "indispensável", "bom de ter", "talvez mais tarde" e, em seguida, apresentaria itens sem particular ordem dentro de cada fila. Se for um sistema de entrega, você pode muito bem usar o tempo de entrega como uma classificação aproximada que vem com uma lacuna natural (e prevenção natural de conflitos, pois nenhuma entrega aconteceria ao mesmo tempo). Sua milhagem pode variar.
fonte
Use um número de ponto flutuante para a coluna da posição.
Você pode reordenar a lista alterando apenas a coluna de posição na linha "movida".
Basicamente, se seu usuário deseja posicionar "vermelho" depois de "azul", mas antes de "amarelo"
Então você só precisa calcular
Após alguns milhões de reposicionamentos, você pode obter números de ponto flutuante tão pequenos que não há "entre" - mas é quase tão provável quanto avistar um unicórnio.
Você pode implementar isso usando um campo inteiro com um intervalo inicial de, digamos, 1000. Portanto, seu oredring inicial seria 1000-> azul, 2000-> amarelo, 3000-> vermelho. Depois de "mover" o vermelho após o azul, você teria 1000-> azul, 1500-> vermelho, 2000-> amarelo.
O problema é que, com um intervalo inicial aparentemente grande de 1000, apenas 10 movimentos o colocam em uma situação como 1000-> azul, 1001-puce, 1004-> biege ......, onde você não poderá mais para inserir qualquer coisa depois de "azul" sem renumerar a lista inteira. Usando números de ponto flutuante, sempre haverá um ponto "intermediário" entre as duas posições.
fonte
"pos": 1310719, + "pos": 638975.5
. Para ser justo, a maioria das pessoas não faz listas do Trello com 4 milhões de entradas, mas o tamanho e o caso de uso da lista do Trello são bastante comuns para conteúdo classificado pelo usuário. E qualquer coisa que possa ser classificada pelo usuário não tem aproximadamente nada a ver com alto desempenho, a velocidade de classificação int vs float é discutível, especialmente considerando que os bancos de dados são principalmente limitados pelo desempenho de IO.Embora o OP tenha abordado brevemente a noção de uso de uma lista vinculada para armazenar a ordem de classificação, ele possui muitas vantagens nos casos em que os itens serão reordenados com frequência.
A questão é - você não ! Ao usar uma lista vinculada, inserção, exclusão e reordenação são
O(1)
operações, e a integridade referencial imposta pelo banco de dados garante que não haja referências quebradas, registros órfãos ou loops.Aqui está um exemplo:
Observe o seguinte:
FK_Sorting
para impedir que os itens se refiram acidentalmente ao item pai errado.UNIQUE INDEX UX_Sorting
executa duas funções:NULL
valor único, cada lista pode ter apenas 1 item "principal".SortAfter
valores duplicados ).As principais vantagens dessa abordagem:
int
oureal
que acabam ficando sem espaço entre os itens após reordenamentos frequentes.Essa abordagem tem desvantagens, no entanto:
ORDER BY
.VIEW
ou TVF que use um CTE para adicionar um derivado contendo uma ordem de classificação incremental - mas isso seria caro para uso em grandes operações.SortAfter
coluna se referirá aos itens que não foram carregados no seu programa.SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad
).UX_Sorting
estiver ativada requer suporte do DBMS para restrições adiadas.NULL
valores na coluna - o que infelizmente significa que uma Lista pode ter vários itens HEAD.State
que é um sinalizador simples para declarar se um item da lista está "ativo" ou não - e o índice exclusivo ignora itens inativos.Solução alternativa 1: precisa da capacidade de executar uma tarefa trivial
ORDER BY
.Aqui está uma VIEW usando uma CTE recursiva que adiciona uma
SortOrder
coluna:Você pode usar esse VIEW em outras consultas nas quais você precisa classificar valores usando
ORDER BY
:Solução alternativa 2: Evitando
UNIQUE INDEX
restrições de violação ao executar operações:Adicione uma
State
coluna àWishlistItems
tabela. A coluna está marcada comoHIDDEN
a maioria das ferramentas ORM (como o Entity Framework) não a inclui ao gerar modelos, por exemplo.Operações:
Adicionando um novo item ao final da lista:
ItemId
último item atual na lista e armazene@tailItemId
- ou useSELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listId
.INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId )
.Reordenando o item 4 para ficar abaixo do item 7
Removendo o item 4 do meio da lista:
Se um item estiver no final da lista (ou seja, onde
NOT EXISTS ( SELECT 1 FROM WishlistItems WHERE SortAfter = @itemId )
), você poderá fazer um únicoDELETE
.Se um item tiver um item classificado após ele, execute as mesmas etapas para reordenar um item, exceto você
DELETE
depois em vez de definirState = 1;
.fonte