Escribí
antes sobre la necesidad de un nuevo tipo de SQL y la introducción de SQL para documentos (N1QL). En este blog, discutiremos la fase de planificación de consultas de N1QL en la próxima versión de Couchbase Sherlock. N1QL ha pasado por varias versiones preliminares para desarrolladores.
Consulte el tutorial en línea en https://query.couchbase.com Descargue la versión preliminar para desarrolladores de Couchbase Sherlock, que incluye SQL para documentos (N1QL).
¿Qué es N1QL? Es un moderno motor de procesamiento de consultas diseñado para proporcionar SQL para documentos (por ejemplo, JSON) en una arquitectura distribuida moderna con un modelo de datos flexible. Las bases de datos modernas se despliegan en clusters masivos y utilizan un modelo de datos flexible (JSON, familia de columnas, etc.). N1QL soporta SQL mejorado sobre este modelo de datos para facilitar el procesamiento de consultas.
Ejecución de consultas: Visión general
Las aplicaciones y sus controladores envían la consulta N1QL a uno de los nodos de consulta disponibles. El nodo de consulta analiza la consulta, utiliza metadatos sobre objetos subyacentes para averiguar el plan de ejecución óptimo, que luego ejecuta. Durante la ejecución, dependiendo de la consulta, utilizando los índices aplicables, el nodo de consulta trabaja con los nodos de índices y datos para recuperar y realizar las operaciones select-join-project. Dado que Couchbase es una base de datos en clúster, puedes escalar los nodos de datos, índice y consulta para adaptarlos a tus objetivos de rendimiento y disponibilidad.
Ejecución de consultas: Vista interior

Esta figura (gracias, al arquitecto de @N1QL Gerald Sangudi por las figuras) muestra todas las posibles fases por las que pasa una consulta para devolver los resultados. No todas las consultas necesitan pasar por todas las fases, algunas pasan por muchas de estas fases múltiples veces. Por ejemplo, la fase de ordenación puede omitirse cuando no hay una cláusula ORDER BY en la consulta; la fase scan-fetch-join varias veces para realizar múltiples uniones.

N1QL analiza la consulta y las opciones de ruta de acceso disponibles para cada espacio de claves (tabla o bucket) de la consulta para crear un plan de consulta y una infraestructura de ejecución. En primer lugar, el planificador debe seleccionar la ruta de acceso para cada bucket, determinar el orden de unión y, a continuación, determinar el tipo de unión. Una vez tomadas estas decisiones, el planificador crea la infraestructura necesaria para ejecutar el plan. Algunas operaciones, como el análisis sintáctico de la consulta y la planificación, se realizan en serie, mientras que otras, como la obtención, la unión y la ordenación, se realizan en paralelo. En este blog, nos centraremos en la breve descripción de la fase de planificación de consultas mediante ejemplos.
Selección de la ruta de acceso:
Opciones de ruta de acceso al espacio de claves (Bucket)
a. Acceso Keyscan. Cuando se dispone de ID de documentos específicos (claves), el método de acceso Keyscan recupera los documentos correspondientes a esos ID. A continuación, se aplica cualquier filtro a ese espacio de claves. El método de acceso Keyscan puede utilizarse cuando un espacio de claves se consulta por sí mismo o durante el procesamiento de uniones. El escaneo de claves se utiliza normalmente para recuperar los documentos que cumplen los requisitos del espacio de claves interno durante el procesamiento conjunto.
b. Acceso PrimaryScan: Equivale al escaneo completo de la tabla en los sistemas de bases de datos relacionales. No se indican los ID de los documentos y no se dispone de métodos de acceso secundarios cualificados para este espacio de claves. N1QL aplicará los filtros correspondientes a cada documento. Este método de acceso es bastante costoso y el tiempo medio de obtención de resultados aumenta linealmente con el número de documentos del bucket.
c. Acceso IndexScan: Se utiliza un escaneo de índice secundario cualificado para filtrar primero el espacio de claves y determinar los ID de los documentos cualificados. A continuación, recupera los documentos del almacén de datos. En Couchbase, el índice secundario puede ser un índice VIEW o un índice secundario global (GSI).
Métodos JOIN
N1QL soporta el método de acceso de bucle anidado para todos los soportes de joins: INNER JOIN y LEFT OUTER JOIN. Aquí está la explicación más simple del método join.
FROM (ORDERS o INNER JOIN CUSTOMER c ON KEYS o.O_C_ID)
Para esta unión, ORDERS se convierte en el espacio de claves INNER y CUSTOMER se convierte en el espacio de claves OUTER. Primero se escanea el espacio de claves de PEDIDOS (utilizando una de las opciones de escaneo de espacios de claves). Para cada documento calificado en ORDERS, hacemos un KEYSCAN en CUSTOMER basado en la llave O_C_D en el documento de ORDERS.
JOIN orden
Los espacios de claves especificados en la cláusula FROM se unen en el orden exacto indicado en la consulta.
En este blog, vamos a ver el planificador trabajando a través de ejemplos. Voy a utilizar 4 keyspaces (cubos) para estos. Al final de este blog se ofrecen ejemplos de documentos para estos cubos. Quienes estén familiarizados con TPCC reconocerán estas tablas.

Cada uno de estos espacios de claves tiene un índice de clave principal y los siguientes índices secundarios.
create index CU_ID_D_ID_W_ID on CLIENTE(C_ID, C_D_ID, C_W_ID) using gsi;
create index ST_W_ID,I_ID on STOCK(S_I_ID, S_W_ID) using gsi;
create index OR_O_ID_D_ID_W_ID on ORDERS(O_ID, O_D_ID, O_W_ID, O_C_ID) using gsi;
create index OL_O_ID_D_ID_W_ID on ORDER_LINE(OL_O_ID, OL_D_ID, OL_W_ID) using gsi;
create index IT_ID on ITEM(I_ID) using gsi;
Ejemplo 1:
Si conoce las claves del documento, especifíquelas con la cláusula USE KEYS para cada espacio de claves. Cuando se especifica una cláusula USE KEYS, se elige la ruta de acceso a KEYSCAN. Dadas las claves, KEYSCAN recuperará los documentos de los respectivos nodos eficientemente. Tras recuperar los documentos específicos, el nodo de consulta aplica el filtro c.C_STATE = "CA".
cbq> EXPLAIN select * from CLIENTE c USE KEYS ["110192", "120143", "827482"] WHERE c.C_STATE = "CA";
{
"requestID": “991e69d2-b6f9-42a1-9bd1-26a5468b0b5f”,
"signature": "json",
"resultados": [
{
"#operator": "Secuencia",
"~children": [
{
"#operator": "KeyScan",
"keys": "["110192", "120143", "827482"]"
},
{
"#operator": "Paralelo",
"~child": {
"#operator": "Secuencia",
"~children": [
{
"#operator": "Fetch",
"as": "c",
"keyspace": "CLIENTE",
"namespace": "default"
},
{
"#operator": "Filtro",
"condición": "((c.ESTADO_C) = "CA")"
},
{
"#operator": "InitialProject",
"result_terms": [
{
"estrella": true
}
]
},
{
"#operator": "FinalProject"
}
]
}
}
]
}
],
"estado": "éxito",
"métricas": {
"elapsedTime": "10.311912ms",
"executionTime": "10.205523ms",
"resultCount": 1,
"resultSize": 1403
}
}
Ejemplo 2:
En este caso, la consulta busca contar todos los clientes de "CA" en el bucket CLIENTE. Como no tenemos un índice en el valor clave c.C_YTD_PAYMENT, se elige un escaneo primario del espacio clave (bucket). Filtro c.C_YTD_PAYMENT < 100
se aplica después de recuperar el documento. Obviamente, para buckets más grandes el escaneo primario lleva tiempo. Como parte de la planificación del rendimiento de la aplicación, cree índices secundarios relevantes sobre los valores clave más utilizados dentro de los filtros.
N1QL paraleliza muchas de las fases dentro del plan de ejecución de la consulta. Para esta consulta, las aplicaciones de obtención y filtrado se paralelizan dentro de la ejecución de la consulta.
cbq> EXPLAIN SELECT c.C_STATE AS state, COUNT(*) AS st_count
DE CLIENTE c
WHERE c.C_YTD_PAYMENT < 100
GROUP BY estado
ORDER BY st_count desc;
"resultados": [
{
"#operator": "Secuencia",
"~children": [
{
"#operator": "Secuencia",
"~children": [
{
"#operator": "PrimaryScan",
"index": "#primary",
"keyspace": "CLIENTE",
"namespace": "default",
"using": "gsi"
},
{
"#operator": "Paralelo",
"~child": {
"#operator": "Secuencia",
"~children": [
{
"#operator": "Fetch",
"as": "c",
"keyspace": "CLIENTE",
"namespace": "default"
},
{
"#operator": "Filtro",
"condición": "((c.C_YTD_PAYMENT) u003c 100)"
},
{
"#operator": "InitialGroup",
"agregados": [
"count(*)"
],
"group_keys": [
“(c.estado)”
]
},
{
"#operator": "IntermediateGroup",
"agregados": [
"count(*)"
],
"group_keys": [
“(c.estado)”
]
}
]
}
},
{
"#operator": "IntermediateGroup",
"agregados": [
"count(*)"
],
"group_keys": [
“(c.estado)”
]
},
{
"#operator": "FinalGroup",
"agregados": [
"count(*)"
],
"group_keys": [
“(c.estado)”
]
},
{
"#operator": "Paralelo",
"~child": {
"#operator": "Secuencia",
"~children": [
{
"#operator": "InitialProject",
"result_terms": [
{
"como": "estado",
"expr": "(c.ESTADO_C)”
},
{
"as": "st_count",
"expr": "count(*)"
}
]
}
]
}
}
]
},
{
"#operator": "Orden",
"sort_terms": [
{
"desc": true,
"expr": "st_count“
}
]
},
{
"#operator": "Paralelo",
"~child": {
"#operator": "FinalProject"
}
}
]
}
],
Ejemplo 3:
En este ejemplo, unimos el espacio clave ORDER_LINE con ITEM. Para cada documento calificado en ORDER_LINE, queremos que coincida con ITEM. La cláusula ON es interesante. Aquí, sólo se especifican las claves para el espacio de claves ORDER_LINE (TO_STRING(ol.OL_I_ID)) y nada para ITEM. Eso es porque está implícitamente unido con la clave de documento del ITEM.
Cláusula FROM de N1QL: ORDER_LINE ol INNER JOIN ITEM i
ON KEYS (TO_STRING(ol.OL_I_ID))
Es equivalente a SQL ORDER_LINE ol INNER JOIN ITEM i
ON (TO_STRING(ol.OL_I_ID) = meta(ITEM).id)
Si el campo no es una cadena, puede convertirse en cadena utilizando la expresión TO_STRING(). También puede construir la clave del documento utilizando varios campos con el documento.
p.ej. FROM ORDERS o LEFT OUTER JOIN CUSTOMER c
ON KEYS (TO_STRING(o.O_C_ID) || TO_STRING(o.O_D_ID))
En resumen, al escribir consultas JOIN en N1QL, es importante entender cómo se construye la clave del documento en el espacio de claves. Corolario es, es pensar en esto durante el modelado de datos.
En primer lugar, para escanear el espacio de claves ORDER_LINE, para el conjunto de filtros dado, basándose en la ruta de acceso disponible, el planificador elige el escaneo de índice en el índice OL_O_ID_D_ID_W_ID. Como hemos comentado antes, la ruta de acceso al otro espacio clave en la unión siempre se escanea utilizando el índice de clave primaria. En este plan, primero hacemos el escaneo de índice en el espacio clave ORDER_LINE empujando hacia abajo los posibles filtros al escaneo de índice. Luego recuperamos el documento calificado, aplicamos filtros adicionales. Si el documento califica, ese documento se une con ITEM.
cbq> EXPLAIN SELECT COUNT(DISTINCT(ol.OL_I_ID)) AS CNT_OL_I_ID
FROM ORDER_LINE ol INNER JOIN ITEM i ON KEYS (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;
{
"requestID": “4e0822fb-0317-48a0-904b-74c607f77b2f”,
"signature": "json",
"resultados": [
{
"#operator": "Secuencia",
"~children": [
{
"#operator": "IndexScan",
"index": "OL_O_ID_D_ID_W_ID",
"keyspace": "ORDER_LINE",
"límite": 9.223372036854776e+18,
"namespace": "default",
"spans": [
{
"Rango": {
"Alto": [
“200”
],
"Inclusión": 1,
"Bajo": [
“100”
]
},
"Buscar": null
}
],
"using": "gsi"
},
{
"#operator": "Paralelo",
"~child": {
"#operator": "Secuencia",
"~children": [
{
"#operator": "Fetch",
"as": "ol",
"keyspace": "ORDER_LINE",
"namespace": "default"
},
{
"#operator": "Join",
"as": "i",
"keyspace": "ITEM",
"namespace": "default",
"on_keys": "to_string((ol.OL_I_ID))”
},
{
"#operator": "Filtro",
"condition": "(((((((ol.OL_W_ID) = 1) y ((ol.OL_D_ID) = 10)) y ((ol.OL_O_ID) u003c 200)) y (100 u003c= (ol.OL_O_ID))) y ((ol.S_W_ID) = 1)) y ((i.PRECIOS) u003c 10))"
},
{
"#operator": "InitialGroup",
"agregados": [
" count(distinct (ol.OL_I_ID))”
],
"group_keys": []
},
{
"#operator": "IntermediateGroup",
"agregados": [
" count(distinct (ol.OL_I_ID))”
],
"group_keys": []
}
]
}
},
{
"#operator": "IntermediateGroup",
"agregados": [
" count(distinct (ol.OL_I_ID))”
],
"group_keys": []
},
{
"#operator": "FinalGroup",
"agregados": [
" count(distinct (ol.OL_I_ID))”
],
"group_keys": []
},
{
"#operator": "Paralelo",
"~child": {
"#operator": "Secuencia",
"~children": [
{
"#operator": "InitialProject",
"result_terms": [
{
"as": "CNT_OL_I_ID",
"expr": "count(distinct (ol.OL_I_ID))”
}
]
},
{
"#operator": "FinalProject"
}
]
}
}
]
}
],
"estado": "éxito",
"métricas": {
"elapsedTime": "272.823508ms",
"executionTime": "272.71231ms",
"resultCount": 1,
"resultSize": 4047
}
}
Ejemplos de documentos:
Los datos se generan a partir de scripts modificados de: https://github.com/apavlo/py-tpcc
CLIENTE
select meta(CLIENTE).id as PKID, * from CLIENTE limit 1;
"resultados": [
{
"CLIENTE": {
"C_BALANCE": -10,
"C_CITY": "ttzotwmuivhof",
"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": "PRESEINGBAR",
"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": "deilobyrnukri",
"C_STREET_2": "goziejuaqbbwe",
"C_W_ID": 1,
"C_YTD_PAYMENT": 10,
"C_ZIP": "316011111"
},
"PKID": "1101938"
}
],
PUNTO
select meta(ITEM).id as PKID, * from ITEM limit 1;
"resultados": [
{
"ITEM": {
"I_DATA": "dmnjrkhncnrujbtkrirbddknxuxiyfabopmhx",
"I_ID": 10425,
"I_IM_ID": 1013,
"I_NAME": "aegfkkcbllssxxz",
"I_PRICE": 60.31
},
"PKID": "10425"
}
],
PEDIDOS
select meta(PEDIDOS).id as PKID, * from PEDIDOS limit 1;
"resultados": [
{
"PEDIDOS": {
"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;
"resultados": [
{
"ORDER_LINE": {
"OL_AMOUNT": 0,
"OL_DELIVERY_D": "2015-03-22 00:50:44.836776",
"OL_DIST_INFO": "oiukbnbcazonubtqziuvcddi",
"OL_D_ID": 10,
"OL_I_ID": 23522,
"OL_NUMBER": 3,
"OL_O_ID": 1389,
"OL_QUANTITY": 5,
"OL_SUPPLY_W_ID": 1,
"OL_W_ID": 1
},
"PKID": "11013893"
}
],
cómo podemos crear GSI en STRING Y FUNCIONES REXEXP COMO CONTAINS,REGEXP_CONTAINS
Cómo entender \"las aplicaciones fetch y filter se paralelizan dentro de la ejecución de la consulta.\"
¿Creo que el filtro debe ejecutarse después de obtener los datos?