Mysql
 sql >> Base de données >  >> RDS >> Mysql

Ordre des requêtes MySQL par champs les plus remplis

MySQL n'a pas de fonction pour compter le nombre de champs non NULL sur une ligne, pour autant que je sache.

Donc, la seule façon à laquelle je peux penser est d'utiliser une condition explicite :

SELECT * FROM mytable
    ORDER BY (IF( column1 IS NULL, 0, 1)
             +IF( column2 IS NULL, 0, 1)
             ...
             +IF( column45 IS NULL, 0, 1)) DESC;

...c'est moche comme un péché, mais ça devrait faire l'affaire.

Vous pouvez également concevoir un TRIGGER pour incrémenter une colonne supplémentaire "fields_filled". Le déclencheur vous coûte le UPDATE , les 45 IF vous font mal sur SELECT; vous devrez modéliser ce qui est le plus pratique.

Notez que l'indexation de tous les champs pour accélérer SELECT vous coûtera lors de la mise à jour (et 45 index différents coûtent probablement autant qu'un balayage de table lors de la sélection, sans dire que le champ indexé est un VARCHAR ). Effectuez quelques tests, mais je pense que la solution 45-IF est probablement la meilleure dans l'ensemble.

MISE À JOUR :Si vous pouvez retravailler la structure de votre table pour la normaliser quelque peu, vous pouvez mettre les champs dans un my_values table. Ensuite, vous auriez une "table d'en-tête" (peut-être avec seulement un identifiant unique) et une "table de données". Les champs vides n'existeraient pas du tout, et vous pourriez ensuite trier en fonction du nombre de champs remplis en utilisant un RIGHT JOIN , en comptant les champs remplis avec COUNT() . Cela accélérerait également considérablement UPDATE opérations, et vous permettrait d'utiliser efficacement les index.

EXEMPLE (de la configuration de la table à la configuration de deux tables normalisées) :

Disons que nous avons un ensemble de Customer enregistrements. Nous aurons un petit sous-ensemble de données "obligatoires" telles que l'identifiant, le nom d'utilisateur, le mot de passe, l'e-mail, etc. ; nous aurons alors un sous-ensemble peut-être beaucoup plus important de données "facultatives" telles que le surnom, l'avatar, la date de naissance, etc. Dans un premier temps supposons que toutes ces données sont varchar (ceci, à première vue, ressemble à une limitation par rapport à la solution à table unique où chaque colonne peut avoir son propre type de données).

Nous avons donc un tableau comme,

ID   username    ....
1    jdoe        etc.
2    jqaverage   etc.
3    jkilroy     etc.

Ensuite, nous avons la table de données facultatives. Ici, John Doe a rempli tous les champs, Joe Q. Moyenne seulement deux, et Kilroy aucun (même s'il était ici).

userid  var   val
1       name  John
1       born  Stratford-upon-Avon
1       when  11-07-1974
2       name  Joe Quentin
2       when  09-04-1962

Afin de reproduire la sortie "table unique" dans MySQL, nous devons créer un VIEW assez complexe avec beaucoup de LEFT JOIN s. Cette vue sera néanmoins très rapide si nous avons un index basé sur (userid, var) (encore mieux si nous utilisons une constante numérique ou un SET au lieu d'un varchar pour le type de données de var :

CREATE OR REPLACE VIEW usertable AS SELECT users.*,
    names.val AS name // (1)
FROM users
    LEFT JOIN userdata AS names ON ( users.id = names.id AND names.var = 'name') // (2)
;

Chaque champ de notre modèle logique, par exemple "nom", sera contenu dans un tuple ( id, 'nom', valeur ) dans la table de données facultative.

Et cela donnera une ligne de la forme <FIELDNAME>s.val AS <FIELDNAME> dans la section (1) de la requête ci-dessus, en faisant référence à une ligne de la forme LEFT JOIN userdata AS <FIELDNAME>s ON ( users.id = <FIELDNAME>s.id AND <FIELDNAME>s.var = '<FIELDNAME>') au paragraphe (2). Nous pouvons donc construire la requête dynamiquement en concaténant la première ligne de texte de la requête ci-dessus avec une section 1 dynamique, le texte "FROM users" et une section 2 construite dynamiquement.

Une fois que nous avons fait cela, les SELECT sur la vue sont exactement identiques à avant - mais maintenant ils récupèrent les données de deux tables normalisées via les JOIN.

EXPLAIN SELECT * FROM usertable;

nous dira que l'ajout de colonnes à cette configuration ne ralentit pas sensiblement les opérations, c'est-à-dire que cette solution évolue raisonnablement bien.

Les INSERTs devront être modifiés (on insère uniquement les données obligatoires, et uniquement dans le premier tableau) et les UPDATE également :on UPDATE soit le tableau des données obligatoires, soit une seule ligne du tableau des données facultatives. Mais si la ligne cible n'est pas là, alors elle doit être INSÉRÉE.

Nous devons donc remplacer

UPDATE usertable SET name = 'John Doe', born = 'New York' WHERE id = 1;

avec un 'upsert', dans ce cas

INSERT INTO userdata VALUES
        ( 1, 'name', 'John Doe' ),
        ( 1, 'born', 'New York' )
    ON DUPLICATE KEY UPDATE val = VALUES(val);

(Nous avons besoin d'un UNIQUE INDEX on userdata(id, var) pour ON DUPLICATE KEY travailler).

En fonction de la taille des lignes et des problèmes de disque, cette modification peut entraîner un gain de performances appréciable.

Notez que si cette modification n'est pas effectuée, les requêtes existantes ne produiront pas d'erreurs - elles échoueront silencieusement .

Ici par exemple on modifie les noms de deux utilisateurs; l'un a un nom enregistré, l'autre a NULL. Le premier est modifié, le second ne l'est pas.

mysql> SELECT * FROM usertable;
+------+-----------+-------------+------+------+
| id   | username  | name        | born | age  |
+------+-----------+-------------+------+------+
|    1 | jdoe      | John Doe    | NULL | NULL |
|    2 | jqaverage | NULL        | NULL | NULL |
|    3 | jtkilroy  | NULL        | NULL | NULL |
+------+-----------+-------------+------+------+
3 rows in set (0.00 sec)
mysql> UPDATE usertable SET name = 'John Doe II' WHERE username = 'jdoe';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> UPDATE usertable SET name = 'James T. Kilroy' WHERE username = 'jtkilroy';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
mysql> select * from usertable;
+------+-----------+-------------+------+------+
| id   | username  | name        | born | age  |
+------+-----------+-------------+------+------+
|    1 | jdoe      | John Doe II | NULL | NULL |
|    2 | jqaverage | NULL        | NULL | NULL |
|    3 | jtkilroy  | NULL        | NULL | NULL |
+------+-----------+-------------+------+------+
3 rows in set (0.00 sec)

Pour connaître le rang de chaque ligne, pour les utilisateurs qui ont un rang, nous récupérons simplement le nombre de lignes de données utilisateur par identifiant :

SELECT id, COUNT(*) AS rank FROM userdata GROUP BY id

Maintenant, pour extraire les lignes dans l'ordre "état rempli", nous faisons :

SELECT usertable.* FROM usertable
    LEFT JOIN ( SELECT id, COUNT(*) AS rank FROM userdata GROUP BY id ) AS ranking
ON (usertable.id = ranking.id)
ORDER BY rank DESC, id;

Le LEFT JOIN garantit que les individus sans rang sont également récupérés, et le classement supplémentaire par id veille à ce que les personnes de rang identique sortent toujours dans le même ordre.