N1QL(发音同 nickel)是Couchbase官方推出的类SQL查询语言,全称是Non-first Normal Form Query Language。它最大的特点是让开发者可以用接近标准SQL的语法去查询半结构化的JSON文档数据。对于熟悉MySQL、PostgreSQL这类关系型数据库的开发者来说,学习N1QL的曲线非常平缓,但JSON文档的灵活性也带来了一些传统SQL中没有的独特语法,比如嵌套路径访问、UNNEST数组展开等。本文将系统地讲解N1QL的基础语法和常用操作,帮助你快速上手Couchbase查询开发。

N1QL与传统SQL的核心区别
N1QL在设计上尽量保持与ANSI SQL的兼容性,大部分SELECT语句几乎可以原封不动地运行。但由于Couchbase是文档型数据库,数据以JSON文档的形式存储在Bucket(桶)中,而不是关系型的表,因此有一些概念上的映射需要先理清:Bucket对应数据库中的表概念,文档的主键对应_id或META().id,文档内部的字段可能是标量、对象或数组,这就决定了查询语法上会有所扩展。
最明显的一个区别是访问嵌套字段时使用点号路径。例如一个用户文档中包含address对象,address里又有city字段,在N1QL中可以直接写成u.address.city。而在传统SQL中,这种结构需要通过多表JOIN才能实现。此外,N1QL还支持在查询中动态处理大小写、类型判断等JSON特有的特性,比如TOSTRING()、IS MISSING等判断,这些都是关系型SQL中不存在的。
另一个重要区别是N1QL默认大小写敏感。字段名name和Name是两个不同的字段,这一点与MySQL默认的行为不同,开发时要特别注意JSON文档中字段命名的一致性。
基础查询:SELECT语句详解
SELECT是N1QL中使用频率最高的语句,基本结构与SQL一致。查询需要指定从哪个Bucket获取数据,并且N1QL强制要求使用USE KEYS或WHERE条件(配合索引)来定位文档。下面看几个由浅入深的例子:
-- 使用文档主键精确查询,效率最高 SELECT * FROM `travel-sample` USE KEYS ["airline_10"]; -- 查询指定字段,并对嵌套路径进行访问 SELECT name, address.city AS city FROM `travel-sample` WHERE type = "airline"; -- 使用LIKE进行模糊查询,LIKE默认区分大小写 SELECT name FROM `travel-sample` WHERE type = "airport" AND airportname LIKE "Los%"; -- ORDER BY排序加LIMIT分页 SELECT name, country FROM `travel-sample` WHERE type = "airline" ORDER BY country ASC, name DESC LIMIT 10 OFFSET 20;
上面的例子中,第一个查询使用了USE KEYS,这是N1QL特有的语法,直接通过文档主键获取数据,性能等同于键值操作,是最高效的查询方式。第三个例子中如果希望忽略大小写进行模糊匹配,可以使用LOWER(airportname) LIKE "los%"的方式处理。
需要注意的是,N1QL中字符串既可以用双引号也可以用单引号包裹,这一点比标准SQL更灵活。但为了避免歧义,建议标识符(Bucket名、字段名)统一用反引号包裹,字符串值统一用双引号包裹。
处理嵌套文档与数组:UNNEST与子文档查询
JSON文档天然支持嵌套结构,这是文档数据库的优势,也是N1QL语法扩展的重点。假设有一个订单文档,其中包含一个items数组,每个元素又是一个对象。要查询数组内部的数据,就需要用到UNNEST操作。
-- 示例文档结构:
-- { "orderId": "A001", "customer": "张三",
-- "items": [ {"product": "键盘", "qty": 2},
-- {"product": "鼠标", "qty": 1} ] }
-- UNNEST将数组展开为多行,类似于JOIN
SELECT o.orderId, o.customer, i.product, i.qty
FROM orders o
UNNEST o.items AS i
WHERE i.qty > 1;
-- 嵌套字段直接访问
SELECT orderId, customer, items[0].product AS firstProduct
FROM orders;
UNNEST的作用是把数组中的每个元素"拍平"成独立的行,这样数组内部的对象就可以像普通字段一样参与WHERE过滤和SELECT输出。如果只希望关联存在的数组元素,可以使用INNER UNNEST;默认的UNNEST行为在数组为空或缺失时不会过滤掉父文档。
除了UNNEST,N1QL还提供了丰富的数组函数,例如ARRAY_LENGTH()获取数组长度、ARRAY_AGG()聚合数组、以及ANY和EVERY谓词用于条件判断。比如要查询所有包含某个商品的订单,可以写成WHERE ANY i IN o.items SATISFIES i.product = "键盘" END,这种写法在数组较大时比UNNEST更高效,因为它不会展开行,只做存在性判断。
数据修改与索引优化
N1QL不仅支持查询,也支持完整的增删改操作。INSERT用于插入文档,UPSERT可以在主键存在时更新、不存在时插入,UPDATE和DELETE则用于修改与删除。下面是常见写法:
-- 插入单个文档
INSERT INTO orders (KEY, VALUE)
VALUES ("order_B002", {"orderId": "B002", "customer": "李四", "items": []});
-- UPSERT:存在则覆盖,不存在则创建
UPSERT INTO orders (KEY, VALUE)
VALUES ("order_B002", {"orderId": "B002", "customer": "李四", "total": 100});
-- 更新嵌套字段
UPDATE orders USE KEYS ["order_B002"]
SET total = 120, status = "paid";
-- 删除文档
DELETE FROM orders USE KEYS ["order_B002"];
性能方面,索引是N1QL查询优化的重中之重。没有合适索引的查询会触发全桶扫描,在数据量大时性能极差。创建索引使用CREATE INDEX语句,Couchbase默认会为每个Bucket创建一个主索引(#primary),但生产环境应尽量避免依赖主索引。
-- 创建普通二级索引 CREATE INDEX idx_airline_country ON `travel-sample`(country) WHERE type = "airline"; -- 复合索引 CREATE INDEX idx_orders_customer_date ON orders(customer, META().id); -- 查看执行计划验证索引是否命中 EXPLAIN SELECT name FROM `travel-sample` WHERE type = "airline" AND country = "France";
上面第一个索引使用了WHERE子句,这叫部分索引,只为特定类型的文档建立索引,可以显著减小索引体积。在日常开发中,建议养成用EXPLAIN查看查询计划的习惯,确认查询是否命中了预期索引。如果发现查询走了全扫描,通常需要调整索引结构或改写查询条件,让WHERE中的过滤条件与索引字段对齐。
总体而言,N1QL入门并不难,掌握SELECT基础语法后,重点攻破嵌套文档访问、UNNEST数组处理和索引设计这三块,就能应对大部分业务场景的查询需求。建议搭配Couchbase自带的Query Workbench多加练习,通过执行计划的反馈不断优化写法,逐步建立对文档型数据库查询的完整认知。