분류

문서용 SQL(N1QL): 쿼리 계획에 대한 간략한 소개입니다.

문서용 SQL: 쿼리 계획에 대한 간략한 소개입니다.

나는 썼다 이전 에서 새로운 종류의 SQL에 대한 필요성과 N1QL(SQL For Documents)을 소개한 바 있습니다. 이 블로그에서는 곧 출시될 Couchbase Sherlock 릴리즈에서 N1QL의 쿼리 계획 단계에 대해 설명합니다. N1QL 자체는 여러 차례 개발자 프리뷰를 거쳤습니다.  다음에서 온라인 튜토리얼을 확인하세요.  https://query.couchbase.com 문서용 SQL(N1QL)이 포함된 Couchbase Sherlock 릴리스의 개발자 미리 보기를 다운로드하세요.
N1QL이란 무엇인가요? 유연한 데이터 모델을 갖춘 최신 분산 아키텍처에서 문서(예: JSON)에 대한 SQL을 제공하도록 설계된 최신 쿼리 처리 엔진입니다. 최신 데이터베이스는 대규모 클러스터에 배포되며, 유연한 데이터 모델(JSON, Column 제품군 등)을 사용합니다. N1QL은 이 데이터 모델 위에 향상된 SQL을 지원하여 쿼리 처리가 더 쉬워집니다.
쿼리 실행: 개요

애플리케이션과 해당 드라이버는 사용 가능한 쿼리 노드 중 하나에 N1QL 쿼리를 제출합니다. 쿼리 노드는 쿼리를 분석하고 기본 개체에 대한 메타 데이터를 사용하여 최적의 실행 계획을 파악한 다음 실행합니다. 실행 중에 쿼리에 따라 해당 인덱스를 사용하여 쿼리 노드는 인덱스 및 데이터 노드와 함께 작동하여 선택-조인-프로젝트 작업을 검색하고 수행합니다. Couchbase는 클러스터형 데이터베이스이므로 성능 및 가용성 목표에 맞게 데이터, 인덱스 및 쿼리 노드를 확장할 수 있습니다.

쿼리 실행: 내부 보기

이 그림(수치를 제공한 @N1QL 아키텍트 제럴드 상구디에게 감사드립니다)은 쿼리가 결과를 반환하기 위해 거치는 모든 가능한 단계를 보여줍니다. 모든 쿼리가 모든 단계를 거쳐야 하는 것은 아니며, 일부 쿼리는 이러한 단계를 여러 번 거치게 됩니다. 예를 들어 쿼리에 ORDER BY 절이 없는 경우 정렬 단계를 건너뛸 수 있으며, 여러 조인을 수행하려면 스캔-조회-조인 단계를 여러 번 거칠 수 있습니다.
 N1QL은 쿼리의 각 키 공간(테이블 또는 버킷)에 대해 쿼리 및 사용 가능한 액세스 경로 옵션을 분석하여 쿼리 계획 및 실행 인프라를 생성합니다. 플래너는 먼저 각 버킷에 대한 액세스 경로를 선택하고, 조인 순서를 결정한 다음, 조인 유형을 결정해야 합니다. 큰 결정이 내려지면 플래너는 계획을 실행하는 데 필요한 인프라를 만듭니다. 쿼리 구문 분석 및 계획과 같은 일부 작업은 순차적으로 수행되는 반면, 가져오기, 조인, 정렬과 같은 다른 작업은 병렬로 수행됩니다. 이 블로그에서는 예제를 통해 쿼리 계획 단계에 대한 간략한 개요를 중점적으로 살펴보겠습니다.
액세스 경로 선택:
    키스페이스(버킷) 액세스 경로 옵션
a. 키스캔 액세스. 특정 문서 ID(키)를 사용할 수 있는 경우, 키스캔 액세스 방법은 해당 ID에 대한 문서를 검색합니다. 그 후에 해당 키스페이스의 모든 필터가 적용됩니다. 키스캔 액세스 방법은 키스페이스가 자체적으로 쿼리될 때 또는 조인 처리 중에 사용할 수 있습니다. 키 스캔은 일반적으로 조인 처리 중에 내부 키 스페이스에 대한 적격 문서를 검색하는 데 사용됩니다.
b. 기본 스캔 액세스: 관계형 데이터베이스 시스템에서 전체 테이블 스캔에 해당합니다. 문서 ID가 제공되지 않으며 이 키 스페이스에 대해 적격 보조 액세스 방법을 사용할 수 없습니다. N1QL은 각 문서에 적용 가능한 필터를 적용합니다. 이 액세스 방법은 비용이 상당히 많이 들며 버킷의 문서 수에 따라 결과를 반환하는 평균 시간이 선형적으로 증가합니다.
c. 인덱스 스캔 액세스: 적격 보조 인덱스 스캔은 먼저 키스페이스를 필터링하고 적격 문서 ID를 결정하는 데 사용됩니다. 그런 다음 데이터 저장소에서 문서를 검색합니다. Couchbase에서 보조 인덱스는 VIEW 인덱스 또는 글로벌 보조 인덱스(GSI)일 수 있습니다.
조인 방법
N1QL은 모든 조인 지원에서 중첩 루프 액세스 방법을 지원합니다: INNER JOIN 및 LEFT OUTER JOIN. 다음은 조인 메서드에 대한 가장 간단한 설명입니다.
FROM (ORDERS o INNER JOIN CUSTOMER c ON KEYS o.O_C_ID)
이 조인에서 ORDERS는 INNER 키스페이스가 되고 CUSTOMER는 OUTER 키스페이스가 됩니다. ORDERS 키스페이스가 먼저 스캔됩니다(키스페이스 스캔 옵션 중 하나 사용). ORDERS의 각 적격 문서에 대해 ORDERS 문서의 키 O_C_D를 기준으로 CUSTOMER에서 키스캔을 수행합니다.
    조인 주문
FROM 절에 지정된 키 스페이스는 쿼리에 지정된 정확한 순서대로 조인됩니다.
이 블로그에서는 예제를 통해 플래너를 살펴보겠습니다. 여기서는 4개의 키 스페이스(버킷)를 사용하겠습니다. 이러한 버킷에 대한 예제 문서는 이 블로그의 마지막에 나와 있습니다. TPCC에 익숙한 분들은 이 표를 알아보실 수 있습니다.
이러한 각 키 공간에는 기본 키 인덱스와 다음과 같은 보조 인덱스가 있습니다.
gsi를 사용하여 CUSTOMER(C_ID, C_D_ID, C_W_ID)에 CU_ID_D_ID_W_ID 인덱스를 생성합니다;
gsi를 사용하여 STOCK(S_I_ID, S_W_ID)에 ST_W_ID,I_ID 인덱스를 생성합니다;
gsi를 사용하여 OR_O_ID_D_ID_W_ID 인덱스를 OR_O_ID(O_ID, O_D_ID, O_W_ID, O_C_ID)에 생성합니다;
gsi를 사용하여 ORDER_LINE(OL_O_ID, OL_D_ID, OL_W_ID)에 인덱스 OL_O_ID_D_ID_W_ID를 생성합니다;
gsi를 사용하여 ITEM(I_ID)에 인덱스 IT_ID를 생성합니다;
예 1:
문서 키를 알고 있는 경우 각 키 공간에 대해 USE KEYS 절로 지정합니다. USE KEYS 절을 지정하면 KEYSCAN 액세스 경로가 선택됩니다. 키가 주어지면 KEYSCAN은 각 키 공간에서 문서를 검색합니다. 노드 를 효율적으로 사용합니다. 특정 문서를 검색한 후 쿼리 노드는 c.C_STATE = "CA" 필터를 적용합니다.
cbq> EXPLAIN select * from CUSTOMER c USE KEYS ["110192", "120143", "827482"] WHERE c.C_STATE = "CA";
{
    "요청ID": “991e69d2-b6f9-42a1-9bd1-26a5468b0b5f”,
    "서명": "json",
    "결과": [
        {
            "1TP5운영자": "시퀀스",
            "~어린이": [
                {
                    "1TP5운영자": "키스캔",
                    "keys": "["110192", "120143", "827482"]"
                },
                {
                    "1TP5운영자": "병렬",
                    "~자식": {
                        "1TP5운영자": "시퀀스",
                        "~어린이": [
                            {
                                "1TP5운영자": "Fetch",
                                "as": "c",
                                "키스페이스": "고객",
                                "네임스페이스": "default"
                            },
                            {
                                "1TP5운영자": "필터",
                                "조건": "((c.C_STATE) = "CA")"
                            },
                            {
                                "1TP5운영자": "초기 프로젝트",
                                "결과_기간": [
                                    {
                                        "별": true
                                    }
                                ]
                            },
                            {
                                "1TP5운영자": "최종 프로젝트"
                            }
                        ]
                    }
                }
            ]
        }
    ],
    "상태": "성공",
    "metrics": {
        "elapsedTime": "10.311912ms",
        "실행 시간": "10.205523ms",
        "resultCount": 1,
        "결과 크기": 1403
    }
}
예 2: 
이 경우 쿼리는 CUSTOMER 버킷에 있는 모든 "CA" 고객을 계산하려고 합니다. 키-값에 대한 인덱스가 없으므로 키 공간(버킷)의 기본 스캔인 c.C_YTD_PAYMENT가 선택됩니다. c.C_YTD_PAYMENT < 100 필터링
은 문서가 검색된 후에 적용됩니다. 물론 큰 버킷의 경우 1차 스캔에는 시간이 걸립니다. 애플리케이션 성능 계획의 일환으로 필터 내에서 자주 사용되는 키 값에 대한 관련 보조 인덱스를 만드세요.
N1QL은 쿼리 실행 계획 내의 많은 단계를 병렬화합니다. 이 쿼리의 경우 쿼리 실행 내에서 가져오기 및 필터링 애플리케이션이 병렬화됩니다.
cbq> EXPLAIN SELECT c.C_STATE AS state, COUNT(*) AS st_count
             고객으로부터 c
             WHERE c.C_YTD_PAYMENT < 100
             상태별 그룹화
             ORDER BY st_count desc;
    "결과": [
        {
            "1TP5운영자": "시퀀스",
            "~어린이": [
                {
                    "1TP5운영자": "시퀀스",
                    "~어린이": [
                        {
                            "1TP5운영자": "PrimaryScan",
                            "index": "#primary",
                            "키스페이스": "고객",
                            "네임스페이스": "기본값",
                            "사용": "gsi"
                        },
                        {
                            "1TP5운영자": "병렬",
                            "~자식": {
                                "1TP5운영자": "시퀀스",
                                "~어린이": [
                                    {
                                        "1TP5운영자": "Fetch",
                                        "as": "c",
                                        "키스페이스": "고객",
                                        "네임스페이스": "default"
                                    },
                                    {
                                        "1TP5운영자": "필터",
                                        "조건": "((c.C_YTD_PAYMENT) u003c 100)"
                                    },
                                    {
                                        "1TP5운영자": "초기그룹",
                                        "집계": [
                                            "count(*)"
                                        ],
                                        "그룹_키": [
                                            “(c.상태)”
                                        ]
                                    },
                                    {
                                        "1TP5운영자": "중간 그룹",
                                        "집계": [
                                            "count(*)"
                                        ],
                                        "그룹_키": [
                                            “(c.상태)”
                                        ]
                                    }
                                ]
                            }
                        },
                        {
                            "1TP5운영자": "중간 그룹",
                            "집계": [
                                "count(*)"
                            ],
                            "그룹_키": [
                                “(c.상태)”
                            ]
                        },
                        {
                            "1TP5운영자": "최종그룹",
                            "집계": [
                                "count(*)"
                            ],
                            "그룹_키": [
                                “(c.상태)”
                            ]
                        },
                        {
                            "1TP5운영자": "병렬",
                            "~자식": {
                                "1TP5운영자": "시퀀스",
                                "~어린이": [
                                    {
                                        "1TP5운영자": "초기 프로젝트",
                                        "결과_기간": [
                                            {
                                                "as": "상태",
                                                "expr": "(c.C_STATE)”
                                            },
                                            {
                                                "as": "st_count",
                                                "expr": "count(*)"
                                            }
                                        ]
                                    }
                                ]
                            }
                        }
                    ]
                },
                {
                    "1TP5운영자": "주문",
                    "sort_terms": [
                        {
                            "desc": true,
                            "expr": "st_count
                        }
                    ]
                },
                {
                    "1TP5운영자": "병렬",
                    "~자식": {
                        "1TP5운영자": "최종 프로젝트"
                    }
                }
            ]
        }
    ],
예 3: 
이 예에서는 키 스페이스 ORDER_LINE을 ITEM과 조인합니다. ORDER_LINE의 각 적격 문서에 대해 ITEM과 일치시키려고 합니다. ON 절이 흥미롭습니다. 여기에서는 키 공간 ORDER_LINE(TO_STRING(ol.OL_I_ID))에 대한 키만 지정하고 ITEM에 대해서는 아무것도 지정하지 않습니다. 이는 암시적으로 ITEM의 문서 키와 결합되어 있기 때문입니다.
N1QL의 FROM 절입니다: ORDER_LINE ol INNER JOIN ITEM i
                  ON 키 (TO_STRING(ol.OL_I_ID))
SQL과 동일합니다: ORDER_LINE ol INNER JOIN ITEM i
         ON (TO_STRING(ol.OL_I_ID) = meta(ITEM).id)
필드가 문자열이 아닌 경우 TO_STRING() 표현식을 사용하여 문자열로 변환할 수 있습니다. 문서와 함께 여러 필드를 사용하여 문서 키를 구성할 수도 있습니다.
예: 주문에서 o 왼쪽 외부 조인 고객 c
     ON KEYS (TO_STRING(o.O_C_ID) || TO_STRING(o.O_D_ID))
요약하자면, N1QL에서 JOIN 쿼리를 작성할 때는 키 공간에서 문서 키가 어떻게 구성되는지 이해하는 것이 중요합니다. 결론은 데이터 모델링 중에 이러한 점을 고려해야 한다는 것입니다.
먼저, 사용 가능한 액세스 경로에 따라 주어진 필터 세트에 대해 ORDER_LINE 키 공간을 스캔하기 위해 플래너는 인덱스에서 인덱스 스캔을 선택합니다. OLO_ID_ID_W_ID. 앞서 설명한 것처럼, 조인의 다른 키 공간에 대한 액세스 경로는 항상 기본 키 인덱스를 사용하는 키스캔입니다. 이 계획에서는 먼저 ORDER_LINE 키 스페이스에서 인덱스 스캔을 수행하여 가능한 필터를 인덱스 스캔에 적용합니다. 그런 다음 적격 문서를 검색하고 추가 필터를 적용합니다. 문서가 적격하면 해당 문서는 ITEM과 결합됩니다.
cbq> EXPLAIN SELECT COUNT(DISTINCT(ol.OL_I_ID)) AS CNT_OL_I_ID
     FROM ORDER_LINE ol INNER JOIN ITEM i ON 키 (TO_STRING(ol.OL_I_ID))
     WHERE ol.OL_W_ID = 1
        AND ol.OL_D_ID = 10
        AND ol.OL_O_ID < 200
        AND ol.OL_O_ID >= 100
        AND ol.S_W_ID = 1
        AND i.I_PRICE < 10.00;
{
    "요청ID": “4e0822fb-0317-48a0-904b-74c607f77b2f”,
    "서명": "json",
    "결과": [
        {
            "1TP5운영자": "시퀀스",
            "~어린이": [
                {
                    "1TP5운영자": "IndexScan",
                    "index": "ol_o_id_d_id_w_id",
                    "키스페이스": "ORDER_LINE",
                    "limit": 9.223372036854776e+18,
                    "네임스페이스": "기본값",
                    "스팬": [
                        {
                            "범위": {
                                "높음": [
                                    “200”
                                ],
                                "포함": 1,
                                "낮음": [
                                    “100”
                                ]
                            },
                            "Seek": null
                        }
                    ],
                    "사용": "gsi"
                },
                {
                    "1TP5운영자": "병렬",
                    "~자식": {
                        "1TP5운영자": "시퀀스",
                        "~어린이": [
                            {
                                "1TP5운영자": "Fetch",
                                "as": "ol",
                                "키스페이스": "ORDER_LINE",
                                "네임스페이스": "default"
                            },
                            {
                                "1TP5운영자": "Join",
                                "as": "i",
                                "키스페이스": "ITEM",
                                "네임스페이스": "기본값",
                                "on_keys": "to_string((ol.OL_I_ID))”
                            },
                            {
                                "1TP5운영자": "필터",
                                "조건": "(((((((ol.OL_W_ID) = 1) 및 ((ol.OL_D_ID) = 10)) 및 ((ol.OL_O_ID) u003c 200)) 및 (100 u003c= (ol.OL_O_ID))) 및 ((ol.S_W_ID) = 1)) 및 ((i.I_PRICE) u003c 10))"
                            },
                            {
                                "1TP5운영자": "초기그룹",
                                "집계": [
                                    "count(distinct (ol.OL_I_ID))”
                                ],
                                "그룹_키": []
                            },
                            {
                                "1TP5운영자": "중간 그룹",
                                "집계": [
                                    "count(distinct (ol.OL_I_ID))”
                                ],
                                "그룹_키": []
                            }
                        ]
                    }
                },
                {
                    "1TP5운영자": "중간 그룹",
                    "집계": [
                        "count(distinct (ol.OL_I_ID))”
                    ],
                    "그룹_키": []
                },
                {
                    "1TP5운영자": "최종그룹",
                    "집계": [
                        "count(distinct (ol.OL_I_ID))”
                    ],
                    "그룹_키": []
                },
                {
                    "1TP5운영자": "병렬",
                    "~자식": {
                        "1TP5운영자": "시퀀스",
                        "~어린이": [
                            {
                                "1TP5운영자": "초기 프로젝트",
                                "결과_기간": [
                                    {
                                        "as": "CNT_OL_I_ID",
                                        "expr": "count(distinct (ol.OL_I_ID))”
                                    }
                                ]
                            },
                            {
                                "1TP5운영자": "최종 프로젝트"
                            }
                        ]
                    }
                }
            ]
        }
    ],
    "상태": "성공",
    "metrics": {
        "elapsedTime": "272.823508ms",
        "실행 시간": "272.71231ms",
        "resultCount": 1,
        "결과 크기": 4047
    }
}
문서 예시:
데이터는 다음의 수정된 스크립트에서 생성됩니다. https://github.com/apavlo/py-tpcc
고객
CUSTOMER 한도 1에서 * 메타(CUSTOMER).id를 PKID로 선택합니다;
    "결과": [
        {
            "CUSTOMER": {
                "C_BALANCE": -10,
                "C_CITY": "트조트무이브호프",
                "C_CREDIT": "GC",
                "c_credit_lim": 50000,
                "C_DATA": “sjlhfnvosawyjedregoctclndqzioadurtnlslwvuyjeowzedlvypsudcuerdzvdpsvjfecouyavnyyemivgrcyxxjsjcmkejvekzetxryhxjlhzkzajiaijammtyioheqfgtbhekdisjypxoymfsaepqkzbitdrpsjppivjatcwxxipjnloeqdswmogstqvkxlzjnffikuexjjofvhxdzleymajmifgzzdbdfvpwuhlujvycwlsgfdfodhfwiepafifbippyonhtahsbigieznbjrmvnjxphzfjuedxuklntghfckfljijfeyznxvwhfvnuhsecqxcmnivfpnawvgjjizdkaewdidhw”,
                "c_delivery_cnt": 0,
                "C_DISCOUNT": 0.3866,
                "C_D_ID": 10,
                "C_FIRST": "ujmduarngl",
                "C_ID": 1938,
                "C_LAST": "프레싱바",
                "C_MIDDLE": "OE",
                "C_PAYMENT_CNT": 1,
                "C_PHONE": "6347232262068241",
                "C_SINCE": "2015-03-22 00:50:42.822518",
                "C_STATE": "ta",
                "C_STREET_1": "데이로비르누크리",
                "C_STREET_2": "고즈주아크바브웨",
                "C_W_ID": 1,
                "C_YTD_PAYMENT": 10,
                "C_ZIP": "316011111"
            },
            "PKID": "1101938"
        }
    ],
ITEM
ITEM 한도 1에서 * 메타(ITEM).id를 PKID로 선택합니다;
    "결과": [
        {
            "ITEM": {
                "I_DATA": "DMNJRKNHCNRUJBTKRIRBDDKNXUXYFABOPHX",
                "I_ID": 10425,
                "I_IM_ID": 1013,
                "I_NAME": "aegfkkcbllssxxz",
                "I_PRICE": 60.31
            },
            "PKID": "10425"
        }
    ],
주문
메타(ORDERS).id를 PKID로, *를 ORDERS 제한 1에서 선택합니다;
    "결과": [
        {
            "ORDERS": {
                "O_ALL_LOCAL": 1,
                "O_CARRIER_ID": 2,
                "O_C_ID": 574,
                "O_D_ID": 10,
                "O_ENTRY_D": "2015-03-22 00:50:44.748030",
                "O_ID": 1244,
                "O_OL_CNT": 12,
                "O_W_ID": 1
            },
            "PKID": "1101244"
        }
    ],
cbq> select meta(ORDER_LINE).id as PKID, * from ORDER_LINE limit 1;
"결과": [
        {
            "ORDER_LINE": {
                "OL_AMOUNT": 0,
                "ol_delivery_d": "2015-03-22 00:50:44.836776",
                "ol_dist_info": "OIUKBNBCAZONUBTQZIUVDDI",
                "OL_D_ID": 10,
                "O_I_ID": 23522,
                "OL_NUMBER": 3,
                "OL_O_ID": 1389,
                "OL_QUANTITY": 5,
                "OL_SUPPLY_W_ID": 1,
                "OL_W_ID": 1
            },
            "PKID": "11013893"
        }
    ],
이 문서 공유하기
받은 편지함에서 카우치베이스 블로그 업데이트 받기
이 필드는 필수 입력 사항입니다.

작성자

게시자 케샤브 머시

케샤브 머시는 Couchbase R&D의 부사장입니다. 이전에는 MapR, IBM, Informix, Sybase에서 근무했으며 데이터베이스 설계 및 개발 분야에서 20년 이상의 경력을 쌓았습니다. IBM Informix에서 SQL 및 NoSQL R&D 팀을 이끌었습니다. Couchbase에서 두 번의 President's Club 상을, IBM에서 두 번의 뛰어난 기술 업적상을 수상했습니다. 인도 마이소르 대학교에서 컴퓨터 과학 및 공학 학사 학위를 받았으며, 24개의 미국 특허를 보유하고 있습니다.

댓글 하나

  1. 스리크리슈나 3월 6, 2016에서 6:06 오전

    CONTAINS, REGEXP_CONTAINS와 같은 문자열 및 REGEXP 함수에서 GSI를 만들려면 어떻게 해야 하나요?

  2. "쿼리 실행 내에서 가져오기 및 필터링 애플리케이션이 병렬 처리됩니다."를 이해하는 방법
    데이터를 가져온 후에 필터가 실행되어야 한다고 생각하나요?

댓글 남기기

카우치베이스 카펠라를 시작할 준비가 되셨나요?

구축 시작

개발자 포털에서 NoSQL을 살펴보고, 리소스를 찾아보고, 튜토리얼을 시작하세요.

카펠라 무료 사용

클릭 몇 번으로 Couchbase를 직접 체험해 보세요. Capella DBaaS는 가장 쉽고 빠르게 시작할 수 있는 방법입니다.

연락하기

카우치베이스 제품에 대해 자세히 알고 싶으신가요? 저희가 도와드리겠습니다.