精华内容
下载资源
问答
  • 写这篇文章的原因是因为从我初学前端开始,就一直有人告诉我:“样式文件在head中通过link引入,script文件在body的末尾引入,这样页面加载速度更。”那么着一切究竟是为什么呢,下面我们来看一下~ (如果真的不...

    前端性能优化第四篇-为什么在head中引入CSS

    写在前面:有很长一段时间没有更新这个专栏,是因为在准备一些课程上的东西,不过这个专栏一定不会烂尾的~

    写这篇文章的原因是因为从我初学前端开始,就一直有人告诉我:“样式表文件在head中通过link引入,script文件在body的末尾引入,这样页面加载速度更快。”那么着一切究竟是为什么呢,下面我们来看一下~

    (如果真的不想看这些铺垫内容,不妨直接跳到最后的总结,嘤嘤嘤~)

    曾经的错误尝试

    下面这个例子摘自《高性能网站建设指南》第五章开头:
    Yahoo!前端团队曾经为了优化门户网站的性能,调整了CSS的位置——由于一个div是在点击某个功能时才会弹出,所以工程师将与这个div有关的CSS在文档的末尾引入,希望以这种方式提高首页的渲染效率。
    但是事实上,状况并不理想,甚至恰恰相反——将CSS在文档head引入反而更快。

    错误原因:逐步呈现被阻止

    逐步呈现

    我们都希望页面的呈现过程是一个逐步呈现的过程,理想的效果是用户能够尽快看到页面上的内容,同时内容可以呈现一个柔滑的增加,毕竟不是所有的用户都有非常良好的网络环境。这样做有几个好处:

    • 让用户感受到系统并没有崩溃,它正在有条不紊的运作
    • 让用户知道自己大概还需等待多长时间
    • 让用户对页面内容有一个概览,同时有内容可以看

    注:上面列出的三点是由 Jakob Nielson 提出的可视化回馈的重要性

    错误

    为了增强用户体验,浏览器也采用了这样的方式来处理一个页面。
    但是,为了避免当样式表改变时重绘页面中的内容,浏览器会在样式表加载好之前阻止内容呈现。所以将样式表放在文档底部,页面上的内容虽然很快加载好了,但是浏览器会延迟可视化渲染,这还造成了一个很可怕的现象——白屏。

    解决方法

    解决上面提到的问题其实很简单,只要将CSS以下面的方式引入:

    <!DOCTYPE html>
    <html>
    <head>
    	<meta charset="utf-8">
    	<title>嘤嘤嘤?</title>
    	<link rel="stylesheet" type="text/css" href="mystyle.css">
    </head>
    <body>
        
    </body>
    </html>
    

    这时又有一个问题产生了,引入CSS有两种方法:link@improt。我们要为什么一般使用link呢?
    吃了语法更简单,link@import的性能好很多,因为@import仍然没有解决白屏问题。
    使用@import会导致下载的无序性,我们的样式表仍然会在最后被下载。

    浏览器为什么这么做

    如果样式表仍然在加载,构建呈现树就是一种性能浪费,因为在所有样式表加载并解析完毕之前无需绘制任何东西。否则我们将会遇到FOUC(无样式内容闪烁)问题。

    浏览器构建页面的顺序我在之前的文章中写到过,戳戳->前端性能优化第二篇
    如果浏览器不做任何操作的话,我们将CSS放在文档后面会导致FOUC问题,具体表现为页面最初呈现一个样式,在短暂的几秒后,样式表加载好,页面布局突然发生了变化。

    而白屏现象则是浏览器对闪烁的弥补措施,避免额外的呈现过程,浏览器得以延迟呈现这个页面。

    总结

    1. 我们应当遵守规范,使用LINK标签,将样式表文件添加到HEAD
    2. 如果我们将样式表放到文档的底部或者使用@import可能会导致“白屏”或者“无样式内容闪烁”,具体会发生什么取决于浏览器的策略。
    展开全文
  • CSS语法,引入方式

    2020-07-07 15:05:38
    为什么要使用CSS?怎么设置CSS样式?语法样式分类行内样式内联样式链接样式导入样式 什么是CSS? Cascading Style sheet (层叠样式)的缩写 用来控制网页外观。(修改和美化HTML) HTML 超文本标记语言 从...

    什么是CSS?

    Cascading Style sheet (层叠样式表)的缩写
    用来控制网页外观。(修改和美化HTML)
    HTML 超文本标记语言 从语义的角度描述页面结构
    CSS 层叠样式表 从审美的角度负责页面样式
    JavaScript 从交互的角度描述页面行为

    为什么要使用CSS?

    1. 使页面代码更少,网页下载更快
    2. 实现了内容与样式的分离,使网站维护更快捷
    3. 使网页与浏览器的兼容性更好

    怎么设置CSS样式?

    CSS的思想就是首先指定对什么“对象"进行设置,设置属性CSS。最后给出该设置的”值“。

    	可以说CSS就是由三个基本部分组成的--”对象“,”属性“和”值“。”对象“很重要的,它制定了对哪些网页元素
    和进行设置 ,因此,他有一个专门的名称———选择器(selector)。
    
    语法
    选择器{
    	属性1:属性值;
    	属性2:属性值;
    }
    注:属性包含在{}中
    	属性和属性值之间用":"进行分割;
    	当有多个属性时用";"进行分割
    

    样式表分类

    内部样式表
    行内样式表
    内联样式表
    外部样式表
    链接样式表
    导入样式表
    行内样式表
        在html标记内,使用style属性定义css样式。
    如:<p style="属性:属性值">内容</p>
    		该语法中style是标记的属性,实际上任何HTML标记都拥有style属性,用来设置行内式。其中属性和值
    的书写规范与css样式规则相同,行内式只对其所在的标记及嵌套在其中的子标记起作用。
    		特点:行内式也是通过标记的属性来控制样式的,这样并没有做到结构分离与表现的分离,所以一般
    很少用。只有在样式规则较少的时候或者只在该元素上使用一次的时候又或者临时修改样式的时候使用
    
    内联样式表
    	将CSS添加到<head>标记之间,并且用<style></style>标记声明的一种样式表
    语法:<style type="text/scss">
    	 	p{
    			属性:属性值;
    		}
    	 </style>
    特点:内联式CSS样式表,只对其所在的HTML页面有效,因此,仅设计一个页面时,使用内联式是个不错的选择。
    	 但如果在一个网站,不建议使用这个方式,因为它不能充分发挥CSS代码的重用优势。
    
    链接样式表
    	链入式是将所有的样式放在一个或多个以.css为扩展名的外部样式表文件中,通过<link/>标记将外部样式表文件链接到HTML文档中。
    语法:<link rel="stylesheet" type="text/css" href="css文件地址"/>
    		href:指定了CSS样式表所在位
    		rel:指定链接到样式表,其值为stylesheet,不可省略
    		type:表示样式表类型为css样式表
    
    导入样式表
    	<head>
    		<style type="text/css">
    			@import url(css文件地址)
    		</style>
    	</head>
    特点:基本不使用,因为页面会先加载html,然后再去加载css,这样就会造成页面样式的延迟。
    注意:导入和链接式样式表优缺点比较
    	导入式会在整个网页装载完后再装载css文件,因此这就导致了一个问题,如果网页比较大的话,则会出现先显示无样式的网页闪烁一下之后,再出现网页的样式。这是导入式固有的一个缺陷。
    	使用链接式时与导入式不同的是它会以网页文件主体装载前,装载css文件,因此显示出来的网页从一开始就是带样式的效果的它不会像导入式那样先显示无样式的网页,然后再显示由样式的网页,这是链接式的优点。
    

    样式表的优先级

    • 行内样式的优先级最高
    • 内嵌样式和链接样式,导入样式,那个后加载,那个优先级高
    展开全文
  • C语言链表操作详解

    万次阅读 多人点赞 2018-12-29 19:01:55
    为什么要使用链表 在未学习链表时,我们常用的存储数据的方式无非就是数组。使用数组存储数据的好处就是查询,但是它的弊端也很明显: 使用前需声明数组的长度,一旦声明长度就不能更改 插入和删除操作需要...

    为什么要使用链表

    在未学习链表时,我们常用的存储数据的方式无非就是数组。使用数组存储数据的好处就是查询快,但是它的弊端也很明显:

    1.  使用前需声明数组的长度,一旦声明长度就不能更改
    2. 插入和删除操作需要移动大量的数组元素,效率慢
    3. 只能存储一种类型的数据.

    而链表则可以实现以上这些数组所不具备的功能,此时引入了结构体来实现创建链表的操作。

    链表的特点:

    1.  n个节点离散分配
    2. 每一个节点之间通过指针相连
    3. 每一个节点有一个前驱节点和一个后继节点
    4. 首节点没有前驱节点,尾节点没有后继节点

    首先先定义一个简单的结构体

    struct link{
           int data;          //定义数据域
           struct link *next; //定义指针域,存储直接后继的节点信息
    };

    数据域的内容可以自己指定,指针域用来存放下一个节点的地址。

    创建链表前须知

    首节点:存放第一个有效数据的节点

    头节点:在单链表的第一个结点之前附设一个结点,它没有直接前驱,称之为头结点,头结点的数据域可以不存储任何信息,指针域指向第一个节点(首节点)的地址。头结点的作用是使所有链表(包括空表)的头指针非空

    头指针:指向头节点的指针

    尾节点:存放最后一个有效数据的节点

    尾指针:指向尾节点的指针

    建立一个单向链表

    方法:定义方法向链表中添加节点来建立一个单向链表

    思路:首先定义一个结构体指针变量p,使用malloc函数为新建节点申请内存空间,让p指向这个节点(指向它刚申请的内存空间),再将其添加到链表中。此时需考虑两种情况:

    1.若单链表为空表,则将新建节点置为头节点

    //若此时只在链表中插入头节点
    struct link *p = head;
    p = (struct link *)malloc(sizeof(struct link));  //让p指向新建节点创建的内存空间
    if(p == NULL){   //新建节点申请内存失败
        exit(0);
    }
    //此时head也指向头节点的地址
    p->next = NULL;     //因为没有后续节点,所以新建节点指针域置空

    2.链表非空,则将新建节点添加到表尾

    //此时已存在头节点,再插入一个节点(首节点)
    struct link *p = NULL,*pr = head;  
    p = (struct link *)malloc(sizeof(struct link));
    if(p == NULL){
        exit(0);
    }
    if(head == NULL){
        head = p;
    }else{
        while(pr->next != NULL){     //当头节点的指针域不为NULL
          pr = pr->next;             //pr指向下一个节点的地址
      }
        pr->next = p;                //使头节点的指针域存储下一个节点的地址
    }

    完整操作

    #include <stdio.h>
    #include <stdlib.h>
    struct link *AppendNode(struct link *head);
    void DisplayNode(struct link *head);
    void DeleteMemory(struct link *head);
    //定义结构体 
    struct link{
    	int data; 				//定义数据域 
    	struct link *next; 			//定义指针域 
    }; 
    int main(){
    	int i =0;         			//定义i,记录创建的节点数 
    	char c;							
    	struct link *head = NULL;		//创建头指针,初始化为NULL 
    	printf("DO you wang to append a new node(Y or N)?");
    	scanf(" %c",&c);				
    	while(c == 'Y' || c == 'y'){	//如果确定继续添加结点 
    		head = AppendNode(head);	//通过函数获得链表的地址,AppendNode函数返回的是链表的头指针
    		                            //可以根据头指针指向的地址来获取链表中保存的数据 
    		DisplayNode(head);			//根据头指针打印链表 
    		printf("DO you want to append a new node(Y or N)?");
    		scanf(" %c",&c);
    		i++; 
    	}
    	printf("%d new nodes hava been appended!\n",i);
    	DeleteMemory(head);			//释放占用的内存 
    }
    struct link *AppendNode(struct link *head){    //声明创建节点函数 
    	struct link *p = NULL,*pr = head;      //创建p指针,初始化为NULL;创建pr指针,通过pr指针来给指针域赋值 
    	int data ;
    	p = (struct link *)malloc(sizeof(struct link)) ; //为指针p申请内存空间,必须操作,因为p是新创建的节点 
    	if(p == NULL){			//如果申请内存失败,则退出程序 
    		printf("NO enough momery to allocate!\n");
    		exit(0);
    	}
    	if(head == NULL){		//如果头指针为NULL,说明现在链表是空表 
    		head = p;								//使head指针指向p的地址(p已经通过malloc申请了内存,所以有地址) 
    	}else{										//此时链表已经有头节点 ,再一次执行了AppendNode函数 
    												//注:假如这是第二次添加节点
    	                                  //因为第一次添加头节点时,pr = head,和头指针一样指向头节点的地址 
    		while(pr->next!= NULL){		//当pr指向的地址,即此时的p的指针域不为NULL(即p不是尾节点) 
    			pr = pr->next;	//使pr指向头节点的指针域
    		}
    		pr->next = p;	//使pr的指针域指向新键节点的地址,此时的next指针域是头节点的指针域 
    	}
    	printf("Input node data:");                   
    	scanf("%d",&data);
    	p->data = data; 			//给p的数据域赋值 
    	p->next = NULL;			//新添加的节点位于表尾,所以它的指针域为NULL 
    	return head;			//返回head的地址 
    }
    void DisplayNode(struct link *head){         	//输出函数,打印链表 
    	struct link *p = head;			// 定义p指针使其指向头节点 
    	int j = 1;									//定义j记录这是第几个数值 
    	while(p != NULL){		//因为p = p->next,所以直到尾节点打印结束 
    		printf("%5d%10d\n",j,p->data);			
    		p = p->next;		//因为节点已经创建成功,所以p的指向由头节点指向下一个节点(每一个节点的指针域都指向了下一个节点) 
    		j++;	
    	}
    }
    void DeleteMemory(struct link *head){			//释放资源函数 
    	struct link *p = head,*pr = NULL;	        //定义p指针指向头节点 
    	while(p != NULL){				//当p的指针域不为NULL 
    		pr = p;									//将每一个节点的地址赋值给pr指针 
    		p = p->next;			        //使p指向下一个节点 
    		free(pr);								//释放此时pr指向节点的内存 
    	}
    }

    第二种创建链表方式-优化

    #include <stdio.h> 
    #include <stdlib.h>
    struct Stu *create(int n);
    void print(struct Stu *head);
    struct Stu{
    	int id;
    	char name[50];
    	struct Stu *next;
    };
    int main(){
    	int n;
    	struct Stu *head = NULL;   //创建头指针 
    	printf("请输入你想要创建的节点个数:\n");
    	scanf("%d",&n);
    	head = create(n);
    	print(head);
    }
    struct Stu *create(int n){
    	struct Stu *head,*node,*end;   						//定义头节点,普通节点,尾节点 
    	head = (struct Stu *)malloc(sizeof(struct Stu)); 	//给头节点申请内存 
    	end = head;        									//若是空表,则头尾地址一致 
    	for(int i=0;i<n;i++){								//利用for循环向链表中添加数据 
    		node = (struct Stu *)malloc(sizeof(struct Stu));//给普通节点申请内存空间 
    		scanf("%d %s",&node->id,node->name);	//给数据域赋值 
    		end->next = node;					//让上一个节点的数据域指向当前节点 
    		end = node;     						//end指向当前节点,最终end指向尾节点 
    	}
    	end->next = NULL;                                   //给end的指针域置空 
    	return head;                                        //返回头节点的地址 
    }
    void print(struct Stu *head){
    	struct Stu *p = head;
    	int j =1;
    	p = p->next;  //不打印头节点的数据域中的值 
    	while(p != NULL){
    		printf("%d\t%d\t%s\n",j,p->id,p->name);
    		p = p->next;
    		j++;
    	}
    }

    前插法创建链表 --逆序输出

    struct link *create(int n){
         struct link *headnode ,*node;
         headnode = (struct link *)malloc(sizeof(struct link));   //为头节点申请内存 
    	 headnode ->next = NULL;     //让头节点的指针域置空 
    	 for(int i=0;i<n;i++){ 
    	 	node = (struct link *)malloc(sizeof(struct link));  //给新建节点申请内存 
    	 	scanf("%d",&node->data);       //新建节点数据域传值 
    	 	node->next = headnode->next;   //新建节点的数据域指向头节点 == 创建尾节点 
    	 	headnode->next = node;         //将新建节点数据域传给头节点 
    	 }	
    	 return headnode;  
    }

     

    删除节点

    void deleteNode(struct Stu *head,int n){         //删除n处的节点 
    	struct  Stu *p = head,*pr = head;
    	int i =0;
    	while(i<n&&p!=NULL){       //到达指定节点,此时p指向指定节点,pr指向上一节点 
    		pr = p;            //将p的地址赋值给pr
    		p = p->next;       //p指向下一节点
    		i++;
    	}
    	if(p!=NULL){               //当p不为空时,即p不能指向尾节点之后的节点
    		pr->next = p->next;
    		free(p);
    	} else{
    		printf("节点不存在!\n"); 
    	}
    } 

    我在这着重解释一下p->next = NULL和p!=NULL的区别,因为我刚开始也经常弄错!!

    • while(p->next != NULL) 循环结束时,此时p的位置是尾节点的位置,但如果用于输出函数的判断条件,则尾节点的数据不会输出。
    • while(p!=NULL) 循环结束时, 此时p指向的位置为尾节点的下一个节点,因为没有申请内存空间,所以是一个未知的区域。

    插入节点

    void insertNode(struct Stu *head,int n){    //插入节点 
    	struct Stu *p = head,*pr;
    	pr = (struct Stu*)malloc(sizeof(struct Stu));  //让pr指向新建节点申请的内存 
    	printf("input data:\n");
    	scanf("%d %s",&pr->id,pr->name);
    	int i = 0;
        //当插入位置是尾节点时,只要在尾节点后再插入一个节点,并让尾节点的指针域指向新建节点,新建节点的指针域置空
        while(i<n&&p!=NULL){             //使p指向将要插入节点的位置 
        	p = p->next;
    		i++;
    	}
    	if(p!=NULL){            //如果p没越界 
    		pr->next = p->next; //将新建节点的地址指向将要插入节点的后一个节点的地址 
    		p->next = pr;       //使插入节点指向新建节点 
    	}else{
    		printf("节点不存在!\n");
    	}
    }

    修改节点

    void change(struct Stu *head,int n){
    	struct Stu *p = head;
    	int i = 0;
    	while(i<n && p!=NULL){      //使p指向需修改节点 
    		p = p->next;
    		i++;
    	}
    	if(p != NULL){             
    	printf("请输入修改之后的值:\n");
    	scanf("%d %s",&p->id,p->name);	
    	}else{
    		printf("节点不存在!\n");
    	} 

    链表的逆序

     

    思路:假如此时链表有两个有效节点,头节点为0号,中间的节点为1号,尾节点为2号

    1.定义三个指针pf指向链表的头节点(0号),tmp,pb初始化为NULL

    2.让pb指向pf的下一个节点(1号),并将此时头节点的指针域置空(变为尾节点)

    3.第一次while循环,让tmp指向pb(1号),然后让pb指向下一个节点,再让tmp让1号节点的指针域(tmp->next = pf)指向pf(上一个节点)(0号),再将pf指向tmp(1号)(pf = tmp)

    4.第二次while循环,让tmp指向pb(2号),然后让pb指向下一个节点,此时pb==NULL,所以这是最后一次循环,再让tmp让2号节点的指针域(tmp->next = pf)指向pf(1号),再将pf指向tmp(2号)

     5.此时链表逆序完成,让头指针指向首节点(2号),返回头指针即可

                                                                              逆序后

    STU *link_reversed_order(STU *head)
    {
        STU *pf = NULL, *pb = NULL, *tmp = NULL; 
        pf = head;  //将头节点的地址赋值给pf 
        if(head == NULL) { //如果链表为空 
            printf("链表为空,不需要逆序!\n");
            return head;
        } else if(head->next == NULL) {  //如果只有一个节点 
            printf("链表只有一个节点,不需要逆序!\n");
            return head;
        } else {
            pb = pf->next;  //pb指向pf的下一个节点 
            head->next = NULL; //头节点的指针域置空(变为尾节点) 
            while(pb != NULL)	//当pb不为空时 
            {
                tmp = pb;	//将pb的地址赋值给temp 
                pb = pb->next; //pb指向下一个节点 
                tmp->next = pf;	//pb的上一个节点的指针域指向pf 
                pf = tmp;	//让pf指向tmp 
            }
            head = pf;
            return head;
        }    
    }*/

    所有操作

    #include <stdio.h> 
    #include <stdlib.h>
    struct Stu *create(int n);
    void print(struct Stu *head);
    void deleteNode(struct Stu *head,int n);
    void insertNode(struct Stu *head,int n);
    void change(struct Stu *head,int n);
    struct Stu{
    	int id;
    	char name[50];
    	struct Stu *next;
    };
    int main(){
    	int n,j,in,cn;
    	char c;
    	struct Stu *head = NULL;   //创建头指针 
    	printf("请输入你想要创建的节点个数:\n");
    	scanf("%d",&n);
    	head = create(n);
    	print(head);
    	while(true){
    	printf("请选择你想要执行的操作:\n");
    	printf("1.插入节点\n2.删除节点\n3.修改节点\n4.退出程序\n");
    	scanf(" %c",&c);
    	if(c =='1'){
    	printf("你想要在哪插入节点:\n");
    	scanf("%d",&in);
    	insertNode(head,in);
    	print(head); 
    	}else if(c == '2'){
    	printf("你想要删除哪个节点的数据:\n");
    	scanf("%d",&j);
    	deleteNode(head,j);
    	print(head);
    	}else if(c =='3'){
    	printf("你想要修改哪个节点的数据:\n");
    	scanf("%d",&cn);
    	change(head,cn);
    	print(head); 
    	}else if(c =='4'){
    		exit(0);
    	} 		
     } 
    }
    struct Stu *create(int n){
    	struct Stu *head,*node,*end;   						//定义头节点,普通节点,尾节点 
    	head = (struct Stu *)malloc(sizeof(struct Stu)); 	//给头节点申请内存 
    	//head->id = n;										//头节点的数据域保存链表的长度 
    	end = head;        									//若是空表,则头尾地址一致 
    	for(int i=0;i<n;i++){								//利用for循环向链表中添加数据 
    		node = (struct Stu *)malloc(sizeof(struct Stu));//给普通节点申请内存空间 
    		scanf("%d %s",&node->id,node->name);			//给数据域赋值 
    		end->next = node;								//让上一个节点的数据域指向当前节点 
    		end = node;     								//end指向当前节点,最终end指向尾节点 
    	}
    	end->next = NULL;                                   //给end的指针域置空 
    	return head;                                        //返回头节点的地址 
    }
    void print(struct Stu *head){
    	struct Stu *p = head;
    	int j =1;
    	p = p->next;  //不打印头节点的数据域中的值 
    	while(p != NULL){
    		printf("%d\t%d\t%s\n",j,p->id,p->name);
    		p = p->next;
    		j++;
    	}
    }
    void deleteNode(struct Stu *head,int n){         //删除n处的节点 
    	struct  Stu *p = head,*pr = head;
    	int i =0;
    	while(i<n&&p!=NULL){       //到达指定节点,此时p指向指定节点,pr指向上一节点 
    		pr = p;
    		p = p->next;
    		i++;
    	}
    	if(p!=NULL){
    		pr->next = p->next;
    		free(p);
    	} else{
    		printf("节点不存在!\n"); 
    	}
    } 
    void insertNode(struct Stu *head,int n){    //插入节点 
    	struct Stu *p = head,*pr;
    	pr = (struct Stu*)malloc(sizeof(struct Stu));  //让pr指向新建节点申请的内存 
    	printf("input data:\n");
    	scanf("%d %s",&pr->id,pr->name);
    	int i = 0;
        //当插入位置是尾节点时,只要在尾节点后再插入一个节点,并让尾节点的指针域指向新建节点,新建节点的指针域置空
        while(i<n&&p!=NULL){             //使p指向将要插入节点的位置 
        	p = p->next;
    		i++;
    	}
    	if(p!=NULL){            //如果p没越界 
    		pr->next = p->next; //将新建节点的地址指向将要插入节点的后一个节点的地址 
    		p->next = pr;       //使插入节点指向新建节点 
    	}else{
    		printf("节点不存在!\n");
    	}
    }
    void change(struct Stu *head,int n){
    	struct Stu *p = head;
    	int i = 0;
    	while(i<n && p!=NULL){      //使p指向需修改节点 
    		p = p->next;
    		i++;
    	}
    	if(p != NULL){             
    	printf("请输入修改之后的值:\n");
    	scanf("%d %s",&p->id,p->name);	
    	}else{
    		printf("节点不存在!\n");
    	} 	 
    } 

     

     

    展开全文
  • 在上一期图解图解MySQL | MySQL DDL为什么成本高?中,我们介绍了: 传统情况下,为添加列需要对表进行重建 腾讯团队为 MySQL 引入了 Instant Add Column 的方案(以下称为 "立刻加列" 功能)可以快速完成.....

    原创作者:图解MySQL


    在上一期图解 图解MySQL | MySQL DDL为什么成本高?中,我们介绍了:

    • 传统情况下,为表添加列需要对表进行重建

    • 腾讯团队为 MySQL 引入了 Instant Add Column 的方案(以下称为 "立刻加列" 功能)可以快速完成 为表添加列 的任务

    同时我们留了以下思考题:

    • "立刻加列" 是如何工作的 ?

    • 所谓 "立刻加列" 是否完全不影响业务,是否是真正的 "立刻" 完成 ?

    本期我们针对这几个问题来进行讨论:

     

    传统情况

    我们先回顾一下,在没有 "立刻加列" 功能时,加列操作是怎么完成的。我们也借此来熟悉一下本期的图例:

    • 当进行 加列操作 时,所有的数据行 都必须要 增加一段数据(图中的 列 4 数据)

    • 如上一期图解所讲,当改变数据行的长度,就需要 重建表空间(图中灰蓝的部分为发生变更的部分)

    • 数据字典中的列定义也会被更新

    以上操作的问题在于 每次加列 操作都需要重建表空间,这就需要大量 IO以及大量的时间


    立刻加列

    "立刻加列" 的过程如下图:

    • "立刻加列" 时,只会变更数据字典中的内容,包括:

      • 列定义中增加 新列的定义

      • 增加 新列的默认值

    • "立刻加列" 后,当要读取表中的数据时:

      • 由于 "立刻加列" 没有 变更行数据,读取的行数据只有 3 列

      • MySQL 会将 新增的第 4 列的默认值,追加到 读取的数据后

    以上过程描述了 如何读取 在 "立刻加列" 之前写入的数据,其实质是:在读取数据的过程中,"伪造" 了一个新列出来

    那么如何读取 在 "立刻加列" 之后 写入的数据呢 ? 过程如下图:

    当读取 行 4 时:

    • 通过判断 数据行的头信息中的instant 标志位,可以知道该行的格式是 "新格式":该行头信息后有一个新字段 "列数"

    • 通过读取 数据行的 "列数" 字段,可以知道 该行数据中多少列有 "真实" 的数据,从而按列数读取数据

    通过上图可以看到:读取 在"立刻加列"前/后写入的数据是不同的流程

    通过以上的讨论,我们可以总结 "立刻加列" 之所以高效的原因是:

    1. 在执行 "立刻加列" 时,不变更数据行的结构

    2. 读取 "旧" 数据时,"伪造"新增的列,使结果正确

    3. 写入 "新" 数据时,使用了新的数据格式(增加了instant标志位 和 "列数" 字段),以区分新旧数据

    4. 读取 "新" 数据时,可以如实读取数据

    那么 我们是否能一直 "伪造" 下去 ? "伪造" 何时会被拆穿 ?

    考虑以下场景:

    1. 用 "立刻加列" 增加列 A

    2. 写入数据行 1

    3. 用 "立刻加列" 增加列 B

    4. 写入数据行 2

    5. 删除列 B

    我们推测一下 "删除列 B" 的最小代价:需要修改 数据行中的instant标志位或 "列数" 字段,这至少会影响到 **"立刻加列" **之后写入的数据行,成本类似于重建数据

    从以上推测可知:当出现 与 "立刻加列" 操作不兼容 的 DDL 操作时,数据表需要进行重建,如下图所示:

    扩展思考题:是否能设计其他的数据格式,取代instant标志位和 "列数" 字段,使得 加列/删列 操作都能 "立刻完成" ?(提示:考虑 加列 - 删列 - 再加列 的情况)


    使用限制

    在了解原理之后,我们来看看 **"立刻加列" **的使用限制,就很容易能理解其中的前两项:

    1. "立刻加列" 的加列位置只能在表的最后,而不能加在其他列之间
      在元数据中,只记录了 数据行 应有多少列,而没有记录 这些列 应出现的位置。所以无法实现指定列的位置

    2. "立刻加列" 不能添加主键列
      加列 不能涉及聚簇索引的变更,否则就变成了 "重建" 操作,不是 "立刻" 完成了

    3. "立刻加列"不支持压缩的表格式
      按照 WL 的说法:"COMPRESSED is no need to supported"(没必要支持不怎么用的格式)

    总结回顾

    我们总结一下上面的讨论:

    1. "立刻加列" 之所以高效的原因是:

      1. 在执行 "立刻加列" 时,不变更数据行的结构

      2. 读取 "旧" 数据时,**"伪造" **新增的列,使结果正确

      3. 写入 "新" 数据时,使用了新的数据格式 (增加了 instant 标志位 和 "列数" 字段),以区分新旧数据

      4. 读取 "新" 数据时,可以如实读取数据

    2. "立刻加列" 的 "伪造" 手法,不能一直维持下去。当发生 与 "立刻加列" 操作不兼容 的 DDL 时,表数据就会发生重建

    回到之前遗留的两个问题:

    • "立刻加列" 是如何工作的 ?

      我们已经解答了这个问题

    • 所谓 "立刻加列" 是否完全不影响业务,是否是真正的 "立刻" 完成 ?

      可以看到:就算是 "立刻加列",也需要变更 数据字典,那么 该上的锁还是逃不掉的。也就是说 这里的 "立刻" 指的是 "不变更数据行的结构",而并非指 "零成本地完成任务"

    本期仍然留下一个思考题:

    • 本文中描述了 在 "立刻加列" 之后 插入 数据行的情况 (数据行会使用新格式)。那么在 "立刻加列" 之后 更新 数据行会发生什么情况呢 ?

    转载于:https://my.oschina.net/actiontechoss/blog/3072064

    展开全文
  • 在上一期图解 图解MySQL | MySQL DDL为什么成本高?中,我们介绍了:传统情况下,为添加列需要对表进行重建腾讯团队为 MySQL 引入了 Instant Add Column 的方案(以下称为 "立刻加列" 功能)可以快速完成 为添加列...
  • 原标题:图解MySQL | [原理解析] MySQL 为添加列 是怎么"立刻"完成的作者:图解MySQL在上一期图解 图解MySQL | MySQL DDL为什么成本高?中,我们介绍了:传统情况下,为添加列需要对表进行重建腾讯团队为 MySQL ...
  • 逻辑回归属于广义线性模型,表达能力有限,单变量离散化为N个后,每个变量有单独的权重,相当于模型引入了非线性,,能够提高模型表达力,加大拟合, 离散特征的增加和减少都很容易,易于模型的快速迭代;...
  • 在上一期图解图解MySQL | MySQL DDL为什么成本高?中,我们介绍了:传统情况下,为添加列需要对表进行重建腾讯团队为 MySQL 引入了 Instant Add Column 的方案(以下称为 "立刻加列" 功能)可以快速完成 为添加列 ...
  • 在上一期图解 MySQL DDL 为什么成本高?> 中,我们介绍了:传统情况下,为添加列需要对表进行重建腾讯团队 为 MySQL 引入了 Instant Add Column 的方案(以下称为 "立刻加列" 功能)可以快速完成 为添加列 的...
  • 原创作者:爱可生开源社区在上一期图解 图解MySQL | MySQL DDL为什么成本高?中,我们介绍了:传统情况下,为添加列需要对表进行重建腾讯团队为 MySQL 引入了 Instant Add Column 的方案(以下称为 "立刻加列" ...
  • 在上一期图解图解MySQL | MySQL DDL为什么成本高?中,我们介绍了:传统情况下,为添加列需要对表进行重建腾讯团队为 MySQL 引入了 Instant Add Column 的方案(以下称为 "立刻加列" 功能)可以快速完成 为添加列 ...
  • sqlserver分区小结

    2016-05-26 16:03:00
    为什么分区?  当一个的数据量太大的时候,我们最想做的一件事是什么?将这个一分为二或者更多分,但是还是这个,只是将其内容存储分开,这样读取就了N倍了  原理:数据是无法放在文件中的,...
  • 单变量离散化为N个后,每个变量有单独的权重,相当于模型引入了非线性,能够提升模型表达能力,加大拟合; 离散特征的增加和减少都很容易,易于模型的快速迭代; ② 速度!速度!速度!稀疏向量内积乘法...
  • 单变量离散化为N个后,每个变量有单独的权重,相当于模型引入了非线性,能够提升模型表达能力,加大拟合;离散特征的增加和减少都很容易,易于模型的快速迭代; 速度。稀疏向量内积乘法运算速度,计算结果方便...
  • 单变量离散化为N个后,每个变量有单独的权重,相当于模型引入了非线性,能够提升模型表达能力,加大拟合;离散特征的增加和减少都很容易,易于模型的快速迭代; 速度。稀疏向量内积乘法运算速度,计算结果...
  • 跳跃(续)

    千次阅读 2013-11-23 12:18:41
    http://blog.csdn.net/qq575787460/article/details/16371287上一篇引入了跳跃...那么跳跃表为什么呢?下来看一个生活中的例子。 如图,A--G分别代表250路公交车经过的车站。有特快,和慢3条线路。 特快的只
  • 上的转发记录

    2015-06-24 09:18:00
    今天这篇文章我想谈下堆上特有的性能问题:转发记录(Forwarding Records)。首先我们要澄清下什么是堆:堆就是没有聚集索引定义的。...为什么会有转发记录? 当堆表里的记录需要移动到不同的物理位置时,S...
  • 为什么引入分区和桶的概念?hive的select会扫描整个的内容, 引入partition 桶【把hive的数据划分为】。partition更粗粒度 桶更细粒度 在小范围的查询上提高效率。分区:partitioned by(分区字段的名字 ...
  • 一次学习: Express Router非常强大,灵活且简单,为什么不将类似的API引入前端? 在底层,prouter使用相同的(很棒的)库来表达解析(因此,它具有相同的灵活性来声明路径)。 阅读有关中间件概念的更多信息。 不...
  • HIT HIT CS&E CS&E 提要 8.1 为什么引入搜索策略 第八章 8.2 基本的搜索策略 树的搜索策略 8.3 优化的搜索策略 8.4 人事安排问题 8.5 旅行商售货问题 张炜 计算机科学与工程系 8.6 0-1背包问题 8.7 A*算法 HIT HIT ...
  • 红黑树快速理解

    2020-06-29 11:43:48
    我觉得认识红黑树的第一步要从为什么发明并使用它开始。 学习数据结构这门课的同学,如果用的是严蔚敏老师的经典教材的话,其中是没有对红黑树进行讲解的,但一定学过二叉搜索树和AVL树(平衡二叉树)。其中二叉搜索...
  • 第2天-css快速入门

    2018-05-09 16:15:00
     css(cascading style sheet,可以译“层叠样式”),是一组格式设置规则,用于控制web页面的外观 如何让一个标签具有样式  第一步:必须保证引入方式正确  第二步:必须让css和html元素产生关联,也就是...
  • MyBatis-Plus快速入门

    2020-06-09 00:10:17
    什么是MyBatis-Plus? MyBatis-Plus(以下简称MP),简化开发而生。 MP是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变。 MP的特性 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如...
  • 前段时间工作比较忙,搬了一段时间前端的砖,看了下大佬们的代码都是写的非常好,公司用的都是Java8的语法,非常...了解了为什么要使用之后,我们开始通过一个例子引入Lambda表达式。 需求1:获取公司大于二十岁的员.
  • 苹果公司通过 iPhone 6s 和 6s Plus 引入了与手机互动的全新方式:按压手势。你也许知道,苹果智能手表和苹果笔记本电脑早已具备这一功能,只是...如果你在想,为什么将力感触控在 iPhone 中改名为 3D 触控,那你真...
  • 为什么叫S/4而不是R/4(S代 Simple,4代 第四代)。因为它利 用新的用户体验技术(SAP Fiori)和内存处理和数据库技术(SAP HANA),以及引入了一个新的引导配置的概念。从部署和应用两方面精简ERP,以适应移动和工业...
  • MongoDB 初见指南

    万次阅读 热门讨论 2016-01-18 20:46:56
    技术若只如初见,那么还会踩坑么? 在系统引入 MongoDB 也有几年了,一开始是因为 MySQL 中有单...为什么是 MongoDB?刚巧赶上公司 DBA 团队引入了这个数据库,有人帮助运维,对业务团队就成了一个自然的选择。不过对

空空如也

空空如也

1 2 3 4 5 ... 9
收藏数 161
精华内容 64
关键字:

为什么引入快表