Une application peut utiliser des requêtes pour rechercher dans le datastore des entités correspondant à des critères de recherche spécifiques, appelés filtres.
Présentation
Une application peut utiliser des requêtes pour rechercher dans le datastore des entités correspondant à des critères de recherche spécifiques, appelés filtres. Par exemple, une application qui effectue le suivi de plusieurs livres d'or pourrait utiliser une requête pour récupérer les messages d'un livre d'or, classés par date :
...
...
Certaines requêtes sont plus complexes que d'autres ; le datastore a besoin d'index intégrés pour ces dernières.
Ces index intégrés sont spécifiés dans un fichier de configuration nommé index.yaml
.
Sur le serveur de développement, si vous exécutez une requête nécessitant un index que vous n'avez pas spécifié, le serveur l'ajoute automatiquement à son fichier index.yaml
.
Cependant, sur votre site Web, une requête nécessitant un index non encore spécifié échoue.
Par conséquent, le cycle de développement type consiste à essayer une nouvelle requête sur le serveur de développement, puis à mettre à jour le site Web de manière à utiliser le fichier index.yaml
automatiquement modifié.
Vous pouvez mettre à jour le fichier index.yaml
indépendamment de l'importation de l'application en exécutant la commande gcloud app deploy index.yaml
.
Si votre datastore comporte de nombreuses entités, la création d'un index à y associer prend beaucoup de temps. Dans ce cas, il peut être utile de mettre à jour les définitions d'index avant de transférer du code utilisant le nouvel index.
Vous pouvez utiliser la console d'administration pour savoir quand la création des index sera terminée.
App Engine Datastore prend en charge de manière native les filtres pour les correspondances exactes (l'opérateur ==) et les comparaisons (les opérateurs <, <=,> et> =).
Il prend en charge la combinaison de plusieurs filtres à l'aide d'une opération booléenne AND
, avec certaines restrictions (voir ci-dessous).
En plus des opérateurs natifs, l'API accepte l'opérateur !=
, la combinaison de groupes de filtres à l'aide de l'opération booléenne OR
et l'opération IN
, qui teste l'égalité à l'une des valeurs possibles (comme l'opérateur "in" de Python).
Ces opérations n'effectuent pas un mappage 1:1 sur les opérations natives du datastore et sont, par conséquent, un peu décalées et relativement lentes.
Elles sont mises en œuvre à l'aide de la fusion en mémoire des flux de résultats. Notez que p != v
est mis en œuvre sous la forme "p < v OR p > v".
(Ceci est important pour les propriétés répétées.)
Restrictions : Le datastore applique certaines restrictions aux requêtes. La violation de ces règles entraîne des exceptions. Par exemple, il est actuellement interdit de combiner trop de filtres, d'utiliser des inégalités pour plusieurs propriétés ou de combiner une inégalité avec un ordre de tri sur une propriété différente. De plus, les filtres faisant référence à plusieurs propriétés nécessitent parfois la configuration d'index secondaires.
Non pris en charge : Le datastore n'accepte pas de correspondances de sous-chaînes, les correspondances non sensibles à la casse ou la recherche dite en texte intégral. Il existe des moyens de mettre en œuvre des correspondances non sensibles à la casse, voire une recherche en texte intégral, à l'aide de propriétés calculées.
Filtrer par valeurs de propriété
Rappelez la classe Account à partir des propriétés NDB :
Généralement, vous ne souhaitez pas récupérer toutes les entités d'un genre donné. Seules celles ayant une valeur spécifique ou une plage de valeurs pour une certaine propriété vous intéresse.
Les objets de propriété surchargent certains opérateurs pour le renvoi des expressions de filtre pouvant être utilisées pour contrôler une requête. Par exemple, pour rechercher toutes les entités de compte dont la propriété userid a exactement la valeur 42, vous pouvez utiliser l'expression :
(Si vous êtes sûr qu'il n'y a qu'un Account
associé à ce userid
, vous préférerez peut-être utiliser userid
comme clé.
Account.get_by_id(...)
est plus rapide que Account.query(...).get()
.)
NDB est compatible avec les opérations suivantes :
property == value
property < value
property <= value
property > value
property >= value
property != value
property.IN([value1, value2])
Pour filtrer sur une inégalité, vous pouvez utiliser la syntaxe suivante :
Cela permet de rechercher toutes les entités Account dont la propriété userid
est supérieure ou égale à 40.
Deux de ces opérations, != et IN, sont mises en œuvre comme des combinaisons des autres, et sont un peu bizarres, comme décrit dans Opérations != et IN.
Vous pouvez spécifier plusieurs filtres :
Cela combine les arguments de filtre spécifiés en renvoyant toutes les entités du compte, dont la valeur d'identifiant est supérieure ou égale à 40 et inférieure à 50.
Remarque : Comme indiqué précédemment, le datastore rejette les requêtes en filtrant sur les inégalités d'une propriété.
Au lieu de spécifier un filtre de requête entier dans une seule expression, il peut être plus commode de le créer par étapes. Par exemple :
query3
équivaut à la variable query
de l'exemple précédent. Notez que les objets de requête étant immuables, la construction de query2
n'affecte pas query1
et celle de query3
n'affecte ni query1
, ni query2
.
Opérations != et IN
Rappelez la classe Article à partir des propriétés NDB :
Les opérations !=
(inégalité) et IN
(appartenance) sont mises en œuvre en combinant d'autres filtres utilisant l'opération OR
. La première d'entre elles,
property != value
est mise en œuvre sous la forme
(property < value) OR (property > value)
Par exemple,
équivaut à
Remarque : Contre toute attente, cette requête ne recherche pas les entités Article
n'incluant pas "perl" en tant que tag.
Elle trouve, au contraire, toutes les entités dont au moins un tag est différent de "perl".
Par exemple, l'entité suivante serait incluse dans les résultats, même si l'un de ses tags est "perl" :
Cependant, cet élément ne serait pas inclus :
Il n'y a aucun moyen de rechercher des entités n'incluant pas de tag égal à "perl".
De même, l'opération IN
property IN [value1, value2, ...]
qui teste l'appartenance à une liste de valeurs possibles, est mise en œuvre comme suit :
(property == value1) OR (property == value2) OR ...
Par exemple,
équivaut à
Remarque : Les requêtes utilisant OR
dédupliquent leurs résultats. Le flux de résultats n'inclut pas l'entité plusieurs fois, même si elle correspond à deux sous-requêtes ou plus.
Interroger des propriétés répétées
La classe Article
définie dans la section précédente sert également d'exemple pour l'interrogation de propriétés répétées. Un filtre, tel que
utilise une valeur unique, même si Article.tags
est une propriété répétée. Vous ne pouvez pas comparer des propriétés répétées aux objets d'une liste (le datastore ne le comprendra pas). Par ailleurs, un filtre comme
ne recherche pas du tout les entités Article
dont la valeur des tags correspond à la liste ['python', 'ruby', 'php']
, mais recherche les entités dont la valeur tags
(considérée comme une liste) contient au moins une de ces valeurs.
L'interrogation d'une valeur None
sur une propriété répétée a un comportement indéfini. C'est la raison pour laquelle il est déconseillé d'utiliser cette méthode.
Combiner des opérations AND et OR
Vous pouvez imbriquer des opérations AND
et OR
de manière arbitraire.
Exemple :
Cependant, en raison de la mise en œuvre de OR
, une requête de ce formulaire trop complexe peut échouer en indiquant une exception. Il est plus prudent de normaliser ces filtres afin qu'il y ait (tout au plus) une seule opération OR
en haut de l'arborescence des expressions et un seul niveau d'opérations AND
en dessous.
Pour effectuer cette normalisation, vous devez vous rappeler à la fois des règles de la logique booléenne et du fonctionnement réel de la mise en œuvre des filtres !=
et IN
:
- Développez les opérateurs
!=
etIN
selon leur forme primitive, où!=
permet de vérifier que la propriété est < ou > à la valeur, etIN
permet de vérifier que la propriété est == à la première, à la seconde ou à la suivante et ainsi de suite jusqu'à la toute dernière valeur de la liste. - Un opérateur
AND
comportant unOR
est équivalent à un opérateurOR
de plusieursAND
appliqués aux opérandesAND
d'origine, avec un seul opérandeOR
substitué auOR
d'origine. Par exemple,AND(a, b, OR(c, d))
correspond àOR(AND(a, b, c), AND(a, b, d))
. - Un opérateur
AND
contenant un opérande qui est lui-même une opérationAND
peut intégrer les opérandes de l'opérationAND
imbriquée dans l'opérandeAND
qui l'englobe. Par exemple,AND(a, b, AND(c, d))
correspond àAND(a, b, c, d)
. - Un opérateur
OR
contenant un opérande qui est lui-même une opérationOR
peut intégrer les opérandes de l'opérationOR
imbriquée dans l'opérandeOR
qui l'englobe. Par exemple,OR(a, b, OR(c, d))
correspond àOR(a, b, c, d)
.
Si vous appliquez ces transformations par étapes à l'exemple de filtre, en utilisant une notation plus simple que celle de Python, vous obtenez ce qui suit :
- Utilisation de la règle n°1 sur les opérateurs
IN
et!=
:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', OR(tags < 'perl', tags > 'perl'))))
- Utilisation de la règle n°2 sur le
OR
imbriqué le plus profondément dans unAND
:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', OR(AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl'))))
- Utilisation de la règle n°4 sur le
OR
imbriqué dans un autreOR
:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl')))
- Utilisation de la règle n°2 sur le
OR
restant imbriqué dans unAND
:OR(AND(tags == 'python', tags == 'ruby'), AND(tags == 'python', tags == 'jruby'), AND(tags == 'python', AND(tags == 'php', tags < 'perl')), AND(tags == 'python', AND(tags == 'php', tags > 'perl')))
- Utilisation de la règle n°3 pour réduire les
AND
imbriqués restants :OR(AND(tags == 'python', tags == 'ruby'), AND(tags == 'python', tags == 'jruby'), AND(tags == 'python', tags == 'php', tags < 'perl'), AND(tags == 'python', tags == 'php', tags > 'perl'))
Attention : Pour certains filtres, cette normalisation peut provoquer une explosion combinatoire. Examinons le AND
de trois clauses OR
comportant deux clauses de base chacune.
En cas de normalisation, il devient un OR
de huit clauses AND
comportant trois clauses de base chacune. Autrement dit, on passe de six termes à 24.
Spécifier des ordres de tri
Vous pouvez utiliser la méthode order()
pour spécifier l'ordre dans lequel une requête renvoie ses résultats. Cette méthode prend une liste d'arguments, chacun étant soit un objet de propriété (à trier par ordre croissant), soit sa négation (indiquant un ordre décroissant). Exemple :
Toutes les entités Greeting
sont récupérées, triées par valeur croissante de leur propriété content
.
Les exécutions d'entités consécutives ayant la même propriété de contenu sont triées par valeur décroissante de leur propriété date
.
Vous pouvez utiliser plusieurs appels order()
pour obtenir le même effet :
Remarque : Lorsque vous combinez des filtres avec order()
, Datastore rejette certaines combinaisons.
En particulier, lorsque vous utilisez un filtre d'inégalité, le premier ordre de tri (le cas échéant) doit spécifier la même propriété que le filtre.
En outre, vous devez parfois configurer un index secondaire.
Requêtes ascendantes
Les requêtes ascendantes permettent d'effectuer des requêtes à forte cohérence vers le datastore. Toutefois, les entités ayant le même ancêtre sont limitées à 1 écriture par seconde. Voici une comparaison simple des compromis et de la structure entre une requête ascendante et une requête non ascendante utilisant des clients et leurs achats associés dans le datastore.
L'exemple de requête non ascendante suivant comporte une entité dans le datastore pour chaque Customer
et une entité dans le datastore pour chaque Purchase
, avec une KeyProperty
pointant vers le client.
Pour rechercher tous les achats appartenant au client, vous pouvez utiliser la requête suivante :
Dans ce cas, le datastore offre un débit en écriture élevé, mais seulement une cohérence à terme. Si un nouvel achat est ajouté, les données obtenues peuvent être obsolètes. Vous pouvez éliminer ce problème en utilisant des requêtes ascendantes.
Pour les clients et les achats avec des requêtes ascendantes, vous conservez la même structure avec deux entités distinctes. La partie client est la même. Cependant, lorsque vous créez des achats, vous n'avez plus besoin de spécifier KeyProperty()
pour les achats. En effet, lorsque vous utilisez des requêtes ascendantes, vous appelez la clé de l'entité client lors de la création d'une entité d'achat.
Chaque achat comporte une clé, ainsi que le client. Cependant, chaque clé d'achat comportera la clé customer_entity. Notez que cela se limite à une écriture par ancêtre par seconde. Ce qui suit permet de créer une entité avec un ancêtre :
Pour interroger les achats d'un client donné, utilisez la requête suivante.
Attributs de requête
Les objets de requête comportent les attributs de données en lecture seule suivants :
Attribut | Type | Par défaut | Description |
---|---|---|---|
kind | str | None | Nom du genre (généralement le nom de la classe) |
ancestor | Key | None | Ancêtre spécifié pour la requête |
filters | FilterNode | None | Expression de filtre |
orders | Order | None | Ordres de tri |
Le fait d'imprimer un objet de requête (ou d'appeler str()
ou repr()
sur celui-ci) produit une représentation sous forme de chaîne bien formatée :
Filtrer par valeur de propriété structurée
Une requête peut filtrer directement sur les valeurs de champ de propriétés structurées.
Par exemple, une requête pour tous les contacts ayant une adresse dont la ville est 'Amsterdam'
ressemblerait à ce qui suit :
Si vous combinez plusieurs filtres de ce type, ceux-ci peuvent correspondre à différentes sous-entités Address
au sein de la même entité Contact.
Exemple :
peut trouver des contacts ayant une adresse dont la ville est 'Amsterdam'
et une autre adresse dont la rue est 'Spear St'
. Toutefois, au moins pour les filtres d'égalité, vous pouvez créer une requête qui renvoie uniquement les résultats contenant plusieurs valeurs dans une même sous-entité :
Si vous utilisez cette technique, les propriétés de la sous-entité égales à None
sont ignorées dans la requête.
Si une propriété a une valeur par défaut, vous devez la définir explicitement sur None
pour l'ignorer dans la requête. Dans le cas contraire, la requête inclut un filtre impliquant que la valeur de la propriété soit égale à la valeur par défaut.
Par exemple, si le modèle Address
avait une propriété country
avec default='us'
, l'exemple ci-dessus ne renverrait que les contacts dont le pays est 'us'
. Pour inclure les contacts d'autres pays, vous devez utiliser le filtre Address(city='San Francisco', street='Spear St',
country=None)
.
Si une sous-entité a des valeurs de propriété égales à None
, elles sont ignorées. Par conséquent, il est inutile de filtrer sur une valeur de propriété de sous-entité égale à None
.
Utiliser des propriétés nommées par chaîne
Parfois, vous souhaitez filtrer ou trier une requête en fonction d'une propriété dont le nom est spécifié par chaîne. Par exemple, si vous laissez l'utilisateur entrer des requêtes de recherche comme tags:python
, il serait intéressant de les transformer comme suit :
Article.query(Article."tags" == "python") # does NOT work
Si le modèle est un Expando
, alors le filtre peut utiliser GenericProperty
, la classe utilisée par le genre Expando
pour les propriétés dynamiques :
L'utilisation de GenericProperty
fonctionne également si votre modèle n'est pas un Expando
. Toutefois, si vous voulez vous assurer que vous n'utilisez que des noms de propriété définis, vous pouvez également utiliser l'attribut de classe _properties
ou utiliser getattr()
pour l'obtenir depuis la classe :
La différence réside dans le fait que getattr()
utilise le "nom Python" de la propriété, tandis que _properties
est indexé selon le "nom du datastore" de la propriété. Ceux-ci ne diffèrent que lorsque la propriété a été déclarée comme suit :
Ici, le nom Python est title
, mais celui du datastore est t
.
Ces approches fonctionnent également pour le tri des résultats de requête :
Itérateurs de requêtes
Lorsqu'une requête est en cours, son état est conservé dans un objet itérateur. (La plupart des applications ne les utilisent pas directement. Il est normalement plus simple d'appeler fetch(20)
que de manipuler l'objet itérateur.)
Il existe deux méthodes de base pour obtenir un tel objet :
- Utiliser la fonction
iter()
intégrée de Python sur un objetQuery
- Appeler la méthode
iter()
de l'objetQuery
La première permet d'utiliser une boucle for
Python (qui appelle implicitement la fonction iter()
) pour effectuer une boucle sur une requête.
La seconde façon qui consiste à utiliser la méthode iter()
de l'objet Query
permet de transmettre des options à l'itérateur afin d'affecter son comportement. Par exemple, pour utiliser une requête contenant uniquement des clés dans une boucle for
, vous pouvez écrire ceci :
Les itérateurs de requêtes ont d'autres méthodes utiles :
Méthode | Description |
---|---|
__iter__()
| Fait partie du protocole d'itération de Python. |
next()
| Renvoie le résultat suivant ou déclenche l'exception StopIteration en l'absence de résultat. |
has_next()
| Renvoie True si un appel next() ultérieur renvoie un résultat, False s'il déclenche StopIteration .Bloque jusqu'à ce que la réponse à cette question soit connue et met en mémoire tampon le résultat (le cas échéant) jusqu'à ce que vous le récupériez avec next() .
|
probably_has_next()
| Semblable à has_next() , mais utilise un raccourci plus rapide (et parfois inexact).Peut renvoyer un faux positif ( True si next() déclenche réellement StopIteration ), mais jamais un faux négatif (False si next() renvoie réellement un résultat).
|
cursor_before()
| Renvoie un curseur de requête représentant un point juste avant le renvoi du dernier résultat. Déclenche une exception si aucun curseur n'est disponible (en particulier, si l'option de requête produce_cursors n'a pas été transmise).
|
cursor_after()
| Renvoie un curseur de requête représentant un point juste après le renvoi du dernier résultat. Déclenche une exception si aucun curseur n'est disponible (en particulier, si l'option de requête produce_cursors n'a pas été transmise).
|
index_list()
| Renvoie la liste des index utilisés par une requête exécutée, y compris les index primaires, composites, de types et à propriété unique. |
Curseurs de requêtes
Un curseur de requête est une petite structure de données opaque représentant un point de reprise dans une requête. Celui-ci est utile pour montrer à un utilisateur une page de résultats à la fois. Il est également utile pour gérer les travaux longs qui doivent éventuellement être arrêtés et repris.
On l'utilise généralement avec la méthode fetch_page()
d'une requête.
Il fonctionne un peu comme fetch()
, à la différence qu'il renvoie un triple (results, cursor, more)
.
L'option more
renvoyée indique qu'il y a probablement plus de résultats. Une interface utilisateur peut utiliser ceci, par exemple, pour supprimer un bouton ou un lien "Page suivante".
Pour demander les pages suivantes, transmettez le curseur renvoyé par un appel à fetch_page()
au suivant. Une erreur BadArgumentError
est déclenchée si vous transmettez un curseur non valide. Notez que la validation vérifie uniquement si la valeur est codée en base64. Vous devrez effectuer toute autre validation nécessaire.
Ainsi, pour permettre à l'utilisateur de voir toutes les entités correspondant à une requête, en procédant une page à la fois, votre code pourrait ressembler à ceci :
...
Notez l'utilisation de urlsafe()
et Cursor(urlsafe=s)
pour sérialiser et désérialiser le curseur.
Cela permet de transmettre un curseur à un client sur le Web dans la réponse à une requête et de le recevoir du client ultérieurement.
Remarque : La méthode fetch_page()
renvoie généralement un curseur même s'il n'y a plus de résultats, mais cela n'est pas garanti. En effet, la valeur de curseur renvoyée peut être None
. Notez également que, dans la mesure où l'option more
est mise en œuvre à l'aide de la méthode probably_has_next()
de l'itérateur, il se peut que, dans de rares circonstances, elle renvoie True
même si la page suivante est vide.
Certaines requêtes NDB ne prennent pas en charge les curseurs de requête, toutefois vous pouvez les réparer.
Si une requête utilise IN
, OR
ou !=
, les résultats de la requête ne fonctionnent pas avec les curseurs à moins d'être triés par clé.
Si une application ne trie pas les résultats par clé et appelle fetch_page()
, elle obtient une erreur BadArgumentError
.
Si
User.query(User.name.IN(['Joe', 'Jane'])).order(User.name).fetch_page(N)
obtient une erreur, remplacez-le par :
User.query(User.name.IN(['Joe', 'Jane'])).order(User.name, User.key).fetch_page(N)
Au lieu de "parcourir" les résultats d'une requête, vous pouvez utiliser la méthode iter()
d'une requête pour obtenir un curseur à un point précis.
Pour ce faire, transmettez produce_cursors=True
à iter()
. Lorsque l'itérateur se trouve au bon endroit, appelez sa méthode cursor_after()
pour obtenir un curseur situé juste à sa suite. (Ou appelez cursor_before()
pour placer un curseur juste avant.)
Notez que l'appel à cursor_after()
ou cursor_before()
peut entraîner un appel bloquant de Datastore à cause de la réexécution d'une partie de la requête afin d'extraire un curseur qui pointe vers le milieu d'un lot.
Pour utiliser un curseur pour parcourir les résultats de la requête dans le sens inverse, créez une requête inverse :
Appeler une fonction pour chaque entité ("mappage")
Supposons que vous deviez obtenir les entités Account
correspondant aux entités Message
renvoyées par une requête.
Vous pourriez écrire quelque chose de semblable à ceci :
Cependant, cette méthode est plutôt inefficace, dans la mesure où elle attend de récupérer une entité pour l'utiliser et ainsi de suite. Le temps d'attente est important. Une autre méthode consiste à écrire une fonction de rappel mappée sur les résultats de la requête :
Cette version fonctionne un peu plus rapidement que la boucle for
simple ci-dessus, dans la mesure où elle offre une certaine simultanéité.
Cependant, comme l'appel à get()
dans callback()
est toujours synchrone, le gain n'est pas énorme.
C'est à ce moment qu'il convient d'utiliser les requêtes Get asynchrones.
GQL
GQL est un langage de type SQL permettant de récupérer des entités ou des clés depuis App Engine Datastore. Alors que les fonctionnalités de GQL diffèrent de celles d'un langage de requête pour base de données relationnelle classique, la syntaxe GQL est semblable à celle de SQL. La syntaxe GQL est décrite dans la documentation de référence de GQL.
Vous pouvez utiliser GQL pour créer des requêtes. Cette opération s'apparente à la création d'une requête avec Model.query()
, mais utilise la syntaxe GQL pour définir le filtre et l'ordre de la requête. Pour l'utiliser :
ndb.gql(querystring)
renvoie un objetQuery
(du même type que celui renvoyé parModel.query()
). Toutes les méthodes habituelles sont disponibles sur ces objetsQuery
:fetch()
,map_async()
,filter()
, etc.Model.gql(querystring)
est une notation abrégée pourndb.gql("SELECT * FROM Model " + querystring)
. En règle générale, querystring se présente plus ou moins sous la forme"WHERE prop1 > 0 AND prop2 = TRUE"
.- Pour interroger des modèles contenant des propriétés structurées, vous pouvez utiliser
foo.bar
dans votre syntaxe GQL pour faire référence à des sous-propriétés. - GQL prend en charge les liaisons de paramètres de type SQL. Une application peut définir une requête, puis y associer des valeurs :
L'appel de la fonction
bind()
d'une requête a pour effet de renvoyer une nouvelle requête et non de modifier la requête d'origine. - Si la classe de modèle remplace la méthode de classe
_get_kind()
, la requête GQL doit utiliser le genre renvoyé par cette fonction, et non le nom de la classe. - Si une propriété du modèle remplace son nom (par exemple,
foo = StringProperty('bar')
) la requête GQL doit utiliser le nom de la propriété remplacée (dans l'exemple,bar
).
Utilisez toujours la fonction de liaison de paramètre si certaines valeurs de votre requête sont des variables fournies par l'utilisateur. Cela évite les attaques syntaxiques.
Rechercher un modèle qui n'a pas été importé (ou, plus généralement, défini) est une erreur.
Utiliser un nom de propriété non défini par la classe de modèle est une erreur, sauf si ce modèle est un Expando.
La spécification d'une limite ou d'un décalage dans la méthode fetch()
de la requête ignore la limite ou le décalage défini par les clauses OFFSET
et LIMIT
de GQL. Ne combinez pas OFFSET
et LIMIT
avec fetch_page()
. Notez que la limite de 1 000 résultats imposée par App Engine sur les requêtes s'applique à la fois au décalage et à la limite.
Si vous êtes habitué à SQL, méfiez-vous des fausses hypothèses lorsque vous utilisez GQL. GQL est traduit dans l'API de requête native NDB. Cela est différent d'un mappeur objet-relationnel classique (comme SQLAlchemy ou le support de base de données Django), dans lequel les appels d'API sont traduits en SQL avant d'être transmis au serveur de base de données. GQL n'est pas compatible avec les modifications du datastore (insertions, suppressions ou mises à jour) et ne prend en charge que les requêtes.