我有一张桌子,有 45 列,但其中只有几列尚未完成。该表会不断更新和添加等。在我的自动完成功能中,我想选择按完成最多的字段排序的这些记录(希望您理解)?
一种解决方案是创建另一个字段(“排名”字段)并创建一个 php 函数来选择 * 记录并为每条记录提供排名。
...但我想知道是否有一种更简单的方法可以只用一个 ORDER BY 来做到这一点?
据我所知,MySQL 没有计算一行中非 NULL 字段数量的功能。
所以我能想到的唯一方法是使用显式条件:
SELECT * FROM mytable
ORDER BY (IF( column1 IS NULL, 0, 1)
+IF( column2 IS NULL, 0, 1)
...
+IF( column45 IS NULL, 0, 1)) DESC;
...它像罪恶一样丑陋,但应该可以解决问题。
您还可以设计一个触发器来增加一个额外的列“fields_filled”。触发器会让你付出代价UPDATE
,45个IF会伤害你SELECT
;您必须对更方便的模型进行建模。
请注意,索引所有字段以加快速度SELECT
将在更新时花费您(并且 45 个不同的索引可能花费与 select 上的表扫描一样多,并不是说索引字段是 a VARCHAR
)。运行一些测试,但我相信 45-IF 解决方案可能是整体上最好的。
更新:
如果您可以修改表结构以对其进行某种程度的规范化,则可以将字段放在my_values
表中。然后你会有一个“头表”(可能只有一个唯一的 ID)和一个“数据表”。空字段根本不存在,然后您可以使用 a 按填充字段的数量排序,用RIGHT JOIN
计算填充字段COUNT()
。这也将大大加快UPDATE
操作,并允许您有效地使用索引。
示例(从表设置到两个规范化表设置):
假设我们有一组Customer
记录。我们将有一小部分“强制性”数据,例如 ID、用户名、密码、电子邮件等;那么我们可能会有更大的“可选”数据子集,例如昵称、头像、出生日期等。作为第一步,让我们假设所有这些数据都是varchar
(乍一看,与每列可能有自己的数据类型的单表解决方案相比,这看起来像是一个限制)。
所以我们有一张桌子,
ID username ....
1 jdoe etc.
2 jqaverage etc.
3 jkilroy etc.
然后我们有可选数据表。在这里,John Doe 填写了所有字段,Joe Q.平均只有两个,而 Kilroy 一个都没有(即使他在这里)。
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
为了在 MySQL 中重现“单表”输出,我们必须创建一个VIEW
包含很多LEFT JOIN
s 的非常复杂的表。如果我们有一个基于的索引,这个视图仍然会非常快(userid, var)
(如果我们使用数字常量或 SET 而不是 varchar 的数据类型会更好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)
;
我们逻辑模型中的每个字段,例如“name”,都将包含在可选数据表中的元组(id、'name'、value)中。
并且它将<FIELDNAME>s.val AS <FIELDNAME>
在上述查询的第 (1) 部分中生成一行表格,引用LEFT JOIN userdata AS <FIELDNAME>s ON ( users.id = <FIELDNAME>s.id AND <FIELDNAME>s.var = '<FIELDNAME>')
第 (2) 部分中的表格行。因此,我们可以通过将上述查询的第一个文本行与动态 Section 1、文本“FROM users”和动态构建的 Section 2 连接来动态构建查询。
一旦我们这样做了,视图上的 SELECT 就与以前完全相同了——但现在它们通过 JOIN 从两个规范化表中获取数据。
EXPLAIN SELECT * FROM usertable;
会告诉我们,在此设置中添加列不会明显减慢操作,即,此解决方案可以很好地扩展。
必须修改 INSERT(我们只插入强制数据,并且只在第一个表中)和 UPDATE:我们要么更新强制数据表,要么更新可选数据表的单行。但是如果目标行不存在,那么它必须被插入。
所以我们必须更换
UPDATE usertable SET name = 'John Doe', born = 'New York' WHERE id = 1;
在这种情况下,带有“upsert”
INSERT INTO userdata VALUES
( 1, 'name', 'John Doe' ),
( 1, 'born', 'New York' )
ON DUPLICATE KEY UPDATE val = VALUES(val);
(我们需要一个UNIQUE INDEX on userdata(id, var)
forON DUPLICATE KEY
才能工作)。
根据行大小和磁盘问题,此更改可能会产生可观的性能提升。
请注意,如果不执行此修改,现有查询将不会产生错误 -它们将静默失败。
这里例如我们修改两个用户的名字;一个确实有记录的名字,另一个有 NULL。第一个被修改,第二个没有。
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)
要知道每一行的排名,对于那些确实有排名的用户,我们只需检索每个 id 的 userdata 行数:
SELECT id, COUNT(*) AS rank FROM userdata GROUP BY id
现在以“填充状态”顺序提取行,我们这样做:
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;
这LEFT JOIN
确保了无等级的个人也被检索,并且额外的排序id
确保了具有相同等级的人总是以相同的顺序出现。