다중 테넌트 SQL Server 데이터베이스의 복합 기본 키


16

ASP Web API, Entity Framework 및 SQL Server / Azure 데이터베이스를 사용하여 다중 테넌트 앱 (단일 데이터베이스, 단일 스키마)을 구축하고 있습니다. 이 응용 프로그램은 1000-5000 고객이 사용합니다. 모든 테이블에는 TenantId(Guid / UNIQUEIDENTIFIER) 필드가 있습니다. 지금은 단일 필드 기본 키인 ID (가이드)를 사용합니다. 그러나 Id 필드 만 사용하여 사용자가 제공 한 데이터가 올바른 테넌트에 대한 데이터인지 확인해야합니다. 예를 들어, 필드 가있는 SalesOrder테이블이 있습니다 CustomerId. 사용자가 판매 주문을 게시 / 업데이트 할 때마다 CustomerId동일한 테넌트 인지 확인해야합니다 . 각 테넌트에 여러 개의 콘센트가있을 수 있으므로 상황이 악화됩니다. 그럼 확인해야 TenantId하고 OutletId. 실제로 유지 관리의 악몽이며 성능이 좋지 않습니다.

TenantId와 함께 기본 키 에 추가 하려고합니다 Id. 그리고 아마도 추가하십시오 OutletId. 의 기본 키 그래서 SalesOrder테이블이 될 것입니다 : Id, TenantId,와 OutletId. 이 방법의 단점은 무엇입니까? 복합 키를 사용하면 성능이 크게 저하됩니까? 복합 키 순서가 중요합니까? 내 문제에 대한 더 나은 해결책이 있습니까?

답변:


34

대규모의 다중 테넌트 시스템 (18 세 이상의 서버로 분산 된 고객, 각 서버는 동일한 스키마, 다른 고객 및 각 서버 당 초당 수천 개의 트랜잭션)을 사용하는 페더레이션 접근 방식을 사용하여 다음과 같이 말할 수 있습니다.

  1. "TenantID"및 "Entity"ID에 대한 ID로 GUID 선택에 동의하는 사람들이 있습니다 (적어도 몇 명). 그러나 아닙니다. 좋은 선택은 아닙니다. 다른 모든 고려 사항을 제외하고는 그 선택만으로 몇 가지 방식으로 피해를 입을 수 있습니다. 시작으로 인한 조각화, 방대한 양의 낭비되는 공간 ( 엔터프라이즈 스토리지 (SAN)에 대해 생각할 때 디스크가 저렴 하지 않다고 말 하거나 각 데이터 페이지로 인해 쿼리가 오래 걸리는 것은 아닙니다) 적은 수의 행이 함께 중 하나를 할 수있는 것보다 유지 INT또는 BIGINT심지어), 더 어려워 지원 및 유지 보수 등의 GUID가 휴대 훌륭합니다. 일부 시스템에서 데이터가 생성 된 후 다른 시스템으로 전송됩니까? 그렇지 않다면, 더 컴팩트 한 데이터 유형 (예를 들어 전환 TINYINT, SMALLINT, INT, 또는 BIGINT를 통해 순차), 및 증분 IDENTITY또는SEQUENCE.

  2. 항목 1을 벗어나면 실제로 사용자 데이터가있는 모든 테이블에 TenantID 필드가 있어야합니다. 그렇게하면 별도의 JOIN 없이도 필터링 할 수 있습니다. 또한 클라이언트 데이터 테이블에 대한 모든 쿼리 TenantID에는 JOIN 조건 및 / 또는 WHERE 절이 있어야합니다 . 또한 실수로 다른 고객의 데이터를 혼합하거나 테넌트 B의 테넌트 A 데이터를 표시하지 않도록 보장합니다.

  3. ID와 함께 TenantId를 기본 키로 추가하려고합니다. 그리고 아마도 OutletId를 추가하십시오. 따라서 판매 주문 테이블의 기본 키는 Id, TenantId, OutletId입니다.

    예. 클라이언트 데이터 테이블의 클러스터형 인덱스는 TenantIDID **를 포함하는 복합 키 여야합니다 . 또한 TenantID클라이언트 데이터 테이블에 대한 쿼리의 98.45 %가 TenantID이전 데이터 기반을 가비지 수집하는 경우를 제외하고 이후에 필요한 클러스터되지 않은 인덱스 (클러스터형 인덱스 키가 포함되어 있기 때문에)가 모든 비 클러스터형 인덱스에 있는지 확인합니다. 의 위에CreatedDate 신경 쓰지 않고 TenantID).

    아니요 OutletID. PK 와 같은 FK는 포함하지 않습니다 . PK는 행을 고유하게 식별해야하며 FK를 추가해도 도움이되지 않습니다. 실제로 OrderID가 각 고유 한 것이 아니라 고유 한 것으로 가정하면 중복 된 데이터의 가능성이 증가 TenantID합니다.OutletID 각 내에서 TenantID.

    또한 OutletID테넌트 A의 콘센트가 테넌트 B와 섞이지 않도록 PK에 추가 할 필요는 없습니다 . 모든 사용자 데이터 테이블이 TenantIDPK TenantID에 있으므로 FK에도 포함됩니다. . 예를 들어, Outlet테이블의 PK를 갖고 (TenantID, OutletID), 상기 Order테이블의 PK 갖는다 (TenantID, OrderID) 의 FK (TenantID, OutletID)상의는 PK를 참조하는 Outlet테이블. FK를 올바르게 정의하면 테넌트 데이터가 혼합되지 않습니다.

  4. 복합 키 순서가 중요합니까?

    글쎄, 여기가 재미있어지는 곳입니다. 어느 분야가 먼저되어야하는지에 대한 논쟁이 있습니다. 좋은 인덱스를 디자인하기위한 "일반적인"규칙은 가장 선택적인 필드를 선행 필드로 선택하는 것입니다. TenantID본질적 으로 가장 선택적인 분야 는 아니다 . ID필드는 대부분의 선택적 필드입니다. 다음은 몇 가지 생각입니다.

    • 먼저 ID : 가장 선택적인 (즉 가장 독특한) 필드입니다. 그러나 자동 증분 필드 (또는 여전히 GUID를 사용하는 경우 임의 임)이므로 각 고객의 데이터가 각 테이블에 분산됩니다. 즉, 고객이 100 개의 행을 필요로하는 경우가 있고 디스크에서 버퍼 풀로 읽어 오는 거의 100 개의 데이터 페이지가 필요합니다 (10 개의 데이터 페이지보다 많은 공간을 차지함). 또한 여러 고객이 동일한 데이터 페이지를 업데이트해야하는 빈도가 높아 지므로 데이터 페이지에서 경합이 증가합니다.

      그러나 서로 다른 ID 값에 대한 통계가 상당히 일관성이 있기 때문에 일반적으로 많은 매개 변수 스니핑 / 잘못된 캐시 계획 문제가 발생하지 않습니다. 가장 최적의 계획은 얻지 못할 수도 있지만 끔찍한 계획은 얻을 가능성이 적습니다. 이 방법은 본질적으로 모든 고객에서 성능을 약간 (약간) 희생하여 덜 빈번한 문제의 이점을 얻습니다.

    • 테넌트 ID 우선 :이것은 전혀 선택적이지 않습니다. TenantID가 100 개만있는 경우 백만 개의 행에서 변형이 거의 없을 수 있습니다. 그러나 SQL Server는 테넌트 A에 대한 쿼리가 500,000 개의 행을 철회하지만 테넌트 B에 대한 동일한 쿼리는 50 개의 행에 불과하다는 것을 알기 때문에 이러한 쿼리에 대한 통계가 더 정확합니다. 이것이 주요 고통 지점입니다. 이 방법을 사용하면 저장 프로 시저의 첫 번째 실행이 테넌트 A에 대한 매개 변수 스니핑 문제가 발생할 가능성이 크게 증가하고 쿼리 최적화 프로그램이 이러한 통계를보고 500k 행을 효율적으로 가져와야한다는 사실을 알 수 있습니다. 그러나 50 행만있는 테넌트 B가 실행될 때 해당 실행 계획은 더 이상 적합하지 않으며 실제로는 부적절합니다. 선행 필드의 순서대로 데이터가 삽입되지 않기 때문에

      그러나 첫 번째 TenantID가 저장 프로 시저를 실행하는 경우 데이터를 (적어도 인덱스 유지 관리를 수행 한 후) 물리적 및 논리적으로 구성하여 데이터 페이지를 충족시키는 데 필요한 데이터 페이지가 훨씬 적으므로 다른 접근 방식보다 성능이 향상되어야합니다. 쿼리. 즉, 동일한 데이터 페이지에 대한 물리적 I / O, 논리적 읽기, 테넌트 간 경합, 버퍼 풀에서 차지하는 공간 낭비 (페이지 수명 기대 향상) 등이 줄어 듭니다.

      이 향상된 성능을 얻는 데는 두 가지 주요 비용이 있습니다. 첫 번째는 그리 어렵지 않습니다 . 조각화 증가에 대응하기 위해 정기적 인 인덱스 유지 관리를 수행 해야합니다 . 두 번째는 약간 덜 재미있다.

      증가 된 매개 변수 스니핑 문제를 해결하려면 테넌트간에 실행 계획을 분리해야합니다. 간단한 접근 방식은 WITH RECOMPILEprocs 또는 OPTION (RECOMPILE)쿼리 힌트에 사용하는 것이지만, 우선 순위 를 두어 얻을 수있는 모든 이점을 없앨 수있는 성능에 영향을 미칩니다 TenantID. 내가 찾은 가장 좋은 방법은를 통해 매개 변수화 된 동적 SQL을 사용하는 것 sp_executesql입니다. 동적 SQL이 필요한 이유는 TenantID를 쿼리 텍스트에 연결하는 것이 일반적으로 가능하지만 일반적으로 매개 변수가되는 다른 모든 술어는 여전히 매개 변수입니다. 예를 들어 특정 주문을 찾고 있다면 다음과 같은 작업을 수행합니다.

      DECLARE @GetOrderSQL NVARCHAR(MAX);
      SET @GetOrderSQL = N'
        SELECT ord.field1, ord.field2, etc.
        FROM   dbo.Orders ord
        WHERE  ord.TenantID = ' + CONVERT(NVARCHAR(10), @TenantID) + N'
        AND    ord.OrderID = @OrderID_dyn;
      ';
      
      EXEC sp_executesql
         @GetOrderSQL,
         N'@OrderID_dyn INT',
         @OrderID_dyn = @OrderID;

      이로 인해 해당 테넌트의 데이터 볼륨과 일치하는 해당 테넌트 ID에 대해서만 재사용 가능한 쿼리 계획을 작성하게됩니다. 동일한 테넌트 A가 다른 테넌트에 대해 저장 프로 시저를 다시 실행하면 @OrderID캐시 된 쿼리 계획을 재사용합니다. 단지 TenantID의 가치 달랐다 쿼리 텍스트를 생성하는 것 같은 저장 프로 시저를 실행하는 다른 세입자하지만, 어떤 쿼리 텍스트의 차이는 다른 계획을 생성하기에 충분하다. 그리고 테넌트 B에 대해 생성 된 계획은 테넌트 B의 데이터 볼륨과 일치 할뿐만 아니라, @OrderID술어가 여전히 매개 변수화되어 있으므로 다른 값의 테넌트 B에 대해서도 재사용 할 수 있습니다 .

      이 방법의 단점은 다음과 같습니다.

      • 단순한 쿼리를 입력하는 것보다 약간 더 많은 작업입니다 (그러나 모든 쿼리는 동적 SQL 일 필요는 없으며 매개 변수 스니핑 문제가 발생하는 쿼리).
      • 시스템에있는 테넌트 수에 따라 각 쿼리는이를 호출하는 테넌트 ID 당 하나의 계획이 필요하므로 계획 캐시의 크기가 증가합니다. 이것은 문제가되지 않지만 적어도 알아야 할 사항입니다.
      • 동적 SQL은 소유권 체인을 중단합니다. 즉 EXECUTE, 스토어드 프로 시저에 대한 권한을 가지고 테이블에 대한 읽기 / 쓰기 액세스를 가정 할 수 없습니다 . 쉽고 덜 안전한 수정은 사용자에게 테이블에 직접 액세스하는 것입니다. 이것은 이상적이지는 않지만 일반적으로 빠르고 쉬운 트레이드 오프입니다. 보다 안전한 방법은 인증서 기반 보안을 사용하는 것입니다. 의미, 인증서를 만든 다음 해당 인증서에서 사용자를 만들고 해당 권한 사용자 에게 부여 합니다 (인증서 기반 사용자 또는 로그인은 자체적으로 SQL Server에 연결할 수 없음). 그런 다음 Dynamic SQL을 사용하는 저장 프로 시저에 서명 ADD SIGNATURE 를 통한 동일한 인증서 .

        모듈 서명 및 인증서에 대한 자세한 내용은 ModuleSigning.Info 를 참조하십시오.
         

    이 결정으로 인해 발생하는 통계 문제를 해결하는 문제와 관련된 추가 주제는 끝 부분의 업데이트 섹션을 참조하십시오 .


** 개인적으로, 나는 모든 테이블에서 PK 필드 이름에 "ID"만 사용하는 것을 좋아하지 않습니다. PK는 항상 "ID"이고 자식 테이블의 필드는 부모 테이블 이름을 포함하십시오. 예를 들면 다음과 같습니다 Orders.ID.-> OrderItems.OrderID. 내가 훨씬 쉽게 가지고있는 데이터 모델을 다루는 찾을 : Orders.OrderID-> OrderItems.OrderID. 더 읽기 쉽고 "모호한 열 참조"오류가 발생하는 횟수를 줄입니다.


최신 정보

  • 시겠습니까 OPTIMIZE FOR UNKNOWN 쿼리 힌트 복합 PK 중 하나 주문에 대한 지원 (SQL 서버 2008을 도입)?

    실제로는 아닙니다. 이 옵션은 매개 변수 스니핑 문제를 해결하지만 한 문제를 다른 문제로 대체합니다. 이 경우 저장 프로 시저 또는 매개 변수화 된 쿼리의 초기 실행에 대한 매개 변수 값에 대한 통계 정보 (일부에서는 확실히 크지 만 일부에서는 평범하고 일부에서는 끔찍한)를 기억하지 않고 행 수를 추정하기위한 데이터 분배 통계. 이는 긍정적, 부정적 또는 전혀 영향을받지 않는 쿼리의 수 (및 정도)에 관한 것입니다. 적어도 매개 변수 스니핑을 사용하면 일부 쿼리가 도움이됩니다. 시스템에 다양한 데이터 볼륨을 가진 테넌트가있는 경우 모든 쿼리의 성능이 저하 될 수 있습니다.

    이 옵션은 입력 매개 변수를 로컬 변수에 복사 한 다음 쿼리에서 로컬 변수를 사용하는 것과 동일한 작업을 수행합니다 (이를 테스트했지만 여기에 대한 여지가 없습니다). 추가 정보는이 블로그 게시물 ( http://www.brentozar.com/archive/2013/06/optimize-for-unknown-sql-server-parameter-sniffing/) 에서 찾을 수 있습니다 . 의견을 읽으면서 Daniel Pepermans는 변형이 제한된 Dynamic SQL의 사용에 관한 광산과 비슷한 결론에 도달했습니다.

  • ID가 클러스터형 인덱스의 선행 필드 인 경우 단일 테넌트의 여러 행을 처리하는 쿼리에 대한 정확한 통계를 얻으려면 클러스터되지 않은 인덱스를 (TenantID, ID)로 설정하거나 (TenantID) 만 있으면 충분합니까?

    예, 도움이 될 것입니다. 내가 몇 년 동안 작업을 언급 한 대형 시스템은 IDENTITY매개 변수 스니핑 문제가 더 선택적이고 줄어들었기 때문에 필드를 주요 필드로 사용 하는 인덱스 디자인을 기반으로했습니다 . 그러나 특정 테넌트 데이터의 상당 부분에 대한 작업이 필요한 경우 성능이 유지되지 않았습니다. 실제로, SAN 컨트롤러가 처리량 측면에서 최대가되기 때문에 모든 데이터를 새 데이터베이스로 마이그레이션하는 프로젝트를 보류해야했습니다. 모든 테넌트 데이터 테이블에 비 클러스터형 인덱스를 추가 (TenantID)로 수정했습니다. ID가 이미 클러스터형 인덱스에 있으므로 비 클러스터형 인덱스의 내부 구조가 자연스럽게 (TenantID, ID)이므로 수행 할 필요가 없습니다 (TenantID, ID).

    이렇게하면 TenantID 기반 쿼리를 훨씬 더 효율적으로 수행 할 수 있다는 즉각적인 문제가 해결되었지만 동일한 순서로 클러스터 된 인덱스 인 경우에 비해 효율적이지 않았습니다. 그리고 이제 우리는 모든 테이블 에 대해 하나 이상의 인덱스를 가졌 습니다. 따라서 사용중인 SAN 공간이 증가하고 백업 크기가 증가했으며 백업을 완료하는 데 시간이 오래 걸리고 차단 및 교착 상태, 성능 INSERTDELETE운영 감소 가능성 등이 증가했습니다 .

    그리고 우리는 여전히 테넌트 데이터가 다른 많은 테넌트 데이터와 혼합되어 많은 데이터 페이지에 분산되어 있다는 비효율적 인 상태를 유지했습니다. 위에서 언급했듯이, 이로 인해이 페이지에서 경합의 양이 증가하고, 특히 해당 페이지의 일부 행이 비활성 상태이지만 아직 가비지 수집되지 않았습니다. 이 방법에서는 버퍼 풀에서 데이터 페이지를 재사용 할 가능성이 훨씬 낮아서 페이지 수명이 매우 낮았습니다. 더 많은 페이지를로드하기 위해 디스크로 돌아가는 데 더 많은 시간이 소요됩니다.


2
이 문제 영역에서 OPTIMIZE FOR UNKNOWN을 고려하거나 테스트 했습니까? 그냥 궁금해서
RLF

1
@RLF 그렇습니다. 우리는 그 옵션을 연구했으며 IDENTITY 필드를 처음으로 얻었을 때 최적의 성능보다 나빠질 것입니다. 나는 이것을 어디서 읽었는지 기억 나지 않지만, 입력 변수를 지역 변수에 재 할당하는 것과 동일한 "평균"통계를 제공 할 것이다. 그러나이 기사는 해당 옵션이 정말 문제가 해결되지 않는 이유에 간다 : brentozar.com/archive/2013/06/...는 코멘트를 읽기, 다니엘 Pepermans 비슷한 결론에 다시 온 : 동적 SQL을 :) 제한된 변화에
솔로몬 Rutzky을

3
클러스터형 인덱스가 켜져 (ID, TenantID)있고 클러스터되지 않은 인덱스를 on에 만들 (TenantID, ID)거나 (TenantID)단일 테넌트의 대부분의 행을 처리하는 쿼리에 대한 정확한 통계를 얻으려면 어떻게해야합니까?
블라디미르 바라 노프

1
@VladimirBaranov 훌륭한 질문입니다. 나는 대답의 끝을 향해 새로운 업데이트 섹션 에서 그것을 해결했다 :-).
Solomon Rutzky

4
동적 SQL에 대한 좋은 점은 고객에 대한 계획을 생성하는 것 입니다.
Max Vernon
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.