Sélectionnez une ligne aléatoire dans une table sqlite

119

J'ai une sqlitetable avec le schéma suivant:

CREATE TABLE foo (bar VARCHAR)

J'utilise cette table comme stockage pour une liste de chaînes.

Comment sélectionner une ligne aléatoire dans ce tableau?

Alex_coder
la source
multiple stackoverflow.com/questions/4114940/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Réponses:

213

Jetez un œil à Sélection d'une ligne aléatoire à partir d'une table SQLite

SELECT * FROM table ORDER BY RANDOM() LIMIT 1;
Adriaan Stander
la source
1
Comment étendre cette solution à une jointure? Lors de l'utilisation, SELECT a.foo FROM a JOIN b ON a.id = b.id WHERE b.bar = 2 ORDER BY RANDOM() LIMIT 1;j'obtiens toujours la même ligne.
Helmut Grohne
Est-il possible de semer le nombre aléatoire. Par exemple, le livre du jour avec unix epoc pour aujourd'hui à midi, il montre donc le même livre toute la journée même si la requête est exécutée plusieurs fois. Oui, je sais que la mise en cache est plus efficace pour ce cas d'utilisation, juste un exemple.
danielson317 le
FWIW ma question est en fait répondue ici. Et la réponse est que vous ne pouvez pas semer le nombre aléatoire. stackoverflow.com/questions/24256258/…
danielson317
31

Les solutions suivantes sont beaucoup plus rapides que celles d'anktastic (le count (*) coûte cher, mais si vous pouvez le mettre en cache, alors la différence ne devrait pas être si grande), qui lui-même est beaucoup plus rapide que le "order by random ()" lorsque vous avez un grand nombre de lignes, même si elles présentent quelques inconvénients.

Si vos rowids sont plutôt compacts (c'est-à-dire quelques suppressions), vous pouvez alors faire ce qui suit (utiliser (select max(rowid) from foo)+1au lieu de max(rowid)+1donne de meilleures performances, comme expliqué dans les commentaires):

select * from foo where rowid = (abs(random()) % (select (select max(rowid) from foo)+1));

Si vous avez des trous, vous essaierez parfois de sélectionner un rowid inexistant, et la sélection renverra un jeu de résultats vide. Si cela n'est pas acceptable, vous pouvez fournir une valeur par défaut comme celle-ci:

select * from foo where rowid = (abs(random()) % (select (select max(rowid) from foo)+1)) or rowid = (select max(rowid) from node) order by rowid limit 1;

Cette deuxième solution n'est pas parfaite: la distribution de probabilité est plus élevée sur la dernière ligne (celle avec le rowid le plus élevé), mais si vous ajoutez souvent des éléments à la table, elle deviendra une cible mobile et la distribution des probabilités devrait être beaucoup mieux.

Encore une autre solution, si vous sélectionnez souvent des éléments aléatoires dans une table avec beaucoup de trous, vous voudrez peut-être créer une table qui contient les lignes de la table d'origine triées dans un ordre aléatoire:

create table random_foo(foo_id);

Puis, périodiquement, remplissez à nouveau le tableau random_foo

delete from random_foo;
insert into random_foo select id from foo;

Et pour sélectionner une ligne aléatoire, vous pouvez utiliser ma première méthode (il n'y a pas de trous ici). Bien sûr, cette dernière méthode a quelques problèmes de concurrence, mais la reconstruction de random_foo est une opération de maintenance qui ne se produira probablement pas très souvent.

Pourtant, encore une autre façon, que j'ai récemment trouvée sur une liste de diffusion , consiste à mettre un déclencheur sur la suppression pour déplacer la ligne avec le plus grand rowid dans la ligne supprimée actuelle, de sorte qu'il ne reste aucun trou.

Enfin, notez que le comportement d'auto-incrémentation de la clé primaire rowid et d'un entier n'est pas identique (avec rowid, quand une nouvelle ligne est insérée, max (rowid) +1 est choisi, alors qu'il est plus élevé-valeur-jamais-vu + 1 pour une clé primaire), donc la dernière solution ne fonctionnera pas avec un auto-incrémentation dans random_foo, mais les autres méthodes le feront.

Suzanne Dupéron
la source
Comme je viens de le voir sur une liste de diffusion, au lieu d'avoir la méthode de secours (méthode 2), vous pouvez simplement utiliser rowid> = [random] au lieu de =, mais c'est en fait extrêmement lent par rapport à la méthode 2.
Suzanne Dupéron
3
C'est une excellente réponse; cependant, il a un problème. SELECT max(rowid) + 1sera une requête lente - elle nécessite une analyse complète de la table. sqlite optimise uniquement la requête SELECT max(rowid). Ainsi, cette réponse serait améliorée par: select * from foo where rowid = (abs(random()) % (select (select max(rowid) from foo)+1)); Voir ceci pour plus d'informations: sqlite.1065341.n5.nabble.com
...
19

Vous devez mettre "ordre par RANDOM ()" sur votre requête.

Exemple:

select * from quest order by RANDOM();

Voyons un exemple complet

  1. Créez un tableau:
CREATE TABLE  quest  (
    id  INTEGER PRIMARY KEY AUTOINCREMENT,
    quest TEXT NOT NULL,
    resp_id INTEGER NOT NULL
);

Insertion de quelques valeurs:

insert into quest(quest, resp_id) values ('1024/4',6), ('256/2',12), ('128/1',24);

Une sélection par défaut:

select * from quest;

| id |   quest  | resp_id |
   1     1024/4       6
   2     256/2       12
   3     128/1       24
--

Un choix aléatoire:

select * from quest order by RANDOM();
| id |   quest  | resp_id |
   3     128/1       24
   1     1024/4       6
   2     256/2       12
--
* Chaque fois que vous sélectionnez, l'ordre sera différent.

Si vous souhaitez renvoyer une seule ligne

select * from quest order by RANDOM() LIMIT 1;
| id |   quest  | resp_id |
   2     256/2       12
--
* Chaque fois que vous sélectionnez, le retour sera différent.

Roberto Góes
la source
Bien que les réponses basées uniquement sur le code ne soient pas interdites, veuillez comprendre qu'il s'agit d'une communauté de questions-réponses, plutôt que d'une communauté de type crowdsourcing, et que, généralement, si l'OP comprenait le code affiché comme une réponse, il serait venu avec une solution similaire par lui-même, et n'aurait pas publié de question en premier lieu. En tant que tel, veuillez fournir un contexte à votre réponse et / ou à votre code en expliquant comment et / ou pourquoi cela fonctionne.
XenoRo
2
Je préfère cette solution, car elle me permet de rechercher n lignes. Dans mon cas, j'avais besoin de 100 échantillons aléatoires de la base de données - ORDER BY RANDOM () combiné avec LIMIT 100 fait exactement cela.
mnr
17

Qu'en est-il de:

SELECT COUNT(*) AS n FROM foo;

puis choisissez un nombre aléatoire m dans [0, n) et

SELECT * FROM foo LIMIT 1 OFFSET m;

Vous pouvez même enregistrer le premier nombre ( n ) quelque part et ne le mettre à jour que lorsque le nombre de bases de données change. De cette façon, vous n'avez pas à faire le SELECT COUNT à chaque fois.

Andres Kievsky
la source
1
C'est une bonne méthode rapide. Cela ne se généralise pas très bien à la sélection de plus d'une ligne, mais l'OP n'en a demandé qu'une, donc je suppose que c'est bien.
Ken Williams
Une chose curieuse à noter est que le temps nécessaire pour trouver le OFFSETsemble augmenter en fonction de la taille du décalage - la ligne 2 est rapide, la ligne 2 millions prend un certain temps, même lorsque toutes les données dans le sont de taille fixe et il devrait pouvoir y chercher directement. Du moins, c'est à quoi cela ressemble dans SQLite 3.7.13.
Ken Williams
@KenWilliams Presque toutes les bases de données ont le même problème avec `` OFFSET ''. C'est une manière très inefficace d'interroger une base de données car elle a besoin de lire autant de lignes même si elle ne retournera que 1.
Jonathan Allen
1
Notez que je parlais cependant de / taille fixe / enregistrements - il devrait être facile de scanner directement le bon octet dans les données ( sans lire autant de lignes), mais ils devraient implémenter l'optimisation explicitement.
Ken Williams
@KenWilliams: il n'y a pas d'enregistrements de taille fixe dans SQLite, il est typé dynamiquement et les données ne doivent pas nécessairement correspondre aux affinités déclarées ( sqlite.org/fileformat2.html#section_2_1 ). Tout est stocké dans des pages b-tree, donc dans tous les cas, il doit faire au moins une recherche b-tree vers la feuille. Pour ce faire efficacement, il aurait besoin de stocker la taille du sous-arbre avec chaque pointeur enfant. Ce serait trop de frais généraux pour peu d'avantages, car vous ne pourrez toujours pas optimiser l'OFFSET pour les jointures, l'ordre par, etc. (et sans ORDER BY, la commande n'est pas définie.)
Yakov Galka
13
SELECT   bar
FROM     foo
ORDER BY Random()
LIMIT    1
Svetlozar Angelov
la source
11
Puisqu'il sélectionnera d'abord tout le contenu de la table, cela ne prendrait-il pas beaucoup de temps pour les grandes tables?
Alex_coder
1
Ne pouvez-vous pas simplement limiter la portée en utilisant des conditions "WHERE"?
jldupont
11

Voici une modification de la solution de @ ank:

SELECT * 
FROM table
LIMIT 1 
OFFSET ABS(RANDOM()) % MAX((SELECT COUNT(*) FROM table), 1)

Cette solution fonctionne également pour les indices avec des lacunes, car nous randomisons un offset dans une plage [0, count). MAXest utilisé pour gérer un cas avec une table vide.

Voici des résultats de test simples sur une table avec 16k lignes:

sqlite> .timer on
sqlite> select count(*) from payment;
16049
Run Time: real 0.000 user 0.000140 sys 0.000117

sqlite> select payment_id from payment limit 1 offset abs(random()) % (select count(*) from payment);
14746
Run Time: real 0.002 user 0.000899 sys 0.000132
sqlite> select payment_id from payment limit 1 offset abs(random()) % (select count(*) from payment);
12486
Run Time: real 0.001 user 0.000952 sys 0.000103

sqlite> select payment_id from payment order by random() limit 1;
3134
Run Time: real 0.015 user 0.014022 sys 0.000309
sqlite> select payment_id from payment order by random() limit 1;
9407
Run Time: real 0.018 user 0.013757 sys 0.000208
vokilam
la source
4

J'ai proposé la solution suivante pour les grandes bases de données sqlite3 :

SELECT * FROM foo WHERE rowid = abs(random()) % (SELECT max(rowid) FROM foo) + 1; 

La fonction abs (X) renvoie la valeur absolue de l'argument numérique X.

La fonction random () renvoie un entier pseudo-aléatoire entre -9223372036854775808 et +9223372036854775807.

L'opérateur% sort la valeur entière de son opérande gauche modulo son opérande droit.

Enfin, vous ajoutez +1 pour éviter que rowid soit égal à 0.

Max
la source
1
Bien essayé mais je ne pense pas que cela fonctionnera. Que faire si une ligne avec rowId = 5 a été supprimée, mais que les rowIds 1,2,3,4,6,7,8,9,10 existent toujours? Ensuite, si le rowId aléatoire choisi est 5, cette requête ne retournera rien.
Calicoder