博客 (52)

本文未完成,部分测试方法、条件或结果可能有误,请谨慎参考! :)

本文基于 MySQL 的 InnoDB BTREE 方法的索引进行测试。

以一张包含 2000 万条记录的表做实验:

CREATE TABLE `dt_read`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `time` datetime(0) NOT NULL,
  `a_id` int(11) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
);

这张表是用于记录文章点击量的,

`id` 为主键,int(11) 自增;

`time` 为非空 datetime,表示文章打开时间,测试数据是从 2017-03-11 至 2018-04-28;

`a_id` 为非空 int(11),表示文章 ID,在此表中不唯一,测试数据是从 1 至 260218。


体验“全表扫描”


首先来体验一下什么是全表扫描,执行下面语句:

SELECT * FROM `dt_read` WHERE `time` < '2020-1-1' LIMIT 10

> 时间: 0.012s


SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' LIMIT 10

> 时间: 7.317s


表中数据是按主键从小到大排列的,当查询条件为 `time` < '2020-1-1' 时,能很快地从表的前端找到 10 条满足条件的数据,所以不再继续判断后面的记录,立刻返回结果,耗时 0.012 秒;但当条件改为 `time` < '2000-1-1' 时,同样逐条判断,直到最后一条也没有找到,这种情况就是所谓的“全表扫描”,耗时 7 秒。


索引对 ORDER BY 的 ASC 和 DESC 的影响


我们给 `time` 建一个索引,同样执行刚才需要全表扫描的语句:

SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' LIMIT 10

> 时间: 0.012s


创建 `time` 的索引后,相当于生成了一张按 `time` 字段排列的新表,这时 MySQL 就能够很快地定位并找到符合条件的记录,避免了全表扫描。


试试按 `time` 倒序排:

SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' ORDER BY `time` DESC LIMIT 10

> 时间: 0.013s


结论:索引对 ORDER BY 的顺序(ASC)和倒序(DESC)都是有效的。


索引字段的次序对 WHERE 和 ORDER BY 的影响


删除所有索引,创建一个新的索引,字段依次为 `time`, `a_id`。

分别执行以下查询:


SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' AND `a_id` < 0 LIMIT 10

> 时间: 0.013s


SELECT * FROM `dt_read` WHERE `a_id` < 0 AND `time` < '2000-1-1' LIMIT 10

> 时间: 0.013s


结论:MySQL 会自动优化 WHERE 条件的次序来匹配最合适的索引。

但在 ORDER BY 中却不是这么回事了:


SELECT * FROM `dt_read` ORDER BY `time`, `a_id` LIMIT 10

> 时间: 0.013s


SELECT * FROM `dt_read` ORDER BY `a_id`, `time` LIMIT 10

> 时间: 14.066s


原因也很好理解,对两个字段进行排序,先后次序肯定会影响结果集,因此只能以 SQL 语句指定的字段次序来 ORDER BY,这样,按索引的字段次序进行 ORDER BY 查询无疑是更快的。


索引中的字段必须依次使用


保持上例创建的索引不变,即 `time`, `a_id`。


SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' AND `a_id` < 0 LIMIT 10

> 时间: 0.013s


SELECT * FROM `dt_read` WHERE `a_id` < 0 LIMIT 10

> 时间: 6.438s


上句合理利用了索引的字段,而下句跳过了 `time`,直接 WHERE 了 `a_id`,这是不受该索引支持的。

我们可以想象一下这张由索引生成的虚拟表,其实就是一张普通的平面二维表格,按索引指定的字段次序进行了排序,所以全表中仅仅是索引指定的第一个字段是按大小排列的,第二个字段是在第一个字段值相同的区域内按大小排列,后同。所以,跳过索引指定的第一个字段直接对第二个字段进行检索,是无法应用该索引的。这个结论也同样也体现在 ORDER BY 语句中:


SELECT * FROM `dt_read` ORDER BY `time`, `a_id` LIMIT 10

> 时间: 0.013s


SELECT * FROM `dt_read` ORDER BY `a_id` LIMIT 10

> 时间: 29.566s


WHERE 和 ORDER BY 混合


保持上例创建的索引不变,即 `time`, `a_id`。


先来执行这两句:


SELECT * FROM `dt_read` ORDER BY `a_id` LIMIT 10

> 时间: 12.29s


SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' ORDER BY `a_id` LIMIT 10

> 时间: 0.013s


仅仅 WHERE 了一个 `time`,对 ORDER BY `a_id` 的效率却有质的提升,是因为 WHERE 中的 `time` 和 ORDER BY 中的 `a_id` 一起找到了索引吗?答案是否定的。

我们把时间改大,让它能马上找到符合条件的数据:


SELECT * FROM `dt_read` WHERE `time` < '2020-1-1' ORDER BY `a_id` LIMIT 10

> 时间: 22.34s


为什么这个语句就不走索引了呢?

其实,一个简单的 SELECT 查询语句,首先执行 WHERE,然后 ORDER BY,最后是 LIMIT。每一步都独自去找了索引,而非 WHERE 和 ORDER BY 混在一起去找索引。必须保证每一步是快的,最终才是快的。

当 `time` < '2000-1-1' 时,WHERE 用到了索引,所以很快,ORDER BY 却没有用到索引,但为什么也很快呢?因为 WHERE 的结果集非常小(示例中为 0 条)。

当 `time` < '2020-1-1' 时,WHERE 也用到了索引,但其结果集非常大(示例中为所有记录),再 ORDER BY `a_id` 就非常慢了,因为我们没有创建以 `a_id` 开头的索引。


现在把索引改成只有 `time` 一个字段。


SELECT * FROM `dt_read` WHERE `time` < '2020-1-1' ORDER BY `a_id` LIMIT 10

> 时间: 6.033s


因为索引里有 `


SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' ORDER BY `a_id` LIMIT 10

> 时间: 0.013s


SELECT * FROM `dt_read` WHERE `a_id` < 0 ORDER BY `time` LIMIT 10

> 时间: 6.033s


第二句先 WHERE `a_id`,后 ORDER BY `time` 是不能匹配所建的索引的。


索引中的字段越多越好


分别在创建索引(`time`)和索引(`time`, `a_id`)的情况下执行下面语句:

本例使用 ORDER BY 而不是 WHERE 来测试是因为,在 WHERE 的多个条件下,如果符合前一条件的筛选结果集过小会导致判断第二条件时数据量不足,无法判断索引是否起作用。


SELECT * FROM `dt_read` ORDER BY `time` LIMIT 10


仅创建索引(`time`)的情况下:

> 时间: 0.013s


仅创建索引(`time`, `a_id`)的情况下:

> 时间: 0.013s


SELECT * FROM `dt_read` ORDER BY `time`, `a_id` LIMIT 10


仅创建索引(`time`)的情况下:

> 时间: 15.015s


仅创建索引(`time`, `a_id`)的情况下:

> 时间: 0.014s


可以看到,在索引字段依次使用的前提下,索引字段数不少于查询字段数才能避免全表扫描。

虽然索引中的字段越多越好,但必须依次使用,否则也是无效索引。


索引对 INSERT / UPDATE / DELETE 的效率影响


分别在创建索引(`time`)和索引(`time`, `a_id`)的情况下执行下面语句:


INSERT INTO `dt_read` (`time`, `a_id`) VALUES ('2018-4-28', 260218)


不建索引的情况下:

> 时间: 0.01s


仅创建索引(`time`)的情况下:

> 时间: 0.01s


同时创建索引(`time`)和索引(`time`, `a_id`)的情况下:

> 时间: 0.01s


UPDATE `dt_read` SET `time` = '2018-4-28' WHERE `id` = 20000000(注:存在该 id 值的记录)


不建索引的情况下:

> 时间: 0.01s


仅创建索引(`time`)的情况下:

> 时间: 0.01s


同时创建索引(`time`)和索引(`time`, `a_id`)的情况下:

> 时间: 0.01s


虽然在 INSERT / UPDATE / DELETE 时数据库会更新索引,但从实测数据来看,索引对其效率的影响可忽略不计。


一些误区


“in 语法效率很低”?

in 语法也是应用索引的,网传 in 会比一个一个 WHERE OR 要慢得多的说法是不靠谱的。in 主键和 in 索引同理。


另外:

对于字符串类型,LIKE '%abc%' 是不能应用索引的,但 LIKE 'abc%' 可以。更多关于字符串类型的索引,请查阅全文索引(FULLTEXT)。

索引的字段是可以指定长度的,类似字符串索引指定前面若干唯一字符就可以优化效率。


本文系个人实践总结,欢迎批评指正!


xoyozo 8 年前
4,877

函数功能:添加/修改/删除/获取 URL 中的参数(锚点敏感)

/// <summary>
/// 设置 URL 参数(设 value 为 undefined 或 null 删除该参数)
/// <param name="url">URL</param>
/// <param name="key">参数名</param>
/// <param name="value">参数值</param>
/// </summary>
function fn_set_url_param(url, key, value) {
    // 第一步:分离锚点
    var anchor = "";
    var anchor_index = url.indexOf("#");

    if (anchor_index >= 0) {
        anchor = url.substr(anchor_index);
        url = url.substr(0, anchor_index);
    }

    // 第二步:删除参数
    key = encodeURIComponent(key);
    var patt;

    // &key=value
    patt = new RegExp("\\&" + key + "=[^&]*", "gi");
    url = url.replace(patt, "");

    // ?key=value&okey=value  -> ?okey=value
    patt = new RegExp("\\?" + key + "=[^&]*\\&", "gi");
    url = url.replace(patt, "?");

    // ?key=value
    patt = new RegExp("\\?" + key + "=[^&]*$", "gi");
    url = url.replace(patt, "");

    // 第三步:追加参数
    if (value != undefined && value != null) {
        value = encodeURIComponent(value);
        url += (url.indexOf("?") >= 0 ? "&" : "?") + key + "=" + value;
    }

    // 第四步:连接锚点
    url += anchor;

    return url;
}

/// <summary>
/// 获取 URL 参数(无此参数返回 null,多次匹配使用“,”连接)
/// <param name="url">URL</param>
/// <param name="key">参数名</param>
/// </summary>
function fn_get_url_param(url, key) {
    key = encodeURIComponent(key);
    var patt = new RegExp("[?&]" + key + "=([^&#]*)", "gi");

    var values = [];

    while ((result = patt.exec(url)) != null) {
        values.push(decodeURIComponent(result[1]));
    }

    if (values.length > 0) {
        return values.join(",");
    } else {
        return null;
    }
}


调用示例:

var url = window.location.href;

// 设置参数
url = fn_set_url_param(url, 'a', 123);

// 获取参数
console.log(fn_get_url_param, url, 'a');


压缩版:

function fn_set_url_param(b,c,e){var a="";var f=b.indexOf("#");if(f>=0){a=b.substr(f);b=b.substr(0,f)}c=encodeURIComponent(c);var d;d=new RegExp("\\&"+c+"=[^&]*","gi");b=b.replace(d,"");d=new RegExp("\\?"+c+"=[^&]*\\&","gi");b=b.replace(d,"?");d=new RegExp("\\?"+c+"=[^&]*$","gi");b=b.replace(d,"");if(e!=undefined&&e!=null){e=encodeURIComponent(e);b+=(b.indexOf("?")>=0?"&":"?")+c+"="+e}b+=a;return b}
function fn_get_url_param(b,c){c=encodeURIComponent(c);var d=new RegExp("[?&]"+c+"=([^&#]*)","gi");var a=[];while((result=d.exec(b))!=null){a.push(decodeURIComponent(result[1]))}if(a.length>0){return a.join(",")}else{return null}};


xoyozo 8 年前
3,902

本文记录 Newtonsoft.Json 的用法,System.Text.Json 请参此文

如何在序列化时间类型时以“年-月-日 时:分:秒”的格式输出?

默认时间类型将序列化为:2017-11-10T18:48:14.1685763+08:00,我们只要 new 一个 IsoDateTimeConverter 即可改变时间类型的输出格式:

var dtc = new IsoDateTimeConverter { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" };
string b = JsonConvert.SerializeObject(DateTime.Now, dtc);


如何将对象序列化为可友好阅读的字符串?

序列化后的字符串默认是“紧凑型”的,如:{"a":1,"b":2}

可以将 Formatting 指定为 Indented 即可输出格式化后的字符串,例:

var obj = new { a = 1, b = 2 };
string a = JsonConvert.SerializeObject(obj, Formatting.Indented);

结果如下:

{

  "a": 1,

  "b": 2

}


枚举类型输出为字符串,而非索引值

[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]

更多用法参:https://www.cnblogs.com/DomoYao/p/Json.html


不序列化属性

[Newtonsoft.Json.JsonIgnore]

当值为 null 时不序列化

[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]


不序列化值为 null 的项

var jSetting = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };

JsonConvert.SerializeObject(obj, Formatting.Indented, jSetting);

xoyozo 8 年前
5,742

重点提前看:


https://xoyozo.net/Blog/Details/MySQL-on-Windows




Connector/Net 6.10.4(暂)不支持在 Visual Studio 2015/2017 添加 ADO.NET 实体数据模型时选择 MySQL Data Provider 数据源。并且,在编译项目时会出现“违反了继承安全性规则”的异常。


只能降回 Connector/Net 6.9.10,NuGet 中将 MySql.DataMySql.Data.Entity 用 6.9.10 和 6.10.4 版本皆可(除非遇到下文中提到的异常)。

重新生成或重启 VS 使之生效。


另外,MySql.DataMySql.Data.Entity 6.9.10 可以配合 EnityFrameworkEntityFramework.zh-Hans 6.1.x6.2.0 使用。


MySql.Data 和 MySql.Data.Entity 6.9.9 在创建/更新实体模型向导中会出现闪退,更换为 6.9.10 试试。

或重新安装 Connector/NET 和 MySQL for Visual Studio 并重启电脑试试。

仍然闪退,参此文:https://xoyozo.net/Blog/Details/mysql-for-vs-flashback


重要提醒,如果在添加 EF 模型时报错:

您的项目引用了最新版的实体框架;但是,找不到进行数据连接所需的与此版本兼容的实体框架数据库提供程序。请退出向导,安装兼容提供程序,重新生成您的项目,然后再执行此操作。

那么在菜单中选择:生成 - 清理解决方案,然后直接添加数据模型。如果在添加前生成了项目(bin 目录下有相关 .dll 文件),那么将出现上述错误。


类型“MySql.Data.MySqlClient.MySqlProviderServices”违反了继承安全性规则。派生类型必须与基类型的安全可访问性匹配或者比基类型的安全可访问性低。

将 MySql.Data 和 MySql.Data.Entity 从 6.10.4 退回 6.9.10 版本即可。


以上问题出现在:

VS 15.4.*

VS 15.4.4

VS 15.4.5

----------------

VS 15.5 已经可以在 EF 向导中连接 MySQL 数据库(Connector/Net 6.10.4),但 MySql.Data.Entity 6.10.4 编译仍然出现“违反了继承安全性规则”。另:MySQL 官网下架了 Connector/Net 6.10.4,退回提供 6.9.10 版本。

2017-12-9 发布的 Connector/Net 6.10.5 仍然存在无法在 EF 向导中连接 MySQL 数据源的问题。


服务器上安装 Connector/Net 6.10.4 没有任何问题。


表“TableDetails”中列“IsPrimaryKey”的值为 DBNull。怎么办?

xoyozo 8 年前
7,258

image.png

更新 .edmx 文件时若提示:

生成模型时出现意外错误。有关详细信息,请参阅输出窗口。异常消息:“StrongTypingException: 表“TableDetails”中列“IsPrimaryKey”的值为 DBNull。”。


可以通过以下方法来解决:

1,控制面板中打开“服务”(或执行命令 services.msc),重新启动“MySQL57”

2,在 MySQL 中执行:

use <<database name>>;
set global optimizer_switch='derived_merge=OFF';

如果提示“1227 - Access denied; you need (at least one of) the SUPER privilege(s) for this operation”,请使用超级用户进行操作。

3,搞定,试试更新 .edmx 吧。


参考资料:https://stackoverflow.com/questions/33575109/mysql-entity-the-value-for-column-isprimarykey-in-table-tabledetails-is

xoyozo 8 年前
5,732
  1. 在 NuGet 中安装 Microsoft.AspNetCore.Session 和 Newtonsoft.Json

  2. 打开 Startup.cs,在 ConfigureServices 方法中加入(视情况配置相关属性)

    services.AddSession(options =>
    {
        // 设置了一个较短的时间,方便测试
        options.IdleTimeout = TimeSpan.FromSeconds(10);
        options.CookieHttpOnly = true;
    });
  3. 在 Configure 方法中加入

    app.UseSession();
  4. 添加 Extensions 文件夹并添加类 SessionExtensions.cs

    using Microsoft.AspNetCore.Http;
    using Newtonsoft.Json;
    
    public static class SessionExtensions
    {
        public static void Set<T>(this ISession session, string key, T value)
        {
            session.SetString(key, JsonConvert.SerializeObject(value));
        }
    
        public static T Get<T>(this ISession session, string key)
        {
            var value = session.GetString(key);
            return value == null ? default(T) :
                                  JsonConvert.DeserializeObject<T>(value);
        }
    }
  5. 使用方法

    HttpContext.Session.Set("abc", 123);
    var v1 = HttpContext.Session.Get<int>("abc");
    HttpContext.Session.Remove("abc");
    var v2 = HttpContext.Session.Get<int>("abc");
    // v1 = 123, v2 = 0
xoyozo 8 年前
5,342

经测试,.NET Framework 4.8 的 Cache.Insert() 已恢复正常。

安装 .NET Framework 4.7 后,不管项目有没有升级到 .NET Framework 4.7,使用 Cache.Insert() 添加的自动过期的缓存都不会过期了,可以使用 Cache.Add() 来解决!

使用 Cache.Insert() 例:

HttpRuntime.Cache.Insert(key, value, null, DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration);

改为使用 Cache.Add():

HttpRuntime.Cache.Add(key, value, null, DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);

如果 Cache 中已保存了具有相同 key 的项,则调用 Add 方法将不作任何更改。(Insert 方法则会覆盖该值)


完整测试代码:

public static DateTime DateTimeNow
{
    get
    {
        if (HttpRuntime.Cache["DateTimeNow"] == null)
        {
            HttpRuntime.Cache.Add("DateTimeNow", DateTime.Now, 
                null, DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration, 
                CacheItemPriority.Default, null);
        }
        return Convert.ToDateTime(HttpRuntime.Cache["DateTimeNow"]);
    }
}


在部分项目中(如 .NET Framework 4 项目中)发现上述改动仍不起作用,不会自动过期,那么可以采用以下方法:


方法一、可以记录将数据添加到 Cache 的时间,获取数据时判断该时间,超时则清除该 Cache 并重新赋值;

方法二、放弃缓存依赖,用定时器清除 Cache 数据。

xoyozo 8 年前
6,178

将字符串作为文本文档输出:

Response.AddHeader("Content-Disposition", "attachment; filename=文件名.txt");
Response.Write(字符串内容);
Response.End();

直接提供服务器文件下载:

FileInfo thefile = new FileInfo(path);
Response.Clear();
Response.ClearHeaders();
Response.Buffer = false;
Response.ContentType = "application/octet-stream";
Response.AppendHeader("Content-Disposition", "attachment;filename=" + HttpUtility.UrlEncode("刮刮卡参与") + id + ".csv");
Response.AppendHeader("Content-Length", thefile.Length.ToString());
Response.WriteFile(thefile.FullName);
Response.Flush();
Response.End();

输出使用 NPOI 创建的 Excel(MemoryStream):

HSSFWorkbook book = new HSSFWorkbook();
……
MemoryStream ms = new MemoryStream();
book.Write(ms);
Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}.xls", scene.d.drama + " " + scene.s.time_show.ToString("yyyy年MM月dd日HH时mm分")));
Response.BinaryWrite(ms.ToArray());
Response.End();
book = null;
ms.Close();
ms.Dispose();

最后,如果要指定文件编码,加上这句就行:

Response.ContentEncoding = Encoding.xxx;

 

xoyozo 9 年前
5,335

// 定义

public enum status { ready, ordered, printed }
public enum statusB { ready = 1, ordered = 3, printed = 5 }

// 将字符串转化为枚举类型

public static status? ConvertStringToEnum_status(string str)
{
  return str != null && Enum.IsDefined(typeof(status), str) ? (status)Enum.Parse(typeof(status), str) : null as status?;
}

// 将数字(索引值)转化为枚举类型

status s = (status)1; // 值为 status.ordered
statusB s = (statusB)1; // 值为 statusB.ready

// 将枚举类型转化为数字(索引值)

int s = (int)status.ordered; // 值为 1
int s = (int)statusB.ready; // 值为 1

// 循环

foreach (status s in Enum.GetValues(typeof(status)))
{
}


当无法序列化或需要序列化为字符串时,在定义枚举前加上:

当使用 System.Text.Json 时为:

[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]

当使用 Newtonsoft.Json 时为:

[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]


xoyozo 9 年前
5,792