재정렬 가능한 목록을 데이터베이스에 저장


54

사용자가 다양한 위시리스트에 항목을 추가 할 수있는 위시리스트 시스템에서 작업 중이며 나중에 사용자가 항목을 다시 주문할 수 있도록하려고합니다. 나는 이것을 유지하면서 엉망으로 변하지 않고 데이터베이스에 저장하는 가장 좋은 방법을 확신하지 못합니다 (이 응용 프로그램은 상당히 큰 사용자 기반에서 사용되므로 아래로 내려 가고 싶지 않습니다) 물건을 정리하기 위해).

처음에는 position열을 시도했지만 다른 항목을 이동할 때 다른 모든 항목의 위치 값을 변경 해야하는 것은 비효율적입니다.

이전 (또는 다음) 값을 참조하기 위해 자체 참조를 사용하는 사람들을 보았지만 다시 목록에서 다른 많은 항목을 업데이트 해야하는 것처럼 보입니다.

내가 본 또 다른 해결책은 십진수를 사용하고 그 사이의 틈에 항목을 붙이는 것입니다. 지금까지 가장 좋은 해결책처럼 보이지만 더 나은 방법이 있어야합니다.

일반적인 목록에는 최대 약 20 개 정도의 항목이 포함되어 있다고 말하고 아마도 50 개로 제한 할 것입니다. 재정렬은 끌어서 놓기를 사용하며 경쟁 조건 등을 방지하기 위해 일괄 적으로 수행 될 것입니다. 아약스 요청. 중요한 경우 postgres (heroku)를 사용하고 있습니다.

누구든지 아이디어가 있습니까?

도움을 청합니다!


약간의 벤치마킹을 수행하고 IO 또는 데이터베이스에 병목 현상이 발생하는지 여부를 알려주시겠습니까?
rwong

stackoverflow 관련 질문 .
Jordão

자체 참조를 사용하면 목록의 한 위치에서 다른 위치로 항목을 이동할 때 2 개의 항목 만 업데이트하면됩니다. en.wikipedia.org/wiki/Linked_list
Pieter B

흠, 왜 링크 된 목록이 대답에 거의 관심을 기울이지 않는지 모르겠습니다.
Christiaan Westerbeek

답변:


32

첫째, 십진수로 영리한 행동을 시도하지 마십시오. REAL그리고 DOUBLE PRECISION정확하고 적절하게 당신이 그들에 넣어 무엇을 표현하지 않을 수 있습니다. NUMERIC정확하지만 올바른 이동 순서로 인해 정밀도가 떨어지고 구현이 나빠질 수 있습니다.

싱글 업 및 다운으로 이동을 제한하면 전체 작업이 매우 쉬워집니다. 순차적으로 번호가 매겨진 항목 목록의 경우 항목을 감소시키고 이전 감소가 발생한 항목의 위치 번호를 증가시켜 항목을 위로 이동할 수 있습니다. (즉, 항목 5이되고 4항목 4이 된 항목 은 5Morons의 답변에 설명 된대로 효과적으로 교환됩니다.) 항목을 아래로 이동하면 반대가됩니다. 목록과 위치를 고유하게 식별하는 것으로 테이블을 인덱싱하고 UPDATE매우 빠르게 실행되는 트랜잭션 내에서 두 가지로 테이블을 인덱싱 할 수 있습니다 . 사용자가 초 인간적인 속도로 목록을 재 배열하지 않는 한 많은 부하를 유발하지는 않습니다.

끌어서 놓기 이동 (예 : 6항목 9과 항목 사이에 앉아 항목 이동 10)은 조금 까다롭기 때문에 새 위치가 이전 위치보다 위 또는 아래인지에 따라 다르게 수행해야합니다. 위의 예에서을 (를) 초과하는 모든 위치 9를 늘리고 항목 6의 위치를 ​​새 위치로 업데이트 10한 다음 6비어있는 지점을 채우는 것보다 큰 모든 위치를 줄임으로써 구멍을 열어야합니다 . 앞에서 설명한 것과 동일한 인덱싱으로이 작업이 빠릅니다. 실제로 트랜잭션에 닿는 행 수를 최소화하여 설명 된 것보다 조금 더 빠르게 진행할 수 있지만 병목 현상이 있음을 입증 할 때까지 필요하지 않은 미세 최적화입니다.

어느 쪽이든, 집에서 만든, 너무 똑똑한 솔루션으로 데이터베이스를 능가하려고 시도하는 것이 일반적으로 성공하지는 않습니다. 소금의 가치가있는 데이터베이스는 매우 훌륭하고 능숙한 사람들에 의해 이러한 작업을 매우 신속하게 수행하도록 신중하게 작성되었습니다.


이것이 바로 몇 년 전 gazillion이 있었던 프로젝트 입찰 준비 시스템에서 처리 한 방식입니다. Access에서도 업데이트 속도가 빨랐습니다.
HLGEM

설명 감사합니다, Blrfl! 후자의 옵션을 시도했지만 목록의 중간에서 항목을 삭제하면 위치에 공백이 생길 것입니다 (매우 순진한 구현이었습니다). 이와 같은 간격을 피하는 쉬운 방법이 있습니까? 아니면 무언가를 다시 주문할 때마다 수동으로해야합니까 (실제로 관리 해야하는 경우)?
Tom Brunoli

2
@ TomBrunoli : 확실히 말하기 전에 구현에 대해 약간 생각해야하지만 트리거를 사용하여 대부분 또는 모든 번호 변경을 자동으로 수행 할 수 있습니다. 예를 들어, 항목 7을 삭제하면 트리거는 삭제가 수행 된 후 동일한 목록의 번호가 7보다 큰 모든 행을 감소시킵니다. 삽입은 동일한 작업을 수행합니다 (항목 7을 삽입하면 모든 행 7 이상이 증가 함). 업데이트 트리거 (예 : 항목 3을 9와 10 사이로 이동)는 다소 복잡하지만 확실히 가능한 영역 내에 있습니다.
Blrfl

나는 실제로 전에 트리거를 보지 않았지만 그것을하는 좋은 방법처럼 보입니다.
Tom Brunoli

1
@ TomBrunoli : 트리거를 사용하여 계단식으로 나타날 수 있습니다. 트랜잭션의 모든 변경 사항이있는 저장 프로 시저가 더 나은 경로 일 수 있습니다.
Blrfl

15

여기에서 동일한 대답 https://stackoverflow.com/a/49956113/10608


해결책 : index문자열을 만드십시오 (문자열은 본질적으로 무한 "임의 정밀도"를 갖기 때문에). 또는 int를 사용하는 경우 index1 대신 100 씩 증가하십시오 .

성능 문제는 이것입니다. 두 개의 정렬 된 항목 사이에 "in between"값이 없습니다.

item      index
-----------------
gizmo     1
              <<------ Oh no! no room between 1 and 2.
                       This requires incrementing _every_ item after it
gadget    2
gear      3
toolkit   4
box       5

대신 다음과 같이하십시오 (아래 더 나은 솔루션).

item      index
-----------------
gizmo     100
              <<------ Sweet :). I can re-order 99 (!) items here
                       without having to change anything else
gadget    200
gear      300
toolkit   400
box       500

더 나은 방법 : Jira가이 문제를 해결하는 방법은 다음과 같습니다. 그들의 "순위"(당신이 색인이라고 부르는 것)는 순위가 매겨진 항목 사이에 많은 호흡 공간을 허용하는 문자열 값입니다.

여기 내가 작업하는 jira 데이터베이스의 실제 예가 있습니다.

   id    | jira_rank
---------+------------
 AP-2405 | 0|hzztxk:
 ES-213  | 0|hzztxs:
 AP-2660 | 0|hzztzc:
 AP-2688 | 0|hzztzk:
 AP-2643 | 0|hzztzs:
 AP-2208 | 0|hzztzw:
 AP-2700 | 0|hzztzy:
 AP-2702 | 0|hzztzz:
 AP-2411 | 0|hzztzz:i
 AP-2440 | 0|hzztzz:r

이 예제를 주목하십시오 hzztzz:i. 문자열 순위의 장점은 두 항목 사이의 공간이 부족, 당신은 아직 아무것도 랭크 다시 할 필요가 없습니다. 초점을 좁히기 위해 문자열에 더 많은 문자를 추가하기 시작합니다.


1
나는 하나의 레코드 만 업데이트 하여이 작업을 수행 할 수있는 방법을 찾으려고 노력 했으며이 대답은 내 머리 속에 생각한 솔루션을 매우 잘 설명합니다.
NSjonas

13

이전 (또는 다음) 값을 참조하기 위해 자체 참조를 사용하는 사람들을 보았지만 다시 목록에서 다른 많은 항목을 업데이트 해야하는 것처럼 보입니다.

왜? 열 (listID, itemID, nextItemID)이있는 연결리스트 테이블 접근을 가정 해보십시오.

목록에 새 항목을 삽입하려면 하나의 삽입과 수정 된 행이 필요합니다.

아이템을 재배치하려면 3 개의 행 수정 (이동중인 아이템, 아이템 이전의 아이템 및 새로운 위치 이전의 아이템)이 필요합니다.

항목을 제거하면 삭제와 수정 된 행이 하나씩 발생합니다.

이 비용은 목록에 10 개의 품목이 있는지 10,000 개의 품목이 있는지에 관계없이 동일하게 유지됩니다. 세 가지 경우 모두 대상 행이 첫 번째 목록 항목 인 경우 수정이 한 번 줄어 듭니다. 마지막 목록 항목 에서 더 자주 작업 하는 경우 다음보다는 prevItemID를 저장하는 것이 좋습니다.


10

"그러나 그것은 매우 비효율적 인 것 같습니다"

당신 그것을 측정 했습니까 ? 아니면 그냥 추측입니까? 증거없이 그러한 가정을하지 마십시오.

"목록 당 20-50 개의 항목"

솔직히 말해서, 그것은 "많은 품목"이 아니며, 그것은 거의 들리지 않습니다.

"포지션 열"접근 방식을 고수하는 것이 좋습니다 (가장 간단한 구현 인 경우). 이러한 작은 목록 크기의 경우 실제 성능 문제가 발생하기 전에 불필요한 최적화를 시작하지 마십시오


6

이것은 실제로 규모와 유스 케이스의 문제입니다.

목록에 몇 개의 항목이 예상됩니까? 수백만의 경우, 나는 십진수 경로가 명백한 것이라고 생각합니다.

6이면 정수 번호를 다시 매기는 것이 확실한 선택입니다. 또한 질문은 목록을 정리하거나 재배 열하 는 방법 입니다. 위쪽 및 아래쪽 화살표를 사용하는 경우 (한 번에 한 슬롯 씩 위 또는 아래로 이동) 정수를 사용한 다음 이동시 이전 (또는 다음)으로 교체합니다.

또한 사용자가 250 번 변경 한 다음 한 번에 커밋 할 수있는 경우 커밋하는 빈도는 다시 번호 매기기가있는 정수를 말하는 것보다 ...

tl; dr : 더 많은 정보가 필요합니다.


편집 : "위시리스트"는 많은 작은 목록처럼 들립니다 (가정, 이것이 틀릴 수도 있습니다). 그래서 나는 번호를 재지 정하는 정수라고 말합니다. (각 목록에는 자체 위치가 있습니다)


좀 더 문맥으로 질문을 업데이트하겠습니다
Tom Brunoli

정밀도가 제한적이기 때문에 소수가 작동하지 않으며, 삽입 된 각 항목은 잠재적으로 1 비트 소요
njzk2

3

목적이 재정렬 작업 당 데이터베이스 작업 수를 최소화하는 것입니다.

그것을 가정

  • 모든 쇼핑 항목은 32 비트 정수로 열거 할 수 있습니다.
  • 사용자의 희망 목록에 대한 최대 크기 제한이 있습니다. (나는 인기있는 웹 사이트가 20-40 항목을 제한으로 사용하는 것을 보았습니다)

사용자의 정렬 된 위시리스트를 한 열에 팩형 정수 (정수 배열) 순서로 저장하십시오. 희망 목록이 재정렬 될 때마다 전체 배열 (단일 행, 단일 열)이 업데이트됩니다. 단일 SQL 업데이트로 수행됩니다.

https://www.postgresql.org/docs/current/static/arrays.html


목표가 다른 경우 "위치 열"접근 방식을 사용하십시오.


"속도"와 관련하여 저장 프로 시저 접근 방식을 벤치마킹해야합니다. 하나의 위시리스트 셔플에 대해 20 개 이상의 개별 업데이트를 실행 하면 속도가 느릴 수 있지만 저장 프로 시저를 사용하는 빠른 방법이있을 수 있습니다.


3

OK 최근에이 까다로운 문제에 직면했으며이 Q & A 게시물의 모든 답변에서 많은 영감을 얻었습니다. 내가 보는 방식으로 각 솔루션에는 장단점이 있습니다.

  • position필드가 간격없이 순차적이어야 하는 경우 기본적으로 전체 목록을 다시 정렬해야합니다. 이것은 O (N) 연산입니다. 장점은 클라이언트 측에서 주문을 얻기 위해 특별한 로직이 필요하지 않다는 것입니다.

  • O (N) 연산을 피하고 싶지만 여전히 정확한 순서를 유지하려면 접근 방식 중 하나가 "이전 (또는 다음) 값을 참조하기 위해 자체 참조"를 사용하는 것입니다. 이것은 교과서 연결 목록 시나리오입니다. 의도적으로 "목록에있는 다른 많은 항목"은 발생하지 않습니다. 그러나이를 위해서는 클라이언트 측 (웹 서비스 또는 모바일 앱)이 순서를 도출하기 위해 연결된 목록 탐색 논리를 구현해야합니다.

  • 일부 변형은 참조 즉 링크 된 목록을 사용하지 않습니다. 그들은 전체 순서를 JSON-ar-in-a-string과 같은 자체 포함 된 블로 브로 표시하도록 선택합니다 [5,2,1,3,...]. 그런 순서는 별도의 장소에 저장됩니다. 이 접근법은 또한 분리 된 순서 블롭을 유지하기 위해 클라이언트 측 코드를 요구하는 부작용이있다.

  • 대부분의 경우 정확한 순서를 저장할 필요가 없으며 각 레코드간에 상대적 순위를 유지하기 만하면됩니다. 따라서 순차 레코드 간 간격을 허용 할 수 있습니다. (1) 100, 200, 300과 같은 간격이있는 정수를 사용하지만 간격이 빨리 없어지면 복구 프로세스가 필요합니다. (2) 자연스러운 간격이있는 10 진수사용 하지만 최종 정밀도 제한으로 살 수 있는지 여부를 결정해야합니다. (3) 이 답변에 설명 된대로 문자열 기반 순위를 사용 하지만 까다로운 구현 트랩을 주의하십시오 .

  • 실제 답변은 "의존적"일 수 있습니다. 비즈니스 요구 사항을 다시 방문하십시오. 예를 들어, 위시리스트 시스템 인 경우 개인적으로 "머스트 해브 (must-have)", "좋아하는 (good-to-have)", "나중에 (labe-better)"로 몇 가지 등급으로 구성된 시스템을 사용하고, 각 계급 내에서 주문하십시오. 전달 시스템 인 경우 전달 시간을 자연 틈이있는 대략적인 순위로 사용할 수 있습니다 (전달이 동시에 발생하지 않으므로 자연 충돌 방지). 귀하의 마일리지가 다를 수 있습니다.


2

위치 열에 부동 소수점 숫자를 사용하십시오.

그런 다음 "이동 된"행의 위치 열만 변경하여 목록을 다시 정렬 할 수 있습니다.

기본적으로 사용자가 "파란색"뒤에 "노란색"앞에 "빨간색"을 배치하려는 경우

그런 다음 계산해야합니다.

red.position = ((yellow.position - blue.position) / 2) + blue.position

몇 백만 재배치 후에는 "소수점"이없는 부동 소수점 숫자가 너무 작을 수 있습니다. 그러나 이것은 유니콘을 보는 것입니다.

초기 간격이 1000 인 정수 필드를 사용하여이를 구현할 수 있습니다. 따라서 초기 정렬은 1000-> blue, 2000-> Yellow, 3000-> Red입니다. 파란색 후에 "이동"한 후에는 1000-> 청색, 1500-> 적색, 2000-> 황색이됩니다.

문제는 1000만큼의 초기 간격이 1000만큼 적 으면 1000-> blue, 1001-puce, 1004-> biege와 같은 상황에 처하게됩니다 ... 더 이상 더 이상 할 수없는 곳입니다 전체 목록의 번호를 다시 지정하지 않고 "파란색"뒤에 아무 것도 삽입하십시오. 부동 소수점 숫자를 사용하면 두 위치 사이에 항상 "반쯤"점이 있습니다.


4
float를 기반으로 한 datbase에서 인덱싱 및 정렬은 정수 보다 비쌉니다 . Ints는 또한 좋은 서수 유형입니다 ... 클라이언트에서 정렬 할 수 있도록 비트로 보낼 필요는 없습니다 (인쇄 할 때 똑같이 렌더링되지만 비트 값이 다른 두 숫자의 차이).

그러나 ints를 사용하는 체계는 순서가 변경 될 때마다 목록의 모든 행 / 대부분을 업데이트해야 함을 의미합니다. float를 사용하면 이동 한 행만 업데이트합니다. 또한 "int보다 더 많은 부동 소수점"은 사용 된 구현 및 하드웨어에 따라 크게 달라집니다. 확실히 여분의 CPU는 행과 관련 인덱스를 업데이트하는 데 필요한 CPU와 비교할 때 중요하지 않습니다.
James Anderson

5
전문가들의 경우이 솔루션은 Trello ( trello.com ) 와 정확히 동일합니다 . 크롬 디버거를 열고 재주문 전후 (카드 드래그 / 드롭)에서 json 출력을 비교하면-가 나타납니다 "pos": 1310719, + "pos": 638975.5. 공정하게 말하면, 대부분의 사람들은 4 백만 개의 항목이 포함 된 trello 목록을 사용하지 않지만 Trello의 목록 크기 및 사용 사례는 사용자가 정렬 할 수있는 콘텐츠에 매우 일반적입니다. 사용자 정렬 가능한 것은 고성능과 거의 관련이 없으며 int 대 float 정렬 속도는 그다지 중요하지 않습니다. 특히 데이터베이스가 주로 IO 성능에 의해 제한된다는 점을 고려하십시오.
zelk

1
@PieterB '64 비트 정수를 사용하지 않는 이유 '는 개발자에게 인체 공학적이라고 말합니다. 평균 플로트에 대해 1.0보다 크거나 같은 비트 심도는 약 1.0보다 크므로 'position'열의 기본값을 1.0으로 설정하고 배가하는 것처럼 쉽게 0.5, 0.25, 0.75를 삽입 할 수 있습니다. 정수를 사용하면 기본값은 2 ^ 30 정도 여야하므로 디버깅 할 때 생각하기가 약간 까다로워집니다. 4073741824가 496359787보다 큽니까? 자릿수 계산을 시작하십시오.
zelk

1
또한 숫자 사이에 공간이 부족한 경우에 대비하면 해결하기가 어렵지 않습니다. 그들 중 하나를 움직입니다. 그러나 중요한 것은 이것이 최선의 방법으로 작동한다는 것인데, 이는 다른 당사자 (예 : trello)에 의한 많은 동시 편집을 처리합니다. 다른 사람이 동시에 같은 일을 했더라도 여전히 글로벌 주문이 있고 두 개의 숫자를 나눌 수 있으며 약간의 무작위 노이즈를 뿌릴 수 있습니다. 그곳에.
zelk
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.