Je vous suggÚre de lire la transcription du rapport de Vladimir Sitnikov début 2016 « PostgreSQL et JDBC extraient tout le jus »


Bon aprÚs-midi Je m'appelle Vladimir Sitnikov. Je travaille pour NetCracker depuis 10 ans. Et je suis surtout intéressé par la productivité. Tout ce qui touche à Java, tout ce qui touche à SQL, c'est ce que j'aime.
Et aujourd'hui, je vais parler de ce que nous avons rencontrĂ© dans l'entreprise lorsque nous avons commencĂ© Ă utiliser PostgreSQL comme serveur de base de donnĂ©es. Et nous travaillons principalement avec Java. Mais ce que je vais vous dire aujourdâhui ne concerne pas seulement Java. Comme l'a montrĂ© la pratique, cela se produit Ă©galement dans d'autres langues.

Nous parlerons:
- sur l'échantillonnage des données.
- à propos de la sauvegarde des données.
- Et aussi sur les performances.
- Et à propos des rùteaux sous-marins qui y sont enterrés.

Commençons par une question simple. Nous sélectionnons une ligne du tableau en fonction de la clé primaire.

La base de donnĂ©es est situĂ©e sur le mĂȘme hĂŽte. Et tout ce farm prend 20 millisecondes.

Ces 20 millisecondes, c'est beaucoup. Si vous avez 100 requĂȘtes de ce type, alors vous passez du temps par seconde Ă faire dĂ©filer ces requĂȘtes, c'est-Ă -dire nous perdons du temps.
Nous nâaimons pas faire ça et regardons ce que la base nous propose pour cela. La base de donnĂ©es nous offre deux options pour exĂ©cuter des requĂȘtes.

La premiĂšre option est une simple demande. Qu'est-ce qu'il y a de bien lĂ -dedans ? Le fait que nous le prenons et lâenvoyons, et rien de plus.

La base de donnĂ©es dispose Ă©galement d'une requĂȘte avancĂ©e, plus dĂ©licate, mais plus fonctionnelle. Vous pouvez envoyer sĂ©parĂ©ment une demande d'analyse, d'exĂ©cution, de liaison de variable, etc.
La requĂȘte super Ă©tendue est quelque chose que nous ne couvrirons pas dans le rapport actuel. Nous voulons peut-ĂȘtre quelque chose de la base de donnĂ©es et il y a une liste de souhaits qui a Ă©tĂ© formĂ©e sous une forme ou une autre, c'est-Ă -dire c'est ce que nous voulons, mais c'est impossible maintenant et l'annĂ©e prochaine. Alors on vient de lâenregistrer et on va faire tourner les principaux personnages.

Et ce que nous pouvons faire, c'est une requĂȘte simple et une requĂȘte Ă©tendue.
Quelle est la particularité de chaque approche ?
Une requĂȘte simple convient pour une exĂ©cution unique. Une fois fait et oubliĂ©. Et le problĂšme est quâil ne prend pas en charge le format de donnĂ©es binaire, câest-Ă -dire quâil ne convient pas Ă certains systĂšmes hautes performances.

RequĂȘte Ă©tendue â vous permet de gagner du temps sur l'analyse. C'est ce que nous avons fait et avons commencĂ© Ă utiliser. Cela nous a vraiment, vraiment aidĂ©. Il n'y a pas que des Ă©conomies sur l'analyse. Il y a des Ă©conomies sur le transfert de donnĂ©es. Le transfert de donnĂ©es au format binaire est beaucoup plus efficace.

Passons Ă la pratique. VoilĂ Ă quoi ressemble une application typique. Cela pourrait ĂȘtre Java, etc.
Nous avons créé une dĂ©claration. ExĂ©cutĂ© la commande. Créé Ă proximitĂ©. OĂč est l'erreur ici ? Quel est le problĂšme? Aucun problĂšme. C'est ce qui est dit dans tous les livres. Câest ainsi quâil faut lâĂ©crire. Si vous voulez des performances maximales, Ă©crivez comme ceci.

Mais la pratique a montrĂ© que cela ne fonctionne pas. Pourquoi? Parce que nous avons une mĂ©thode « proche ». Et lorsque nous faisons cela, du point de vue de la base de donnĂ©es, il sâavĂšre que câest comme un fumeur travaillant avec une base de donnĂ©es. Nous avons dit "PARSE EXECUTE DEALLOCATE".
Pourquoi toute cette crĂ©ation et ce dĂ©chargement supplĂ©mentaires d'instructions ? Personne nâen a besoin. Mais ce qui se passe gĂ©nĂ©ralement dans PreparedStatements, c'est que lorsque nous les fermons, ils ferment tout ce qui se trouve dans la base de donnĂ©es. Ce n'est pas ce que nous voulons.

Nous voulons, comme les personnes en bonne santĂ©, travailler avec la base. Nous avons pris et prĂ©parĂ© notre dĂ©claration une fois, puis nous l'avons exĂ©cutĂ©e plusieurs fois. En fait, plusieurs fois - c'est une fois dans toute la vie des applications - elles ont Ă©tĂ© analysĂ©es. Et nous utilisons le mĂȘme identifiant d'instruction sur diffĂ©rents REST. C'est notre objectif.

Comment pouvons-nous y parvenir?

C'est trÚs simple : pas besoin de fermer les instructions. Nous l'écrivons ainsi : « préparer » « exécuter ».


Si nous lançons quelque chose comme ça, alors il est clair que quelque chose va dĂ©border quelque part. Si ce n'est pas clair, vous pouvez l'essayer. Ăcrivons un benchmark qui utilise cette mĂ©thode simple. CrĂ©ez une dĂ©claration. Nous le lançons sur une version du pilote et constatons qu'il plante assez rapidement avec la perte de toute la mĂ©moire dont il disposait.
Il est clair que de telles erreurs sont faciles Ă corriger. Je n'en parlerai pas. Mais je dirai que la nouvelle version fonctionne beaucoup plus rapidement. La mĂ©thode est stupide, mais quand mĂȘme.

Comment travailler correctement ? Que devons-nous faire pour cela ?
En réalité, les applications ferment toujours les instructions. Dans tous les livres, ils disent de le fermer, sinon la mémoire fuira.
Et PostgreSQL ne sait pas comment mettre en cache les requĂȘtes. Il faut que chaque session crĂ©e ce cache pour elle-mĂȘme.
Et nous ne voulons pas non plus perdre de temps en analyse.

Et comme d'habitude, nous avons deux options.
La premiĂšre option est de prendre cela et de dire que nous enveloppons tout dans PgSQL. Il y a une cache lĂ -bas. Il met tout en cache. Cela s'avĂ©rera gĂ©nial. Nous avons vu cela. Nous avons 100500 XNUMX demandes. Ne marche pas. Nous nâacceptons pas de transformer manuellement les demandes en procĂ©dures. Non non.
Nous avons une deuxiĂšme option : prenez-le et coupez-le nous-mĂȘmes. Nous ouvrons les sources et commençons Ă couper. Nous avons vu et vu. Il s'est avĂ©rĂ© que ce n'est pas si difficile Ă faire.

Celui-ci est apparu en aoĂ»t 2015. Il existe dĂ©sormais une version plus moderne. Et tout est gĂ©nial. Cela fonctionne tellement bien quâon ne change rien dans lâapplication. Et nous avons mĂȘme arrĂȘtĂ© de penser en direction de PgSQL, c'est-Ă -dire cela nous suffisait amplement pour rĂ©duire tous les frais gĂ©nĂ©raux Ă presque zĂ©ro.
En consĂ©quence, les instructions prĂ©parĂ©es par le serveur sont activĂ©es Ă la 5Ăšme exĂ©cution afin d'Ă©viter de gaspiller de la mĂ©moire dans la base de donnĂ©es Ă chaque requĂȘte unique.

Vous vous demandez peut-ĂȘtre : oĂč sont les chiffres ? Qu'obtenez-vous ? Et ici je ne donnerai pas de chiffres, car chaque demande a le sien.
Nos requĂȘtes Ă©taient telles que nous avons passĂ© environ 20 millisecondes Ă analyser les requĂȘtes OLTP. Il y avait 0,5 millisecondes pour l'exĂ©cution, 20 millisecondes pour l'analyse. RequĂȘte â 10 Ko de texte, 170 lignes de plan. Il s'agit d'une requĂȘte OLTP. Il demande 1, 5, 10 lignes, parfois plus.
Mais nous ne voulions pas du tout perdre 20 millisecondes. Nous l'avons réduit à 0. Tout est trÚs bien.
Que pouvez-vous retenir dâici ? Si vous avez Java, prenez la version moderne du pilote et rĂ©jouissez-vous.
Si vous parlez une langue diffĂ©rente, rĂ©flĂ©chissez : peut-ĂȘtre en avez-vous aussi besoin ? Car du point de vue du langage final, par exemple, si PL 8 ou si vous avez LibPQ, alors il n'est pas Ă©vident pour vous que vous perdiez du temps non pas sur l'exĂ©cution, sur l'analyse, et cela mĂ©rite d'ĂȘtre vĂ©rifiĂ©. Comment? Tout est gratuit.

Sauf quâil y a des erreurs et quelques particularitĂ©s. Et nous en parlerons maintenant. La majeure partie portera sur l'archĂ©ologie industrielle, sur ce que nous avons trouvĂ©, sur ce que nous avons dĂ©couvert.

Si la demande est gĂ©nĂ©rĂ©e dynamiquement. Ăa arrive. Quelqu'un colle les chaĂźnes ensemble, ce qui gĂ©nĂšre une requĂȘte SQL.
Pourquoi est-il mauvais ? C'est dommage car à chaque fois on se retrouve avec une chaßne différente.
Et le hashCode de cette chaĂźne diffĂ©rente doit ĂȘtre relu. Il s'agit en rĂ©alitĂ© d'une tĂąche du processeur : trouver un long texte de requĂȘte, mĂȘme dans un hachage existant, n'est pas si simple. Par consĂ©quent, la conclusion est simple : ne gĂ©nĂ©rez pas de demandes. Stockez-les dans une variable. Et rĂ©jouissez-vous.

Prochain problĂšme. Les types de donnĂ©es sont importants. Il existe des ORM qui disent que peu importe le type de NULL existant, qu'il y en ait un. Si Int, alors nous disons setInt. Et si NULL, alors que ce soit toujours VARCHAR. Et quelle diffĂ©rence cela fait-il au final, qu'y ait-il NULL ? La base de donnĂ©es elle-mĂȘme comprendra tout. Et cette image ne fonctionne pas.
En pratique, la base de données s'en fiche du tout. Si vous avez dit la premiÚre fois qu'il s'agit d'un nombre, et la deuxiÚme fois que vous avez dit qu'il s'agit d'un VARCHAR, alors il est impossible de réutiliser les instructions préparées par le serveur. Et dans ce cas, nous devons recréer notre déclaration.

Si vous exĂ©cutez la mĂȘme requĂȘte, assurez-vous que les types de donnĂ©es de votre colonne ne sont pas confondus. Vous devez faire attention Ă NULL. Il s'agit d'une erreur courante que nous avons rencontrĂ©e aprĂšs avoir commencĂ© Ă utiliser PreparedStatements.

D'accord, allumĂ©. Peut-ĂȘtre qu'ils ont pris le chauffeur. Et la productivitĂ© a chutĂ©. Les choses ont mal tournĂ©.
Comment cela peut-il arriver? Est-ce un bug ou une fonctionnalitĂ©? Malheureusement, il n'a pas Ă©tĂ© possible de comprendre s'il s'agissait d'un bug ou d'une fonctionnalitĂ©. Mais il existe un scĂ©nario trĂšs simple pour reproduire ce problĂšme. Elle nous a tendu une embuscade de maniĂšre complĂštement inattendue. Et cela consiste Ă Ă©chantillonner littĂ©ralement Ă partir dâune seule table. Bien entendu, nous avons eu davantage de demandes de ce type. En rĂšgle gĂ©nĂ©rale, ils comprenaient deux ou trois tables, mais il existe un tel scĂ©nario de lecture. Prenez nâimporte quelle version de votre base de donnĂ©es et jouez-la.

Le fait est que nous avons deux colonnes, chacune étant indexée. Il y a un million de lignes dans une colonne NULL. Et la deuxiÚme colonne ne contient que 20 lignes. Lorsque nous exécutons sans variables liées, tout fonctionne bien.
Si nous commençons à exécuter avec des variables liées, c'est-à -dire que nous exécutons le "?" ou « 1 $ » pour notre demande, qu'obtenons-nous finalement ?

La premiĂšre exĂ©cution est comme prĂ©vu. Le second est un peu plus rapide. Quelque chose a Ă©tĂ© mis en cache. TroisiĂšme, quatriĂšme, cinquiĂšme. Puis bang - et quelque chose comme ça. Et le pire, c'est que cela se produit Ă la sixiĂšme exĂ©cution. Qui savait quâil Ă©tait nĂ©cessaire de procĂ©der exactement Ă six exĂ©cutions pour comprendre quel Ă©tait le vĂ©ritable plan dâexĂ©cution ?

Qui est coupable ? Ce qui s'est passĂ©? La base de donnĂ©es contient une optimisation. Et il semble optimisĂ© pour le cas gĂ©nĂ©rique. Et, en consĂ©quence, Ă partir dâun moment donnĂ©, elle passe Ă un plan gĂ©nĂ©rique, qui, malheureusement, peut sâavĂ©rer diffĂ©rent. Cela peut s'avĂ©rer ĂȘtre le mĂȘme, ou cela peut ĂȘtre diffĂ©rent. Et il existe une sorte de valeur seuil qui conduit Ă ce comportement.
Que peux-tu y faire? Ici, bien entendu, il est plus difficile de présumer quoi que ce soit. Il existe une solution simple que nous utilisons. C'est +0, OFFSET 0. Vous connaissez sûrement de telles solutions. Nous le prenons simplement et ajoutons « +0 » à la demande et tout va bien. Je te montrerai plus tard.
Et il existe une autre option : examinez les plans plus attentivement. Le développeur doit non seulement rédiger une demande, mais aussi dire « expliquer analyser » 6 fois. Si c'est 5, ça ne marchera pas.
Et il existe une troisiÚme option : écrire une lettre aux pirates de pgsql. J'ai écrit, cependant, il n'est pas encore clair s'il s'agit d'un bug ou d'une fonctionnalité.

Pendant que nous nous demandons sâil sâagit dâun bug ou dâune fonctionnalitĂ©, corrigeons-le. Prenons notre demande et ajoutons "+0". Tout va bien. Deux symboles et vous nâavez mĂȘme pas besoin de penser Ă ce que câest ou Ă ce que câest. TrĂšs simple. Nous avons simplement interdit Ă la base de donnĂ©es d'utiliser un index sur cette colonne. Nous nâavons pas dâindex sur la colonne « +0 » et câest tout, la base de donnĂ©es nâutilise pas lâindex, tout va bien.

C'est la rĂšgle de 6, expliquez-vous. DĂ©sormais, dans les versions actuelles, vous devez le faire 6 fois si vous avez des variables liĂ©es. Si vous n'avez pas de variables liĂ©es, c'est ce que nous faisons. Et finalement, câest prĂ©cisĂ©ment cette demande qui Ă©choue. Ce n'est pas une chose dĂ©licate.
Il semblerait, combien est-il possible ? Un bug ici, un bug lĂ . En fait, le bug est partout.

Regardons de plus prĂšs. Par exemple, nous avons deux schĂ©mas. SchĂ©ma A avec tableau S et schĂ©ma B avec tableau S. RequĂȘte â sĂ©lectionnez des donnĂ©es dans une table. Qu'aurons-nous dans ce cas ? Nous aurons une erreur. Nous aurons tout ce qui prĂ©cĂšde. La rĂšgle est la suivante : un bug est partout, nous aurons tout ce qui prĂ©cĂšde.

Maintenant la question est : « Pourquoi ? » Il semblerait qu'il existe une documentation selon laquelle si nous avons un schĂ©ma, alors il existe une variable "search_path" qui nous indique oĂč chercher la table. Il semblerait qu'il existe une variable.
Quel est le problĂšme? Le problĂšme est que les instructions prĂ©parĂ©es par le serveur ne soupçonnent pas que search_path peut ĂȘtre modifiĂ© par quelqu'un. Cette valeur reste en quelque sorte constante pour la base de donnĂ©es. Et certaines parties peuvent ne pas acquĂ©rir de nouvelles significations.

Bien entendu, cela dĂ©pend de la version sur laquelle vous testez. Cela dĂ©pend de la gravitĂ© de la diffĂ©rence entre vos tables. Et la version 9.1 exĂ©cutera simplement les anciennes requĂȘtes. Les nouvelles versions peuvent dĂ©tecter le bug et vous informer que vous avez un bug.

Comment le traiter ? Il existe une recette simple : ne la faites pas. Il n'est pas nécessaire de modifier search_path pendant l'exécution de l'application. Si vous changez, il est préférable de créer une nouvelle connexion.
Vous pouvez discuter, c'est-Ă -dire ouvrir, discuter, ajouter. Peut-ĂȘtre pouvons-nous convaincre les dĂ©veloppeurs de bases de donnĂ©es que lorsque quelqu'un modifie une valeur, la base de donnĂ©es devrait en informer le client : « Ăcoutez, votre valeur a Ă©tĂ© mise Ă jour ici. Peut-ĂȘtre avez-vous besoin de rĂ©initialiser les dĂ©clarations et de les recrĂ©er ? » DĂ©sormais, la base de donnĂ©es se comporte secrĂštement et ne signale en aucune maniĂšre que les instructions ont changĂ© quelque part Ă l'intĂ©rieur.
Et j'insiste encore une fois : c'est quelque chose qui n'est pas typique de Java. Nous verrons la mĂȘme chose en PL/pgSQL un Ă un. Mais il y sera reproduit.

Essayons une sélection supplémentaire de données. Nous choisissons et choisissons. Nous avons une table avec un million de lignes. Chaque ligne fait un kilo-octet. Environ un gigaoctet de données. Et nous avons une mémoire de travail dans la machine Java de 128 mégaoctets.
Comme recommandé dans tous les livres, nous utilisons le traitement de flux. Autrement dit, nous ouvrons resultSet et lisons les données à partir de là petit à petit. Est-ce que ça marchera? Est-ce que cela tombera de la mémoire ? Veux-tu lire un peu ? Faisons confiance à la base de données, faisons confiance à Postgres. Nous n'y croyons pas. Allons-nous tomber en OutOFMemory ? Qui a connu OutOfMemory ? Qui a réussi à le réparer aprÚs ça ? Quelqu'un a réussi à le réparer.
Si vous avez un million de lignes, vous ne pouvez pas simplement choisir. OFFSET/LIMIT est requis. Qui est pour cette option ? Et qui est favorable Ă jouer avec autoCommit ?
Ici, comme d'habitude, l'option la plus inattendue s'avĂšre correcte. Et si vous dĂ©sactivez soudainement autoCommit, cela vous aidera. Pourquoi donc? La science nâen sait rien.

Mais par défaut, tous les clients se connectant à une base de données Postgres récupÚrent l'intégralité des données. PgJDBC ne fait pas exception à cet égard : il sélectionne toutes les lignes.
Il existe une variante du thÚme FetchSize, c'est-à -dire que vous pouvez dire au niveau d'une instruction distincte qu'ici, veuillez sélectionner les données par 10, 50. Mais cela ne fonctionne pas tant que vous n'avez pas désactivé autoCommit. AutoCommit désactivé - il commence à fonctionner.
Mais parcourir le code et définir setFetchSize partout n'est pas pratique. Par conséquent, nous avons défini un paramÚtre qui indiquera la valeur par défaut pour l'ensemble de la connexion.

C'est ce que nous avons dit. Le paramĂštre a Ă©tĂ© configurĂ©. Et quâavons-nous obtenu ? Si nous sĂ©lectionnons de petits montants, si, par exemple, nous sĂ©lectionnons 10 lignes Ă la fois, nous avons alors des frais gĂ©nĂ©raux trĂšs importants. Par consĂ©quent, cette valeur doit ĂȘtre fixĂ©e Ă environ une centaine.

Idéalement, bien sûr, vous devez encore apprendre à le limiter en octets, mais la recette est la suivante : définissez defaultRowFetchSize sur plus de cent et soyez heureux.

Passons Ă l'insertion de donnĂ©es. L'insertion est plus facile, il existe diffĂ©rentes options. Par exemple, INSĂRER, VALEURS. C'est une bonne option. Vous pouvez dire « INSERT SELECT ». En pratique, c'est la mĂȘme chose. Il n'y a aucune diffĂ©rence de performances.
Les livres disent que vous devez exécuter une instruction Batch, les livres disent que vous pouvez exécuter des commandes plus complexes avec plusieurs parenthÚses. Et Postgres a une fonctionnalité merveilleuse : vous pouvez faire COPY, c'est-à -dire le faire plus rapidement.

Si vous le mesurez, vous pourrez à nouveau faire des découvertes intéressantes. Comment voulons-nous que cela fonctionne ? Nous ne voulons pas analyser ni exécuter de commandes inutiles.

En pratique, TCP ne nous permet pas de faire cela. Si le client est occupĂ© Ă envoyer une requĂȘte, la base de donnĂ©es ne lit pas les requĂȘtes pour tenter de nous envoyer des rĂ©ponses. Le rĂ©sultat final est que le client attend que la base de donnĂ©es lise la demande et que la base de donnĂ©es attend que le client lise la rĂ©ponse.

Et donc le client est obligé d'envoyer périodiquement un paquet de synchronisation. Interactions réseau supplémentaires, perte de temps supplémentaire.
Et plus on en ajoute, plus la situation empire. Le conducteur est assez pessimiste et les ajoute assez souvent, environ une fois toutes les 200 lignes, selon la taille des lignes, etc.

Il arrive que vous corrigiez une seule ligne et que tout s'accĂ©lĂšre 10 fois. Ăa arrive. Pourquoi? Comme d'habitude, une constante comme celle-ci a dĂ©jĂ Ă©tĂ© utilisĂ©e quelque part. Et la valeur « 128 » signifiait ne pas utiliser le traitement par lots.

C'est bien que cela ne soit pas inclus dans la version officielle. Découvert avant le début de la sortie. Toutes les significations que je donne sont basées sur des versions modernes.

Essayons-le. Nous mesurons InsertBatch simple. Nous mesurons InsertBatch plusieurs fois, c'est-Ă -dire la mĂȘme chose, mais il existe de nombreuses valeurs. DĂ©placement dĂ©licat. Tout le monde ne peut pas faire cela, mais câest une dĂ©marche si simple, bien plus facile que COPIER.

Vous pouvez faire COPIER.

Et vous pouvez le faire sur des structures. Déclarez le type par défaut de l'utilisateur, transmettez le tableau et INSERT directement dans la table.
Si vous ouvrez le lien : pgjdbc/ubenchmsrk/InsertBatch.java, alors ce code est sur GitHub. Vous pouvez voir spécifiquement quelles demandes y sont générées. Cela n'a pas d'importance.

Nous avons lancĂ©. Et la premiĂšre chose que nous avons rĂ©alisĂ©, câest que ne pas utiliser le batch est tout simplement impossible. Toutes les options de batching sont nulles, c'est-Ă -dire que le temps d'exĂ©cution est pratiquement nul par rapport Ă une exĂ©cution unique.

Nous insérons des données. C'est un tableau trÚs simple. Trois colonnes. Et que voit-on ici ? Nous constatons que ces trois options sont à peu prÚs comparables. Et COPIER est, bien sûr, meilleur.

C'est Ă ce moment-lĂ que nous insĂ©rons des piĂšces. Quand nous avons dit qu'une valeur VALEURS, deux valeurs VALEURS, trois valeurs VALEURS, ou nous en avons indiquĂ© 10 sĂ©parĂ©es par une virgule. C'est juste horizontal maintenant. 1, 2, 4, 128. On peut voir que l'encart de lot, dessinĂ© en bleu, lui permet de se sentir beaucoup mieux. Autrement dit, lorsque vous en insĂ©rez un Ă la fois ou mĂȘme lorsque vous en insĂ©rez quatre Ă la fois, cela devient deux fois mieux, simplement parce que nous avons mis un peu plus de VALEURS. Moins dâopĂ©rations EXECUTE.
Utiliser COPY sur de petits volumes est extrĂȘmement peu prometteur. Je n'ai mĂȘme pas dessinĂ© les deux premiers. Ils vont au paradis, c'est-Ă -dire ces nombres verts pour COPIER.
COPY doit ĂȘtre utilisĂ© lorsque vous disposez d'au moins une centaine de lignes de donnĂ©es. La surcharge liĂ©e Ă lâouverture de cette connexion est importante. Et pour ĂȘtre honnĂȘte, je nâai pas creusĂ© dans cette direction. J'ai optimisĂ© Batch, mais pas COPY.
Que faisons-nous ensuite? Nous l'avons essayé. Nous comprenons qu'il faut utiliser soit des structures, soit un bain astucieux qui combine plusieurs significations.

Que devriez-vous retenir du rapport dâaujourdâhui ?
- PreparedStatement est notre tout. Cela donne beaucoup pour la productivité. Cela produit un gros échec dans la pommade.
- Et vous devez faire EXPLIQUER ANALYSER 6 fois.
- Et nous devons diluer OFFSET 0 et des astuces comme +0 afin de corriger le pourcentage restant de nos requĂȘtes problĂ©matiques.
Source: habr.com
