관계형 데이터베이스에 주문 정보를 저장하는 방법


20

정렬 된 정보를 관계형 데이터베이스에 올바르게 저장하는 방법을 이해하려고합니다.

예를 들면 :

노래로 구성된 재생 목록이 있다고 가정 해 보겠습니다. 내 관계형 데이터베이스 내에 Playlists일부 메타 데이터 (이름, 작성자 등)가 포함 된 의 테이블이 있습니다. 또한 노래 특정 정보 (이름, 아티스트, 지속 시간 등)를 Songs포함하는 이라는 테이블이 있습니다 playlist_id.

기본적으로 새 노래가 재생 목록에 추가되면 끝에 추가됩니다. Song-ID로 주문할 때 (오름차순) 순서는 추가 순서입니다. 그러나 사용자가 재생 목록에서 노래를 다시 주문할 수 있다면 어떻게해야합니까?

나는 각각의 장단점이있는 몇 가지 아이디어를 생각해 냈습니다.

  1. 이라는 열 order이며 정수 입니다. 노래가 이동되면 이전 위치와 새 위치 사이의 모든 노래 순서가 변경되어 변경 사항이 반영됩니다. 이 방법의 단점은 노래가 움직일 때마다 많은 쿼리를 수행해야하며 이동 알고리즘이 다른 옵션만큼 사소하지 않다는 것입니다.
  2. 이라는 열 order10 진수 ( NUMERIC)입니다. 노래가 이동되면 인접한 두 숫자 사이에 부동 소수점 값이 할당됩니다. 단점 : 십진 필드는 더 많은 공간을 차지하므로 몇 번의 변경 후에 범위를 다시 분배하지 않으면 정밀도가 떨어질 수 있습니다.
  3. 다른 방법은 다른 곡을 참조 하는 previousnext필드 를 갖는 것 입니다. (또는 현재 재생 목록의 첫 번째 노래와 마지막 노래 인 경우 NULL입니다. 기본적으로 링크 된 목록 을 만듭니다 ). 단점 : '목록에서 X 번째 노래 찾기'와 같은 쿼리는 더 이상 상수 시간이 아니라 선형 시간입니다.

실제로 가장 자주 사용되는 절차는 무엇입니까? 다음 중 중대형 데이터베이스에서 가장 빠른 절차는 무엇입니까? 이것을 보관하는 다른 방법이 있습니까?

편집 : 단순화를 위해 예에서 곡은 하나의 재생 목록 (다 대일 관계)에만 속합니다. 물론, Junction Table을 사용할 수도 있으므로 song⟷playlist는 다 대다 관계입니다 (그리고 그 테이블에 위의 전략 중 하나를 적용하십시오).


1
옵션 1 (정수 순서)을 100 단계로 사용할 수 있습니다. 그런 다음 한 곡을 이동하면 순서를 바꿀 필요가 없으며 100 사이의 값만 사용하면됩니다. 때때로 곡 사이의 간격을 다시 잡기 위해 새로운 번호를 다시 매길 필요가 있습니다.
knut

4
"이것의 단점은 노래가 움직일 때마다 많은 쿼리를 수행해야한다는 것입니다."! - update songorder set order = order - 1 where order >= 12 & order <= 42; update songorder set order = 42 where id = 123;-하지 서른두 업데이트 있다고. 순서에 고유 한 제약 조건을 적용하려는 경우 3입니다.

2
다른 것이 필요하다는 사실을 모르면 옵션 1을 사용하십시오. 데이터베이스에 익숙하지 않은 한 프로그래머는 데이터베이스가 이런 종류의 것을 매우 잘 이해한다는 것을 이해하지 못한다. DB를 작동시키는 것을 두려워하지 마십시오.
GrandmasterB

1
Queries like 'find the Xth Song in the list' are no longer constant-time옵션 2의 경우도 마찬가지입니다.
Doc Brown

2
@ MikeNakis : 비용이 많이 드는 것처럼 보이지만 모든 작업은 서버에서 수행되며 이는 일반적으로 이러한 종류의 작업에 최적화되어 있습니다. 이 기술은 수백만 행이있는 테이블에는 사용하지 않지만 2 천 개만있는 테이블에 대해서는 할인하지 않습니다.
TMN

답변:


29

데이터베이스는 특정 사안에 최적화되어 있습니다. 많은 행을 빠르게 업데이트하는 것이 그 중 하나입니다. 데이터베이스에서 작업을 수행 할 때 특히 그렇습니다.

치다:

order song
1     Happy Birthday
2     Beat It
3     Never Gonna Give You Up
4     Safety Dance
5     Imperial March

그리고 당신 Beat It은 끝 으로 이동하고 싶습니다 , 당신은 두 가지 쿼리를 가질 것입니다 :

update table 
  set order = order - 1
  where order >= 2 and order <= 5;

update table
  set order = 5
  where song = 'Beat It'

그리고 그게 다야. 이것은 매우 큰 숫자로 아주 잘 확장됩니다. 데이터베이스의 가상 재생 목록에 수천 곡의 노래를 넣고 한 위치에서 다른 위치로 노래를 이동하는 데 걸리는 시간을 확인하십시오. 이들은 매우 표준화 된 형태를 가지고 있기 때문에 :

update table 
  set order = order - 1
  where order >= ? and order <= ?;

update table
  set order = ?
  where song = ?

매우 효율적으로 재사용 할 수있는 준비된 진술이 두 개 있습니다.

이것은 몇 가지 중요한 이점을 제공합니다. 테이블의 순서는 추론 할 수있는 것입니다. 세 번째 노래는order 항상 3입니다. 이를 보장하는 유일한 방법은 연속 정수를 순서대로 사용하는 것입니다. 의사 연결 목록이나 10 진수 또는 공백이있는 정수를 사용한다고해서이 속성을 보장 할 수는 없습니다. 이 경우 n 번째 노래를 얻는 유일한 방법은 전체 테이블을 정렬하고 n 번째 레코드를 얻는 것입니다.

실제로 이것은 생각보다 훨씬 쉽습니다. 수행하려는 작업을 파악하고 두 개의 업데이트 문을 생성하고 다른 사람들이이 두 개의 업데이트 문을보고 수행중인 작업을 인식하는 것은 간단합니다.


2
나는이 접근법을 좋아하기 시작했다.
Mike Nakis

2
@MikeNakis 잘 작동합니다. 비슷한 아이디어, 수정 된 프리오더 트리를 기반으로하는 이진 트리도 있습니다 . 이해하는 데 약간의 시간이 걸리지 만 계층 적 데이터에 대한 아주 좋은 쿼리를 수행 할 수 있습니다. 큰 나무에서도 성능 문제가 없었습니다. 코드에 대해 추론 할 수 있다는 것은 간단한 코드에 필요한 성능이 부족함을 보여줄 때까지 (그리고 극단적 인 상황에서만) 강조 될 부분입니다.

키워드 order이므로 order by키워드 사용에 문제가 있습니까?
kojow7

@ kojow7, 필드 이름에 키워드와 충돌하는 이름이 있으면 "" "표시로 묶어야합니다.
Andri

이 방법은 의미가 있지만 order재생 목록에 새 노래를 추가 할 때 가치 를 얻는 가장 좋은 방법은 무엇입니까 ? 에 9를 삽입 할 수있는 더 좋은 방법이있다, 그것은 9 노래의 말 order하는 것보다 COUNT기록을 추가하기 전에이?
delashum

3

우선, 당신이 한 일에 대한 설명에서 명확하지 않지만 , 어떤 노래가 어떤 재생 목록에 속하는지 설명하는 PlaylistSongsa PlaylistId및 a 가 포함 된 표가 필요합니다 SongId.

이 표에는 주문 정보를 추가해야합니다.

내가 가장 좋아하는 메커니즘은 실수입니다. 최근에 구현했으며 매력처럼 작동했습니다. 노래를 특정 위치로 이동하려면 새 Ordering값을 Ordering이전 노래 및 다음 노래 값의 평균으로 계산합니다 . 64 비트 실수를 사용하면 지옥이 얼어 붙을 정도로 거의 정밀도가 떨어지지 만 실제로 후손을 위해 소프트웨어를 작성하는 경우 멋진 둥근 정수 Ordering값을 각 노래의 모든 노래에 다시 할당하는 것이 좋습니다 가끔씩 재생 목록을 만듭니다.

추가 보너스로 여기에 작성한 코드가 있습니다. 물론 당신은 그것을 그대로 사용할 수 없으며, 지금 당신을 위해 그것을 소독하는 것이 너무 많은 일이 될 것이므로 아이디어를 얻을 수 있도록 게시하고 있습니다.

클래스는 ParameterTemplate(무엇이든 묻지 않습니다!) 메소드는이 템플릿이 속한 매개 변수 템플릿 목록을 부모로부터 가져옵니다 ActivityTemplate. (무엇이든 묻지 마십시오!) 코드에는 정밀도가 떨어지지 않도록주의해야합니다. 제수는 테스트에 사용됩니다. 단위 테스트는 큰 제수를 사용하여 정밀도가 빨리 떨어 지므로 정밀도 보호 코드가 트리거됩니다. 두 번째 방법은 공용이며 "내부 용이며 호출하지 마십시오". 테스트 코드가이를 호출 할 수 있습니다. (내 테스트 코드는 테스트 코드와 같은 패키지에없는 때문에 패키지 전용 할 수 없습니다.) 순서를 제어하는 필드가 호출 Ordering을 통해 액세스, getOrdering()setOrdering(). 최대 절전 모드를 통해 객체 관계형 매핑을 사용하고 있기 때문에 SQL이 표시되지 않습니다.

/**
 * Moves this {@link ParameterTemplate} to the given index in the list of {@link ParameterTemplate}s of the parent {@link ActivityTemplate}.
 *
 * The index must be greater than or equal to zero, and less than or equal to the number of entries in the list.  Specifying an index of zero will move this item to the top of
 * the list. Specifying an index which is equal to the number of entries will move this item to the end of the list.  Any other index will move this item to the position
 * specified, also moving other items in the list as necessary. The given index cannot be equal to the current index of the item, nor can it be equal to the current index plus
 * one.  If the given index is below the current index of the item, then the item will be moved so that its new index will be equal to the given index.  If the given index is
 * above the current index, then the new index of the item will be the given index minus one.
 *
 * NOTE: this method flushes the persistor and refreshes the parent node so as to guarantee that the changes will be immediately visible in the list of {@link
 * ParameterTemplate}s of the parent {@link ActivityTemplate}.
 *
 * @param toIndex the desired new index of this {@link ParameterTemplate} in the list of {@link ParameterTemplate}s of the parent {@link ActivityTemplate}.
 */
public void moveAt( int toIndex )
{
    moveAt( toIndex, 2.0 );
}

/**
 * For internal use only; do not invoke.
 */
public boolean moveAt( int toIndex, double divisor )
{
    MutableList<ParameterTemplate<?>> parameterTemplates = getLogicDomain().getMutableCollections().newArrayList();
    parameterTemplates.addAll( getParentActivityTemplate().getParameterTemplates() );
    assert parameterTemplates.getLength() >= 1; //guaranteed since at the very least, this parameter template must be in the list.
    int fromIndex = parameterTemplates.indexOf( this );
    assert 0 <= toIndex;
    assert toIndex <= parameterTemplates.getLength();
    assert 0 <= fromIndex;
    assert fromIndex < parameterTemplates.getLength();
    assert fromIndex != toIndex;
    assert fromIndex != toIndex - 1;

    double order;
    if( toIndex == 0 )
    {
        order = parameterTemplates.fetchFirstElement().getOrdering() - 1.0;
    }
    else if( toIndex == parameterTemplates.getLength() )
    {
        order = parameterTemplates.fetchLastElement().getOrdering() + 1.0;
    }
    else
    {
        double prevOrder = parameterTemplates.get( toIndex - 1 ).getOrdering();
        parameterTemplates.moveAt( fromIndex, toIndex );
        double nextOrder = parameterTemplates.get( toIndex + (toIndex > fromIndex ? 0 : 1) ).getOrdering();
        assert prevOrder <= nextOrder;
        order = (prevOrder + nextOrder) / divisor;
        if( order <= prevOrder || order >= nextOrder ) //if the accuracy of the double has been exceeded
        {
            parameterTemplates.clear();
            parameterTemplates.addAll( getParentActivityTemplate().getParameterTemplates() );
            for( int i = 0; i < parameterTemplates.getLength(); i++ )
                parameterTemplates.get( i ).setOrdering( i * 1.0 );
            rocs3dDomain.getPersistor().flush();
            rocs3dDomain.getPersistor().refresh( getParentActivityTemplate() );
            moveAt( toIndex );
            return true;
        }
    }
    setOrdering( order );
    rocs3dDomain.getPersistor().flush();
    rocs3dDomain.getPersistor().refresh( getParentActivityTemplate() );
    assert getParentActivityTemplate().getParameterTemplates().indexOf( this ) == (toIndex > fromIndex ? toIndex - 1 : toIndex);
    return false;
}

정수 순서를 사용하고 재정렬이 너무 비싸다고 생각되면 X마다 하나씩 점프하여 재정렬 수를 줄이려고합니다. 여기서 X는 재정렬을 줄이는 데 필요한 양입니다 (예 : 20). 초보자로서 괜찮을 것입니다.
Warren P

1
@WarrenP 그렇습니다. 나는 이것이 또한 이런 식으로 수행 될 수 있다는 것을 알고 있습니다. 이것이 바로 "최고"또는 "하나"접근 대신 "내가 가장 좋아하는"접근이라고 불렀습니다.
Mike Nakis

0

나를 위해 일한 것은 100 개 항목의 작은 목록에 대해 하이브리드 접근법을 취하는 것이 었습니다.

  1. 10 진수 SortOrder 열이지만 0.5 차이를 저장하기에 충분한 정밀도 만 있습니다 (예 : Decimal (8,2) 또는 기타).
  2. 정렬 할 때 현재 행이있는 위치의 위와 아래에있는 행의 PK를 가져옵니다 (존재하는 경우). (예를 들어 항목을 첫 번째 위치로 이동하면 위의 행이 없습니다)
  3. 현재, 이전 및 다음 행의 PK를 서버에 게시하여 정렬을 수행합니다.
  4. 이전 행이있는 경우 현재 행의 위치를 ​​이전 + 0.5로 설정하십시오. 다음 행만있는 경우 현재 행의 위치를 ​​다음-0.5로 설정하십시오.
  5. 다음으로 새로운 정렬 순서에 따라 SQL Server Row_Number 함수를 사용하여 모든 위치를 업데이트하는 Stored proc이 있습니다. row_number 함수는 정수 서수를 제공하므로 순서를 1,1.5,2,3,4,6에서 1,2,3,4,5,6으로 변환합니다.

따라서 10 진수 열에 저장된 간격없이 정수 순서로 끝납니다. 상당히 깨끗합니다. 그러나 업데이트해야 할 행이 수십만 개가되면 한 번에 크게 확장되지 않을 수 있습니다. 그러나 그렇다면 왜 처음에 사용자 정의 정렬을 사용합니까? (참고 : 수백만 명의 사용자가있는 큰 테이블이 있지만 각 사용자가 정렬 할 수백 개의 항목 만 가지고 있다면 어쨌든 where 절을 사용하여 변경 사항을 한 명의 사용자로 제한하기 때문에 위의 접근 방식을 사용할 수 있습니다 )

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.