Monday, April 11, 2016

Configuration of Central Management Server and Registration of Servers using PowerShell Script

Central Management Server (CMS) is powerful tool for the DBAs, it is very useful when we try run a same script on group of servers to make a configuration changes or to list the databases from all the SQL Servers in your company or setting any database level setting or there are many cases where this CMS comes handy. 

Create and Configure Central Management Server: Choose a server to use as the CMS server. Open SSMS and click on registered Servers form View menu. 













It will open a Registered Servers pane which contains a node saying Central Management Servers, Right click on that Node and click on Register Central management Sever as below. 












It will open a pop window like below, enter the server that you use as CMS in the Server name then click test and save.

























Now it is time to register the Servers on CMS to use the CMS power. We can register the servers manually but if you have lot of servers it will tedious job to register individual servers. Here we can use the power of Powers hell to bulk register servers.

Registering Servers using PowerShell: Create a table SERVERS with a column name as SERVERNAME on the CMS Server. And just load that table with all the SQL Server servers to be registered on the CMS as below.














Create Powershell script as below and name it as RegSvr.ps1 and note the location where you saved the script. Replace CMSSERVER with the CMS server that you chose, DATABASE is the DB name on the CMSSERVER where you have create the SERVERS table with the list of registered servers. And choose the DIRECTORY and SUBDIRECTORY names if you have different server groups for different purposes. Keep reset of the things as it is. 















Running the PowerShell Script: Connect to the CMS server and right on server and click the Start PowerShell. When you see the PowerShell window run SL ,then in the next line enter the name of the PowerShell script Regsvr.ps1 as in the below screen shot.



























As it register, we can see the results as below..











We can see the registered servers as below, click on node and select new query to run a script against all the servers falling under that node. Shown as below. 






























Attaching the PowerShell script here:


$cmsservers = invoke-sqlcmd -serverinstance 'CMSSERVER'-database 'DATABASE' -query "SELECT SERVERNAME FROM Servers"
cd 'SQLSERVER:\sqlregistration\Central Management Server Group\CMSSERVER\DIRECTORY\SUBDIRECTORY'
foreach ($server in $cmsservers)
{
$svrname = $server.serverName
$regsvrname = $svrname
if ($regsvrname -like "*\*")
{
$regsvrname = $regsvrname.replace("\", "%5C")
}
$svrname
new-item $regsvrname -itemtype registration -value "server= $regsvrname;integrated security=true"
}

I have used the post from SQLTIPS on registering servers on CMS, you can find the post here.
Thanks, Hope it helps. 

Sunday, April 10, 2016

SQL Server Isolation Levels

There are 5 different Isolation levels available in SQL Server. They are Read Committed, Read Uncommitted, Repeatable Read, Snapshot and Serializable.  There is another flavor of Isolation know as Read Committed Snapshot as well. Before we start learning about Isolation Levels, let's learn about ACID nature of SQL Server as well from here.  

Lets create a sample DB and table as below to play with different isolation levels:
USE MASTER
CREATE DATABASE IsolationDB
GO

USE IsolationDB
GO
CREATE TABLE Isotab
(
EmployeeID INT IDENTITY(1,1),
FirstName VARCHAR(256),
LastName varchar(256),
Gender varchar(10)
CONSTRAINT pk_Isotab PRIMARY KEY (EmployeeID)
)
GO

Read Committed: This is the default Isolation of the SQL Server, It means selects will only returns the committed data. The select transactions will issue a Shared lock on the requested data, and the Select statement will wait until any another transactions with exclusive locks that trying to modify the data are completed. Once the shared lock is captured other transactions trying to modify that data will request an exclusive lock and wait until this Read Committed transaction finishes.

Run both the queries:
Query 1:
USE IsolationDB
GO
TRUNCATE TABLE dbo.Isotab
GO
DBCC CHECKIDENT ('Isotab', RESEED,0)
GO
USE IsolationDB
GO
BEGIN TRAN
INSERT INTO DBO.Isotab (FirstName,LastName,Gender)
VALUES
('Don','Trump', 'M'),
('Hillary','Clinton', 'F'),
('Mike','Jackson', 'F')
WAITFOR DELAY '00:00:10'
ROLLBACK

Query 2:
USE IsolationDB
GO
SELECT * FROM dbo.Isotab
GO

You will see query 2 just spins and waits for query 1 to finish. You will see also a blocking on the session that running query 2 which gets blocked by session that running query 1. At the end Query 2 returns an empty set as the Query 1 rollbacks all the modifications.

Read Uncommitted: This is the lowest isolation level. Read uncommitted issues\requests no shared locks on the data, it allows the Selects to read data that is currently being modified in other transactions. It also allows other transactions to modify  data that Selects are reading. This where we can see some unexpected results in a variety of different ways. For example data returned by the select is partial as an update was running in another transaction causing some of rows to come back with the updated values and may be few records got deleted .

To see the effect of read uncommitted lets run Query 1 in one tab of SSMS and then quickly run Query 2 in another tab before Query 1 completes. We will see the Query 2 returns the data which is not committed on Query 1.

Query 1:
USE IsolationDB
GO
TRUNCATE TABLE dbo.Isotab
GO
DBCC CHECKIDENT ('Isotab', RESEED,0)
GO
USE IsolationDB
GO
BEGIN TRAN
INSERT INTO DBO.Isotab (FirstName,LastName,Gender)
VALUES
('Don','Trump', 'M'),
('Hillary','Clinton', 'F'),
('Mike','Jackson', 'F')
WAITFOR DELAY '00:00:10'
ROLLBACK

Query 2:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
GO
USE IsolationDB
GO
SELECT * FROM dbo.Isotab
GO

From above we found that Query 2 will not wait for Query 1 to complete. This why Query 2 produced the the dirty data. Remember Query 1 rolls back all its changes however Query 2 has returned the data anyway, this is because it didn't wait for all the other transactions with exclusive locks on this data it just returned what was there at the time.

There is a shortcut for querying data with the read uncommitted isolation level with using the NOLOCK table hint as below, which will yield the same result as Query 2.
USE IsolationDB
GO
SELECT * FROM dbo.Isotab WITH (NOLOCK)
GO

Repeatable Read: This is similar to Read Committed but with the additional guarantee that if a transaction issues a same select twice it will get the same results both times. It does this by holding on to the shared locks it obtains on the records it reads until the end of the transaction. This means any transactions that try to modify these records are forced to wait for the read transaction to complete. But it will allow new records to get added, it does not stop new records being inserted so it is possible to get Phantom Reads with this isolation level.

Before we start testing this truncate our test table on our test database.


USE IsolationDB
GO
TRUNCATE TABLE dbo.Isotab
GO
DBCC CHECKIDENT ('Isotab', RESEED,0)
GO

Run the below Queries simultaneously in two sessions on SSMS.
Query 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
USE IsolationDB
GO
BEGIN TRAN
SELECT * FROM dbo.Isotab
GO
WAITFOR DELAY '00:00:15'  
SELECT * FROM dbo.Isotab
COMMIT

Query 2:
USE IsolationDB
GO
INSERT INTO DBO.Isotab (FirstName,LastName,Gender)
VALUES
('Don','Trump', 'M'),
('Hillary','Clinton', 'F'),
('Mike','Jackson', 'M')

Below is the result set for Query 1 one select set is empty and another select set has 3 records. Query 2 with inserts completed as soon as it ran after Query 1 started and it wont wait for Query 1 to finish as Query 2 is not running Update or Deletes. 













Now Repeat the Query 1 and Query 2, but this time run a update Statement in Query 2.

Query 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
USE IsolationDB
GO
BEGIN TRAN
SELECT * FROM dbo.Isotab
GO
WAITFOR DELAY '00:00:15'  
SELECT * FROM dbo.Isotab
COMMIT

Query 2:
USE IsolationDB
GO
UPDATE isotab
SET Gender='F' WHERE lastname='Trump'

Below is the result set for Query 1 , This time both select sets are equal with 3 records and same data. This time Query 2 with update is waited for Query 1 complete as Query 2 is not running Update. We will see same result if Query 2 running a delete. 















Serializable: This isolation level also eliminates Phantom Reads as well. This is similar to Read Committed but with the additional guarantee that if you issue the same select twice in a transaction you will get the same results both times. It does this by holding on to the shared locks it obtains on the records it reads until the end of the transaction, This means any transactions that try Insert\Delete\Update to these records are forced to wait for the read transaction to complete.

Before we start testing this truncate our test table on our test database.
USE IsolationDB
GO
TRUNCATE TABLE dbo.Isotab
GO
DBCC CHECKIDENT ('Isotab', RESEED,0)
GO

Run the below Queries simultaneously in two sessions on SSMS.
Query 1:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
USE IsolationDB
GO
BEGIN TRAN
SELECT * FROM dbo.Isotab
GO
WAITFOR DELAY '00:00:15'  
SELECT * FROM dbo.Isotab
COMMIT

Query 2:
USE IsolationDB
GO
INSERT INTO DBO.Isotab (FirstName,LastName,Gender)
VALUES
('Don','Trump', 'M'),
('Hillary','Clinton', 'F'),
('Mike','Jackson', 'M')

Below is the result set for Query 1, both select sets are with zero records. The Query 2 with Insert waited for Query 1 to complete unlike Repeatable Read which allows Inserts. Query 2 will finish and inserts records after Query 1 is finished. We will see same result for Query 2 running an update or Insert.













Snapshot: This provides the same guarantees as Serializable. The difference is the way it works. It works entirely different than the Serializable.  When we use the Snapshot it doesn't block other queries from Inserting or Updating or Deleting the data touched by the snapshot transaction unlike Serializable. Instead Snapshot uses row versioning when the data is changed the old version is kept in tempdb so existing transactions will see the version without the change. When all transactions that started before the changes are complete the previous row version is removed from tempdb. This means that even if another transaction has made changes you will always get the same results as you did the first time in that transaction.

The positive side of Snaphot isolation is not blocking any transactions that try modifying the data while running the select transaction but it uses extra resources on the SQL Server to hold multiple versions of your changes in the tempdb.

To use the snapshot isolation level we need to enable it on the database by running the following command. 


ALTER DATABASE IsolationDB  
SET ALLOW_SNAPSHOT_ISOLATION ON  

Or we can enable it from Database properties:














Before we start testing this truncate our test table on our test database.
USE IsolationDB
GO
TRUNCATE TABLE dbo.Isotab
GO
DBCC CHECKIDENT ('Isotab', RESEED,0)
GO

Query 1:
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
USE IsolationDB
GO
BEGIN TRAN
SELECT * FROM dbo.Isotab
GO
WAITFOR DELAY '00:00:10'  
SELECT * FROM dbo.Isotab
COMMIT 

Query 2:
USE IsolationDB
GO
INSERT INTO DBO.Isotab (FirstName,LastName,Gender)
VALUES
('Don','Trump', 'M'),
('Hillary','Clinton', 'F'),
('Mike','Jackson', 'M')

Below is the result set for Query 1, both select sets are with zero records. The Query 2 with Insert Doesn't waited for Query 1 to complete unlike Serializable which makes insert\delete\updates to wait. Query 2 finished and inserted records before Query 1 is finished. We will see same result for Query 2 running an update or Delete.













Note: Even though we have enabled the Snapshot isolation at database level we still need to use the SET TRANSACTION ISOLATION LEVEL SNAPSHOT to use the Snapshot Isolation, other wise it will use the Default Isolation (Read Committed). And we can use any Isolation on session base though SNAPSHOT Isolation is enabled at database level.

In any Isolation (Read Committed, Read Uncommitted, Repeatable Read and Serializable) the Update\Delete transaction which is issued later will wait until the prior Update\Delete transaction to complete and later Transaction will end as successful transaction as soon as prior transaction completes. 

But In Snapshot Isolation Conflict will occur and later transaction will end with failure as soon as the the prior transaction complete. Lets test this.

Before we start testing this truncate our test table on our test database and set DB Isolation to Snapshot:
USE IsolationDB
GO
TRUNCATE TABLE dbo.Isotab
GO
DBCC CHECKIDENT ('Isotab', RESEED,0)
GO
---- insert some test data
INSERT INTO DBO.Isotab (FirstName,LastName,Gender)
VALUES
('Don','Trump', 'M'),
('Hillary','Clinton', 'F'),
('Mike','Jackson', 'F')
---- Change DB to use Snapshot Isolation
ALTER DATABASE IsolationDB SET ALLOW_SNAPSHOT_ISOLATION ON

Query 1:
USE IsolationDB
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
BEGIN TRAN
UPDATE Isotab
SET Gender='Female' WHERE LastName='Trump'
WAITFOR DELAY '00:00:15'
COMMIT

Query 2:
USE IsolationDB
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
BEGIN TRAN
UPDATE Isotab
SET Gender='Male' WHERE LastName='Trump'
COMMIT

Now first run Query 1 which try changes Trump gender to Female and it waits for 15 secs before it commit. Meanwhile we have figured out the mistake and tried to change it to Male using the Query 2. Query 2 will wait until the Query 1 finishes and fails to change it to male. And the Query 2 error looks like below. And it doesn't happen in any other Isolation Level. 










Read Committed Snapshot: To rectify this Update Conflict issue with Snapshot Isolation we have an option which is Enabling Is_Read_Committed_Snapshot_On. It works more like Read_Committed while modifications and works like Snapshot while selects. But there few differences.

Before we start testing this truncate our test table on our test database and set DB Isolation to Read_Committed_Snapshot:


USE IsolationDB
GO
TRUNCATE TABLE dbo.Isotab
GO
DBCC CHECKIDENT ('Isotab', RESEED,0)
GO
---- insert some test data
BEGIN TRAN
INSERT INTO DBO.Isotab (FirstName,LastName,Gender)
VALUES
('Don','Trump', 'M'),
('Hillary','Clinton', 'F'),
('Mike','Jackson', 'F')
COMMIT
---- Change DB to use Snapshot Isolation
ALTER DATABASE [IsolationDB] SET READ_COMMITTED_SNAPSHOT ON WITH NO_WAIT
GO

Query 1:
USE IsolationDB
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRAN
UPDATE Isotab
SET Gender='Female' WHERE LastName='Trump'
WAITFOR DELAY '00:00:15'
COMMIT 

Query 2:
USE IsolationDB
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRAN
UPDATE Isotab
SET Gender='Male' WHERE LastName='Trump'
COMMIT

Now first run Query 1 which try changes Trump gender to Female and it waits for 15 secs before it commit. Meanwhile we have figured out the mistake and tried to change it to Male using the Query 2. Query 2 will wait until the Query 1 finishes and changes it back to male.  Below is the result.





Key Differences between Snapshot and Read Committed Snapshot are Snapshot isolation provides Transaction Level Read Consistency where as Read Committed Snapshot Isolation provide Statement Level Read Consistency And you can see a Nice Video on these two Isolation Levels here (Venkat has given a great and simple presentation on this). 

We can check the Isolation Level that we are using at session level by running the DBCC USEROPTIONS command.



















Thanks
Hope it helps some one. 

Friday, April 8, 2016

SQL Server Database Page Verify Options

There are two different possible Page Verify options namely TORN_PAGE_DETECTION and CHECKSUM are available from SQL Server 2005 and above. The TORN_PAGE_DETECTION is only Page Verify option available in SQL Servers 2000 and older.  

On SQL 2005:





On SQL Server 2000:

By default the new databases created in SQL Server 2005 or above will have CHECKSUM as the Page Verify option. But for the databases that upgraded or migrated from older versions to SQL Server 2005 or above will retain the TORN_PAGE_DETECTION as page verify, and we need to modify the option using database properties with GUI or we can run the below command. 

ALTER DATABASE SET PAGE_VERIFY CHECKSUM

Functionality of TORN_PAGE_DETECTION vs CHECKSUM:

Below is the high level description of the each Page verify option and how they work from BOL

When TORN_PAGE_DETECTION is specified, a specific 2-bit pattern for each 512-byte sector in the 8-kilobyte (KB) database page is saved and stored in the database page header when the page is written to disk. When the page is read from disk, the torn bits stored in the page header are compared to the actual page sector information.

When CHECKSUM is specified, the Database Engine calculates a CHECKSUM over the contents of the whole page and stores the value in the page header when a page is written to disk. When the page is read from disk, the CHECKSUM is recomputed and compared to the CHECKSUM value stored in the page header.

When NONE is specified, database page writes will not generate a CHECKSUM or TORN_PAGE_DETECTION value. SQL Server will not verify a CHECKSUM or torn page during a read even if a CHECKSUM or TORN_PAGE_DETECTION value is present in the page header.

It is very important to have CHECKSUM as the page verify option on all the databases on SQL Server 2005 and above.  I want to show you how efficient is the CHECKSUM over the TORN_PAGE_DETECTION in capturing the data corruptions\modifications made outside the  SQL Server supported methods. 

Lets Start with TORN_PAGE_DETECTION:  For testing purposes I am creating a test database with TORN_PAGE_DETECTION and table, and inserting few records.

USE MASTER
CREATE DATABASE PageVerifyDB
GO
ALTER DATABASE PageVerifyDB SET PAGE_VERIFY TORN_PAGE_DETECTION ;
GO

USE PageVerifyDB
GO
CREATE TABLE PageTab
(
EmployeeID INT IDENTITY(1,1),
FirstName VARCHAR(256),
LastName varchar(256),
Gender varchar(10)
CONSTRAINT pk_pagetable PRIMARY KEY (EmployeeID)
)
GO

INSERT INTO DBO.PageTab (FirstName,LastName,Gender)
VALUES
('Don','Trump', 'M'),
('Hillary','Clinton', 'F'),
('Mike','Jackson', 'M')

This is how the data looks like on the PageTab table.













Now let's corrupt the data on this table using the HexEditor. You can learn how to use this HexEditor from here, Kendra Little has given a nice presentation on HexEditor usage. And please remember to not use this HexEditor on any production\Dev\Test\QA environments. It is strictly not a good practice. 


To know which page has the data that we looking to corrupt. We can run the DBCC IND.
DBCC IND ('PageVerifyDB','Pagetab',1)






I want to corrupt the page 283 on mdf, so to find offset value multiple Page number with (8*2014).   

Offset Value = 283*8*1024 = 2318336

And I wan to change Don Trump Gender to F. And lets use the HexEditor, before that we need to make the database offline as the HexEditor may fails to grab access on the mdf file. 

Opening the mdf file with HexEditor:
























Going to the Offset:

Modifying the Values using HexEditor:

Now keep the Database back Online and try select data from it. We don't have any issue selecting the data. 

It looks like below:











So TORN_PAGE_DETECTION will allow us to change the Don Trump gender from M to F.

Now Start with CHECKSUM: I Will do the same exercise again by dropping and recreating our test DB with CHECKSUM as page verify option.

USE MASTER
Drop DATABASE PageVerifyDB
GO

USE MASTER
CREATE DATABASE PageVerifyDB
GO
ALTER DATABASE PageVerifyDB SET PAGE_VERIFY CHECKSOME;
GO

USE PageVerifyDB
GO
CREATE TABLE PageTab
(
EmployeeID INT IDENTITY(1,1),
FirstName VARCHAR(256),
LastName varchar(256),
Gender varchar(10)
CONSTRAINT pk_pagetable PRIMARY KEY (EmployeeID)
)
GO

INSERT INTO DBO.PageTab (FirstName,LastName,Gender)
VALUES
('Don','Trump', 'M'),
('Hillary','Clinton', 'F'),
('Mike','Jackson', 'M')

This is how the data looks like on the PageTab table.


Now corrupting the same data but this time it is residing on different page, now it is 242. Offset value is 1982464. Make the database offline.

Opening the mdf file with HexEditor:


Going to the Offset:






















Modifying the Values using HexEditor:






















Now keep the Database back Online and try select data from it. We will see red colored text in result pane, which reads like below.  








The CHECKSOME wont allow us to change the Don Trump's gender from M to F. 

This concludes how both the page verify option works and which is efficient. And using the NONE as page verify option is worst than Torn_page_detection.

There are lot of myths around the Page Verify options especially the CHECKSUMS, you can read from Paul Randol's post here.

Thanks, Hope it is helpful.

Here is the Kendra Little's article here regarding the Page Verify options, I just take her article as a reference to my post. I just reproduced her article here. 


Wednesday, April 6, 2016

Transactional Replication Subscription on AlwayOn Availability Group

We had a little team meeting to discuss about the using the AAG database as subscriber for transnational replication. As soon as I came out of meeting started creating POC for this scenario. I have an existing Publisher and Distributor servers, real quick I have created a publication and tried to create the subscription (through GUI) to Always On Availability Group DB and  thought to use the Lister name as the Subscriber server while create the subscription. 

And it thrown the below error:
TITLE: Connect to Server
Cannot connect to AAG_1_Listener.
ADDITIONAL INFORMATION:
SQL Server replication requires the actual server name to make a connection to the server. Specify the actual server name, 'PrimaryAGNODE'. (Replication.Utilities)



I have did some research and find below Microsoft article, It has all the instructions on How to Create Transactional Subscription in an AlwaysOn Environment. The trick is " The subscription must be created by using a Transact-SQL script and cannot be created using Management Studio".

Steps to follow for creating Transactional Subscription in AlwaysOn Availability Group:

  1. Before creating the subscription, add the subscriber database to the appropriate AlwaysOn availability group.
  2. Add the subscriber's availability group Listener as a linked server to all nodes of the availability group. This step ensures that all potential failover partners are aware of and can connect to the listener. (This step not really Needed)
  3. Using the below script in the Creating a Transactional Replication Push Subscription section below, create the subscription using the name of the availability group listener of the subscriber. After a failover, the listener name will always remain valid, whereas the actual server name of the subscriber will depend on the actual node that became the new primary.
-- commands to execute at the publisher, in the publisher database:
use [publisher_database_name_Here]
EXEC sp_addsubscription @publication = N'publication_name_Here', 
       @subscriber = N'availability_group_listener_name_Here', 
       @destination_db = N'subscriber_database_name_Here', 
       @subscription_type = N'Push', 
       @sync_type = N'automatic', @article = N'all', @update_mode = N'read only', @subscriber_type = 0;
GO

EXEC sp_addpushsubscription_agent @publication = N'publication_name_Here', 
       @subscriber = N'availability_group_listener_name_Here', 
       @subscriber_db = N'subscriber_database_name_Here', 
       @job_login = null, @job_password = null, @subscriber_security_mode = 1;
GO
    4. If creating a pull subscription:

  • In Management Studio, on the primary subscriber node, open the SQL Server Agent tree.
  • Identify the Pull Distribution Agent job and edit the job.
  • On the Run Agent job step, check the -Publisher and -Distributor parameters. Make sure that these parameters contain the correct direct server and instance names of the publisher and distributor server.
  • Change the -Subscriber parameter to the subscriber's availability group listener name.
If we follow above steps, we don't need to do anything even though faiover happens. 

Things That I Noticed: 

I have noticed that the subscriptions are created on all nodes where ever the Subscription DB is participated in AllwaysOn Availability Group.

The distribution agent is responsible for sending the replicated data to the Subscriber Db which is the Current Primary Replica in the AAG. Further AlwaysOn will take care of distributing data from Primary Replica to the other secondary Replicas.  



Below is the link to the original Microsoft article



Thanks, Hope it helps.

Find Last Reboot Time of Local or Remote Windows System

There are different ways to find the Uptime or Last boot time of a windows machine using Command Prompt or Power Shell or Task manager or etc. 

Using the Command prompt:  
We have use the systeminfo command to find the last boot time but it wil get us lot of other details which we are not interested in. So we have to use systeminfo in conjunction with the pipe operator (|) with the find command to search the just that particular (boot time) information. 

On Local Server: 
systeminfo | find "System Boot Time:"
systeminfo | find "System Up Time:"

On Remote Server:
systeminfo /S   | find "System Boot Time:"
systeminfo /S   | find "System Up Time:"

/S for specifying the Remote Server Name
/U for specifying the user name
/P for specifying the password

Sometime System Boot Time work but sometime System Up Time work.  Depending on how the Microsoft decides to change things. 



We can use systeminfo command from command prompt but it will get us lot of system related information as shown below. 
























Using the Task Manager: 
But not all windows give this information. The OS older than Windows Server 2008 don't show this info on task manager.


















Using the Event Viewer: 
To figure out when the Windows was last rebooted, Open up Event Viewer, go to the Windows Logs -> System log, and then filter by Event ID 6006, which indicates that the event log service was shut down. One of the last things that happens before a reboot. This technique won't help when there was a power outage, but we can filter by Event ID 6005 to see when the system was last turned on. Event Id 6005 shows shows when the event log service was started again.

Thanks, Hope it helps.